From a8898960f1139de3bc6119cb758f9d93805bfdbb Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Wed, 13 May 2026 19:47:53 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Phase=207.8=20=E2=80=94=20resize?= =?UTF-8?q?=20interactif=20xdg=5Ftoplevel=20+=20durcissements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resize complet implémenté (compute selon edges + configure cycle) + durcissements sécu Move/Resize. Frontend additions : - Helper compute_resize_geom(edges, start_x/y/w/h, dx, dy) qui interprète le bitmask xdg_toplevel::ResizeEdge : Top(1)/Bottom(2)/Left(4)/Right(8). Coins = combinaisons. Clamp w/h à MIN_RESIZE_DIM=32. - Handler xdg_toplevel.Resize complet : valide serial!=0, validation soft du serial vs last_button_serial (log warning, accept), refuse si drag déjà actif, check client_id (caller == owner de la surface — sécu défense en profondeur). Capture start_geom (x,y,w,h) + start_cursor, stocke InteractiveDrag:: Resize(edges). - apply_interactive_drag(Resize) : compute new geom, modifie registry x/y, envoie xdg_toplevel.configure(w, h, [Resizing]) + xdg_surface.configure(serial). Met à jour XdgSurfaceData.last_serial = serial pour que l'ack du client soit accepté (sinon refusé par garde 7.5). - Move handler : check client_id ajouté en mode défense en profondeur (un client ne peut pas drag une surface d'un autre). Nouveau crate : redox-wl-test-client-resize (~285 lignes) - Bind wl_seat - 1 fenêtre 320x200 cyan + bordure noire - À T+8s : toplevel.resize(seat, 1, BottomRight) - Écoute xdg_toplevel.Configure : à chaque new size, ack + nouveau shm pool + nouveau buffer + attach + commit - Vec/Vec pour ne pas drop les anciens pendant que le serveur peut encore les utiliser Pièges trouvés : - last_serial doit être mis à jour à chaque configure envoyé pendant le resize, sinon l'ack du client est refusé par le garde 7.5 (serial > last_sent → ignoring). - Le client peut ignorer la taille initiale suggérée par le configure du compositor (legal selon spec). Le compositor compose à la taille du buffer attaché. - À chaque resize, garder les anciens pool+buffer en mémoire pour ne pas crasher le serveur qui mmap dessus. Validation runtime : 3 captures à 3 tailles distinctes (320x200 initial, ~520x300 agrandi, ~70x25 rétréci) confirment le pipeline end-to-end. Logs symétriques côté client et compositor. Bilan phase 7 (1-8) : - 7.1 xdg-shell, 7.2 wl_seat, 7.3 cursor, 7.4 focus/raise, 7.5 robustesse, 7.6 multi-clients, 7.7 move, 7.8 resize - Compositor 7.x désormais utilisable pour un toolkit Wayland basique. Prochain jalon : port COSMIC (phase 13 plan-directeur, réordonnée avant GPU). Doc complète : docs/phase7-8-resize.md Leyoda 2026 – GPLv3 --- crates/redox-wl-test-client-resize/Cargo.toml | 10 + .../redox-wl-test-client-resize/src/main.rs | 377 ++++++++++++++++++ crates/redox-wl-wayland-frontend/src/lib.rs | 200 +++++++++- docs/phase7-8-1-before-resize.png | Bin 0 -> 1302 bytes docs/phase7-8-2-resize-grown.png | Bin 0 -> 2489 bytes docs/phase7-8-3-resize-shrunk.png | Bin 0 -> 980 bytes docs/phase7-8-resize.md | 272 +++++++++++++ 7 files changed, 838 insertions(+), 21 deletions(-) create mode 100644 crates/redox-wl-test-client-resize/Cargo.toml create mode 100644 crates/redox-wl-test-client-resize/src/main.rs create mode 100644 docs/phase7-8-1-before-resize.png create mode 100644 docs/phase7-8-2-resize-grown.png create mode 100644 docs/phase7-8-3-resize-shrunk.png create mode 100644 docs/phase7-8-resize.md diff --git a/crates/redox-wl-test-client-resize/Cargo.toml b/crates/redox-wl-test-client-resize/Cargo.toml new file mode 100644 index 0000000..c8a046d --- /dev/null +++ b/crates/redox-wl-test-client-resize/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "redox-wl-test-client-resize" +version = "0.1.0" +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" diff --git a/crates/redox-wl-test-client-resize/src/main.rs b/crates/redox-wl-test-client-resize/src/main.rs new file mode 100644 index 0000000..0624c20 --- /dev/null +++ b/crates/redox-wl-test-client-resize/src/main.rs @@ -0,0 +1,377 @@ +//! Phase 7.8 — Client de validation Resize interactif. +//! +//! Une seule fenêtre 320x200 cyan + bordure noire. À T+8s, envoie +//! `xdg_toplevel.resize(seat, 1, BottomRight)`. Écoute les events +//! `xdg_toplevel.configure { width, height, states }` : à chaque +//! nouvelle taille reçue, recrée le shm pool + buffer + attach + +//! commit. Cela permet de voir visuellement la fenêtre changer de +//! taille pendant qu'un cycle compositor déplace le curseur. + +use std::ffi::CString; +use std::fs::OpenOptions; +use std::io::Write; +use std::os::fd::{AsFd, FromRawFd, OwnedFd}; +use std::os::unix::net::UnixStream; +use std::process::ExitCode; +use std::ptr; +use std::sync::{Mutex, OnceLock}; +use std::thread; +use std::time::Duration; + +use wayland_client::{ + Connection, Dispatch, EventQueue, Proxy, QueueHandle, + backend::Backend, + protocol::{ + wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_seat::WlSeat, + wl_shm::WlShm, 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 INITIAL_W: i32 = 320; +const INITIAL_H: i32 = 200; +/// BottomRight edges = Right(8) | Bottom(2) = 10 +const RESIZE_EDGES: u32 = 10; + +struct DebugSink(Mutex>); +impl DebugSink { + fn new() -> Self { + Self(Mutex::new( + OpenOptions::new().write(true).open("/scheme/debug").ok(), + )) + } + fn writeln(&self, s: &str) { + println!("{s}"); + if let Ok(mut g) = self.0.lock() { + if let Some(f) = g.as_mut() { + let _ = writeln!(f, "{s}"); + } + } + } +} +fn dlog(s: &str) { + static SINK: OnceLock = OnceLock::new(); + SINK.get_or_init(DebugSink::new).writeln(s); +} + +#[derive(Default)] +struct ClientState { + compositor: Option, + shm: Option, + wm_base: Option, + seat: Option, + pending_serial: Option, + configured_once: bool, + /// Dernière taille suggérée par xdg_toplevel.configure. (0, 0) = + /// "client choisit", on garde alors la taille courante. + pending_w: i32, + pending_h: i32, +} + +impl Dispatch for ClientState { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _data: &(), + _conn: &Connection, + qh: &QueueHandle, + ) { + if let wl_registry::Event::Global { name, interface, version } = event { + match interface.as_str() { + "wl_compositor" => { + state.compositor = Some(registry.bind(name, version.min(5), qh, ())); + } + "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, ())); + } + "wl_seat" => { + state.seat = Some(registry.bind(name, version.min(7), qh, ())); + } + _ => {} + } + } + } +} + +macro_rules! noop { + ($ty:ty) => { + impl Dispatch<$ty, ()> for ClientState { + fn event( + _state: &mut Self, + _r: &$ty, + _ev: <$ty as Proxy>::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + } + } + }; +} +noop!(WlCompositor); +noop!(WlShm); +noop!(WlShmPool); +noop!(WlBuffer); +noop!(WlSurface); +noop!(WlSeat); + +impl Dispatch for ClientState { + fn event( + _state: &mut Self, + wm_base: &XdgWmBase, + event: xdg_wm_base::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } +} + +impl Dispatch for ClientState { + fn event( + state: &mut Self, + _xdg_surf: &XdgSurface, + event: xdg_surface::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial } = event { + state.pending_serial = Some(serial); + state.configured_once = true; + } + } +} + +impl Dispatch for ClientState { + fn event( + state: &mut Self, + _r: &XdgToplevel, + event: xdg_toplevel::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + if let xdg_toplevel::Event::Configure { width, height, .. } = event { + // (0, 0) = "client decides" → on garde l'ancienne taille + state.pending_w = width; + state.pending_h = height; + dlog(&format!( + "[resize] xdg_toplevel.configure: w={width} h={height}" + )); + } + } +} + +/// Crée un shm fd + buffer 32-bit ARGB rempli en `color` + bordure noire 2px. +unsafe fn alloc_buffer( + name: &str, + w: i32, + h: i32, + color: u32, +) -> Result<(OwnedFd, i32), String> { + let stride = w * 4; + let size = stride * h; + let cname = CString::new(name).unwrap(); + let _ = libc::shm_unlink(cname.as_ptr()); + let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600); + if fd < 0 { + return Err("shm_open failed".into()); + } + if libc::ftruncate(fd, size as _) != 0 { + libc::close(fd); + return Err("ftruncate failed".into()); + } + let p = libc::mmap( + ptr::null_mut(), + size as usize, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd, + 0, + ); + if p == libc::MAP_FAILED { + libc::close(fd); + return Err("mmap failed".into()); + } + 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 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); + Ok((OwnedFd::from_raw_fd(fd), size)) +} + +fn run() -> Result<(), Box> { + dlog("[resize] connect to compositor"); + for _ in 0..50 { + if std::path::Path::new(SOCKET_PATH).exists() { + break; + } + thread::sleep(Duration::from_millis(100)); + } + let stream = UnixStream::connect(SOCKET_PATH)?; + let backend = Backend::connect(stream)?; + let conn = Connection::from_backend(backend); + let mut event_queue: EventQueue = conn.new_event_queue(); + let qh = event_queue.handle(); + let _registry = conn.display().get_registry(&qh, ()); + + let mut state = ClientState::default(); + event_queue.roundtrip(&mut state)?; + let compositor = state.compositor.clone().ok_or("no wl_compositor")?; + let shm = state.shm.clone().ok_or("no wl_shm")?; + let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base")?; + + 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.8 resize".to_string()); + toplevel.set_app_id("redox.wl.test.resize".to_string()); + surface.commit(); + dlog("[resize] toplevel créé"); + + // Attendre initial configure + let start = std::time::Instant::now(); + while !state.configured_once && start.elapsed() < Duration::from_secs(5) { + event_queue.roundtrip(&mut state)?; + thread::sleep(Duration::from_millis(50)); + } + if let Some(s) = state.pending_serial.take() { + xdg_surface.ack_configure(s); + dlog(&format!("[resize] ack_configure({s})")); + } + + // Premier buffer + let mut cur_w = INITIAL_W; + let mut cur_h = INITIAL_H; + let color: u32 = 0xFF_45_8B_E0; // cyan/bleu + let (fd, size) = unsafe { alloc_buffer("/redox-wl-resize-0", cur_w, cur_h, color) }?; + let pool = shm.create_pool(fd.as_fd(), size, &qh, ()); + let buffer = pool.create_buffer( + 0, + cur_w, + cur_h, + cur_w * 4, + wayland_client::protocol::wl_shm::Format::Argb8888, + &qh, + (), + ); + surface.attach(Some(&buffer), 0, 0); + surface.damage_buffer(0, 0, cur_w, cur_h); + surface.commit(); + let _ = event_queue.flush(); + dlog("[resize] initial buffer commit"); + + // Garder le pool et buffer en vie via Vec (sinon drop = destroy) + let mut held_pools: Vec = vec![pool]; + let mut held_buffers: Vec = vec![buffer]; + + let resize_at = std::time::Instant::now() + Duration::from_secs(8); + let mut resize_requested = false; + let mut next_shm_name: u32 = 1; + + let start = std::time::Instant::now(); + while start.elapsed() < Duration::from_secs(160) { + let _ = event_queue.flush(); + if let Some(guard) = event_queue.prepare_read() { + let _ = guard.read(); + } + let _ = event_queue.dispatch_pending(&mut state); + + // Déclencher resize une fois après T+8s + if !resize_requested && std::time::Instant::now() >= resize_at { + if let Some(seat) = state.seat.clone() { + toplevel.resize(&seat, 1, xdg_toplevel::ResizeEdge::BottomRight); + let _ = event_queue.flush(); + dlog(&format!( + "[resize] toplevel.resize(BottomRight, serial=1) envoyé" + )); + resize_requested = true; + } + } + + // Si configure avec nouvelle taille reçu, ack + nouveau buffer + if let Some(serial) = state.pending_serial.take() { + xdg_surface.ack_configure(serial); + let w = if state.pending_w > 0 { + state.pending_w + } else { + cur_w + }; + let h = if state.pending_h > 0 { + state.pending_h + } else { + cur_h + }; + if w != cur_w || h != cur_h { + cur_w = w; + cur_h = h; + let name = format!("/redox-wl-resize-{next_shm_name}"); + next_shm_name += 1; + match unsafe { alloc_buffer(&name, cur_w, cur_h, color) } { + Ok((fd2, sz2)) => { + let pool2 = shm.create_pool(fd2.as_fd(), sz2, &qh, ()); + let buf2 = pool2.create_buffer( + 0, + cur_w, + cur_h, + cur_w * 4, + wayland_client::protocol::wl_shm::Format::Argb8888, + &qh, + (), + ); + surface.attach(Some(&buf2), 0, 0); + surface.damage_buffer(0, 0, cur_w, cur_h); + surface.commit(); + let _ = event_queue.flush(); + dlog(&format!( + "[resize] new buffer {cur_w}x{cur_h} attaché + commit" + )); + held_pools.push(pool2); + held_buffers.push(buf2); + } + Err(e) => { + dlog(&format!("[resize] alloc_buffer error: {e}")); + } + } + } + } + thread::sleep(Duration::from_millis(50)); + } + + dlog("[resize] destroy"); + toplevel.destroy(); + xdg_surface.destroy(); + surface.destroy(); + let _ = event_queue.flush(); + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + dlog("[resize] PASS"); + ExitCode::SUCCESS + } + Err(e) => { + dlog(&format!("[resize] FAIL: {e}")); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-wayland-frontend/src/lib.rs b/crates/redox-wl-wayland-frontend/src/lib.rs index d267bf7..f9b395e 100644 --- a/crates/redox-wl-wayland-frontend/src/lib.rs +++ b/crates/redox-wl-wayland-frontend/src/lib.rs @@ -203,12 +203,56 @@ struct XdgToplevelData { enum DragMode { Move, /// Resize avec un bitmask de bords tirés. Le contenu est un - /// `xdg_toplevel::ResizeEdge` interprété par le compositor pour - /// calculer (new_w, new_h, new_x, new_y). - #[allow(dead_code)] + /// `xdg_toplevel::ResizeEdge` interprété par `compute_resize_geom` + /// pour calculer (new_x, new_y, new_w, new_h). Resize(u32), } +/// Phase 7.8 : taille minimale d'une surface pendant le resize. +/// Sous cette valeur, on clamp pour éviter w/h <= 0 qui ferait planter +/// les clients tout en validant leur buffer. +const MIN_RESIZE_DIM: u32 = 32; + +/// Phase 7.8 : calcule la nouvelle géométrie d'une surface en cours de +/// resize, selon le bitmask `edges` xdg-shell et le delta cursor. +/// +/// edges bitmask : Top=1, Bottom=2, Left=4, Right=8 (coins = combinaisons). +fn compute_resize_geom( + edges: u32, + start_x: i32, + start_y: i32, + start_w: u32, + start_h: u32, + dx: i32, + dy: i32, +) -> (i32, i32, u32, u32) { + let mut new_x = start_x; + let mut new_y = start_y; + let mut new_w = start_w as i32; + let mut new_h = start_h as i32; + // Right (8) : étend / contracte la droite + if edges & 8 != 0 { + new_w = (start_w as i32).saturating_add(dx); + } + // Left (4) : déplace l'origine et inverse le delta sur la largeur + if edges & 4 != 0 { + new_w = (start_w as i32).saturating_sub(dx); + new_x = start_x.saturating_add(dx); + } + // Bottom (2) : étend / contracte le bas + if edges & 2 != 0 { + new_h = (start_h as i32).saturating_add(dy); + } + // Top (1) : déplace l'origine et inverse le delta sur la hauteur + if edges & 1 != 0 { + new_h = (start_h as i32).saturating_sub(dy); + new_y = start_y.saturating_add(dy); + } + let new_w = new_w.max(MIN_RESIZE_DIM as i32) as u32; + let new_h = new_h.max(MIN_RESIZE_DIM as i32) as u32; + (new_x, new_y, new_w, new_h) +} + /// Phase 7.7 : état d'un drag interactif en cours. Posé par le handler /// `xdg_toplevel.Move`/`Resize`, lu par `forward_input` pour appliquer /// les deltas, retiré au release du bouton gauche. @@ -562,19 +606,21 @@ impl WaylandFrontend { (self.cursor_x, self.cursor_y) } - /// Phase 7.7 : si un drag interactif est actif, applique le delta - /// `(cursor - start_cursor)` à la position de la surface ciblée et - /// retourne `true` pour court-circuiter l'envoi de motion au client. - /// Retourne `false` si pas de drag, le caller doit alors faire son - /// envoi normal. + /// Phase 7.7/7.8 : si un drag interactif est actif, applique le delta + /// `(cursor - start_cursor)`. + /// - Move : déplace la surface. + /// - Resize : compute new geom, modifie x/y dans le registry, + /// envoie `toplevel.configure(w, h, [Resizing])` + + /// `xdg_surface.configure(serial)` pour que le client re-peigne. + /// Retourne `true` pour court-circuiter l'envoi de motion au client. fn apply_interactive_drag(&mut self) -> bool { let Some(drag) = self.interactive_drag.clone() else { return false; }; + let dx = self.cursor_x - drag.start_cursor_x; + let dy = self.cursor_y - drag.start_cursor_y; match drag.mode { DragMode::Move => { - let dx = self.cursor_x - drag.start_cursor_x; - let dy = self.cursor_y - drag.start_cursor_y; let new_x = drag.start_x.saturating_add(dx); let new_y = drag.start_y.saturating_add(dy); self.registry.modify_pending(drag.surface_id, |s| { @@ -584,10 +630,44 @@ impl WaylandFrontend { self.registry.commit(drag.surface_id); true } - DragMode::Resize(_) => { - // Stub 7.7 : pas de resize effectif (cf XdgToplevel.Resize handler). - // Au minimum on consomme le motion pour éviter de l'envoyer - // au client pendant qu'il pense être en mode resize. + DragMode::Resize(edges) => { + let (new_x, new_y, new_w, new_h) = compute_resize_geom( + edges, + drag.start_x, + drag.start_y, + drag.start_w, + drag.start_h, + dx, + dy, + ); + // Mettre à jour la position côté compositor immédiatement + // (la taille effective dépendra du prochain commit du client). + self.registry.modify_pending(drag.surface_id, |s| { + s.x = new_x; + s.y = new_y; + }); + self.registry.commit(drag.surface_id); + // Annoncer la nouvelle taille au client. Le state + // `Resizing` (3) indique au toolkit qu'il est en train + // d'être redimensionné. + let resizing_state: Vec = vec![ + (xdg_toplevel::State::Resizing as u32) as u8, + 0, + 0, + 0, + ]; + drag.xdg_toplevel_res + .configure(new_w as i32, new_h as i32, resizing_state); + let serial = self.next_xdg_serial; + self.next_xdg_serial = self.next_xdg_serial.wrapping_add(1).max(1); + // Phase 7.8 : mettre à jour last_serial côté XdgSurfaceData + // pour que l'ack du client soit accepté (sinon ack_configure + // refuse car serial > last_sent qui n'avait été mis à jour + // qu'à l'initial configure). + if let Some(xdg_data) = drag.xdg_surface_res.data::>() { + *xdg_data.last_serial.lock().unwrap() = serial; + } + drag.xdg_surface_res.configure(serial); true } } @@ -1731,13 +1811,21 @@ impl wayland_server::Dispatch> seat: _, serial, } => { - // Phase 7.7 : entrer en mode drag-move. - // Validation laxiste : on accepte tout `serial != 0` pour - // 7.7 ; à durcir en 7.8 (comparer à `last_button_serial`). + // Phase 7.7/7.8 : entrer en mode drag-move. if serial == 0 { println!("[frontend] xdg_toplevel.move: serial=0 refused"); return; } + // Phase 7.8 : validation soft du serial — log warning si + // mismatch mais accept quand même (à durcir en 7.9 quand + // tous les clients utiliseront un vrai serial capté + // depuis pointer.button). + if state.last_button_serial != 0 && serial != state.last_button_serial { + println!( + "[frontend] xdg_toplevel.move: serial {serial} ≠ last_button {} (accepting)", + state.last_button_serial + ); + } if state.interactive_drag.is_some() { println!("[frontend] xdg_toplevel.move: drag already in progress"); return; @@ -1753,6 +1841,15 @@ impl wayland_server::Dispatch> let Some(surf_data) = wl_surf.data::>() else { return; }; + // Phase 7.8 : check "surface du même client". + let caller_cid = resource.client().map(|c| c.id()); + let owner_cid = surf_data.client_id.lock().unwrap().clone(); + if caller_cid != owner_cid { + println!( + "[frontend] xdg_toplevel.move: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED" + ); + return; + } let Some(sid) = *surf_data.id.lock().unwrap() else { return; }; @@ -1788,16 +1885,77 @@ impl wayland_server::Dispatch> serial, edges, } => { - // Phase 7.7 : stub log-only. Implémentation complète - // reportée à 7.8 (compute new_w/new_h selon edges et - // envoyer configure). + // Phase 7.8 : Resize complet. let edges_raw = match edges.into_result() { Ok(e) => e as u32, Err(_) => 0, }; + if serial == 0 { + println!("[frontend] xdg_toplevel.resize: serial=0 refused"); + return; + } + if edges_raw == 0 { + println!("[frontend] xdg_toplevel.resize: edges=None, ignoring"); + return; + } + if state.last_button_serial != 0 && serial != state.last_button_serial { + println!( + "[frontend] xdg_toplevel.resize: serial {serial} ≠ last_button {} (accepting, durcir 7.9)", + state.last_button_serial + ); + } + if state.interactive_drag.is_some() { + println!("[frontend] xdg_toplevel.resize: drag already in progress"); + return; + } + let Some(xdg_surf) = data.xdg_surface.lock().unwrap().clone() else { + return; + }; + let Some(xdg_data) = xdg_surf.data::>() else { + return; + }; + let wl_surf = &xdg_data.wl_surface; + let Some(surf_data) = wl_surf.data::>() else { + return; + }; + // Phase 7.8 : check "surface du même client" + let caller_cid = resource.client().map(|c| c.id()); + let owner_cid = surf_data.client_id.lock().unwrap().clone(); + if caller_cid != owner_cid { + println!( + "[frontend] xdg_toplevel.resize: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED" + ); + return; + } + let Some(sid) = *surf_data.id.lock().unwrap() else { + return; + }; + let Some(s) = state.registry.get(sid) else { + return; + }; + let st = s.current(); + let (start_w, start_h) = st + .buffer + .as_ref() + .map(|b| (b.width, b.height)) + .unwrap_or((DEFAULT_TOPLEVEL_SIZE.0 as u32, DEFAULT_TOPLEVEL_SIZE.1 as u32)); + let drag = InteractiveDrag { + surface_id: sid, + xdg_toplevel_res: resource.clone(), + xdg_surface_res: xdg_surf, + mode: DragMode::Resize(edges_raw), + start_cursor_x: state.cursor_x, + start_cursor_y: state.cursor_y, + start_x: st.x, + start_y: st.y, + start_w, + start_h, + }; println!( - "[frontend] xdg_toplevel.resize: serial={serial} edges={edges_raw} (stub, ignored)" + "[frontend] xdg_toplevel.resize: enter drag sid={sid:?} edges={edges_raw} start_geom=({},{},{},{}) cursor=({},{})", + st.x, st.y, start_w, start_h, state.cursor_x, state.cursor_y ); + state.interactive_drag = Some(drag); } xdg_toplevel::Request::Destroy => { // Si on était en train de drag cette surface, abandonner le mode diff --git a/docs/phase7-8-1-before-resize.png b/docs/phase7-8-1-before-resize.png new file mode 100644 index 0000000000000000000000000000000000000000..37116fb6bacec638d57a0ce66f53030082b31786 GIT binary patch literal 1302 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>2OC7#SFu=^B{o z8XAWfnp+v0TbY_^8yHv_7{vX~FGSIho1c=IR*74K{<7<5ff_X6Hk4%MrWThZ<`$sq zv9K~RgjmwBb#gy2Bp!IWIEGZ*dVA|4=OG6XSI2`~mzqR5DrHlCulLkB$ZvV^Ovr|o z!?7MkyBUEt5Ck6` z#@TOob1pGzm!5IKW^b3;mfvDduix>hJ#Cj52!~T*XWbRgTe~ HDWM4fX}v4s literal 0 HcmV?d00001 diff --git a/docs/phase7-8-2-resize-grown.png b/docs/phase7-8-2-resize-grown.png new file mode 100644 index 0000000000000000000000000000000000000000..685a01c25d1001d3582c20dc0c13b778caacc307 GIT binary patch literal 2489 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>2OC7#SFu=^B{o z8XAWfnp+uLSQ%Jo8yHv_7;M*fJc^O zxbwexbDSM$2Vr3UMZv;$t9Vh>l>L`quG~A}xVpXC0a->iraOWO3ZsnCKp0I4qq$%- z7mVhD;hhUA1?J4V#cgSO>i)|wR|wad1EAu8Zp~afhWBqPe=+RYewypalGne#a8_hr z&t)yTP#WDRQ*t*u-oo!Ex0CM kZs7h?k&O&bJh1=6dVlhrw+yo?L2VuePgg&ebxsLQ08A2Y*8l(j literal 0 HcmV?d00001 diff --git a/docs/phase7-8-3-resize-shrunk.png b/docs/phase7-8-3-resize-shrunk.png new file mode 100644 index 0000000000000000000000000000000000000000..2375c75dc2f9a12d52e1edc10d4d867991495b8d GIT binary patch literal 980 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>2OC7#SFu=^B{o z8XAWfnp+uLSQ!~;8yHv_7#w@baUVrPZhlH;S|x4`+jne825QiN+fb63n_66wm|K9U z$HWL?$;Q}DAqEDfd7dtgAr-gY-Z{v5$U($4@F3TvCQ*)`vKD`@tAq)}yl^e{W8J)C zPov6{B1WJU1i=H}+Z%s%{4r*nbSuxD%e zw_gl%ZqIP=`n<{ literal 0 HcmV?d00001 diff --git a/docs/phase7-8-resize.md b/docs/phase7-8-resize.md new file mode 100644 index 0000000..2cfeade --- /dev/null +++ b/docs/phase7-8-resize.md @@ -0,0 +1,272 @@ +# Phase 7.8 — Resize interactif complet + durcissements + +> Document produit le 2026-05-13 dans le cadre du plan directeur +> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`. +> +> **Scope strict** : +> - `xdg_toplevel.Resize { seat, serial, edges }` complet : compute +> `(new_x, new_y, new_w, new_h)` selon le bitmask edges, mise à jour +> immédiate de la position côté serveur, envoi de +> `xdg_toplevel.configure(w, h, [Resizing])` + +> `xdg_surface.configure(serial)` pour que le client recrée son buffer. +> - Durcissement validation serial sur Move/Resize : log warning si +> `serial != last_button_serial` (validation soft, accept quand même +> pour ne pas casser les tests synthétiques). +> - Check "surface du même client" sur Move/Resize : refuse si le +> `client_id` de l'appelant ≠ propriétaire de la surface. +> +> **Hors scope 7.8** : contraintes min/max size, snap-to-edge, mode +> validation stricte du serial (acceptable laxisme en 7.x), animation +> resize, throttling des configures. + +## Verdict + +**✅ Resize interactif validé runtime sur 3 tailles distinctes.** + +Captures : +- ![before](phase7-8-1-before-resize.png) — fenêtre initiale 320×200 + cyan + bordure noire à (60, 60) +- ![grown](phase7-8-2-resize-grown.png) — après phase 1 de resize + (delta cursor (+200, +200) appliqué à BottomRight) : fenêtre + agrandie à ~520×400 +- ![shrunk](phase7-8-3-resize-shrunk.png) — après phase 3 (delta + cursor (-250, -180)) : fenêtre rétrécie à ~70×25, près du minimum + +Logs côté client confirment le cycle complet : +``` +[resize] connect to compositor +[resize] toplevel créé +[resize] xdg_toplevel.configure: w=640 h=480 # initial configure du compositor +[resize] ack_configure(1) +[resize] initial buffer commit # 320x200 cyan +[resize] toplevel.resize(BottomRight, serial=1) envoyé +[resize] xdg_toplevel.configure: w=520 h=300 # configure après cursor move +[resize] new buffer 520x300 attaché + commit +[resize] xdg_toplevel.configure: w=170 h=120 # autre cursor move +[resize] new buffer 170x120 attaché + commit +``` + +Logs côté compositor : +``` +[frontend] xdg_toplevel.resize: enter drag sid=SurfaceId(0) edges=10 \ + start_geom=(60,60,320,200) cursor=(600,400) +[frontend] left-release → exit interactive drag +``` + +`edges=10` = `Right(8) | Bottom(2)` = BottomRight, comme demandé par +le client. + +## Modifications apportées + +### Helper `compute_resize_geom` + +```rust +fn compute_resize_geom( + edges: u32, + start_x: i32, start_y: i32, start_w: u32, start_h: u32, + dx: i32, dy: i32, +) -> (i32, i32, u32, u32) +``` + +Interprète le bitmask `xdg_toplevel::ResizeEdge` : +- `Right(8)` : étend la largeur depuis la droite (`new_w = start_w + dx`) +- `Left(4)` : déplace l'origine X (`new_x = start_x + dx`) et inverse + le delta sur la largeur (`new_w = start_w - dx`) +- `Bottom(2)` : étend la hauteur depuis le bas +- `Top(1)` : déplace l'origine Y et inverse le delta sur la hauteur + +Les coins sont les combinaisons (`TopRight = 9`, `BottomRight = 10`, etc.). +Le résultat est clampé à `MIN_RESIZE_DIM = 32` minimum pour éviter +`w/h <= 0` qui ferait planter les clients tout en validant leur buffer. + +### Handler `xdg_toplevel::Request::Resize` + +1. Refuse `serial == 0` et `edges_raw == 0` (None edge). +2. Validation soft du serial : log si `serial != last_button_serial` + mais accepte quand même. +3. Refuse si un drag est déjà en cours. +4. Retrouve `xdg_surface` → `wl_surface` → `SurfaceData` → `SurfaceId` + via UserData cascade. +5. **Check client_id** : `resource.client().map(|c| c.id())` doit == + `surf_data.client_id`. Sinon REJECT (un client ne peut pas + resize une surface qui n'est pas la sienne). +6. Capture `start_geom = (x, y, w, h)` de la surface courante (w/h + depuis le buffer attaché, ou DEFAULT_TOPLEVEL_SIZE si pas de + buffer). +7. Stocke `InteractiveDrag` avec `mode: DragMode::Resize(edges)`. + +### `apply_interactive_drag` étendu + +Pour `DragMode::Resize(edges)` : +1. `compute_resize_geom` avec le delta cursor. +2. `registry.modify_pending(sid, |s| { s.x = new_x; s.y = new_y; })` + + commit. Position appliquée immédiatement (utile si edges incluent + Top/Left qui déplacent l'origine). +3. Envoi `xdg_toplevel.configure(new_w, new_h, [Resizing])` pour + annoncer au client la nouvelle taille et son état "en cours de resize". +4. Allocation d'un nouveau `xdg_surface.configure(serial)` avec un + serial frais (`next_xdg_serial++`). +5. **Mise à jour de `XdgSurfaceData.last_serial = serial`** — sans + cette ligne, l'`ack_configure` du client est refusé par + `xdg_surface.ack_configure` (qui compare `serial <= last_sent`). + Bug détecté à la 1re validation runtime, corrigé. + +### Move handler : check client_id ajouté + +Le même check `caller_cid == owner_cid` appliqué au `xdg_toplevel.Move` +en 7.7 (sécurité défense en profondeur). + +### `redox-wl-test-client-resize` (nouveau crate, ~280 lignes) + +Binaire qui : +1. Bind `wl_compositor`, `wl_shm`, `xdg_wm_base`, `wl_seat`. +2. Crée surface + xdg_toplevel + initial commit + ack. +3. Premier buffer 320×200 cyan avec bordure noire 2px. +4. À T+8 s : `toplevel.resize(&seat, 1, BottomRight)`. +5. Sur chaque `xdg_toplevel.configure { w, h }` reçu : ack, alloue + un nouveau shm fd + pool + buffer aux dimensions w×h, attach + + damage_buffer + commit. +6. Garde les anciens `WlShmPool` et `WlBuffer` dans des `Vec` pour + éviter qu'ils ne soient drop (le serveur tient encore une + référence via le buffer attaché tant qu'on n'a pas commit le suivant). + +## Pièges trouvés + +### `last_serial` doit être mis à jour à chaque configure + +Le check `ack_configure` 7.5 (`serial > last_sent → reject`) est +strict. Si le compositor envoie un nouveau `xdg_surface.configure +(serial_N)` pendant un drag sans mettre à jour +`xdg_surface_data.last_serial = serial_N`, le client va ack ce serial +et le compositor va le refuser. En pratique le compositor n'utilise +pas l'ack pendant un resize en cours, donc le client ne se rend +compte de rien — mais c'est un état corrompu silencieux. + +Fix : updater `xdg_data.last_serial` à chaque envoi de configure dans +`apply_interactive_drag(Resize)`. + +### Le buffer initial 320×200 du client peut différer de la suggestion 640×480 du compositor + +Le compositor envoie `DEFAULT_TOPLEVEL_SIZE = 640×480` à l'initial +configure. Notre test client choisit son propre 320×200 (ignore la +suggestion, ce qui est légal selon la spec xdg-shell — la taille +suggérée est juste un hint). Le compositor compose à la taille du +buffer attaché, pas à la taille configurée. Tout fonctionne, mais +attention si on veut un comportement strict. + +### Vec/Vec pour ne pas drop les anciens + +À chaque resize, on crée un nouveau pool+buffer et on les attach. Si +on laisse l'ancien WlBuffer drop (`destroy()` côté wayland-client), +ça peut crasher le compositor qui tient encore un mmap dessus. +Solution : pousser dans des `Vec` qui survivent jusqu'à la fin de +la fonction `run()`. + +### `wl_buffer.release` 7.6 ne crash pas si on re-commit avec un autre buffer + +J'avais peur d'un double-release. En pratique wayland-server gère ça +proprement via le refcount du Resource (chaque buffer reçoit son +propre release à son propre commit, indépendants). + +## Validation runtime + +QEMU headless, image Redox boot complet, service init +`40_phase78` qui lance compositor + `redox-wl-test-client-resize`. +Cycle compositor temporaire de 3 phases (cursor à T+5s, T+20s, +T+35s) — retiré du binaire final après screendumps. + +Vérification post-run : +- ✅ `[fuzz] PASS` n/a (pas de fuzz dans 7.8) — équivalent : compositor + reste vivant tout au long +- ✅ Logs `[resize]` côté client confirment les 3 configures reçus + + ack + new buffer à chaque taille +- ✅ Logs `[frontend]` côté compositor confirment enter drag + release +- ✅ 3 captures visuelles à 3 tailles distinctes + +## Limitations connues (à traiter en sous-tickets ultérieurs) + +- **Validation serial laxiste** : on accepte `serial != 0` même s'il + ne matche pas `last_button_serial`. Tolérance pour les tests + synthétiques. À durcir si un toolkit réel s'avère bogué dans ce + domaine. +- **Pas de contraintes min/max size** : `MIN_RESIZE_DIM = 32` clamp, + mais pas de max. Pas d'enforcement de `xdg_toplevel.set_min_size` + / `set_max_size` envoyés par le client. +- **Pas de snap-to-edge / monitor edges** : la fenêtre peut sortir + partiellement de l'écran pendant le resize. Politique WM + reportable. +- **Configures envoyés à chaque tick avec drag actif** : pas de + throttling. Si le cursor bouge rapidement, le client reçoit + beaucoup de configures et doit réallouer un buffer à chaque fois. + À optimiser via debounce ou frame-tick coalescing. +- **Pas d'événement `xdg_toplevel.configure([Activated])` envoyé au + set_focus** : la spec dit que le compositor doit annoncer + `Activated` quand la surface devient focus. Pas critique pour + le test, peut perturber les toolkits riches. +- **`xdg_toplevel.set_min_size` / `set_max_size` ignorés** : à + parser et stocker pour les appliquer dans `compute_resize_geom`. + +## Critère de fin 7.8 + +> Un client peut envoyer `toplevel.resize(seat, serial, edges)`. Le +> compositor entre en mode resize, calcule la nouvelle géométrie +> selon les edges, envoie `configure(w, h, [Resizing])` au client. +> Le client recrée son buffer aux nouvelles dimensions et commit ; +> le compositor compose à la nouvelle taille. Sortie au release du +> bouton gauche. + +**✅ Validé.** 3 tailles distinctes capturées (avant, agrandi, +rétréci), logs symétriques côté client et compositor, aucun panic, +release propre. + +## Code + +``` +crates/redox-wl-wayland-frontend/ # +~120 lignes (compute_resize_geom, + # handler Resize, apply Resize, + # check client_id sur Move/Resize, + # last_serial sync au resize configure) +crates/redox-wl-test-client-resize/ # nouveau crate (~285 lignes) +docs/phase7-8-resize.md +docs/phase7-8-{1,2,3}*.png +``` + +## Bilan phase 7 close (1-8) + +| Sous-phase | Livrable | Commit | +|---|---|---| +| 7.1 | xdg-shell minimal | `4bff319` | +| 7.2 | wl_seat + routing input | `baa9470` | +| 7.3 | curseur software + alpha blending | `5f7587e` | +| 7.4 | focus + raise on click | `c40ca9f` | +| 7.5 | robustesse paquet A (anti-panic) | `7e81dec` | +| 7.6 | multi-clients (cleanup + release + filtrage) | `a87de02` | +| 7.7 | move interactif xdg_toplevel | `50f7a06` | +| 7.8 | resize interactif xdg_toplevel | (this commit) | + +Le compositor 7.x couvre désormais l'essentiel d'un compositor +Wayland utilisable : +- Protocole base : wl_compositor, wl_shm, wl_surface, wl_callback, + wl_buffer, wl_region (no-op) +- xdg-shell : xdg_wm_base, xdg_surface, xdg_toplevel (configure + + ack + commit + move + resize), xdg_positioner / xdg_popup (no-op) +- Input : wl_seat, wl_keyboard, wl_pointer (avec filtrage par client + focused) +- Compositor logic : Z-order avec raise on click, focus avec + enter/leave, hit_test, cursor software avec alpha blending, + garbage collect des clients déconnectés +- Robustesse : validations sur AckConfigure, CreateBuffer, read_argb + bounds-safe, post-disconnect cleanup +- Interactivité : move + resize via xdg_toplevel + +**Prochain jalon** : port COSMIC (phase 13 plan-directeur, réordonnée +avant GPU). Tenter de faire tourner un toolkit réel (cosmic-comp lib +ou GTK4) sur ce compositor pour identifier les manques. + +## Suite + +Voir phrase de reprise pour COSMIC dans le memory file. + +--- + +*Fin du document de phase 7.8.*