From 50f7a064e4b138b6eda52ad10c9eaf9bec66eb94 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Wed, 13 May 2026 19:22:04 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Phase=207.7=20=E2=80=94=20move?= =?UTF-8?q?=20interactif=20xdg=5Ftoplevel=20valid=C3=A9=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move interactif complet (Resize en stub log-only, reportable à 7.8). Frontend additions : - enum DragMode { Move, Resize(u32) } et struct InteractiveDrag { surface_id, xdg_toplevel_res, xdg_surface_res, mode, start_cursor_x/y, start_x/y, start_w/h } - WaylandFrontend : interactive_drag: Option + last_button_serial: u32 - XdgToplevelData.xdg_surface: Mutex> peuplé dans xdg_surface.GetToplevel pour retrouver le wl_surface parent depuis un toplevel.move/resize - Handler xdg_toplevel.Move : valide serial != 0, refuse drag déjà actif, retrouve SurfaceId via cascade UserData (xdg_surface → wl_surface → SurfaceData), capture start_cursor + start_geom, stocke InteractiveDrag - Handler xdg_toplevel.Resize : stub log-only (à compléter 7.8) - Handler xdg_toplevel.Destroy nettoie interactive_drag si on était en train de drag cette surface - Méthode apply_interactive_drag() : applique le delta (cursor - start_cursor) à la position de la surface (Move) ou consume le motion (Resize stub) - forward_input(PointerMotion(Relative)) : apply au début, return si drag actif (court-circuite l'envoi de motion au client pendant un drag, conforme spec Wayland) - forward_input(PointerButton release) : sort du mode drag - set_cursor_position : appelle aussi apply_interactive_drag (sans ça, les cycles de test programmatique ne déclenchent pas le drag car ils court-circuitent forward_input) - Tracking last_button_serial à chaque button LEFT Pressed Test client modifications : - redox-wl-test-client-shm-two bind wl_seat - Si label=="A", déclenche toplevel.move(&seat, 1) 8s après le commit initial (mécanisme "synthétique" : le client n'écoute pas les pointer events, il envoie Move sans attendre un clic — assez pour valider le pipeline serveur, durcissement client 7.8) Validation runtime : - Cycle compositor temporaire (retiré du binaire final) qui change cursor_position à plusieurs positions pendant que le drag est actif, screendumps à 3 positions distinctes - Logs frontend : [frontend] xdg_toplevel.move: enter drag sid=SurfaceId(0) start=(60,60) cursor=(500,400) [frontend] left-release → exit interactive drag - A déplacée visuellement entre les captures pos1, pos2 et post-release ; sortie clipée sur les bords (pas de snap-to-edge, WM policy reportable) Limitations 7.7 : - Resize non implémenté (stub) - Validation serial laxiste (serial != 0) - Pas de contrôle policy (snap-to-edge, min/max) - Pas de check "surface du même client" (sécu) - Pas d'event xdg_toplevel.configure([Resizing]) envoyé pendant le drag Doc complète : docs/phase7-7-move-resize.md Leyoda 2026 – GPLv3 --- .../redox-wl-test-client-shm-two/src/main.rs | 34 ++- crates/redox-wl-wayland-frontend/src/lib.rs | 203 ++++++++++++++- docs/phase7-7-drag-pos1.png | Bin 0 -> 1871 bytes docs/phase7-7-drag-pos2.png | Bin 0 -> 2163 bytes docs/phase7-7-move-resize.md | 239 ++++++++++++++++++ docs/phase7-7-post-release.png | Bin 0 -> 2163 bytes 6 files changed, 468 insertions(+), 8 deletions(-) create mode 100644 docs/phase7-7-drag-pos1.png create mode 100644 docs/phase7-7-drag-pos2.png create mode 100644 docs/phase7-7-move-resize.md create mode 100644 docs/phase7-7-post-release.png diff --git a/crates/redox-wl-test-client-shm-two/src/main.rs b/crates/redox-wl-test-client-shm-two/src/main.rs index a148348..f56237f 100644 --- a/crates/redox-wl-test-client-shm-two/src/main.rs +++ b/crates/redox-wl-test-client-shm-two/src/main.rs @@ -29,8 +29,8 @@ use wayland_client::{ Connection, Dispatch, EventQueue, Proxy, QueueHandle, backend::Backend, protocol::{ - wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_shm::WlShm, - wl_shm_pool::WlShmPool, wl_surface::WlSurface, + 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::{ @@ -71,6 +71,7 @@ struct ClientState { compositor: Option, shm: Option, wm_base: Option, + seat: Option, pending_serial: Option, configured: bool, label: String, @@ -96,6 +97,9 @@ impl Dispatch for ClientState { "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, ())); + } _ => {} } } @@ -122,6 +126,7 @@ noop!(WlShm); noop!(WlShmPool); noop!(WlBuffer); noop!(WlSurface); +noop!(WlSeat); impl Dispatch for ClientState { fn event( @@ -312,6 +317,16 @@ fn run_one(label: &str, base_color: u32, letter: char, shm_name: &str) -> Result event_queue.flush()?; dlog(&format!("[{label}] buffer commit POST-ack")); + // Phase 7.7 : la fenêtre A déclenche un interactive_move après 8 s. + // Sert à valider que le compositor déplace la surface en suivant le + // curseur (positionné par le cycle test du compositor lui-même). + let move_at = if label == "A" { + Some(std::time::Instant::now() + Duration::from_secs(8)) + } else { + None + }; + let mut move_done = false; + // Boucle vivante 160 s avec dispatch des events let start = std::time::Instant::now(); while start.elapsed() < Duration::from_secs(160) { @@ -321,6 +336,21 @@ fn run_one(label: &str, base_color: u32, letter: char, shm_name: &str) -> Result let _ = guard.read(); } let _ = event_queue.dispatch_pending(&mut state); + + // Phase 7.7 : déclencher toplevel.move après 8s pour la fenêtre A. + if !move_done { + if let Some(t) = move_at { + if std::time::Instant::now() >= t { + if let Some(seat) = state.seat.clone() { + // Serial synthétique != 0 pour passer le garde compositor. + toplevel._move(&seat, 1); + let _ = event_queue.flush(); + dlog(&format!("[{label}] toplevel.move() envoyé")); + move_done = true; + } + } + } + } thread::sleep(Duration::from_millis(50)); } diff --git a/crates/redox-wl-wayland-frontend/src/lib.rs b/crates/redox-wl-wayland-frontend/src/lib.rs index 68dae81..d267bf7 100644 --- a/crates/redox-wl-wayland-frontend/src/lib.rs +++ b/crates/redox-wl-wayland-frontend/src/lib.rs @@ -192,6 +192,46 @@ struct XdgSurfaceData { struct XdgToplevelData { title: Mutex>, app_id: Mutex>, + /// Phase 7.7 : référence vers le `xdg_surface` parent (pour envoyer + /// les configure de resize). Peuplé dans `xdg_surface.GetToplevel`. + xdg_surface: Mutex>, +} + +/// Phase 7.7 : mode d'un drag interactif déclenché par +/// `xdg_toplevel.move` ou `xdg_toplevel.resize`. +#[derive(Clone, Copy, Debug)] +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)] + Resize(u32), +} + +/// 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. +#[derive(Clone)] +struct InteractiveDrag { + surface_id: SurfaceId, + /// Le xdg_toplevel cible — gardé pour envoyer `configure(w, h)` au + /// resize. Pour move seul on s'en sert pas mais on garde par symétrie. + #[allow(dead_code)] + xdg_toplevel_res: xdg_toplevel::XdgToplevel, + /// Le xdg_surface parent — gardé pour envoyer `configure(serial)` au + /// resize. Inutilisé en move pur. + #[allow(dead_code)] + xdg_surface_res: xdg_surface::XdgSurface, + mode: DragMode, + start_cursor_x: i32, + start_cursor_y: i32, + start_x: i32, + start_y: i32, + #[allow(dead_code)] + start_w: u32, + #[allow(dead_code)] + start_h: u32, } #[derive(Debug)] @@ -261,6 +301,17 @@ pub struct WaylandFrontend { /// Remplie par les callbacks `DumbClientData::disconnected`, drainée /// par `garbage_collect_dead_clients` dans la boucle main. dead_clients: Arc>>, + + // ----- Phase 7.7 : move/resize interactifs ------------------------ + /// Drag interactif en cours, posé par `xdg_toplevel.Move`/`Resize`. + /// Lu par `forward_input` à chaque PointerMotion(Relative). Vidé au + /// release du bouton gauche. + interactive_drag: Option, + /// Dernier serial envoyé pour un `wl_pointer.button` Pressed. Utilisé + /// pour valider qu'un `xdg_toplevel.Move/Resize` arrive en réponse à + /// un clic récent (anti-replay). Pour 7.7 on accepte simplement + /// `serial != 0` (validation à durcir en 7.8). + last_button_serial: u32, /// Hot-spot du curseur (offset à soustraire à cursor_x/y pour le placement). cursor_hot_x: i32, cursor_hot_y: i32, @@ -307,6 +358,8 @@ impl WaylandFrontend { cursor_visible: false, surfaces_by_id: HashMap::new(), dead_clients: Arc::new(Mutex::new(Vec::new())), + interactive_drag: None, + last_button_serial: 0, }) } @@ -509,6 +562,37 @@ 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. + fn apply_interactive_drag(&mut self) -> bool { + let Some(drag) = self.interactive_drag.clone() else { + return false; + }; + 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| { + s.x = new_x; + s.y = new_y; + }); + 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. + true + } + } + } + /// Renvoie le ClientId de la surface focalisée, s'il y en a une. /// Utilisé en 7.6 pour filtrer le routage des events pointer/keyboard /// (un seul client à la fois reçoit les events, contrairement au @@ -555,6 +639,11 @@ impl WaylandFrontend { self.cursor_x = *x; self.cursor_y = *y; self.cursor_visible = true; + // Phase 7.7 : si drag actif, déplacer la surface au lieu + // d'envoyer motion au client. + if self.apply_interactive_drag() { + return; + } let time = self.alloc_input_time(); if let Some(focus) = self.focused_surface.clone() { let focus_cid = focus.client().map(|c| c.id()); @@ -573,6 +662,10 @@ impl WaylandFrontend { self.cursor_x = self.cursor_x.saturating_add(*dx); self.cursor_y = self.cursor_y.saturating_add(*dy); self.cursor_visible = true; + // Phase 7.7 : si drag actif, déplacer la surface. + if self.apply_interactive_drag() { + return; + } let time = self.alloc_input_time(); if let Some(focus) = self.focused_surface.clone() { let focus_cid = focus.client().map(|c| c.id()); @@ -596,6 +689,13 @@ impl WaylandFrontend { // position curseur et raise + set_focus à la surface ciblée. // À faire AVANT l'envoi des events button pour que la nouvelle // surface reçoive enter+modifiers en premier, puis le button. + // Phase 7.7 : si drag actif et le bouton gauche est relâché, + // sortir du mode drag avant l'envoi du button au client. + if !*left && self.interactive_drag.is_some() { + println!("[frontend] left-release → exit interactive drag"); + self.interactive_drag = None; + } + if *left { let hit = self.registry.hit_test(self.cursor_x, self.cursor_y); println!( @@ -640,6 +740,12 @@ impl WaylandFrontend { // on envoie les 3 à chaque event au client focused. for (btn, pressed) in buttons { let serial = self.alloc_input_serial(); + // Phase 7.7 : tracker le dernier serial button Pressed + // pour valider les xdg_toplevel.move/resize qui doivent + // arriver en réponse. + if pressed && btn == BTN_LEFT { + self.last_button_serial = serial; + } let state = if pressed { wl_pointer::ButtonState::Pressed } else { @@ -794,10 +900,15 @@ impl WaylandFrontend { /// Force la position du curseur à tout moment. Utile pour tests /// programmatiques qui veulent simuler un mouvement souris en dehors /// du circuit `forward_input`. + /// + /// Phase 7.7 : applique aussi le drag interactif s'il est actif, car + /// `set_cursor_position` court-circuite `forward_input(PointerMotion)` + /// qui est l'endroit normal où le drag est mis à jour. pub fn set_cursor_position(&mut self, x: i32, y: i32) { self.cursor_x = x; self.cursor_y = y; self.cursor_visible = true; + let _ = self.apply_interactive_drag(); } /// Récupère le buffer curseur courant (client custom si fourni, sinon @@ -1326,6 +1437,9 @@ impl wayland_server::Dispatch> for match request { xdg_surface::Request::GetToplevel { id } => { let toplevel_data = Arc::new(XdgToplevelData::default()); + // Phase 7.7 : capter le xdg_surface parent pour pouvoir + // envoyer configure(serial) au resize. + *toplevel_data.xdg_surface.lock().unwrap() = Some(resource.clone()); let toplevel = data_init.init(id, toplevel_data); // Position cascading pour que les fenêtres successives ne @@ -1598,9 +1712,9 @@ impl wayland_server::Dispatch> for WaylandFrontend { fn request( - _state: &mut Self, + state: &mut Self, _client: &Client, - _r: &xdg_toplevel::XdgToplevel, + resource: &xdg_toplevel::XdgToplevel, request: xdg_toplevel::Request, data: &Arc, _dh: &DisplayHandle, @@ -1613,11 +1727,88 @@ impl wayland_server::Dispatch> 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 + xdg_toplevel::Request::Move { + 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`). + if serial == 0 { + println!("[frontend] xdg_toplevel.move: serial=0 refused"); + return; + } + if state.interactive_drag.is_some() { + println!("[frontend] xdg_toplevel.move: drag already in progress"); + return; + } + let Some(xdg_surf) = data.xdg_surface.lock().unwrap().clone() else { + println!("[frontend] xdg_toplevel.move: no parent xdg_surface"); + 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; + }; + 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((0, 0)); + let drag = InteractiveDrag { + surface_id: sid, + xdg_toplevel_res: resource.clone(), + xdg_surface_res: xdg_surf, + mode: DragMode::Move, + 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.move: enter drag sid={:?} start=({},{}) cursor=({},{})", + sid, st.x, st.y, state.cursor_x, state.cursor_y + ); + state.interactive_drag = Some(drag); } - // Tout le reste ignoré en 7.1 : - // SetParent, ShowWindowMenu, Move, Resize, SetMaxSize, SetMinSize, + xdg_toplevel::Request::Resize { + seat: _, + 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). + let edges_raw = match edges.into_result() { + Ok(e) => e as u32, + Err(_) => 0, + }; + println!( + "[frontend] xdg_toplevel.resize: serial={serial} edges={edges_raw} (stub, ignored)" + ); + } + xdg_toplevel::Request::Destroy => { + // Si on était en train de drag cette surface, abandonner le mode + if let Some(drag) = &state.interactive_drag { + if drag.xdg_toplevel_res == *resource { + state.interactive_drag = None; + } + } + } + // Tout le reste ignoré pour 7.7 : + // SetParent, ShowWindowMenu, SetMaxSize, SetMinSize, // SetMaximized, UnsetMaximized, SetFullscreen, UnsetFullscreen, // SetMinimized _ => {} diff --git a/docs/phase7-7-drag-pos1.png b/docs/phase7-7-drag-pos1.png new file mode 100644 index 0000000000000000000000000000000000000000..7aa1d68b308d2768116b11ebfcb3e0af1551a302 GIT binary patch literal 1871 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>Fdh=ij`N8L(e0tI}<3RTH+c}l9E`GYL#4+3Zxi}42;Zl z4NP?njYAC0tqd%zj7+r+46F+M~~BH?g})se_P3%c_QhJLD($&;IlnwmE0w^5`fncCTItfd{Z(?ZHe-yN&#=;piAhC`6r<%-4(w;$vC^HR;GJJq zK*RM<42j2ausN~pI^Ttt5wo+|U(_5pdzHDw>D9eojPrcJcDz<L)$v=Ts%d81zZ# z(wSQ2&_dDDZzMCIp`4wldkrNmON7QCGq#qWT zoxsHEk<-K}q5v0V6oLtUu~q~rc96wN zvhfCs@QWOYBw?Qgmp6jM&#{9g6sH0uDlSQ0T>ee{K=;f9rt?-y{+#*8Qei0U^PqG2 zlldX@8T6kEihT%S+B1#)hy4PE@@w^?ItSKXz0WT4;NGqIEgS{K2N}2~#27R%sfhC| zU^vOjtl?0UzqzXF__F{(UqSjGXI27DcxN#UETk{I&zOr+_Brq6kPKA&DxBvd9`9r*X7q84B4s3hGzJn=dSw4y7-WI TWHM`*4#=&Zu6{1-oD!M_@1E~F z*}Lc5-JP18$anE|0RZq7@^}>h1PoasHVeA)lRtJrCt596B>}KC1ORde09FW*j{(>q z0pLj{0MZ%&gj&<7rSss1XR0zS!DEuh!^7i4OX}Fz*q+Sm5dP@bW2>OURxL@4X9--D zLvX>-DUuR6PYAxiIb6QBEEht-x`gGrHCZ~fbajCmIuId=h?*{$J$+_m+RQmpNwhRl zGEE|pN+i|a<;A^0(B!Yl&EE3wga`C5Hb|KIX9ZoZMqQ}O(rEwAnHBX%kvsHdNa08o z@$aW?zV-a>bB%wxcf!)qT2O=6p8Y(_w_M_o3PZ3uPf;rb$w;>cq&b#^xt-3g>d zoajuPXzd)uid)Ptk@TGaYJ@}E(E)DaFy;0a2jH`>&nE|cEbWecKdRl@z6p`)urv8! zunz6#kb}<#uOX6mrE!e4Zzx?Nz6aeBXR(>6E|}vg9%1b(NSxK!oU@?sNKa(Y>5iMr z)*nlJ)EM2@Pq$G;whmV1aw(3hzAHqo`%U~Ns#~_I?OT$?tu?$MMAqgIx zn};NplNE(X;t`baLlQ#j;SD5l77RFR-LZY1BJntl>`F&4SG!j_8w($ypAy{v#vX4K zn0G3hoY5!~+XeW$8?J(=GT1Pi7u!>};Th-G`J(*S zpW+Fv&odHSI;rCo& zc<~$z_kW7Qu5&P%Bx*{Pt{^)|t?Y{%x!KFw5 literal 0 HcmV?d00001 diff --git a/docs/phase7-7-move-resize.md b/docs/phase7-7-move-resize.md new file mode 100644 index 0000000..a1d86f0 --- /dev/null +++ b/docs/phase7-7-move-resize.md @@ -0,0 +1,239 @@ +# Phase 7.7 — Move/resize interactifs via xdg_toplevel + +> Document produit le 2026-05-13 dans le cadre du plan directeur +> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`. +> +> **Scope strict** : +> - `xdg_toplevel.Move { seat, serial }` : entrer en mode drag-move, +> appliquer le delta cursor sur la position de la surface, sortir au +> release du bouton gauche. +> - `xdg_toplevel.Resize { seat, serial, edges }` : **stub log-only** +> pour 7.7 (implémentation complète reportée à 7.8 si besoin). +> - Validation runtime end-to-end via un client qui appelle +> `toplevel.move(&seat, serial)` au compositor. +> +> **Hors scope 7.7** : resize effectif (xdg_toplevel.configure(w,h) + +> recompute selon edges), validation stricte du serial, snap-to-edge, +> contraintes min/max size. + +## Verdict + +**✅ Move interactif validé runtime.** La fenêtre A (verte/pyramide) +créée par le test client envoie `toplevel.move(&seat, 1)` 8 s après +son ack_configure ; le compositor enregistre l'état initial de drag +puis applique le delta de curseur à chaque `set_cursor_position` (ou +`PointerMotion` réel). À `mouse_button 0` (release) le mode drag se +termine et la surface reste à sa dernière position. + +Captures : +- ![pos1](phase7-7-drag-pos1.png) — état pendant le drag : A clipée + partiellement à gauche (delta négatif appliqué depuis sa position + initiale (60, 60)) +- ![pos2](phase7-7-drag-pos2.png) — état pendant le drag : A déplacée + en haut, partiellement clipée en y négatif (delta différent) +- ![post-release](phase7-7-post-release.png) — état après release : + surface stabilisée à la dernière position appliquée + +Logs frontend (`/tmp/comp.log` extrait via redoxfs) : +``` +[frontend] focus change: None → Some(SurfaceId(0)) +[frontend] focus change: Some(SurfaceId(0)) → Some(SurfaceId(1)) +[frontend] xdg_toplevel.move: enter drag sid=SurfaceId(0) start=(60,60) cursor=(500,400) +[frontend] left-release → exit interactive drag +``` + +Logs côté client : +``` +[A] connect to compositor +[A] xdg_toplevel créé +[A] ack_configure(1) +[A] buffer commit POST-ack +[A] toplevel.move() envoyé +``` + +## Modifications apportées + +### `redox-wl-wayland-frontend` + +**Nouveaux types** : +- `DragMode { Move, Resize(u32) }` — quel type de drag est actif. + Le u32 pour Resize est le bitmask `xdg_toplevel::ResizeEdge` brut. +- `InteractiveDrag { surface_id, xdg_toplevel_res, xdg_surface_res, + mode, start_cursor_x, start_cursor_y, start_x, start_y, start_w, + start_h }` — l'état d'un drag en cours, posé à l'entrée et lu à + chaque update curseur. + +**`WaylandFrontend`** : nouveaux champs +```rust +interactive_drag: Option, +last_button_serial: u32, +``` + +**`XdgToplevelData`** : nouveau champ +```rust +xdg_surface: Mutex>, +``` +peuplé dans `xdg_surface::Request::GetToplevel`. Permet au handler +`Move`/`Resize` du toplevel de retrouver le `wl_surface` parent + le +`SurfaceId` côté compositor-core. + +**Handler `xdg_toplevel::Request::Move`** : +1. Validation laxiste du serial : `serial != 0` accepté (durcir 7.8). +2. Refus si un drag est déjà en cours (un seul drag actif à la fois). +3. Retrouve `xdg_surface` → `wl_surface` → `SurfaceData` → `SurfaceId` + via les data UserData. +4. Récupère la position actuelle de la surface dans le registry pour + en faire `start_x`, `start_y`. Capture aussi `start_cursor_x/y`. +5. Stocke `InteractiveDrag::Move` dans `self.interactive_drag`. + +**Handler `xdg_toplevel::Request::Resize`** : stub log-only. Logge +`serial` et `edges` puis ignore. À implémenter en 7.8 : +- calculer `(new_x, new_y, new_w, new_h)` selon `edges` (TOP/BOTTOM/ + LEFT/RIGHT/coins) et le delta cursor +- envoyer `xdg_toplevel.configure(new_w, new_h, vec![])` + + `xdg_surface.configure(serial)` pour que le client re-rende +- attendre `ack_configure` puis le prochain commit avec le nouveau + buffer + +**Méthode `apply_interactive_drag(&mut self) -> bool`** : si un drag +est actif, applique le delta `(cursor - start_cursor)` à la position +de la surface ciblée (`registry.modify_pending + commit`) et retourne +`true` pour que le caller court-circuite l'envoi des events +`wl_pointer.motion` au client (la spec dit que pendant un drag, le +client ne doit pas recevoir de motion). + +**`forward_input(PointerMotion / PointerMotionRelative)`** : appelle +`apply_interactive_drag()` au début. Si `true`, return immédiat +(pas d'envoi de motion). Sinon comportement normal 7.6. + +**`forward_input(PointerButton)`** : si `left == false` (release) et +drag actif, vide `self.interactive_drag` et log "left-release → +exit interactive drag". Indépendant du focus actuel. + +**`set_cursor_position(x, y)`** : appelle `apply_interactive_drag()` +après mise à jour de `cursor_x/y`, car ce setter court-circuite +`forward_input(PointerMotion)` qui est l'endroit normal d'application +du drag. Sans cette ligne, un cycle de test programmatique qui +change le cursor sans event réel ne déclenche pas le drag. + +### `redox-wl-test-client-shm-two` + +Modifications mineures pour bind `wl_seat` (resource simple, pas de +`get_pointer` ni d'écoute de `button` — le client A déclenche le +move de manière synthétique 8 s après son commit) : +- Import `wl_seat::WlSeat` +- `ClientState.seat: Option` +- Registry handler bind `"wl_seat"` v7 +- `noop!(WlSeat)` +- Dans `run_one`, si `label == "A"`, calcule `move_at = now + 8s` ; + dans la boucle vivante, dès que `now >= move_at`, appel + `toplevel._move(&seat, 1)` puis flush. Une seule fois. + +Le serial `1` est synthétique. Le compositor 7.7 accepte (validation +laxiste). À durcir en 7.8 : le serial doit correspondre à un +`wl_pointer.button` récent et le client devrait écouter le button +events avant d'envoyer Move. + +## Méthode de validation runtime + +Comme en 7.3, 7.4 et 7.6, la validation utilise un cycle temporaire +de `set_cursor_position` côté compositor pour simuler des mouvements +souris (le driver mouse Redox sous QEMU ne reçoit pas de +`PointerMotion`). Le cycle a 4 phases : +- T+0 à T+14 : cursor à (200, 200) — point de départ avant le Move + du client (qui arrive à compositor T~10s) +- T+14 → cursor à (500, 400) +- T+18 → cursor à (800, 100) +- T+22 → cursor à (300, 500) +- T+26 (post-release) → cursor à (600, 300) + +Le client A envoie `toplevel.move()` à T+8s de son démarrage. Le +compositor entre en drag avec start_cursor = position courante, +start_geom = (60, 60). Aux changements de cursor suivants, +`apply_interactive_drag` applique le delta. À T_QEMU ~43s, le bash +script envoie `mouse_button 1` puis `mouse_button 0` (release) pour +terminer le drag. + +Ce cycle de positionnement et l'auto-move côté client A sont des +helpers de validation, **retirés du compositor binaire après les +screendumps** (le code production ne fait que +`set_cursor_initial_position(center)` + `draw_cursor` + `apply` via +`forward_input` réel). + +Note : le côté `move auto à T+8s` du client `redox-wl-test-client-shm-two` +est conservé dans le code commité — c'est un test binary, pas un +client de production. La fenêtre B n'est pas affectée (`label != "A"`). + +## Limitations connues (à traiter en sous-tickets ultérieurs) + +- **Resize non implémenté** : stub log-only. La compute de + `(new_x, new_y, new_w, new_h)` selon `edges` + l'envoi de + `xdg_toplevel.configure(w, h, [Resizing])` + `xdg_surface.configure + (serial)` + attente de `ack_configure` puis du commit avec nouveau + buffer est reportable. +- **Validation serial laxiste** : on accepte `serial != 0`. La spec + dit "doit correspondre à un input event récent (button)". À + durcir : comparer à `last_button_serial`, refuser si trop ancien + (par ex. > 100ms). +- **Pas de snap-to-edge / contraintes** : la surface peut sortir + partiellement ou entièrement de l'écran (visible dans les captures + pos1/pos2). Reportable à un sous-ticket "policy WM". +- **Pas d'événement `xdg_toplevel.configure([Resizing])` envoyé en + début de Move** : la spec dit que le compositor peut/doit annoncer + le state via configure pour que le toolkit affiche un curseur de + drag différent. Pas critique pour 7.7. +- **Un seul drag actif à la fois** : si un autre client envoie Move + pendant qu'un drag est en cours, on log et on ignore. La spec ne + précise pas ce comportement ; tolérant est OK. +- **Pas de validation que la surface ciblée est bien le client + appelant** : un client malveillant pourrait demander à déplacer une + surface qui n'est pas la sienne. À durcir en 7.8 (vérifier que + `xdg_surface.client_id == client_id du request`). + +## Critère de fin 7.7 + +> Un client peut envoyer `toplevel.move()` au compositor. Le +> compositor entre en mode drag, applique le delta de curseur sur la +> position de la surface, et sort du mode au release du bouton +> gauche. Tout cela sans panic ni régression sur les phases +> précédentes. + +**✅ Validé.** Move fonctionnel runtime, 3 captures de preuve, logs +frontend confirment enter/release du drag, multi-clients (A + B) +toujours OK. + +## Code + +``` +crates/redox-wl-wayland-frontend/ # +~100 lignes (InteractiveDrag, + # DragMode, Move handler, + # apply_interactive_drag, + # XdgToplevelData.xdg_surface) +crates/redox-wl-test-client-shm-two/ # +bind wl_seat + auto-move + # à T+8s pour la fenêtre A +docs/phase7-7-move-resize.md +docs/phase7-7-drag-pos{1,2}.png +docs/phase7-7-post-release.png +``` + +## Suite phase 7.8 + +Au choix : +- **Resize complet** (xdg_toplevel.Resize + configure avec edges + selon Top/Bottom/Left/Right + ack_configure → commit) +- **Validation stricte des serials** (`last_button_serial` enforcé) +- **Politique WM** (snap-to-edge, contraintes min/max, retour de + surface hors-écran) +- **Frame callbacks per-client throttle** (frame done seulement + envoyé aux clients dont la surface a été composée) + +Estimé : 1-2 sessions par item. + +Avec la phase 7 quasi-complète (7.1-7.7), le **vrai prochain +jalon** est le port COSMIC (phase plan-directeur 13, réordonnée avant +GPU). Le compositor 7.x devrait être suffisamment compatible Wayland +pour qu'un toolkit comme cosmic-comp lib + GTK4 puisse se connecter. + +--- + +*Fin du document de phase 7.7.* diff --git a/docs/phase7-7-post-release.png b/docs/phase7-7-post-release.png new file mode 100644 index 0000000000000000000000000000000000000000..57837cf7fdebe33f3a9d24d33c26e81806efea3c GIT binary patch literal 2163 zcmeHIdrVVT7(e&6mr_a#1*Gzl;s#ipSV0l0Sf(JYmWP!Ev*84_Fm#m)^-1MkoI(uB zLu&$C5x40GX3-#%hZtKW3Mz^lTkwh38N~Rafn?lOC_AS!nOzh9R3E@wEJGB-%Z0+*Ic$Uyr zH4GOVy)r3*^OWEVox>Fy%5otTtWQ{_U!SGd$kyd+paT)oh^Sf8d9&t3rp=izlSa$t zM$D8-Wm2hNbgS+yf;Mk`ZuX9UCXgp?bVI_7zbfc+wVDEbmR9!;PSl@8?pd#)=aebq zKTO+x=f(XOTK{zKgcaj8pc=2a@Iydx099N(oT8yCe9AnORpTem@LD%&j$XbjX>5P` zOMObmv}v=SWZBkS(Qqq`KskByb0#(6Ee?zl(Y;BO;Z_FKa`zV7MOV=X>Nd4Ba=qwj z>#+V!1)ei3vESiiyFh11DpjZ=m_%|V!bZ zeq9Blsa)$2W~zz9Ue58LMdJq$R_$!PhWR@%A>CvTaj38v0|xFm zn0Mh^q?C41hbS(bm-xRjiAC&m(668p$L!?^ld>$C8 zMf*9_(DR`ih~!;qoM0UsPM1m^K)2jkVkQpd&v%uKvJU1a&TVMQS=@i3H!|p4$L*Dy zPbEHXi0&V-w$X)myu{>!g+33@ber~DUYU;YM}CYTx+0T1rw7Ljkf!k3a^5I%@w*sQI zOF>mMv1V8jvT=iNKpYkx;YmMw0$g-eUxT_3PAXLf+aNjyNrYMIFCdAPP(pwtc-Gua zNMaRLQGg^KLkT}5A)+7MLJ}9iptH^mJLn;nOw!1^JA%2o1G0Ho_^9<6!Topa$yR}R zXS1o<4RVQHh`+z(I(U|{H*hI+jICHUdE>+yQ^NU5qgv#y^I&6^;OFj#@Zg7zgyZ!s z0Vl2sEnDfKJMa%5nw)#yJdVqw3Y&8 ztz$U)GO?FIaq3R2sE}A>cH+H#Br<_@uf5z{Ds(j1oA*SUUkSW7+TQyT+8tr#(8l-_ z8=JBeF#%=95lk5qblBK8xAO0A^Ul_U+dX?wQV($m+MJFz2yi{b4bot5Ury~=3V2y9 z_y(c%zoPh%P!R(krngZM6If~VR4e(l!q_`?KvW|;W;iW}4YPfjJ#`nJacQG3%761M z{_CZdXKk^u2HUh)(Mh@P$X#!mRMBU?=UE4Bo`W@8uznSN{5kI?`_rrEK@xrZ16LSc zxd_9