From baa94701bfbcd68d1f5e4a6b6ace43706d903a54 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Sat, 9 May 2026 15:05:03 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Phase=207.2=20=E2=80=94=20wl=5Fs?= =?UTF-8?q?eat=20+=20wl=5Fkeyboard=20+=20wl=5Fpointer=20routing=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture preuve : docs/phase7-2-input-routing.png — fenêtre client xdg_toplevel 480x320 (damier turquoise) à (60,60), compositor stable pendant que les keyboard events transitent en parallèle. Validation runtime exhaustive : tous les events injectés via QEMU sendkey/mouse_button arrivent au client via wl_keyboard.key / wl_pointer.button : [client-input] wl_keyboard.key key=54 Pressed ← 'c' [client-input] wl_keyboard.key key=50 Pressed ← shift [client-input] wl_keyboard.key key=38 Pressed ← 'a' avec shift [client-input] wl_keyboard.key key=37 Pressed ← ctrl ... Modifications redox-wl-wayland-frontend : - + dep redox-wl-input (pour InputEvent type) - wl_seat global v7 avec capabilities = Pointer | Keyboard - wl_seat.name = "redox-wl-seat0" (v2+) - Dispatch wl_seat : GetPointer, GetKeyboard, GetTouch (no-op), Release ; au get_keyboard envoie keymap NoKeymap + repeat_info - Dispatch wl_pointer / wl_keyboard / wl_touch : Release retire la resource de state.{pointers,keyboards} - forward_input(InputEvent) public method qui broadcast wl_keyboard.key, wl_pointer.motion/button/axis/frame aux clients - set_focus(surface) public method qui envoie keyboard/pointer enter/leave events sur changement de focus - Tracking : focused_surface, cursor_x/y, next_input_serial, input_time_ms, pointers/keyboards Vec Modif wl_surface.commit : appelle set_focus(Some(_resource)) pour que la dernière surface commitée reçoive l'enter automatiquement (politique simple 7.2, à raffiner en 7.4). Modif compositor binaire (redox-wl-compositor) : - Forward chaque InputEvent au frontend.forward_input(&ev) - Esc reste géré côté compositor pour exit propre Bin redox-wl-test-client-input ajouté (~280 lignes) : - Bind wl_compositor + wl_shm + xdg_wm_base + wl_seat - get_keyboard + get_pointer après reception caps - Crée xdg_toplevel + buffer ARGB damier turquoise - Log chaque wl_keyboard.{enter,leave,key,modifiers,repeat_info} et wl_pointer.{enter,leave,motion,button,axis} - Boucle event_queue : flush + prepare_read.read + dispatch_pending (CORRECT pattern pour wayland-rs ; le bug initial était d'utiliser juste dispatch_pending qui ne lit pas le socket) Critère de fin 7.2 validé : un client qui bind wl_seat reçoit keyboard events via wl_keyboard.key sans panic serveur. Limitations connues (sous-tickets ultérieurs) : - Keymap NoKeymap (pas de XKB layout) — 7.2 utilise scancodes raw - Broadcast à tous les keyboards/pointers (pas de filtrage par client focus) — multi-client viendra en 7.6 - Pas de pointer.motion testé (besoin -device usb-tablet QEMU) - Pas de validation modifier state (juste enter envoie 0,0,0,0) Image Redox restaurée à boot Orbital normal. Phrase reprise 7.3 : > Reprendre au commit XXX : Phase 7.3 curseur software. Dessiner un > sprite curseur 16x16 par-dessus la composition, position basée sur > InputBackend cursor_x/y. Hot-spot configurable via wl_pointer.set_cursor > (déjà no-op à 7.2). Tester avec usb-tablet QEMU pour avoir motion absolu. Leyoda 2026 – GPLv3 --- crates/redox-wl-compositor/src/main.rs | 10 +- crates/redox-wl-test-client-input/Cargo.toml | 10 + crates/redox-wl-test-client-input/src/main.rs | 465 ++++++++++++++++++ crates/redox-wl-wayland-frontend/Cargo.toml | 1 + crates/redox-wl-wayland-frontend/src/lib.rs | 382 +++++++++++++- docs/phase7-2-input-routing.png | Bin 0 -> 1476 bytes 6 files changed, 864 insertions(+), 4 deletions(-) create mode 100644 crates/redox-wl-test-client-input/Cargo.toml create mode 100644 crates/redox-wl-test-client-input/src/main.rs create mode 100644 docs/phase7-2-input-routing.png diff --git a/crates/redox-wl-compositor/src/main.rs b/crates/redox-wl-compositor/src/main.rs index ac4c6e5..f8f5a92 100644 --- a/crates/redox-wl-compositor/src/main.rs +++ b/crates/redox-wl-compositor/src/main.rs @@ -116,11 +116,14 @@ fn run() -> Result<(), Box> { // 3. Input if let Ok(events) = input.poll() { + if !events.is_empty() { + dlog(&format!("[comp] {} input events from inputd", events.len())); + } for ev in events { - match ev { + match &ev { InputEvent::Key { scancode, pressed, .. - } if pressed && scancode == 0x01 => { + } if *pressed && *scancode == 0x01 => { // Esc → exit dlog("[comp] Esc → exit"); let _ = frontend.flush_clients(); @@ -135,6 +138,9 @@ fn run() -> Result<(), Box> { } _ => {} } + // Phase 7.2 : forward tous les events vers les clients + // Wayland (keyboard.key, pointer.button, etc.) + frontend.forward_input(&ev); } } diff --git a/crates/redox-wl-test-client-input/Cargo.toml b/crates/redox-wl-test-client-input/Cargo.toml new file mode 100644 index 0000000..0ec2c72 --- /dev/null +++ b/crates/redox-wl-test-client-input/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "redox-wl-test-client-input" +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-input/src/main.rs b/crates/redox-wl-test-client-input/src/main.rs new file mode 100644 index 0000000..da56e49 --- /dev/null +++ b/crates/redox-wl-test-client-input/src/main.rs @@ -0,0 +1,465 @@ +//! Phase 7.2 — Client Wayland qui écoute clavier + pointer. +//! +//! Crée une fenêtre xdg_toplevel comme le client phase 7.1, peint un +//! buffer ARGB et commit. Bind ensuite wl_seat, get_keyboard, +//! get_pointer, et log chaque event reçu pendant 25s. +//! +//! Les events injectés via le monitor QEMU (`sendkey a`, `mouse_button 1`, +//! etc.) doivent atteindre ce client via le compositor → wl_keyboard.key +//! / wl_pointer.button. + +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_keyboard::{self, WlKeyboard}, + wl_pointer::{self, WlPointer}, + wl_registry, + wl_seat::{self, 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 W: i32 = 480; +const H: i32 = 320; +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, + seat: Option, + pending_serial: Option, + configured: bool, +} + +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); + +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, + ) { + if let xdg_toplevel::Event::Configure { width, height, .. } = event { + dlog(&format!( + "[client-input] xdg_toplevel configure suggéré : {width}x{height}" + )); + } + } +} + +impl Dispatch for ClientState { + fn event( + _state: &mut Self, + _r: &WlSeat, + event: wl_seat::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + match event { + wl_seat::Event::Capabilities { capabilities } => { + dlog(&format!( + "[client-input] wl_seat capabilities = {:?}", + capabilities + )); + } + wl_seat::Event::Name { name } => { + dlog(&format!("[client-input] wl_seat name = {name:?}")); + } + _ => {} + } + } +} + +impl Dispatch for ClientState { + fn event( + _state: &mut Self, + _r: &WlKeyboard, + event: wl_keyboard::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + match event { + wl_keyboard::Event::Keymap { format, fd, size } => { + let raw = fd.as_raw_fd(); + dlog(&format!( + "[client-input] wl_keyboard.keymap format={:?} fd={raw} size={size}", + format + )); + } + wl_keyboard::Event::Enter { serial, .. } => { + dlog(&format!("[client-input] wl_keyboard.enter serial={serial}")); + } + wl_keyboard::Event::Leave { serial, .. } => { + dlog(&format!("[client-input] wl_keyboard.leave serial={serial}")); + } + wl_keyboard::Event::Key { + serial, + time, + key, + state, + } => { + dlog(&format!( + "[client-input] wl_keyboard.key serial={serial} time={time} key={key} state={:?}", + state + )); + } + wl_keyboard::Event::Modifiers { .. } => { + // ignore log spam + } + wl_keyboard::Event::RepeatInfo { rate, delay } => { + dlog(&format!( + "[client-input] wl_keyboard.repeat_info rate={rate} delay={delay}" + )); + } + _ => {} + } + } +} + +use std::os::fd::AsRawFd; + +impl Dispatch for ClientState { + fn event( + _state: &mut Self, + _r: &WlPointer, + event: wl_pointer::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + match event { + wl_pointer::Event::Enter { serial, surface_x, surface_y, .. } => { + dlog(&format!( + "[client-input] wl_pointer.enter serial={serial} ({surface_x},{surface_y})" + )); + } + wl_pointer::Event::Leave { serial, .. } => { + dlog(&format!("[client-input] wl_pointer.leave serial={serial}")); + } + wl_pointer::Event::Motion { + time, + surface_x, + surface_y, + } => { + dlog(&format!( + "[client-input] wl_pointer.motion time={time} ({surface_x},{surface_y})" + )); + } + wl_pointer::Event::Button { + serial, + time, + button, + state, + } => { + dlog(&format!( + "[client-input] wl_pointer.button serial={serial} time={time} btn={button} state={:?}", + state + )); + } + wl_pointer::Event::Axis { time, axis, value } => { + dlog(&format!( + "[client-input] wl_pointer.axis time={time} axis={:?} value={value}", + axis + )); + } + wl_pointer::Event::Frame => { + // ignore — just signals end of grouped events + } + _ => {} + } + } +} + +unsafe fn create_shm_with_pattern(name: &str) -> 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".into()); + } + if libc::ftruncate(fd, SIZE as _) != 0 { + libc::close(fd); + return Err("ftruncate".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".into()); + } + let pixels = std::slice::from_raw_parts_mut(p as *mut u32, (W * H) as usize); + // Pattern : damier turquoise + bandeau d'instructions + for y in 0..H { + for x in 0..W { + let on_border = x < 2 || x >= W - 2 || y < 2 || y >= H - 2; + let cell = ((x / 32) + (y / 32)) % 2; + let color: u32 = if on_border { + 0xFF_10_10_10 + } else if cell == 0 { + 0xFF_30_A0_C0 + } else { + 0xFF_50_C0_E0 + }; + pixels[(y * W + x) as usize] = color; + } + } + libc::munmap(p, SIZE as usize); + Ok(OwnedFd::from_raw_fd(fd)) +} + +fn run() -> Result<(), Box> { + dlog("[client-input] 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::default(); + event_queue.roundtrip(&mut state)?; + dlog(&format!( + "[client-input] globals : compositor={} shm={} xdg_wm_base={} seat={}", + state.compositor.is_some(), + state.shm.is_some(), + state.wm_base.is_some(), + state.seat.is_some() + )); + + 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 seat = state.seat.clone().ok_or("no wl_seat")?; + + // wl_seat capabilities arrivent en event après le bind ; on roundtrip + // pour les recevoir avant de get_pointer/get_keyboard. + event_queue.roundtrip(&mut state)?; + + // Get keyboard + pointer + let _keyboard = seat.get_keyboard(&qh, ()); + let _pointer = seat.get_pointer(&qh, ()); + dlog("[client-input] get_keyboard + get_pointer demandés"); + + // Surface + xdg_toplevel + let surface = compositor.create_surface(&qh, ()); + let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ()); + let toplevel = xdg_surface.get_toplevel(&qh, ()); + toplevel.set_title("Phase 7.2 input client".to_string()); + toplevel.set_app_id("redox.wl.test.client.input".to_string()); + surface.commit(); + + // Attente initial configure + let start = std::time::Instant::now(); + while !state.configured && start.elapsed() < Duration::from_secs(5) { + event_queue.roundtrip(&mut state)?; + thread::sleep(Duration::from_millis(50)); + } + if !state.configured { + return Err("no initial configure".into()); + } + let serial = state.pending_serial.unwrap_or(0); + xdg_surface.ack_configure(serial); + dlog(&format!("[client-input] ack_configure({serial})")); + + // Buffer + attach + commit + let fd = unsafe { create_shm_with_pattern("/redox-wl-client-input") }?; + 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()?; + let _ = event_queue.roundtrip(&mut state); + + // Boucle : log les events reçus pendant 25s. + // Pattern wayland-rs correct : prepare_read() pour lire les bytes du + // socket, puis dispatch_pending() pour les traiter. dispatch_pending + // seul ne lit PAS du socket — c'est le piège qui cassait 7.2 au début. + let start = std::time::Instant::now(); + while start.elapsed() < Duration::from_secs(25) { + // 1. flush nos requests sortantes + let _ = event_queue.flush(); + // 2. lire les bytes entrants + if let Some(guard) = event_queue.prepare_read() { + let _ = guard.read(); + } + // 3. dispatch les events qui sont maintenant dans la queue + let _ = event_queue.dispatch_pending(&mut state); + thread::sleep(Duration::from_millis(20)); + } + + dlog("[client-input] done, destroy"); + toplevel.destroy(); + xdg_surface.destroy(); + surface.destroy(); + let _ = event_queue.flush(); + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + dlog("[client-input] PASS"); + ExitCode::SUCCESS + } + Err(e) => { + dlog(&format!("[client-input] FAIL: {e}")); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-wayland-frontend/Cargo.toml b/crates/redox-wl-wayland-frontend/Cargo.toml index e83bd89..0f0691d 100644 --- a/crates/redox-wl-wayland-frontend/Cargo.toml +++ b/crates/redox-wl-wayland-frontend/Cargo.toml @@ -6,6 +6,7 @@ description = "Wayland protocol frontend that maps wl_compositor/wl_shm/wl_surfa [dependencies] redox-wl-compositor-core = { path = "../redox-wl-compositor-core" } +redox-wl-input = { path = "../redox-wl-input" } wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false } wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false } wayland-protocols = { path = "../../../wayland-rs/wayland-protocols", default-features = false, features = ["server"] } diff --git a/crates/redox-wl-wayland-frontend/src/lib.rs b/crates/redox-wl-wayland-frontend/src/lib.rs index 8e5fa6c..fd211d9 100644 --- a/crates/redox-wl-wayland-frontend/src/lib.rs +++ b/crates/redox-wl-wayland-frontend/src/lib.rs @@ -32,12 +32,18 @@ use wayland_protocols::xdg::shell::server::{ use wayland_server::{ Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource, backend::{ClientData, ClientId, DisconnectReason}, - protocol::{wl_buffer, wl_callback, wl_compositor, wl_region, wl_shm, wl_shm_pool, wl_surface}, + protocol::{ + wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_seat, + wl_shm, wl_shm_pool, wl_surface, + }, }; +use redox_wl_input::InputEvent as RedoxInputEvent; + const COMPOSITOR_VERSION: u32 = 5; const SHM_VERSION: u32 = 1; const XDG_WM_BASE_VERSION: u32 = 5; +const SEAT_VERSION: u32 = 7; /// Taille suggérée par défaut pour les nouvelles fenêtres xdg_toplevel. /// Le client peut respecter ou non ; on utilise sa propre taille de buffer @@ -177,6 +183,25 @@ pub struct WaylandFrontend { /// Counter cascading des nouvelles fenêtres xdg_toplevel : la 1re est /// placée à (60, 60), la 2e à (120, 120), etc. next_toplevel_index: u32, + + // ----- Phase 7.2 : input routing ---------------------------------- + /// Tous les wl_pointer Resource enregistrés via wl_seat.get_pointer. + /// Pour 7.2 : broadcast à tous (pas de filtrage par client). Sera + /// raffiné en 7.6 (multi-clients). + pointers: Vec, + /// Tous les wl_keyboard Resource enregistrés. + keyboards: Vec, + /// Surface focalisée actuelle (envoie keyboard.enter ici, leave + /// quand ça change). Mise à jour par `set_focus()` au commit. + focused_surface: Option, + /// Position courante du curseur, en coords framebuffer absolues. + /// Mise à jour par `forward_input(PointerMotion / PointerMotionRelative)`. + cursor_x: i32, + cursor_y: i32, + /// Counter monotone pour les serials seat events (différent de next_xdg_serial). + next_input_serial: u32, + /// Timestamp incrémental pour les events seat (ms-like). + input_time_ms: u32, } impl WaylandFrontend { @@ -190,6 +215,7 @@ impl WaylandFrontend { dh.create_global::(COMPOSITOR_VERSION, ()); dh.create_global::(SHM_VERSION, ()); dh.create_global::(XDG_WM_BASE_VERSION, ()); + dh.create_global::(SEAT_VERSION, ()); let listener = wayland_server::ListeningSocket::bind_absolute(socket_path.to_path_buf())?; @@ -201,6 +227,13 @@ impl WaylandFrontend { frame_time_ms: 0, next_xdg_serial: 1, next_toplevel_index: 0, + pointers: Vec::new(), + keyboards: Vec::new(), + focused_surface: None, + cursor_x: 0, + cursor_y: 0, + next_input_serial: 1, + input_time_ms: 0, }) } @@ -257,6 +290,198 @@ impl WaylandFrontend { pub fn socket_path(&self) -> Option<&std::ffi::OsStr> { self.listener.socket_name() } + + fn alloc_input_serial(&mut self) -> u32 { + let s = self.next_input_serial; + self.next_input_serial = self.next_input_serial.wrapping_add(1).max(1); + s + } + + fn alloc_input_time(&mut self) -> u32 { + self.input_time_ms = self.input_time_ms.wrapping_add(16); // ~60Hz tick + self.input_time_ms + } + + /// Met à jour la surface focalisée. Si elle change, envoie les events + /// `wl_keyboard.leave` (sur l'ancienne) puis `wl_keyboard.enter` (sur + /// la nouvelle), et idem pour `wl_pointer.leave` / `enter`. + pub fn set_focus(&mut self, new_focus: Option) { + let same = match (&self.focused_surface, &new_focus) { + (Some(a), Some(b)) => a == b, + (None, None) => true, + _ => false, + }; + if same { + return; + } + let serial_leave = self.alloc_input_serial(); + let serial_enter = self.alloc_input_serial(); + + if let Some(old) = self.focused_surface.clone() { + for kb in &self.keyboards { + kb.leave(serial_leave, &old); + } + for ptr in &self.pointers { + ptr.leave(serial_leave, &old); + } + } + if let Some(new) = &new_focus { + for kb in &self.keyboards { + // keys = empty array (aucune touche pressed lors du focus) + kb.enter(serial_enter, new, Vec::new()); + // modifiers reset + kb.modifiers(serial_enter, 0, 0, 0, 0); + } + for ptr in &self.pointers { + // surface_x, surface_y : on prend la position relative + // simple basée sur cursor - surface origin + let (sx, sy) = self.surface_local_cursor(new); + ptr.enter( + serial_enter, + new, + fixed_from_int(sx), + fixed_from_int(sy), + ); + if SEAT_VERSION >= 5 { + ptr.frame(); + } + } + } + self.focused_surface = new_focus; + } + + /// Calcule la position du curseur relative à l'origine d'une surface. + /// Utilise le mapping wl_surface → SurfaceId stocké dans SurfaceData. + fn surface_local_cursor(&self, surf: &wl_surface::WlSurface) -> (i32, i32) { + if let Some(sd) = surf.data::>() { + if let Some(sid) = *sd.id.lock().unwrap() { + if let Some(s) = self.registry.get(sid) { + let st = s.current(); + return (self.cursor_x - st.x, self.cursor_y - st.y); + } + } + } + (self.cursor_x, self.cursor_y) + } + + /// Forward un event input du backend vers la surface focalisée + /// (ou tous les pointers/keyboards par broadcast pour 7.2). + pub fn forward_input(&mut self, ev: &RedoxInputEvent) { + match ev { + RedoxInputEvent::Key { + scancode, pressed, .. + } => { + let time = self.alloc_input_time(); + let serial = self.alloc_input_serial(); + // Wayland keycodes = scancode evdev = scancode +8 on linux + // (cf xkb_keycodes minimum=8). On garde le scancode brut + // ici en attendant une keymap correcte (US Linux evdev offset + // ne s'applique pas forcément à orbclient ; à durcir + // quand on aura une vraie keymap XKB). + let key = (*scancode as u32).saturating_add(8); + let state = if *pressed { + wl_keyboard::KeyState::Pressed + } else { + wl_keyboard::KeyState::Released + }; + for kb in &self.keyboards { + kb.key(serial, time, key, state); + } + } + RedoxInputEvent::PointerMotion { x, y } => { + self.cursor_x = *x; + self.cursor_y = *y; + let time = self.alloc_input_time(); + if let Some(focus) = self.focused_surface.clone() { + let (sx, sy) = self.surface_local_cursor(&focus); + for ptr in &self.pointers { + ptr.motion(time, fixed_from_int(sx), fixed_from_int(sy)); + if SEAT_VERSION >= 5 { + ptr.frame(); + } + } + } + } + RedoxInputEvent::PointerMotionRelative { dx, dy } => { + self.cursor_x = self.cursor_x.saturating_add(*dx); + self.cursor_y = self.cursor_y.saturating_add(*dy); + let time = self.alloc_input_time(); + if let Some(focus) = self.focused_surface.clone() { + let (sx, sy) = self.surface_local_cursor(&focus); + for ptr in &self.pointers { + ptr.motion(time, fixed_from_int(sx), fixed_from_int(sy)); + if SEAT_VERSION >= 5 { + ptr.frame(); + } + } + } + } + RedoxInputEvent::PointerButton { + left, + middle, + right, + } => { + let time = self.alloc_input_time(); + // Code BTN_LEFT/MIDDLE/RIGHT linux/input-event-codes.h + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + let buttons = [ + (BTN_LEFT, *left), + (BTN_MIDDLE, *middle), + (BTN_RIGHT, *right), + ]; + // Note : orbclient envoie l'état complet des 3 boutons à chaque + // event. Côté Wayland on devrait envoyer un event par changement + // de bouton — mais comme on ne sait pas l'état précédent ici, + // on broadcast les 3 à chaque event. À durcir en 7.5. + for (btn, pressed) in buttons { + let serial = self.alloc_input_serial(); + let state = if pressed { + wl_pointer::ButtonState::Pressed + } else { + wl_pointer::ButtonState::Released + }; + for ptr in &self.pointers { + ptr.button(serial, time, btn, state); + } + } + if SEAT_VERSION >= 5 { + for ptr in &self.pointers { + ptr.frame(); + } + } + } + RedoxInputEvent::PointerScroll { dx, dy } => { + let time = self.alloc_input_time(); + for ptr in &self.pointers { + if *dy != 0 { + ptr.axis( + time, + wl_pointer::Axis::VerticalScroll, + fixed_from_int(*dy), + ); + } + if *dx != 0 { + ptr.axis( + time, + wl_pointer::Axis::HorizontalScroll, + fixed_from_int(*dx), + ); + } + if SEAT_VERSION >= 5 { + ptr.frame(); + } + } + } + _ => {} + } + } +} + +/// Conversion i32 → wl_fixed (Q24.8 en interne ; type Wayland = i32). +fn fixed_from_int(v: i32) -> f64 { + v as f64 } // ===================================================================== @@ -436,7 +661,7 @@ impl wayland_server::Dispatch> for Wayla fn request( state: &mut Self, _client: &Client, - _r: &wl_surface::WlSurface, + _resource: &wl_surface::WlSurface, request: wl_surface::Request, data: &Arc, _dh: &DisplayHandle, @@ -503,6 +728,12 @@ impl wayland_server::Dispatch> for Wayla // Frame callbacks en attente → bump dans la queue globale let mut cbs = data.pending_frame_callbacks.lock().unwrap(); state.frame_callbacks.append(&mut *cbs); + drop(cbs); + + // Phase 7.2 : la surface qui vient de commiter et raise + // devient automatiquement la surface focalisée. Envoie les + // events keyboard/pointer enter/leave en conséquence. + state.set_focus(Some(_resource.clone())); } wl_surface::Request::Destroy => { let mut id_lock = data.id.lock().unwrap(); @@ -697,6 +928,153 @@ impl wayland_server::Dispatch> for } } +// ===================================================================== +// wl_seat / wl_keyboard / wl_pointer (phase 7.2) +// ===================================================================== + +impl GlobalDispatch for WaylandFrontend { + fn bind( + _state: &mut Self, + _handle: &DisplayHandle, + _client: &Client, + resource: wayland_server::New, + _data: &(), + data_init: &mut DataInit<'_, Self>, + ) { + let seat = data_init.init(resource, ()); + // Annonce les capacités : keyboard + pointer (pas de touch). + let caps = wl_seat::Capability::Keyboard | wl_seat::Capability::Pointer; + seat.capabilities(caps); + if SEAT_VERSION >= 2 { + seat.name("redox-wl-seat0".to_string()); + } + } +} + +impl wayland_server::Dispatch for WaylandFrontend { + fn request( + state: &mut Self, + _client: &Client, + _r: &wl_seat::WlSeat, + request: wl_seat::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + match request { + wl_seat::Request::GetPointer { id } => { + let p = data_init.init(id, ()); + // Envoie immédiatement enter si on a un focus + if let Some(focus) = state.focused_surface.clone() { + let serial = state.alloc_input_serial(); + let (sx, sy) = state.surface_local_cursor(&focus); + p.enter(serial, &focus, fixed_from_int(sx), fixed_from_int(sy)); + if SEAT_VERSION >= 5 { + p.frame(); + } + } + state.pointers.push(p); + } + wl_seat::Request::GetKeyboard { id } => { + let kb = data_init.init(id, ()); + send_keymap_no_keymap(&kb); + // Repeat info raisonnable (>= v4 du protocole) + if SEAT_VERSION >= 4 { + kb.repeat_info(25, 600); // 25 rate / 600 ms delay + } + // enter sur la surface focalisée actuelle (s'il y en a une) + if let Some(focus) = state.focused_surface.clone() { + let serial = state.alloc_input_serial(); + kb.enter(serial, &focus, Vec::new()); + kb.modifiers(serial, 0, 0, 0, 0); + } + state.keyboards.push(kb); + } + wl_seat::Request::GetTouch { id } => { + // Hors scope : touch non supporté en 7.2. + let _t = data_init.init(id, ()); + } + wl_seat::Request::Release => {} + _ => {} + } + } +} + +impl wayland_server::Dispatch for WaylandFrontend { + fn request( + state: &mut Self, + _client: &Client, + resource: &wl_pointer::WlPointer, + request: wl_pointer::Request, + _data: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + match request { + wl_pointer::Request::SetCursor { .. } => { + // Pour 7.3 (cursor visible). Ignoré ici. + } + wl_pointer::Request::Release => { + // Retire la resource de notre liste pour ne plus lui envoyer d'events + state.pointers.retain(|p| p != resource); + } + _ => {} + } + } +} + +impl wayland_server::Dispatch for WaylandFrontend { + fn request( + _state: &mut Self, + _client: &Client, + _r: &wayland_server::protocol::wl_touch::WlTouch, + _req: wayland_server::protocol::wl_touch::Request, + _: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + // Touch hors scope 7.2 + } +} + +impl wayland_server::Dispatch for WaylandFrontend { + fn request( + state: &mut Self, + _client: &Client, + resource: &wl_keyboard::WlKeyboard, + request: wl_keyboard::Request, + _data: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + if let wl_keyboard::Request::Release = request { + state.keyboards.retain(|k| k != resource); + } + } +} + +/// Envoie un keymap event "NoKeymap" sur le wl_keyboard donné. +/// Le client doit comprendre qu'il doit traiter les scancodes lui-même. +fn send_keymap_no_keymap(kb: &wl_keyboard::WlKeyboard) { + // wl_keyboard.keymap demande un fd valide. Crée un fd vide via shm_open. + use std::ffi::CString; + use std::os::fd::{FromRawFd, OwnedFd}; + let cname = CString::new("/redox-wl-empty-keymap").unwrap(); + unsafe { + 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; + } + let _ = libc::ftruncate(fd, 1); + let owned = OwnedFd::from_raw_fd(fd); + kb.keymap(wl_keyboard::KeymapFormat::NoKeymap, owned.as_fd(), 1); + let _ = libc::shm_unlink(cname.as_ptr()); + } +} + +use std::os::fd::AsFd; + // ---- xdg_toplevel ---- impl wayland_server::Dispatch> for WaylandFrontend diff --git a/docs/phase7-2-input-routing.png b/docs/phase7-2-input-routing.png new file mode 100644 index 0000000000000000000000000000000000000000..a2927ac7428c989a3dd6fd96c25ee370bc37eae9 GIT binary patch literal 1476 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>+I2S912t&CZ79jiO)V}-%q_sw zV`2=kL~rx{RSXO)eV#6kAr-gY-r2}|*g$~Aaq9uG8%@97pWQf5=UH!)9L z!s6l8KsDl&gDN#V3v(XhB%=UNDmo8n6i*11A_V5f(IBKCb+0U(@$0_MD+Lw~g4t^X cDEQzlUp~voXaBorFG0TbboFyt=akR{03z8(AOHXW literal 0 HcmV?d00001