🎉 Phase 7.1 — xdg-shell minimal validé runtime
Capture preuve : docs/phase7-1-xdg-toplevel.png — fenêtre 320x240 du
client externe positionnée à (60, 60) par le compositor (cascade par
défaut), avec bandes verticales arc-en-ciel + bordure noire 2px.
Différence visuelle vs phase 6.4 : la fenêtre est maintenant
positionnée par le compositor (pas plaquée à 0,0).
Modifications redox-wl-wayland-frontend :
- + dep wayland-protocols (feature server)
- xdg_wm_base global v5
- XdgSurfaceData { wl_surface, last_serial, acked_serial }
- XdgToplevelData { title, app_id }
- SurfaceData.xdg_pending_initial_configure : bloque l'affichage tant
que ack-configure pas reçu (sémantique xdg-shell standard)
- WaylandFrontend.next_xdg_serial : counter monotone serial
- WaylandFrontend.next_toplevel_index : cascade position +60 par fenêtre
- DEFAULT_TOPLEVEL_SIZE = 640x480 envoyé en initial configure
- Dispatch xdg_wm_base : GetXdgSurface, CreatePositioner (no-op),
Pong (no-op), Destroy (no-op)
- Dispatch xdg_positioner + xdg_popup : no-op (out of scope)
- Dispatch xdg_surface :
- GetToplevel → cascade pos + envoie xdg_toplevel.configure(640,480)
+ xdg_surface.configure(serial)
- AckConfigure(serial) → enregistre + débloque affichage
- Destroy → cache la surface
- Dispatch xdg_toplevel :
- SetTitle / SetAppId → stocke
- tout le reste ignoré (move, resize, maximized, ...)
redox-wl-test-client-shm adapté :
- + dep wayland-protocols (feature client)
- bind 3 globals : wl_compositor, wl_shm, xdg_wm_base
- Dispatch XdgWmBase::Event::Ping → pong (au cas où)
- Dispatch XdgSurface::Event::Configure → store serial
- Dispatch XdgToplevel::Event::Configure / Close → log
- Workflow runtime :
1. create_surface + get_xdg_surface + get_toplevel
2. set_title / set_app_id
3. surface.commit (initial empty)
4. attente Configure event (timeout 5s)
5. ack_configure(serial)
6. shm + buffer + attach + commit POST-ack
7. boucle 25s
8. destroy propre
Init submodule wayland-rs/wayland-protocols/protocols (XML files
gitlab.freedesktop.org/wayland/wayland-protocols).
Validation runtime QEMU complète :
[client] globals : compositor=true shm=true xdg_wm_base=true
[client] xdg_toplevel configure suggéré : 640x480
[client] initial configure reçu, serial=1
[client] ack_configure(1)
[client] buffer attach + damage + commit POST-ack
[comp] tick=30..450 surfaces=1 elapsed=1.2..18.2s
PNG capturée à T+14s : surface visible à (60,60), bandes RGB+jaune+
violet+orange comme attendu.
Critère de fin 7.1 validé : client xdg-shell crée toplevel, reçoit
configure, ack, commit, affiche via shm, sans panic serveur, 18s
stable, destroy propre.
Limitations connues : focus, cursor, move/resize, multi-client,
robustesse — sous-tickets 7.2-7.7.
Image Redox restaurée à boot Orbital normal.
docs/phase7-1-xdg-shell.md : compte-rendu complet, cycle de vie,
limitations, plan 7.2.
Leyoda 2026 – GPLv3
This commit is contained in:
parent
8a897d975d
commit
4bff319c7f
6 changed files with 600 additions and 36 deletions
|
|
@ -26,14 +26,27 @@ use std::path::Path;
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use redox_wl_compositor_core::{SurfaceBuffer, SurfaceId, SurfaceRegistry};
|
||||
use wayland_protocols::xdg::shell::server::{
|
||||
xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base,
|
||||
};
|
||||
use wayland_server::{
|
||||
Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource,
|
||||
backend::{ClientData, ClientId, DisconnectReason},
|
||||
protocol::{wl_buffer, wl_compositor, wl_region, wl_shm, wl_shm_pool, wl_surface, wl_callback},
|
||||
protocol::{wl_buffer, wl_callback, wl_compositor, wl_region, wl_shm, wl_shm_pool, wl_surface},
|
||||
};
|
||||
|
||||
const COMPOSITOR_VERSION: u32 = 5;
|
||||
const SHM_VERSION: u32 = 1;
|
||||
const XDG_WM_BASE_VERSION: u32 = 5;
|
||||
|
||||
/// Taille suggérée par défaut pour les nouvelles fenêtres xdg_toplevel.
|
||||
/// Le client peut respecter ou non ; on utilise sa propre taille de buffer
|
||||
/// quand il fait son commit.
|
||||
const DEFAULT_TOPLEVEL_SIZE: (i32, i32) = (640, 480);
|
||||
|
||||
/// Décalage cascading entre fenêtres successives, pour qu'elles
|
||||
/// n'apparaissent pas toutes à (0, 0).
|
||||
const CASCADE_OFFSET: i32 = 60;
|
||||
|
||||
/// Pool SHM mmap'd côté compositor. Garde le `OwnedFd` pour qu'inputd ou
|
||||
/// le kernel ne libère pas la zone tant qu'on a des buffers en référence.
|
||||
|
|
@ -115,6 +128,29 @@ struct SurfaceData {
|
|||
pending_buffer: Mutex<Option<BufferData>>,
|
||||
/// Frame callbacks en attente (à signaler après le prochain present).
|
||||
pending_frame_callbacks: Mutex<Vec<wl_callback::WlCallback>>,
|
||||
/// Si une xdg_surface a été créée pour cette wl_surface, true tant
|
||||
/// que le premier ack_configure n'a pas été reçu. Pendant cette
|
||||
/// période, les buffers attachés ne doivent pas être affichés
|
||||
/// (xdg-shell spec : le rôle xdg_surface "désactive" le rendu
|
||||
/// jusqu'au premier configure-ack).
|
||||
xdg_pending_initial_configure: Mutex<bool>,
|
||||
}
|
||||
|
||||
/// Données par-xdg_surface : référence à la wl_surface sous-jacente +
|
||||
/// dernier serial envoyé + ack reçu.
|
||||
struct XdgSurfaceData {
|
||||
wl_surface: wl_surface::WlSurface,
|
||||
last_serial: Mutex<u32>,
|
||||
/// Le dernier serial qui a été ack par le client. Tant que c'est
|
||||
/// inférieur à `last_serial`, le compositor doit ignorer les commits.
|
||||
acked_serial: Mutex<u32>,
|
||||
}
|
||||
|
||||
/// Données par-xdg_toplevel : title, app_id, ref vers son xdg_surface.
|
||||
#[derive(Default)]
|
||||
struct XdgToplevelData {
|
||||
title: Mutex<Option<String>>,
|
||||
app_id: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -135,6 +171,12 @@ pub struct WaylandFrontend {
|
|||
frame_callbacks: Vec<wl_callback::WlCallback>,
|
||||
/// Counter monotone pour les `done` events (timestamp en ms simulé)
|
||||
frame_time_ms: u32,
|
||||
/// Counter monotone pour les serials xdg_surface.configure.
|
||||
/// Incrémenté à chaque envoi.
|
||||
next_xdg_serial: u32,
|
||||
/// Counter cascading des nouvelles fenêtres xdg_toplevel : la 1re est
|
||||
/// placée à (60, 60), la 2e à (120, 120), etc.
|
||||
next_toplevel_index: u32,
|
||||
}
|
||||
|
||||
impl WaylandFrontend {
|
||||
|
|
@ -147,6 +189,7 @@ impl WaylandFrontend {
|
|||
let mut dh = display.handle();
|
||||
dh.create_global::<Self, wl_compositor::WlCompositor, _>(COMPOSITOR_VERSION, ());
|
||||
dh.create_global::<Self, wl_shm::WlShm, _>(SHM_VERSION, ());
|
||||
dh.create_global::<Self, xdg_wm_base::XdgWmBase, _>(XDG_WM_BASE_VERSION, ());
|
||||
|
||||
let listener = wayland_server::ListeningSocket::bind_absolute(socket_path.to_path_buf())?;
|
||||
|
||||
|
|
@ -156,6 +199,8 @@ impl WaylandFrontend {
|
|||
listener,
|
||||
frame_callbacks: Vec::new(),
|
||||
frame_time_ms: 0,
|
||||
next_xdg_serial: 1,
|
||||
next_toplevel_index: 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -254,6 +299,7 @@ impl wayland_server::Dispatch<wl_compositor::WlCompositor, ()> for WaylandFronte
|
|||
id: Mutex::new(Some(surface_id)),
|
||||
pending_buffer: Mutex::new(None),
|
||||
pending_frame_callbacks: Mutex::new(Vec::new()),
|
||||
xdg_pending_initial_configure: Mutex::new(false),
|
||||
};
|
||||
data_init.init(id, Arc::new(data));
|
||||
}
|
||||
|
|
@ -424,6 +470,16 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
|
|||
Some(id) => id,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Si la surface a un rôle xdg_surface et n'a pas encore été
|
||||
// ack-configure, on ignore le commit côté affichage (xdg-shell
|
||||
// spec : "the surface is in an unconfigured state, the client
|
||||
// can attach buffers but they are not visible").
|
||||
let pending_xdg = *data.xdg_pending_initial_configure.lock().unwrap();
|
||||
if pending_xdg {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lire le buffer attaché (s'il y en a un)
|
||||
let bd_opt = data.pending_buffer.lock().unwrap().clone();
|
||||
if let Some(bd) = bd_opt {
|
||||
|
|
@ -473,3 +529,202 @@ impl wayland_server::Dispatch<wl_callback::WlCallback, ()> for WaylandFrontend {
|
|||
// wl_callback n'a pas de requests, juste l'event `done`
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// xdg-shell (phase 7.1)
|
||||
// =====================================================================
|
||||
|
||||
// ---- xdg_wm_base (global) ----
|
||||
impl GlobalDispatch<xdg_wm_base::XdgWmBase, ()> for WaylandFrontend {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_handle: &DisplayHandle,
|
||||
_client: &Client,
|
||||
resource: wayland_server::New<xdg_wm_base::XdgWmBase>,
|
||||
_data: &(),
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
data_init.init(resource, ());
|
||||
}
|
||||
}
|
||||
|
||||
impl wayland_server::Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandFrontend {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_r: &xdg_wm_base::XdgWmBase,
|
||||
request: xdg_wm_base::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
xdg_wm_base::Request::GetXdgSurface { id, surface } => {
|
||||
// Marquer la wl_surface comme ayant un rôle xdg : tant que
|
||||
// le 1er ack_configure n'est pas reçu, ses commits ne sont
|
||||
// pas affichés (cf wl_surface.commit ci-dessus).
|
||||
if let Some(sd) = surface.data::<Arc<SurfaceData>>() {
|
||||
*sd.xdg_pending_initial_configure.lock().unwrap() = true;
|
||||
}
|
||||
let xdg_data = Arc::new(XdgSurfaceData {
|
||||
wl_surface: surface,
|
||||
last_serial: Mutex::new(0),
|
||||
acked_serial: Mutex::new(0),
|
||||
});
|
||||
data_init.init(id, xdg_data);
|
||||
}
|
||||
xdg_wm_base::Request::CreatePositioner { id } => {
|
||||
// Pour 7.1 : positioner no-op (popups hors scope).
|
||||
data_init.init(id, ());
|
||||
}
|
||||
xdg_wm_base::Request::Pong { .. } => {
|
||||
// Réponse à un ping qu'on n'envoie jamais en 7.1 → ignore.
|
||||
}
|
||||
xdg_wm_base::Request::Destroy => {
|
||||
// Le client se désengage. Pas d'action particulière côté
|
||||
// serveur — les xdg_surface restantes seront détruites
|
||||
// séparément.
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- xdg_positioner (no-op pour 7.1, popups hors scope) ----
|
||||
impl wayland_server::Dispatch<xdg_positioner::XdgPositioner, ()> for WaylandFrontend {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_r: &xdg_positioner::XdgPositioner,
|
||||
_req: xdg_positioner::Request,
|
||||
_: &(),
|
||||
_dh: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- xdg_popup (no-op pour 7.1) ----
|
||||
impl wayland_server::Dispatch<xdg_popup::XdgPopup, ()> for WaylandFrontend {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_r: &xdg_popup::XdgPopup,
|
||||
_req: xdg_popup::Request,
|
||||
_: &(),
|
||||
_dh: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- xdg_surface ----
|
||||
impl wayland_server::Dispatch<xdg_surface::XdgSurface, Arc<XdgSurfaceData>> for WaylandFrontend {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
resource: &xdg_surface::XdgSurface,
|
||||
request: xdg_surface::Request,
|
||||
data: &Arc<XdgSurfaceData>,
|
||||
_dh: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
xdg_surface::Request::GetToplevel { id } => {
|
||||
let toplevel_data = Arc::new(XdgToplevelData::default());
|
||||
let toplevel = data_init.init(id, toplevel_data);
|
||||
|
||||
// Position cascading pour que les fenêtres successives ne
|
||||
// s'empilent pas toutes à (0, 0).
|
||||
let idx = state.next_toplevel_index;
|
||||
state.next_toplevel_index = state.next_toplevel_index.saturating_add(1);
|
||||
let pos_x = CASCADE_OFFSET * (1 + idx as i32);
|
||||
let pos_y = CASCADE_OFFSET * (1 + idx as i32);
|
||||
|
||||
if let Some(sd) = data.wl_surface.data::<Arc<SurfaceData>>() {
|
||||
if let Some(sid) = *sd.id.lock().unwrap() {
|
||||
state.registry.modify_pending(sid, |s| {
|
||||
s.x = pos_x;
|
||||
s.y = pos_y;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initial configure : envoyer au client une taille suggérée
|
||||
// et un serial. Le client doit ack_configure puis commit
|
||||
// pour que la surface devienne visible.
|
||||
let serial = state.next_xdg_serial;
|
||||
state.next_xdg_serial = state.next_xdg_serial.wrapping_add(1);
|
||||
*data.last_serial.lock().unwrap() = serial;
|
||||
|
||||
toplevel.configure(
|
||||
DEFAULT_TOPLEVEL_SIZE.0,
|
||||
DEFAULT_TOPLEVEL_SIZE.1,
|
||||
Vec::new(), // pas de states (Maximized, Activated, etc.)
|
||||
);
|
||||
resource.configure(serial);
|
||||
}
|
||||
xdg_surface::Request::GetPopup { id, .. } => {
|
||||
// Hors scope 7.1 — popups non supportés. On init avec
|
||||
// un () pour ne pas violer le protocole.
|
||||
data_init.init(id, ());
|
||||
}
|
||||
xdg_surface::Request::SetWindowGeometry { .. } => {
|
||||
// Stocké conceptuellement mais ignoré pour 7.1 (la geometry
|
||||
// sert pour les decorations CSD que nous ne supportons pas).
|
||||
}
|
||||
xdg_surface::Request::AckConfigure { serial } => {
|
||||
*data.acked_serial.lock().unwrap() = serial;
|
||||
// Si c'était l'initial configure, débloquer l'affichage
|
||||
if let Some(sd) = data.wl_surface.data::<Arc<SurfaceData>>() {
|
||||
*sd.xdg_pending_initial_configure.lock().unwrap() = false;
|
||||
}
|
||||
}
|
||||
xdg_surface::Request::Destroy => {
|
||||
// Le client détruit son rôle xdg. La wl_surface peut survivre
|
||||
// mais elle perd son rôle de fenêtre toplevel ; on cache la
|
||||
// surface pour ne plus la dessiner.
|
||||
if let Some(sd) = data.wl_surface.data::<Arc<SurfaceData>>() {
|
||||
if let Some(sid) = *sd.id.lock().unwrap() {
|
||||
state.registry.modify_pending(sid, |s| s.visible = false);
|
||||
state.registry.commit(sid);
|
||||
}
|
||||
*sd.xdg_pending_initial_configure.lock().unwrap() = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- xdg_toplevel ----
|
||||
impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
||||
for WaylandFrontend
|
||||
{
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_r: &xdg_toplevel::XdgToplevel,
|
||||
request: xdg_toplevel::Request,
|
||||
data: &Arc<XdgToplevelData>,
|
||||
_dh: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
xdg_toplevel::Request::SetTitle { title } => {
|
||||
*data.title.lock().unwrap() = Some(title);
|
||||
}
|
||||
xdg_toplevel::Request::SetAppId { app_id } => {
|
||||
*data.app_id.lock().unwrap() = Some(app_id);
|
||||
}
|
||||
xdg_toplevel::Request::Destroy => {
|
||||
// wl_surface destroy gérera la suppression côté registry
|
||||
}
|
||||
// Tout le reste ignoré en 7.1 :
|
||||
// SetParent, ShowWindowMenu, Move, Resize, SetMaxSize, SetMinSize,
|
||||
// SetMaximized, UnsetMaximized, SetFullscreen, UnsetFullscreen,
|
||||
// SetMinimized
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue