🎉 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
|
|
@ -6,4 +6,5 @@ edition = "2021"
|
|||
[dependencies]
|
||||
wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false }
|
||||
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
|
||||
wayland-protocols = { path = "../../../wayland-rs/wayland-protocols", default-features = false, features = ["client"] }
|
||||
libc = "0.2"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
//! Phase 6.4 — Client Wayland test.
|
||||
//! Phase 7.1 — Client Wayland xdg-shell.
|
||||
//!
|
||||
//! Se connecte au socket Wayland exposé par le compositor à
|
||||
//! `/tmp/redox-wl-comp.sock`, crée une surface 320x240, peint un
|
||||
//! pattern ARGB déterministe (dégradé orangé), commit. Reste connecté
|
||||
//! 30s pour qu'on puisse capturer l'écran.
|
||||
//! Se connecte à `/tmp/redox-wl-comp.sock`, bind les globals
|
||||
//! wl_compositor + wl_shm + xdg_wm_base, crée une fenêtre xdg_toplevel,
|
||||
//! attend l'initial configure, ack, peint un buffer ARGB et commit.
|
||||
//!
|
||||
//! C'est le test critique de phase 6.4 : si le compositor affiche
|
||||
//! ce qui suit dans la frame QEMU, on a un VRAI compositor Wayland
|
||||
//! qui rend des pixels venant d'un client externe.
|
||||
//! Vs phase 6.4 : on passe par xdg-shell (placement managé par le
|
||||
//! compositor) au lieu de wl_surface seul.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::fs::OpenOptions;
|
||||
|
|
@ -28,6 +26,11 @@ use wayland_client::{
|
|||
wl_shm_pool::WlShmPool, wl_surface::WlSurface,
|
||||
},
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{
|
||||
xdg_surface::{self, XdgSurface},
|
||||
xdg_toplevel::{self, XdgToplevel},
|
||||
xdg_wm_base::{self, XdgWmBase},
|
||||
};
|
||||
|
||||
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
|
||||
const W: i32 = 320;
|
||||
|
|
@ -60,6 +63,10 @@ fn dlog(s: &str) {
|
|||
struct ClientState {
|
||||
compositor: Option<WlCompositor>,
|
||||
shm: Option<WlShm>,
|
||||
wm_base: Option<XdgWmBase>,
|
||||
/// Le serial du dernier configure reçu, à ack.
|
||||
pending_serial: Option<u32>,
|
||||
configured: bool,
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
|
||||
|
|
@ -79,6 +86,9 @@ impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
|
|||
"wl_shm" => {
|
||||
state.shm = Some(registry.bind(name, version.min(1), qh, ()));
|
||||
}
|
||||
"xdg_wm_base" => {
|
||||
state.wm_base = Some(registry.bind(name, version.min(5), qh, ()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +116,63 @@ noop!(WlShmPool);
|
|||
noop!(WlBuffer);
|
||||
noop!(WlSurface);
|
||||
|
||||
// xdg_wm_base : doit répondre aux pings (mais on n'envoie pas) ; on ignore
|
||||
impl Dispatch<XdgWmBase, ()> for ClientState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
wm_base: &XdgWmBase,
|
||||
event: xdg_wm_base::Event,
|
||||
_: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let xdg_wm_base::Event::Ping { serial } = event {
|
||||
wm_base.pong(serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// xdg_surface : reçoit les configure, on stocke le serial pour ack
|
||||
impl Dispatch<XdgSurface, ()> for ClientState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_xdg_surf: &XdgSurface,
|
||||
event: xdg_surface::Event,
|
||||
_: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let xdg_surface::Event::Configure { serial } = event {
|
||||
state.pending_serial = Some(serial);
|
||||
state.configured = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// xdg_toplevel : configure (size + states) + close
|
||||
impl Dispatch<XdgToplevel, ()> for ClientState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_r: &XdgToplevel,
|
||||
event: xdg_toplevel::Event,
|
||||
_: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
xdg_toplevel::Event::Configure { width, height, .. } => {
|
||||
dlog(&format!(
|
||||
"[client] xdg_toplevel configure suggéré : {width}x{height}"
|
||||
));
|
||||
}
|
||||
xdg_toplevel::Event::Close => {
|
||||
dlog("[client] xdg_toplevel close request");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_shm_with_pattern(name: &str) -> Result<OwnedFd, String> {
|
||||
let cname = CString::new(name).unwrap();
|
||||
let _ = libc::shm_unlink(cname.as_ptr());
|
||||
|
|
@ -133,14 +200,23 @@ unsafe fn create_shm_with_pattern(name: &str) -> Result<OwnedFd, String> {
|
|||
return Err("mmap failed".into());
|
||||
}
|
||||
|
||||
// Pattern ARGB : dégradé orangé + bandes diagonales pour reconnaître
|
||||
// Pattern ARGB : bandes verticales colorées + numéro de fenêtre simulé
|
||||
let pixels = std::slice::from_raw_parts_mut(p as *mut u32, (W * H) as usize);
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let r: u32 = (200 + (x * 55 / W)) as u32 & 0xFF;
|
||||
let g: u32 = (80 + (y * 100 / H)) as u32 & 0xFF;
|
||||
let b: u32 = (((x + y) & 0xFF) as u32).saturating_sub(100);
|
||||
pixels[(y * W + x) as usize] = (0xFF << 24) | (r << 16) | (g << 8) | b;
|
||||
let band = (x / 32) % 6;
|
||||
let color: u32 = match band {
|
||||
0 => 0xFF_F2_55_44,
|
||||
1 => 0xFF_F0_8E_3A,
|
||||
2 => 0xFF_E8_C5_3A,
|
||||
3 => 0xFF_5A_C0_50,
|
||||
4 => 0xFF_45_8B_E0,
|
||||
5 => 0xFF_8C_60_D0,
|
||||
_ => 0xFF_FF_FF_FF,
|
||||
};
|
||||
// Bordure noire 2px
|
||||
let on_border = x < 2 || x >= W - 2 || y < 2 || y >= H - 2;
|
||||
pixels[(y * W + x) as usize] = if on_border { 0xFF_10_10_10 } else { color };
|
||||
}
|
||||
}
|
||||
libc::munmap(p, SIZE as usize);
|
||||
|
|
@ -150,7 +226,7 @@ unsafe fn create_shm_with_pattern(name: &str) -> Result<OwnedFd, String> {
|
|||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dlog("[client] connect to compositor");
|
||||
|
||||
// Attendre que le socket existe (compositor démarré)
|
||||
// Attente du socket
|
||||
for i in 0..50 {
|
||||
if std::path::Path::new(SOCKET_PATH).exists() {
|
||||
break;
|
||||
|
|
@ -171,36 +247,64 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let mut state = ClientState::default();
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
dlog(&format!(
|
||||
"[client] globals : compositor={} shm={}",
|
||||
"[client] globals : compositor={} shm={} xdg_wm_base={}",
|
||||
state.compositor.is_some(),
|
||||
state.shm.is_some()
|
||||
state.shm.is_some(),
|
||||
state.wm_base.is_some()
|
||||
));
|
||||
|
||||
let compositor = state
|
||||
.compositor
|
||||
.clone()
|
||||
.ok_or("no wl_compositor global")?;
|
||||
let compositor = state.compositor.clone().ok_or("no wl_compositor global")?;
|
||||
let shm = state.shm.clone().ok_or("no wl_shm global")?;
|
||||
let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base global")?;
|
||||
|
||||
// Crée shm + pattern
|
||||
let fd = unsafe { create_shm_with_pattern("/redox-wl-client-shm") }?;
|
||||
dlog(&format!("[client] shm créé, peint {}x{} ARGB", W, H));
|
||||
|
||||
// wl_shm_pool + wl_buffer
|
||||
let pool = shm.create_pool(fd.as_fd(), SIZE, &qh, ());
|
||||
let buffer = pool.create_buffer(0, W, H, STRIDE, wayland_client::protocol::wl_shm::Format::Argb8888, &qh, ());
|
||||
|
||||
// wl_surface
|
||||
// 1. Création wl_surface + xdg_surface + xdg_toplevel
|
||||
let surface = compositor.create_surface(&qh, ());
|
||||
let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
|
||||
let toplevel = xdg_surface.get_toplevel(&qh, ());
|
||||
toplevel.set_title("Phase 7.1 client".to_string());
|
||||
toplevel.set_app_id("redox.wl.test.client.shm".to_string());
|
||||
surface.commit(); // commit initial pour signaler "prêt à recevoir configure"
|
||||
dlog("[client] xdg_toplevel créé, attente initial configure");
|
||||
|
||||
// 2. Attendre l'initial configure
|
||||
let start = std::time::Instant::now();
|
||||
while !state.configured && start.elapsed() < Duration::from_secs(5) {
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
if !state.configured {
|
||||
return Err("no initial configure received".into());
|
||||
}
|
||||
let serial = state.pending_serial.unwrap_or(0);
|
||||
dlog(&format!("[client] initial configure reçu, serial={serial}"));
|
||||
|
||||
// 3. Ack le configure
|
||||
xdg_surface.ack_configure(serial);
|
||||
state.pending_serial = None;
|
||||
dlog(&format!("[client] ack_configure({serial})"));
|
||||
|
||||
// 4. Préparer + attacher le buffer
|
||||
let fd = unsafe { create_shm_with_pattern("/redox-wl-client-shm-71") }?;
|
||||
let pool = shm.create_pool(fd.as_fd(), SIZE, &qh, ());
|
||||
let buffer = pool.create_buffer(
|
||||
0,
|
||||
W,
|
||||
H,
|
||||
STRIDE,
|
||||
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
surface.damage_buffer(0, 0, W, H);
|
||||
surface.commit();
|
||||
dlog("[client] surface attach + damage + commit envoyés");
|
||||
dlog("[client] buffer attach + damage + commit POST-ack");
|
||||
|
||||
event_queue.flush()?;
|
||||
let _ = event_queue.roundtrip(&mut state);
|
||||
|
||||
// Reste connecté ~25s pour qu'on puisse capturer
|
||||
// 5. Boucle vivante 25 secondes
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < Duration::from_secs(25) {
|
||||
let _ = event_queue.dispatch_pending(&mut state);
|
||||
|
|
@ -208,10 +312,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
|
||||
dlog("[client] done, exit propre");
|
||||
let _ = surface; // tag explicite pour rappeler que la surface vit jusqu'à la fin
|
||||
let _ = buffer;
|
||||
let _ = pool;
|
||||
dlog("[client] done, destroy propre");
|
||||
toplevel.destroy();
|
||||
xdg_surface.destroy();
|
||||
surface.destroy();
|
||||
let _ = event_queue.flush();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ description = "Wayland protocol frontend that maps wl_compositor/wl_shm/wl_surfa
|
|||
redox-wl-compositor-core = { path = "../redox-wl-compositor-core" }
|
||||
wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false }
|
||||
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
|
||||
wayland-protocols = { path = "../../../wayland-rs/wayland-protocols", default-features = false, features = ["server"] }
|
||||
libc = "0.2"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
202
docs/phase7-1-xdg-shell.md
Normal file
202
docs/phase7-1-xdg-shell.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Phase 7.1 — xdg-shell minimal
|
||||
|
||||
> Document produit le 2026-05-09 dans le cadre du plan directeur
|
||||
> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`.
|
||||
>
|
||||
> **Scope strict** validé par user :
|
||||
> - implémenter `xdg_wm_base`
|
||||
> - gérer `get_xdg_surface(wl_surface)`
|
||||
> - gérer `xdg_surface.get_toplevel`
|
||||
> - envoyer `configure` (xdg_toplevel + xdg_surface)
|
||||
> - recevoir `ack_configure`
|
||||
> - gérer `commit` après configure
|
||||
> - supporter `set_title` / `set_app_id` (stockés sans usage UI)
|
||||
> - destroy propre via `xdg_toplevel.destroy` / `xdg_surface.destroy`
|
||||
>
|
||||
> **Hors scope 7.1** : focus, cursor, move/resize, decorations,
|
||||
> multi-client avancé.
|
||||
|
||||
## Verdict
|
||||
|
||||
**✅ Cycle xdg-shell complet validé runtime.**
|
||||
|
||||
Capture preuve :  — fenêtre 320×240
|
||||
positionnée à `(60, 60)` par le compositor (cascade par défaut), avec
|
||||
bandes verticales arc-en-ciel + bordure noire 2px peintes par le client.
|
||||
|
||||
## Cycle de vie xdg-shell implémenté
|
||||
|
||||
```
|
||||
client compositor
|
||||
────── ──────────
|
||||
Connection::connect_to_env
|
||||
get_registry
|
||||
◄──── wl_compositor + wl_shm + xdg_wm_base
|
||||
advertised
|
||||
bind globals
|
||||
compositor.create_surface ────►
|
||||
SurfaceData { id: registry.create(),
|
||||
xdg_pending: false }
|
||||
wm_base.get_xdg_surface ────►
|
||||
XdgSurfaceData { wl_surface, serial: 0 }
|
||||
sd.xdg_pending_initial_configure = true
|
||||
xdg_surface.get_toplevel ────►
|
||||
XdgToplevelData::default()
|
||||
next_toplevel_index += 1
|
||||
surface.x = +60, surface.y = +60 (cascade)
|
||||
next_xdg_serial = 1
|
||||
◄──── xdg_toplevel.configure(640, 480, [])
|
||||
◄──── xdg_surface.configure(serial=1)
|
||||
────────────────────────────────────────
|
||||
toplevel.set_title(...) ────► data.title = Some(...)
|
||||
toplevel.set_app_id(...) ────► data.app_id = Some(...)
|
||||
xdg_surface.ack_configure(1)────► data.acked_serial = 1
|
||||
sd.xdg_pending_initial_configure = false
|
||||
shm.create_pool(fd, size) ────► ShmPool::new(fd) → mmap
|
||||
pool.create_buffer(...) ────► BufferData { pool, offset, w, h, ... }
|
||||
surface.attach(buffer) ────► sd.pending_buffer = Some(buf)
|
||||
surface.damage_buffer(...) ────► (no-op tracking 7.1)
|
||||
surface.commit ────►
|
||||
!xdg_pending → continue
|
||||
read shm pixels via mmap
|
||||
SurfaceBuffer::from_pixels(...)
|
||||
registry.modify_pending(id, |s|
|
||||
s.buffer = Some(sb); s.visible = true)
|
||||
registry.commit(id)
|
||||
registry.raise(id)
|
||||
|
||||
(toplevel.destroy) ────► no-op (wl_surface destroy fera le reste)
|
||||
(xdg_surface.destroy) ────► surface.visible = false; commit
|
||||
(surface.destroy) ────► registry.destroy(id)
|
||||
```
|
||||
|
||||
## Modifications apportées
|
||||
|
||||
### `redox-wl-wayland-frontend`
|
||||
|
||||
Ajout dep `wayland-protocols` (feature `server`).
|
||||
|
||||
Constants :
|
||||
- `XDG_WM_BASE_VERSION: u32 = 5`
|
||||
- `DEFAULT_TOPLEVEL_SIZE: (i32, i32) = (640, 480)`
|
||||
- `CASCADE_OFFSET: i32 = 60`
|
||||
|
||||
Nouveaux UserData :
|
||||
- `XdgSurfaceData { wl_surface, last_serial, acked_serial }`
|
||||
- `XdgToplevelData { title, app_id }`
|
||||
|
||||
Modif `SurfaceData` :
|
||||
- ajout `xdg_pending_initial_configure: Mutex<bool>` qui bloque l'affichage
|
||||
tant que pas ack-configure (sémantique xdg-shell standard)
|
||||
|
||||
Modif `WaylandFrontend` :
|
||||
- `next_xdg_serial: u32` (counter monotone pour les configure serials)
|
||||
- `next_toplevel_index: u32` (counter cascading)
|
||||
|
||||
Dispatch ajoutés :
|
||||
- `GlobalDispatch<XdgWmBase>` + `Dispatch<XdgWmBase>`
|
||||
- `Dispatch<XdgPositioner>` (no-op)
|
||||
- `Dispatch<XdgPopup>` (no-op)
|
||||
- `Dispatch<XdgSurface, Arc<XdgSurfaceData>>` :
|
||||
- `GetToplevel` → cascade position + envoie initial configure
|
||||
- `AckConfigure(serial)` → débloque affichage
|
||||
- `Destroy` → cache la surface
|
||||
- `Dispatch<XdgToplevel, Arc<XdgToplevelData>>` :
|
||||
- `SetTitle`/`SetAppId` → stocke
|
||||
- reste : ignoré (move, resize, set_*, etc.)
|
||||
|
||||
### `redox-wl-test-client-shm`
|
||||
|
||||
Ajout dep `wayland-protocols` (feature `client`).
|
||||
|
||||
Bind 3 globals : `wl_compositor`, `wl_shm`, `xdg_wm_base`.
|
||||
|
||||
Séquence runtime :
|
||||
1. `compositor.create_surface()`
|
||||
2. `wm_base.get_xdg_surface(&surface)`
|
||||
3. `xdg_surface.get_toplevel()`
|
||||
4. `toplevel.set_title("Phase 7.1 client")`
|
||||
5. `toplevel.set_app_id("redox.wl.test.client.shm")`
|
||||
6. `surface.commit()` — initial commit pour signaler "ready"
|
||||
7. Attente du `xdg_surface.configure(serial)` event (avec timeout 5s)
|
||||
8. `xdg_surface.ack_configure(serial)`
|
||||
9. `shm.create_pool(fd)` + `pool.create_buffer` + pattern ARGB
|
||||
10. `surface.attach(buffer)` + `damage_buffer` + `commit`
|
||||
11. Boucle 25s vivante
|
||||
12. `toplevel.destroy()` + `xdg_surface.destroy()` + `surface.destroy()`
|
||||
|
||||
Bonus : Dispatch sur `XdgWmBase::Event::Ping` répond `pong(serial)` (le
|
||||
compositor ne l'envoie pas en 7.1 mais c'est gratuit côté client).
|
||||
|
||||
## Preuve runtime
|
||||
|
||||
Logs serial QEMU complets :
|
||||
|
||||
```
|
||||
[client] connect to compositor
|
||||
[comp] CRTC pris
|
||||
[comp] Wayland socket : /tmp/redox-wl-comp.sock
|
||||
[client] globals : compositor=true shm=true xdg_wm_base=true
|
||||
[client] xdg_toplevel créé, attente initial configure
|
||||
[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 surfaces=1 elapsed=1.2s
|
||||
... (compositeur stable 18s avec 1 surface) ...
|
||||
[comp] tick=450 surfaces=1 elapsed=18.2s
|
||||
```
|
||||
|
||||
Capture frame T+14s : fenêtre client à `(60, 60)`, bandes RGB+orange+jaune+
|
||||
violet visibles avec bordure noire.
|
||||
|
||||
## Limitations connues (à traiter en sous-tickets ultérieurs)
|
||||
|
||||
- **Pas de focus** → toutes les surfaces sont "actives", pas de keyboard
|
||||
enter/leave events. Phase 7.4.
|
||||
- **Pas de cursor visible** côté écran. Phase 7.3.
|
||||
- **Pas de move/resize interactifs** (xdg_toplevel.Move/Resize ignorés).
|
||||
Phase 7.7.
|
||||
- **Window geometry ignoré** (utilisé pour les decorations CSD que nous
|
||||
ne supportons pas).
|
||||
- **Multi-client non testé** (1 seul client en 7.1). Phase 7.6.
|
||||
- **Pas de validation des serials** : si un client envoie un mauvais
|
||||
ack_configure, on accepte sans broncher. À durcir en phase 7.5
|
||||
(robustesse).
|
||||
|
||||
## Critère de fin 7.1
|
||||
|
||||
> Un client Wayland externe qui utilise xdg-shell peut créer une fenêtre
|
||||
> toplevel visible, recevoir son configure, ack, commit, puis afficher
|
||||
> via shm sans panic serveur.
|
||||
|
||||
**✅ Validé.** Test runtime 18+ secondes, surface visible, aucun panic
|
||||
serveur ni client, destroy propre à la fin.
|
||||
|
||||
## Code
|
||||
|
||||
```
|
||||
crates/redox-wl-wayland-frontend/ # +220 lignes pour xdg-shell (5 Dispatch + helpers)
|
||||
└── Cargo.toml # + wayland-protocols/server
|
||||
|
||||
crates/redox-wl-test-client-shm/ # adapté entièrement à xdg-shell
|
||||
└── Cargo.toml # + wayland-protocols/client
|
||||
```
|
||||
|
||||
Submodule `wayland-rs/wayland-protocols/protocols/` initialisé via
|
||||
`git submodule update --init --depth 1`.
|
||||
|
||||
## Suite phase 7.2
|
||||
|
||||
`wl_seat` + `wl_pointer` + `wl_keyboard` :
|
||||
- Routage des events `InputBackend` vers la surface focalisée
|
||||
- Décision XKB (recommandation : `xkeysym` Rust pur, layout US uniquement
|
||||
au début ; xkbcommon C reportable)
|
||||
- Premier client qui réagit aux events keyboard
|
||||
- Tester via QEMU `sendkey` avec un client qui log les events reçus
|
||||
|
||||
Estimé 2 sessions.
|
||||
|
||||
---
|
||||
|
||||
*Fin du document de phase 7.1.*
|
||||
BIN
docs/phase7-1-xdg-toplevel.png
Normal file
BIN
docs/phase7-1-xdg-toplevel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Loading…
Add table
Add a link
Reference in a new issue