From c40ca9fcc8e867bdabd7f58f83b8d80cb15e57a1 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Wed, 13 May 2026 10:21:05 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Phase=207.4=20=E2=80=94=20focus?= =?UTF-8?q?=20+=20raise=20on=20click=20valid=C3=A9s=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur clic gauche, le compositor fait hit_test à la position curseur, raise la surface ciblée au top du Z-order et transfère le keyboard focus à cette surface (broadcast wl_keyboard.leave/enter via le set_focus déjà implémenté en 7.2). Frontend additions : - HashMap dans WaylandFrontend, peuplée au wl_compositor.create_surface (capture du retour de data_init.init), nettoyée au wl_surface.destroy - Au wl_surface.destroy : clear focused_surface et cursor_surface_id si la surface détruite était l'une de ces références (évite les wl_surface fantômes dans les events suivants) - forward_input(PointerButton.left=true) déclenche registry.hit_test(cursor_x, cursor_y), puis si la cible n'est pas une surface curseur : registry.raise + set_focus(target) - println! tracing pour [frontend] left-click et focus change Nouveau crate : redox-wl-test-client-shm-two - Binaire qui fork() : parent = fenêtre A (verte, pyramide), enfant = fenêtre B (magenta, double cercle) après sleep 800ms - 2 connexions Wayland indépendantes au même socket compositor - timeout 160s aligné sur le compositor 180s Validation runtime : 4 captures synchronisées via cycle de positionnement curseur temporaire (retiré après) prouvent les 2 transitions de Z-order : - initial : B au top (commit le plus récent) - click@(80,80) → hit A → A passe au top - click@(400,280) → hit B → B repasse au top Traces /tmp/comp.log (extraites via redoxfs) confirment : [frontend] left-click @ (80, 80) → hit_test = Some(SurfaceId(0)) [frontend] focus change: Some(SurfaceId(1)) → Some(SurfaceId(0)) [frontend] left-click @ (400, 280) → hit_test = Some(SurfaceId(1)) [frontend] focus change: Some(SurfaceId(0)) → Some(SurfaceId(1)) Pipeline validé end-to-end : mouse_button QEMU → ps2d → inputd → InputBackend::poll → RedoxInputEvent::PointerButton → forward_input → hit_test → raise + set_focus → wl_keyboard.leave/enter broadcast. Doc complète : docs/phase7-4-focus-raise.md. Leyoda 2026 – GPLv3 --- .../redox-wl-test-client-shm-two/Cargo.toml | 10 + .../redox-wl-test-client-shm-two/src/main.rs | 385 ++++++++++++++++++ crates/redox-wl-wayland-frontend/src/lib.rs | 60 ++- docs/phase7-4-1-initial-B-top.png | Bin 0 -> 1616 bytes docs/phase7-4-2-after-click-A-raised.png | Bin 0 -> 1930 bytes docs/phase7-4-3-after-click-B-raised.png | Bin 0 -> 1602 bytes docs/phase7-4-focus-raise.md | 199 +++++++++ 7 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 crates/redox-wl-test-client-shm-two/Cargo.toml create mode 100644 crates/redox-wl-test-client-shm-two/src/main.rs create mode 100644 docs/phase7-4-1-initial-B-top.png create mode 100644 docs/phase7-4-2-after-click-A-raised.png create mode 100644 docs/phase7-4-3-after-click-B-raised.png create mode 100644 docs/phase7-4-focus-raise.md diff --git a/crates/redox-wl-test-client-shm-two/Cargo.toml b/crates/redox-wl-test-client-shm-two/Cargo.toml new file mode 100644 index 0000000..bbc4a02 --- /dev/null +++ b/crates/redox-wl-test-client-shm-two/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "redox-wl-test-client-shm-two" +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-shm-two/src/main.rs b/crates/redox-wl-test-client-shm-two/src/main.rs new file mode 100644 index 0000000..a148348 --- /dev/null +++ b/crates/redox-wl-test-client-shm-two/src/main.rs @@ -0,0 +1,385 @@ +//! Phase 7.4 — Client à deux fenêtres pour valider focus + raise on click. +//! +//! Le parent fork 1 enfant. Chacun se connecte indépendamment au socket +//! Wayland du compositor, crée son propre xdg_toplevel avec une couleur +//! différente (vert pastel pour A, magenta pour B), et reste vivant 160 s. +//! +//! Avec le cascading offset du compositor (60, 60 puis 120, 120), les deux +//! fenêtres se chevauchent partiellement. La fenêtre B est créée en dernier +//! donc affichée au-dessus initialement. +//! +//! Pour valider le clic-raise : avec un cycle programmatique côté +//! compositor qui repositionne le curseur sur A puis sur B et envoie un +//! mouse_button via QEMU monitor, on peut voir le Z-order changer dans +//! les screendumps. + +use std::env; +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_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 W: i32 = 300; +const H: i32 = 200; +const STRIDE: i32 = W * 4; +const SIZE: i32 = STRIDE * H; + +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, + pending_serial: Option, + configured: bool, + label: String, +} + +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, ())); + } + _ => {} + } + } + } +} + +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); + +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 = true; + } + } +} + +impl Dispatch for ClientState { + fn event( + _state: &mut Self, + _r: &XdgToplevel, + _event: xdg_toplevel::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + } +} + +/// Génère un pattern de remplissage uni avec bordure noire 2px, et un +/// gros caractère central A ou B pour distinguer visuellement les +/// fenêtres dans les screendumps. +unsafe fn create_shm_pattern(name: &str, base: u32, letter: char) -> Result { + 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 { base }; + } + } + // Gros bloc 80x80 au centre formant la lettre (A : triangle ; B : rectangle barré) + let cx = W / 2; + let cy = H / 2; + let lc: u32 = 0xFF_FF_FF_FF; + match letter { + 'A' => { + // pyramide simple 80x80 + for dy in -40i32..=40i32 { + let halfw = 40 - dy.abs(); + for dx in -halfw..=halfw { + let x = cx + dx; + let y = cy + dy; + if x >= 0 && x < W && y >= 0 && y < H { + pixels[(y * W + x) as usize] = lc; + } + } + } + // un trou horizontal au tiers pour faire un "A" + for dy in 10i32..=14i32 { + let halfw = 40 - dy.abs(); + for dx in -halfw + 4..=halfw - 4 { + let x = cx + dx; + let y = cy + dy; + if x >= 0 && x < W && y >= 0 && y < H { + pixels[(y * W + x) as usize] = base; + } + } + } + } + 'B' | _ => { + // 2 cercles empilés simulés par 2 carrés arrondis + for dy in -40i32..=40i32 { + for dx in -30i32..=30i32 { + let x = cx + dx; + let y = cy + dy; + let in_top = dy < -2 && (dx * dx + (dy + 20) * (dy + 20)) <= 20 * 20; + let in_bot = dy > 2 && (dx * dx + (dy - 20) * (dy - 20)) <= 20 * 20; + if (in_top || in_bot) && x >= 0 && x < W && y >= 0 && y < H { + pixels[(y * W + x) as usize] = lc; + } + } + } + } + } + libc::munmap(p, SIZE as usize); + Ok(OwnedFd::from_raw_fd(fd)) +} + +fn run_one(label: &str, base_color: u32, letter: char, shm_name: &str) -> Result<(), Box> { + dlog(&format!("[{label}] connect to compositor")); + + for i in 0..50 { + if std::path::Path::new(SOCKET_PATH).exists() { + break; + } + if i == 49 { + return Err("compositor socket missing after 5s".into()); + } + 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 { + label: label.to_string(), + ..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(format!("Phase 7.4 — fenêtre {label}")); + toplevel.set_app_id(format!("redox.wl.test.client.shm-two.{label}")); + surface.commit(); + dlog(&format!("[{label}] xdg_toplevel créé")); + + 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)); + } + let serial = state.pending_serial.ok_or("no initial configure")?; + xdg_surface.ack_configure(serial); + state.pending_serial = None; + dlog(&format!("[{label}] ack_configure({serial})")); + + let fd = unsafe { create_shm_pattern(shm_name, base_color, letter) }?; + 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(); + event_queue.flush()?; + dlog(&format!("[{label}] buffer commit POST-ack")); + + // Boucle vivante 160 s avec dispatch des events + let start = std::time::Instant::now(); + while start.elapsed() < Duration::from_secs(160) { + // Pattern flush + prepare_read + dispatch (cf phase 7.2 memo) + let _ = event_queue.flush(); + if let Some(guard) = event_queue.prepare_read() { + let _ = guard.read(); + } + let _ = event_queue.dispatch_pending(&mut state); + thread::sleep(Duration::from_millis(50)); + } + + dlog(&format!("[{label}] destroy propre")); + toplevel.destroy(); + xdg_surface.destroy(); + surface.destroy(); + let _ = event_queue.flush(); + Ok(()) +} + +fn run() -> Result<(), Box> { + // Mode enfant : le parent nous a re-exec avec ce flag. + if env::var("REDOX_WL_CHILD").as_deref() == Ok("B") { + // Décalage pour que l'ordre A→B soit déterministe : on attend 1s + thread::sleep(Duration::from_millis(800)); + return run_one("B", 0xFF_8C_60_D0, 'B', "/redox-wl-client-shm-two-B"); + } + + // Parent : fork + let pid = unsafe { libc::fork() }; + if pid < 0 { + return Err("fork failed".into()); + } + if pid == 0 { + // Côté enfant : exec self avec REDOX_WL_CHILD=B + // On peut aussi appeler run_one directement (pas besoin d'exec). + unsafe { + libc::setenv( + CString::new("REDOX_WL_CHILD").unwrap().as_ptr(), + CString::new("B").unwrap().as_ptr(), + 1, + ); + } + // Inline : pas besoin d'exec, on fait directement le client B. + thread::sleep(Duration::from_millis(800)); + return run_one("B", 0xFF_8C_60_D0, 'B', "/redox-wl-client-shm-two-B"); + } + + // Parent : c'est le client A. + let res = run_one("A", 0xFF_5A_C0_50, 'A', "/redox-wl-client-shm-two-A"); + + // Attendre l'enfant + let mut status: i32 = 0; + unsafe { + libc::waitpid(pid, &mut status, 0); + } + res +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + dlog("[parent] PASS"); + ExitCode::SUCCESS + } + Err(e) => { + dlog(&format!("[parent] 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 0e14c0e..aecde46 100644 --- a/crates/redox-wl-wayland-frontend/src/lib.rs +++ b/crates/redox-wl-wayland-frontend/src/lib.rs @@ -215,6 +215,10 @@ pub struct WaylandFrontend { /// dernier `wl_pointer.set_cursor`. `None` = pas de curseur custom, /// on dessine le sprite par défaut (flèche 16x16). cursor_surface_id: Option, + /// Phase 7.4 : mapping inverse SurfaceId → wl_surface, pour pouvoir + /// faire set_focus(target) après un hit_test au clic. Peuplé au + /// `wl_compositor.create_surface`, nettoyé au `wl_surface.destroy`. + surfaces_by_id: HashMap, /// Hot-spot du curseur (offset à soustraire à cursor_x/y pour le placement). cursor_hot_x: i32, cursor_hot_y: i32, @@ -259,6 +263,7 @@ impl WaylandFrontend { cursor_hot_x: 0, cursor_hot_y: 0, cursor_visible: false, + surfaces_by_id: HashMap::new(), }) } @@ -339,6 +344,18 @@ impl WaylandFrontend { if same { return; } + let old_sid = self + .focused_surface + .as_ref() + .and_then(|s| s.data::>()) + .and_then(|d| *d.id.lock().unwrap()); + let new_sid = new_focus + .as_ref() + .and_then(|s| s.data::>()) + .and_then(|d| *d.id.lock().unwrap()); + println!( + "[frontend] focus change: {old_sid:?} → {new_sid:?}" + ); let serial_leave = self.alloc_input_serial(); let serial_enter = self.alloc_input_serial(); @@ -448,6 +465,32 @@ impl WaylandFrontend { middle, right, } => { + // Phase 7.4 : sur clic gauche (left pressed), hit_test à la + // 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. + if *left { + let hit = self.registry.hit_test(self.cursor_x, self.cursor_y); + println!( + "[frontend] left-click @ ({}, {}) → hit_test = {:?}", + self.cursor_x, self.cursor_y, hit + ); + if let Some(sid) = hit { + // Ne pas réagir aux surfaces curseur (elles ne devraient pas + // apparaître dans hit_test car visible=false, mais double-belt). + if let Some(surf) = self.surfaces_by_id.get(&sid).cloned() { + if !surf + .data::>() + .map(|d| d.is_cursor.load(Ordering::Relaxed)) + .unwrap_or(false) + { + self.registry.raise(sid); + self.set_focus(Some(surf)); + } + } + } + } + let time = self.alloc_input_time(); // Code BTN_LEFT/MIDDLE/RIGHT linux/input-event-codes.h const BTN_LEFT: u32 = 0x110; @@ -723,7 +766,10 @@ impl wayland_server::Dispatch for WaylandFronte xdg_pending_initial_configure: Mutex::new(false), is_cursor: AtomicBool::new(false), }; - data_init.init(id, Arc::new(data)); + let surf = data_init.init(id, Arc::new(data)); + // Phase 7.4 : enregistrer le mapping SurfaceId → WlSurface + // pour permettre raise_at_cursor / set_focus depuis un hit_test. + state.surfaces_by_id.insert(surface_id, surf); } wl_compositor::Request::CreateRegion { id } => { // Région no-op : on alloue la resource pour que le client @@ -946,6 +992,18 @@ impl wayland_server::Dispatch> for Wayla let mut id_lock = data.id.lock().unwrap(); if let Some(id) = id_lock.take() { state.registry.destroy(id); + // Phase 7.4 : retirer du mapping inverse + clear focus + // si c'était la surface focalisée (sinon des events seraient + // envoyés à un wl_surface fantôme). + if let Some(removed) = state.surfaces_by_id.remove(&id) { + if state.focused_surface.as_ref() == Some(&removed) { + state.focused_surface = None; + } + } + // Si c'était le curseur custom, retomber sur le sprite par défaut. + if state.cursor_surface_id == Some(id) { + state.cursor_surface_id = None; + } } } _ => {} diff --git a/docs/phase7-4-1-initial-B-top.png b/docs/phase7-4-1-initial-B-top.png new file mode 100644 index 0000000000000000000000000000000000000000..745617b668f60a55cd12773d5ad9736bf73954dc GIT binary patch literal 1616 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>&kK3jF{7zbD~Bcgc}; zKn`P)x4R2Vf5y!~Acwug)7O>#6)P{hnqKs=*mj_hYKdz^NlIc#s#S7PDv)9@GB7gJ zH89mRG!8K^w=%Y{GP2Y*Ft9Q(I9If$3PnS1eoAIqC2kG-^+k>WHE6(XD9OxCEiOsS zEx^=cVg|9~hOAFHFhImTT^vIyZoR#GG53mth(lo14Y38xPV6)IJpQj$_$X}M*%WYW zpYnN2)rj!(QPqFGofZdLL>Tn74im2lJP(61p zL--e=2fzRR}``)Et$vM6cUu7!}*hYXooX-AZ+5YqQ zn^`N=nN&)P8uH&Cn7^N$`-2~ghgZWADDmnE!~K`;nwmqYV?lcy!G^QlTu zs6U-803vrM07;N;-o^M!vb724BTe%NfXXVgo1Fzt`ySIz=WR2L!KHbAT^@QlbpINOe@0Ea-r Y7W=+K?ynA*cWHt6p00i_>zopr0K$2g?EnA( literal 0 HcmV?d00001 diff --git a/docs/phase7-4-2-after-click-A-raised.png b/docs/phase7-4-2-after-click-A-raised.png new file mode 100644 index 0000000000000000000000000000000000000000..7b8d0d792dc39ef2ff0f5becf700211c493f72ab GIT binary patch literal 1930 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>&nD|Nnna!Udpk^tbg% zK#DQR+uem(kjLi}ki%Z$>Fdh=ij|jLO)vUbY&%d$wZt`|BqgyV)hf9t6-Y4{85o)A z8kp)D8iyE|TNzte85?OE7+4t?Fx>wkhoT`jKP5A*61RrJCWl2p4H|G8N-}d(i%Sx7 z3o!MVm_aPLA?s7lz`z#h>EaktaqI2fx4F{c5-ow-7l@tUJG6p%DUEC)^ZSE|F)3$TWR1U~7sdzQGa8A%* z6jF6q#Nsi9p;Jg9aG(XV3=Y^oW7?zpccx*6-ht1q4K+7n8m6DV!D44~SLwk2$r~BY zgLqsUjz<86?|>K~4gYx_fQ5jpZ|0_rOm7TD9!zJPGs}VTn*`T~t@{^piX`;UWZ2L5 z-~vd4Ps9JO=EfjpLJzhx&Y1~P#`$6QKR30223s|zKXnZ5P}aizogl4^4D~VxV60d9 z2~4avl5G#nXD@)V;(tZY2O1K=_u)EY43yRTZ<;#LxG?6LY=-Gj*5>_JLB<_rxG#4A z%3A+ry9LlC8^s=MWsI2vGKBj>R(-ax#sN8R#yI%{TR|F@GUR{RZURy!^k6Mx49H&* zoFCHawL!|b8Slv*mej?Cuk1HD9^FBBP^6N*2`)}X>0I?o2+?PM_7^I_} zdC%hghe0fN<~`{Q{Qe6Vs%3aTtozsJ2V(Jj*vDvN1+=Zu;DGgSYmoC289P7OD+I1< z>ilG@5Lm~lcq;#(%TDPwjql7tPx~?!U7N?bGjw^n(~iTpf(x%Je-4o;kF3%A;W3-( z$?T((1=poMOJRApxytN}`)Lsc`vNPmz;o;>r#M%-SSrubSpG!#sjZ?*Ww21-JyG{5 z+f#*})*f`JOtuPSW}LT-J)pr=jmd_ee*r_c4@-rVoCBknBjowrBcTVGWeltl zDQX8kzhL0naKfiy`9(&Ngoz6o&QE66IH2UrI8O~^z%rl#d>{i@E2LyV25^6{F$WnS z_8>D4WB@R!-2fR7)3AIa$bf?k&p9@<4~iUtl1Z-|p2iZP49vqjAC_^-e|ml+UMQiT zEXP{2{?IvK2PBoicjG!n*W$F<>H7-QT#)*ET-8RWraJSg>=aeoMGNClFoLF)2A z>Q1LI?%QL-z2Ov4*Dj8gn?oDU&yQ`{a#QobbalpSKx+B&{V}ZDZ)zX7E}g`3BY88! ze3l3|@`@awzc0<_+V5*+%E&~xup3nY4grI=`E$R#W&9c><_6+>y85}Sb4q9e0O{Q* A=l}o! literal 0 HcmV?d00001 diff --git a/docs/phase7-4-3-after-click-B-raised.png b/docs/phase7-4-3-after-click-B-raised.png new file mode 100644 index 0000000000000000000000000000000000000000..371d3799c27073a487bcb9ef64f4eddf83907e73 GIT binary patch literal 1602 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>&m|Nx1O;|9_xxwaxiC zK#DQR+uenwKjUT~ki%Z$>Fdh=ij|jLO)vUbY&%d$wZt`|BqgyV)hf9t6-Y4{85o)A z8kp)D8iyE|TNztenV4uB7+4t?WS%-=i=rVnKP5A*61N74={1c&4H|G8N-}d(i%Sx7 z3$W-hgIIDy)~6g8Ae^2qjv*Dd-rnu*y<#BZ5EwN>EF*CNlac5dzyE6$4k~Sw*L8aR zBL|Xa*X*Ifl$!=ie2y!n+9_xX2YLSVWd;fh zDL{zwgG+4KRi65CXufaqwB&U@Rp|gSV735=+?@aTsIDG;AdQyzKmi1Kb=OVnByUg_gHq!S;=(g9aGHN z7!YUHPNteTCe|H&CzBWiV?+ zeUb2{Su>exVpw;~naQ*#ZvSrXIG{mx@*RQ?tkk$aXdO6;91Fx|pDU?eUqc@nlx8up h5YDKhD!?I-ux0)--MfZ)|Bf1i_@1tQF6*2UngHB_r*;4U literal 0 HcmV?d00001 diff --git a/docs/phase7-4-focus-raise.md b/docs/phase7-4-focus-raise.md new file mode 100644 index 0000000..4fdc5f2 --- /dev/null +++ b/docs/phase7-4-focus-raise.md @@ -0,0 +1,199 @@ +# Phase 7.4 — Focus + raise on click + +> Document produit le 2026-05-13 dans le cadre du plan directeur +> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`. +> +> **Scope strict** : +> - sur clic gauche, faire `registry.hit_test(cursor_x, cursor_y)` +> - si la surface ciblée est non-cursor : `registry.raise(sid)` + +> `set_focus(matching wl_surface)` (transfert du keyboard focus) +> - ajouter un mapping inverse `SurfaceId → wl_surface::WlSurface` pour +> pouvoir retrouver la WlSurface depuis le hit_test +> - tester avec 2 fenêtres xdg_toplevel SHM en parallèle (fork) +> +> **Hors scope 7.4** : focus-follows-mouse, click-through, move/resize +> interactifs (7.7), filtrage par client des events pointer (7.6), +> validation des serials ack_configure (7.5). + +## Verdict + +**✅ Click-to-raise + focus transfer validés runtime, 2 clients SHM +parallèles, 4 captures de preuve.** + +| Capture | État | +|---|---| +| `phase7-4-1-initial-B-top.png` | B violet créée en dernier → au top du Z-order, A vert visible sous B. Curseur sur A (80, 80). | +| `phase7-4-2-after-click-A-raised.png` | Après clic à (80, 80) sur A : **A passe au top**, B passe en-dessous. Curseur a migré sur B (cycle programmé). | +| `phase7-4-3-after-click-B-raised.png` | Après clic à (400, 280) sur B : **B repasse au top**, A passe en-dessous. | + +Trace `/tmp/comp.log` côté Redox (extraite via redoxfs après le run) : + +``` +[comp] phase 0: cursor → A (80, 80) +[frontend] focus change: None → Some(SurfaceId(0)) # A créée +[frontend] focus change: Some(SurfaceId(0)) → Some(SurfaceId(1)) # B créée (commit auto-focus) +[comp] 1 input events from inputd # mouse_button 1 = press +[frontend] left-click @ (80, 80) → hit_test = Some(SurfaceId(0)) # A trouvée +[frontend] focus change: Some(SurfaceId(1)) → Some(SurfaceId(0)) # focus → A +[comp] 1 input events from inputd # mouse_button 0 = release +[comp] phase 1: cursor → B (400, 280) +[comp] 1 input events from inputd # 2e clic press +[frontend] left-click @ (400, 280) → hit_test = Some(SurfaceId(1)) # B trouvée +[frontend] focus change: Some(SurfaceId(0)) → Some(SurfaceId(1)) # focus → B +[comp] 1 input events from inputd # release +``` + +Le pipeline complet est validé end-to-end : +`mouse_button QEMU → ps2d → inputd → InputBackend::poll → +RedoxInputEvent::PointerButton → WaylandFrontend::forward_input → +SurfaceRegistry::hit_test → registry.raise + set_focus → +wl_keyboard.leave/enter broadcast`. + +## Modifications apportées + +### `redox-wl-wayland-frontend` + +**`WaylandFrontend`** : nouveau champ +```rust +surfaces_by_id: HashMap, +``` +peuplé dans le handler `wl_compositor::Request::CreateSurface` (en +récupérant le retour de `data_init.init(id, ...)`), nettoyé dans +`wl_surface::Request::Destroy` avec en plus le clear de +`focused_surface` et `cursor_surface_id` si la surface détruite était +ces références (évite les wl_surface fantômes dans les events suivants). + +**`forward_input(PointerButton { left, .. })`** : avant l'envoi des +events button aux pointers, si `left == true` : +1. `self.registry.hit_test(cursor_x, cursor_y) -> Option` +2. Si `Some(sid)` et la surface n'est pas un curseur + (`is_cursor.load() == false`), récupérer la `wl_surface` dans + `surfaces_by_id`, faire `registry.raise(sid)` puis + `set_focus(Some(surf))`. + +Le `set_focus` envoie déjà les `wl_keyboard.leave` / `wl_keyboard.enter` +et `wl_pointer.leave` / `wl_pointer.enter` sur les bons wl_surface, et +ses modifiers reset (cf phase 7.2). Pas de duplication de code. + +**Instrumentation** : `println!` du frontend à chaque clic +(`[frontend] left-click @ (...) → hit_test = ...`) et à chaque +changement de focus (`[frontend] focus change: old_sid → new_sid`). +Sort sur stdout du processus compositor ; le service init redirige +vers `/tmp/comp.log` côté Redox pour analyse post-run. + +### `redox-wl-test-client-shm-two` (nouveau crate) + +Binaire qui `fork()` une fois. Parent et enfant exécutent chacun la +même séquence de connexion Wayland + xdg-shell, mais avec : +- des couleurs ARGB différentes (vert pastel pour A, magenta pour B) +- un grand pictogramme central distinctif (pyramide tronquée "A" / + deux disques empilés "B") pour identifier la fenêtre dans les + screendumps +- un sleep 800 ms côté enfant pour garantir l'ordre A créée → B créée +- timeout 160 s aligné sur le compositor + +Le compositor place automatiquement A à `(60, 60)` et B à `(120, 120)` +grâce à son cascade offset 7.1, ce qui produit l'overlap voulu pour +tester le raise. + +Au début (avant tout clic) : B est au top car commit le plus récent +→ politique 6.4 "dernière surface commitée = au-dessus" (à raffiner +en phase 7.6/7.7). + +## Méthode de validation runtime + +Limitation runtime héritée 7.3 : Redox n'a pas de driver mouse +fonctionnel sous QEMU + virtio-vga (PS/2 mouse_button OK, PS/2 et USB +tablet mouse_move muets). Pour positionner le curseur dans une zone +précise avant le clic, le compositor a embarqué temporairement un +cycle de 4 phases qui appelaient `frontend.set_cursor_position(x, y)` +toutes les 15 s entre `(80, 80)` (uniquement sur A) et `(400, 280)` +(uniquement sur B). + +Pendant chaque phase, le script de test envoie via QMP : +``` +mouse_button 1 # press +mouse_button 0 # release +screendump /tmp/screen-N.ppm +``` + +Le clic, lui, passe par le pipeline réel et déclenche le code +production `forward_input → hit_test → raise + set_focus`. Le cycle +de positionnement est retiré du binaire après validation (comme en +7.3) ; seul le câblage production est commité. + +Le service init Redox utilisé (purement temporaire) : + +``` +# /usr/lib/init.d/40_phase74_focus +requires_weak 30_console +nowait /usr/bin/launch_phase74.sh + +# /usr/bin/launch_phase74.sh +#!/bin/sh +sleep 2 +/usr/bin/redox-wl-compositor > /tmp/comp.log 2>&1 & +sleep 4 +/usr/bin/redox-wl-test-client-shm-two > /tmp/client.log 2>&1 +``` + +À noter : l'init Redox ne sait pas parser correctement +`nowait sh -c "..."` avec guillemets dans la définition de service — +il faut passer par un script wrapper sur disque pour fork + redirection. + +## Limitations connues (à traiter en sous-tickets ultérieurs) + +- **Events pointer/keyboard broadcast** : le clic raise + focus est + correct, mais les events button/key sont encore broadcast à tous + les `wl_pointer` / `wl_keyboard` (pas filtrage par client focused). + Reportable à 7.6 (multi-clients). +- **Focus follows mouse non implémenté** : on doit cliquer pour + changer le focus (modèle click-to-focus). Optionnel selon politique + WM, pas dans le scope. +- **Pas de transition press/release tracked** : à chaque event + `PointerButton { left: true, .. }`, on rejoue le hit_test. En + pratique inputd n'envoie qu'1 event par transition donc OK, mais à + durcir en 7.5 si on observe des re-raises spurieux. +- **Surface détruite tout en étant focused** : géré (focused_surface + cleared, cursor_surface_id cleared), mais pas de re-focus auto sur + la prochaine surface du Z-order. Le client suivant qui commit + reprendra le focus via la politique 6.4 — acceptable pour 7.4. +- **Pas encore de fenêtre "popup"** : focus-stealing par xdg_popup + reportable. + +## Critère de fin 7.4 + +> Un clic gauche dans la fenêtre cliquée la fait passer au top du +> Z-order et lui transfère le keyboard focus, sans déstabiliser le +> compositor, multi-clients OK. + +**✅ Validé.** 2 clients SHM en parallèle, 2 transitions Z-order +distinctes, traces frontend explicites confirmant le hit_test et +les changements de focus. + +## Code + +``` +crates/redox-wl-wayland-frontend/ # +~30 lignes (HashMap mapping, hit_test branch, logs) +crates/redox-wl-test-client-shm-two/ # nouveau crate (~330 lignes) +``` + +## Suite phase 7.5 + +Robustesse paquet A : tests négatifs sur les protocoles. Clients qui : +- ferment sans destroy propre +- envoient ack_configure avec un mauvais serial +- détruisent un buffer encore attaché +- attachent un buffer avec width/height/stride invalides +- détruisent un xdg_surface avant son toplevel +- envoient des messages au mauvais moment du cycle de vie + +But : le compositor doit dans tous les cas soit ignorer proprement, +soit fermer la connexion du client fautif avec un `post_error`, mais +JAMAIS paniquer. + +Estimé : 1-2 sessions. + +--- + +*Fin du document de phase 7.4.*