🎉 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
This commit is contained in:
Votre Nom 2026-05-09 15:05:03 +02:00
parent 4bff319c7f
commit baa94701bf
6 changed files with 864 additions and 4 deletions

View file

@ -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<wl_pointer::WlPointer>,
/// Tous les wl_keyboard Resource enregistrés.
keyboards: Vec<wl_keyboard::WlKeyboard>,
/// Surface focalisée actuelle (envoie keyboard.enter ici, leave
/// quand ça change). Mise à jour par `set_focus()` au commit.
focused_surface: Option<wl_surface::WlSurface>,
/// 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::<Self, wl_compositor::WlCompositor, _>(COMPOSITOR_VERSION, ());
dh.create_global::<Self, wl_shm::WlShm, _>(SHM_VERSION, ());
dh.create_global::<Self, xdg_wm_base::XdgWmBase, _>(XDG_WM_BASE_VERSION, ());
dh.create_global::<Self, wl_seat::WlSeat, _>(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<wl_surface::WlSurface>) {
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::<Arc<SurfaceData>>() {
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<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
fn request(
state: &mut Self,
_client: &Client,
_r: &wl_surface::WlSurface,
_resource: &wl_surface::WlSurface,
request: wl_surface::Request,
data: &Arc<SurfaceData>,
_dh: &DisplayHandle,
@ -503,6 +728,12 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> 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<xdg_surface::XdgSurface, Arc<XdgSurfaceData>> for
}
}
// =====================================================================
// wl_seat / wl_keyboard / wl_pointer (phase 7.2)
// =====================================================================
impl GlobalDispatch<wl_seat::WlSeat, ()> for WaylandFrontend {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: wayland_server::New<wl_seat::WlSeat>,
_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<wl_seat::WlSeat, ()> 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<wl_pointer::WlPointer, ()> 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<wayland_server::protocol::wl_touch::WlTouch, ()> 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<wl_keyboard::WlKeyboard, ()> 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<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
for WaylandFrontend