🎉 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:
parent
4bff319c7f
commit
baa94701bf
6 changed files with 864 additions and 4 deletions
|
|
@ -116,11 +116,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
// 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<dyn std::error::Error>> {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
// Phase 7.2 : forward tous les events vers les clients
|
||||
// Wayland (keyboard.key, pointer.button, etc.)
|
||||
frontend.forward_input(&ev);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
10
crates/redox-wl-test-client-input/Cargo.toml
Normal file
10
crates/redox-wl-test-client-input/Cargo.toml
Normal file
|
|
@ -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"
|
||||
465
crates/redox-wl-test-client-input/src/main.rs
Normal file
465
crates/redox-wl-test-client-input/src/main.rs
Normal file
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
docs/phase7-2-input-routing.png
Normal file
BIN
docs/phase7-2-input-routing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Loading…
Add table
Add a link
Reference in a new issue