redox-wayland-compositor/crates/redox-wl-test-client-input/src/main.rs
Votre Nom baa94701bf 🎉 Phase 7.2 — wl_seat + wl_keyboard + wl_pointer routing input
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<Resource>

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
2026-05-09 15:05:03 +02:00

465 lines
14 KiB
Rust

//! 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<Option<std::fs::File>>);
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<DebugSink> = OnceLock::new();
SINK.get_or_init(DebugSink::new).writeln(s);
}
#[derive(Default)]
struct ClientState {
compositor: Option<WlCompositor>,
shm: Option<WlShm>,
wm_base: Option<XdgWmBase>,
seat: Option<WlSeat>,
pending_serial: Option<u32>,
configured: bool,
}
impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_data: &(),
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
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<Self>,
) {
}
}
};
}
noop!(WlCompositor);
noop!(WlShm);
noop!(WlShmPool);
noop!(WlBuffer);
noop!(WlSurface);
impl Dispatch<XdgWmBase, ()> for ClientState {
fn event(
_state: &mut Self,
wm_base: &XdgWmBase,
event: xdg_wm_base::Event,
_: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
if let xdg_wm_base::Event::Ping { serial } = event {
wm_base.pong(serial);
}
}
}
impl Dispatch<XdgSurface, ()> for ClientState {
fn event(
state: &mut Self,
_xdg_surf: &XdgSurface,
event: xdg_surface::Event,
_: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
if let xdg_surface::Event::Configure { serial } = event {
state.pending_serial = Some(serial);
state.configured = true;
}
}
}
impl Dispatch<XdgToplevel, ()> for ClientState {
fn event(
_state: &mut Self,
_r: &XdgToplevel,
event: xdg_toplevel::Event,
_: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
if let xdg_toplevel::Event::Configure { width, height, .. } = event {
dlog(&format!(
"[client-input] xdg_toplevel configure suggéré : {width}x{height}"
));
}
}
}
impl Dispatch<WlSeat, ()> for ClientState {
fn event(
_state: &mut Self,
_r: &WlSeat,
event: wl_seat::Event,
_: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
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<WlKeyboard, ()> for ClientState {
fn event(
_state: &mut Self,
_r: &WlKeyboard,
event: wl_keyboard::Event,
_: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
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<WlPointer, ()> for ClientState {
fn event(
_state: &mut Self,
_r: &WlPointer,
event: wl_pointer::Event,
_: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
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<OwnedFd, String> {
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<dyn std::error::Error>> {
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<ClientState> = 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
}
}
}