🎉 Phase 7.4 — focus + raise on click validés runtime
Sur clic gauche, le compositor fait hit_test à la position curseur, raise la surface ciblée au top du Z-order et transfère le keyboard focus à cette surface (broadcast wl_keyboard.leave/enter via le set_focus déjà implémenté en 7.2). Frontend additions : - HashMap<SurfaceId, wl_surface::WlSurface> dans WaylandFrontend, peuplée au wl_compositor.create_surface (capture du retour de data_init.init), nettoyée au wl_surface.destroy - Au wl_surface.destroy : clear focused_surface et cursor_surface_id si la surface détruite était l'une de ces références (évite les wl_surface fantômes dans les events suivants) - forward_input(PointerButton.left=true) déclenche registry.hit_test(cursor_x, cursor_y), puis si la cible n'est pas une surface curseur : registry.raise + set_focus(target) - println! tracing pour [frontend] left-click et focus change Nouveau crate : redox-wl-test-client-shm-two - Binaire qui fork() : parent = fenêtre A (verte, pyramide), enfant = fenêtre B (magenta, double cercle) après sleep 800ms - 2 connexions Wayland indépendantes au même socket compositor - timeout 160s aligné sur le compositor 180s Validation runtime : 4 captures synchronisées via cycle de positionnement curseur temporaire (retiré après) prouvent les 2 transitions de Z-order : - initial : B au top (commit le plus récent) - click@(80,80) → hit A → A passe au top - click@(400,280) → hit B → B repasse au top Traces /tmp/comp.log (extraites via redoxfs) confirment : [frontend] left-click @ (80, 80) → hit_test = Some(SurfaceId(0)) [frontend] focus change: Some(SurfaceId(1)) → Some(SurfaceId(0)) [frontend] left-click @ (400, 280) → hit_test = Some(SurfaceId(1)) [frontend] focus change: Some(SurfaceId(0)) → Some(SurfaceId(1)) Pipeline validé end-to-end : mouse_button QEMU → ps2d → inputd → InputBackend::poll → RedoxInputEvent::PointerButton → forward_input → hit_test → raise + set_focus → wl_keyboard.leave/enter broadcast. Doc complète : docs/phase7-4-focus-raise.md. Leyoda 2026 – GPLv3
This commit is contained in:
parent
5f7587e79e
commit
c40ca9fcc8
7 changed files with 653 additions and 1 deletions
10
crates/redox-wl-test-client-shm-two/Cargo.toml
Normal file
10
crates/redox-wl-test-client-shm-two/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "redox-wl-test-client-shm-two"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false }
|
||||
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
|
||||
wayland-protocols = { path = "../../../wayland-rs/wayland-protocols", default-features = false, features = ["client"] }
|
||||
libc = "0.2"
|
||||
385
crates/redox-wl-test-client-shm-two/src/main.rs
Normal file
385
crates/redox-wl-test-client-shm-two/src/main.rs
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
//! Phase 7.4 — Client à deux fenêtres pour valider focus + raise on click.
|
||||
//!
|
||||
//! Le parent fork 1 enfant. Chacun se connecte indépendamment au socket
|
||||
//! Wayland du compositor, crée son propre xdg_toplevel avec une couleur
|
||||
//! différente (vert pastel pour A, magenta pour B), et reste vivant 160 s.
|
||||
//!
|
||||
//! Avec le cascading offset du compositor (60, 60 puis 120, 120), les deux
|
||||
//! fenêtres se chevauchent partiellement. La fenêtre B est créée en dernier
|
||||
//! donc affichée au-dessus initialement.
|
||||
//!
|
||||
//! Pour valider le clic-raise : avec un cycle programmatique côté
|
||||
//! compositor qui repositionne le curseur sur A puis sur B et envoie un
|
||||
//! mouse_button via QEMU monitor, on peut voir le Z-order changer dans
|
||||
//! les screendumps.
|
||||
|
||||
use std::env;
|
||||
use std::ffi::CString;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::os::fd::{AsFd, FromRawFd, OwnedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::ExitCode;
|
||||
use std::ptr;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
|
||||
backend::Backend,
|
||||
protocol::{
|
||||
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_shm::WlShm,
|
||||
wl_shm_pool::WlShmPool, wl_surface::WlSurface,
|
||||
},
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{
|
||||
xdg_surface::{self, XdgSurface},
|
||||
xdg_toplevel::{self, XdgToplevel},
|
||||
xdg_wm_base::{self, XdgWmBase},
|
||||
};
|
||||
|
||||
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
|
||||
const W: i32 = 300;
|
||||
const H: i32 = 200;
|
||||
const STRIDE: i32 = W * 4;
|
||||
const SIZE: i32 = STRIDE * H;
|
||||
|
||||
struct DebugSink(Mutex<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>,
|
||||
pending_serial: Option<u32>,
|
||||
configured: bool,
|
||||
label: String,
|
||||
}
|
||||
|
||||
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, ()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Génère un pattern de remplissage uni avec bordure noire 2px, et un
|
||||
/// gros caractère central A ou B pour distinguer visuellement les
|
||||
/// fenêtres dans les screendumps.
|
||||
unsafe fn create_shm_pattern(name: &str, base: u32, letter: char) -> Result<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 failed".into());
|
||||
}
|
||||
if libc::ftruncate(fd, SIZE as _) != 0 {
|
||||
libc::close(fd);
|
||||
return Err("ftruncate failed".into());
|
||||
}
|
||||
let p = libc::mmap(
|
||||
ptr::null_mut(),
|
||||
SIZE as usize,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_SHARED,
|
||||
fd,
|
||||
0,
|
||||
);
|
||||
if p == libc::MAP_FAILED {
|
||||
libc::close(fd);
|
||||
return Err("mmap failed".into());
|
||||
}
|
||||
let pixels = std::slice::from_raw_parts_mut(p as *mut u32, (W * H) as usize);
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let on_border = x < 2 || x >= W - 2 || y < 2 || y >= H - 2;
|
||||
pixels[(y * W + x) as usize] = if on_border { 0xFF_10_10_10 } else { base };
|
||||
}
|
||||
}
|
||||
// Gros bloc 80x80 au centre formant la lettre (A : triangle ; B : rectangle barré)
|
||||
let cx = W / 2;
|
||||
let cy = H / 2;
|
||||
let lc: u32 = 0xFF_FF_FF_FF;
|
||||
match letter {
|
||||
'A' => {
|
||||
// pyramide simple 80x80
|
||||
for dy in -40i32..=40i32 {
|
||||
let halfw = 40 - dy.abs();
|
||||
for dx in -halfw..=halfw {
|
||||
let x = cx + dx;
|
||||
let y = cy + dy;
|
||||
if x >= 0 && x < W && y >= 0 && y < H {
|
||||
pixels[(y * W + x) as usize] = lc;
|
||||
}
|
||||
}
|
||||
}
|
||||
// un trou horizontal au tiers pour faire un "A"
|
||||
for dy in 10i32..=14i32 {
|
||||
let halfw = 40 - dy.abs();
|
||||
for dx in -halfw + 4..=halfw - 4 {
|
||||
let x = cx + dx;
|
||||
let y = cy + dy;
|
||||
if x >= 0 && x < W && y >= 0 && y < H {
|
||||
pixels[(y * W + x) as usize] = base;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'B' | _ => {
|
||||
// 2 cercles empilés simulés par 2 carrés arrondis
|
||||
for dy in -40i32..=40i32 {
|
||||
for dx in -30i32..=30i32 {
|
||||
let x = cx + dx;
|
||||
let y = cy + dy;
|
||||
let in_top = dy < -2 && (dx * dx + (dy + 20) * (dy + 20)) <= 20 * 20;
|
||||
let in_bot = dy > 2 && (dx * dx + (dy - 20) * (dy - 20)) <= 20 * 20;
|
||||
if (in_top || in_bot) && x >= 0 && x < W && y >= 0 && y < H {
|
||||
pixels[(y * W + x) as usize] = lc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
libc::munmap(p, SIZE as usize);
|
||||
Ok(OwnedFd::from_raw_fd(fd))
|
||||
}
|
||||
|
||||
fn run_one(label: &str, base_color: u32, letter: char, shm_name: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
dlog(&format!("[{label}] connect to compositor"));
|
||||
|
||||
for i in 0..50 {
|
||||
if std::path::Path::new(SOCKET_PATH).exists() {
|
||||
break;
|
||||
}
|
||||
if i == 49 {
|
||||
return Err("compositor socket missing after 5s".into());
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
let stream = UnixStream::connect(SOCKET_PATH)?;
|
||||
let backend = Backend::connect(stream)?;
|
||||
let conn = Connection::from_backend(backend);
|
||||
let mut event_queue: EventQueue<ClientState> = conn.new_event_queue();
|
||||
let qh = event_queue.handle();
|
||||
let _registry = conn.display().get_registry(&qh, ());
|
||||
|
||||
let mut state = ClientState {
|
||||
label: label.to_string(),
|
||||
..ClientState::default()
|
||||
};
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
let compositor = state.compositor.clone().ok_or("no wl_compositor")?;
|
||||
let shm = state.shm.clone().ok_or("no wl_shm")?;
|
||||
let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base")?;
|
||||
|
||||
let surface = compositor.create_surface(&qh, ());
|
||||
let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
|
||||
let toplevel = xdg_surface.get_toplevel(&qh, ());
|
||||
toplevel.set_title(format!("Phase 7.4 — fenêtre {label}"));
|
||||
toplevel.set_app_id(format!("redox.wl.test.client.shm-two.{label}"));
|
||||
surface.commit();
|
||||
dlog(&format!("[{label}] xdg_toplevel créé"));
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
while !state.configured && start.elapsed() < Duration::from_secs(5) {
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
let serial = state.pending_serial.ok_or("no initial configure")?;
|
||||
xdg_surface.ack_configure(serial);
|
||||
state.pending_serial = None;
|
||||
dlog(&format!("[{label}] ack_configure({serial})"));
|
||||
|
||||
let fd = unsafe { create_shm_pattern(shm_name, base_color, letter) }?;
|
||||
let pool = shm.create_pool(fd.as_fd(), SIZE, &qh, ());
|
||||
let buffer = pool.create_buffer(
|
||||
0,
|
||||
W,
|
||||
H,
|
||||
STRIDE,
|
||||
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
surface.damage_buffer(0, 0, W, H);
|
||||
surface.commit();
|
||||
event_queue.flush()?;
|
||||
dlog(&format!("[{label}] buffer commit POST-ack"));
|
||||
|
||||
// Boucle vivante 160 s avec dispatch des events
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < Duration::from_secs(160) {
|
||||
// Pattern flush + prepare_read + dispatch (cf phase 7.2 memo)
|
||||
let _ = event_queue.flush();
|
||||
if let Some(guard) = event_queue.prepare_read() {
|
||||
let _ = guard.read();
|
||||
}
|
||||
let _ = event_queue.dispatch_pending(&mut state);
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
|
||||
dlog(&format!("[{label}] destroy propre"));
|
||||
toplevel.destroy();
|
||||
xdg_surface.destroy();
|
||||
surface.destroy();
|
||||
let _ = event_queue.flush();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Mode enfant : le parent nous a re-exec avec ce flag.
|
||||
if env::var("REDOX_WL_CHILD").as_deref() == Ok("B") {
|
||||
// Décalage pour que l'ordre A→B soit déterministe : on attend 1s
|
||||
thread::sleep(Duration::from_millis(800));
|
||||
return run_one("B", 0xFF_8C_60_D0, 'B', "/redox-wl-client-shm-two-B");
|
||||
}
|
||||
|
||||
// Parent : fork
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
return Err("fork failed".into());
|
||||
}
|
||||
if pid == 0 {
|
||||
// Côté enfant : exec self avec REDOX_WL_CHILD=B
|
||||
// On peut aussi appeler run_one directement (pas besoin d'exec).
|
||||
unsafe {
|
||||
libc::setenv(
|
||||
CString::new("REDOX_WL_CHILD").unwrap().as_ptr(),
|
||||
CString::new("B").unwrap().as_ptr(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
// Inline : pas besoin d'exec, on fait directement le client B.
|
||||
thread::sleep(Duration::from_millis(800));
|
||||
return run_one("B", 0xFF_8C_60_D0, 'B', "/redox-wl-client-shm-two-B");
|
||||
}
|
||||
|
||||
// Parent : c'est le client A.
|
||||
let res = run_one("A", 0xFF_5A_C0_50, 'A', "/redox-wl-client-shm-two-A");
|
||||
|
||||
// Attendre l'enfant
|
||||
let mut status: i32 = 0;
|
||||
unsafe {
|
||||
libc::waitpid(pid, &mut status, 0);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
dlog("[parent] PASS");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
dlog(&format!("[parent] FAIL: {e}"));
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -215,6 +215,10 @@ pub struct WaylandFrontend {
|
|||
/// dernier `wl_pointer.set_cursor`. `None` = pas de curseur custom,
|
||||
/// on dessine le sprite par défaut (flèche 16x16).
|
||||
cursor_surface_id: Option<SurfaceId>,
|
||||
/// Phase 7.4 : mapping inverse SurfaceId → wl_surface, pour pouvoir
|
||||
/// faire set_focus(target) après un hit_test au clic. Peuplé au
|
||||
/// `wl_compositor.create_surface`, nettoyé au `wl_surface.destroy`.
|
||||
surfaces_by_id: HashMap<SurfaceId, wl_surface::WlSurface>,
|
||||
/// Hot-spot du curseur (offset à soustraire à cursor_x/y pour le placement).
|
||||
cursor_hot_x: i32,
|
||||
cursor_hot_y: i32,
|
||||
|
|
@ -259,6 +263,7 @@ impl WaylandFrontend {
|
|||
cursor_hot_x: 0,
|
||||
cursor_hot_y: 0,
|
||||
cursor_visible: false,
|
||||
surfaces_by_id: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -339,6 +344,18 @@ impl WaylandFrontend {
|
|||
if same {
|
||||
return;
|
||||
}
|
||||
let old_sid = self
|
||||
.focused_surface
|
||||
.as_ref()
|
||||
.and_then(|s| s.data::<Arc<SurfaceData>>())
|
||||
.and_then(|d| *d.id.lock().unwrap());
|
||||
let new_sid = new_focus
|
||||
.as_ref()
|
||||
.and_then(|s| s.data::<Arc<SurfaceData>>())
|
||||
.and_then(|d| *d.id.lock().unwrap());
|
||||
println!(
|
||||
"[frontend] focus change: {old_sid:?} → {new_sid:?}"
|
||||
);
|
||||
let serial_leave = self.alloc_input_serial();
|
||||
let serial_enter = self.alloc_input_serial();
|
||||
|
||||
|
|
@ -448,6 +465,32 @@ impl WaylandFrontend {
|
|||
middle,
|
||||
right,
|
||||
} => {
|
||||
// Phase 7.4 : sur clic gauche (left pressed), hit_test à la
|
||||
// position curseur et raise + set_focus à la surface ciblée.
|
||||
// À faire AVANT l'envoi des events button pour que la nouvelle
|
||||
// surface reçoive enter+modifiers en premier, puis le button.
|
||||
if *left {
|
||||
let hit = self.registry.hit_test(self.cursor_x, self.cursor_y);
|
||||
println!(
|
||||
"[frontend] left-click @ ({}, {}) → hit_test = {:?}",
|
||||
self.cursor_x, self.cursor_y, hit
|
||||
);
|
||||
if let Some(sid) = hit {
|
||||
// Ne pas réagir aux surfaces curseur (elles ne devraient pas
|
||||
// apparaître dans hit_test car visible=false, mais double-belt).
|
||||
if let Some(surf) = self.surfaces_by_id.get(&sid).cloned() {
|
||||
if !surf
|
||||
.data::<Arc<SurfaceData>>()
|
||||
.map(|d| d.is_cursor.load(Ordering::Relaxed))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.registry.raise(sid);
|
||||
self.set_focus(Some(surf));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let time = self.alloc_input_time();
|
||||
// Code BTN_LEFT/MIDDLE/RIGHT linux/input-event-codes.h
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
|
|
@ -723,7 +766,10 @@ impl wayland_server::Dispatch<wl_compositor::WlCompositor, ()> for WaylandFronte
|
|||
xdg_pending_initial_configure: Mutex::new(false),
|
||||
is_cursor: AtomicBool::new(false),
|
||||
};
|
||||
data_init.init(id, Arc::new(data));
|
||||
let surf = data_init.init(id, Arc::new(data));
|
||||
// Phase 7.4 : enregistrer le mapping SurfaceId → WlSurface
|
||||
// pour permettre raise_at_cursor / set_focus depuis un hit_test.
|
||||
state.surfaces_by_id.insert(surface_id, surf);
|
||||
}
|
||||
wl_compositor::Request::CreateRegion { id } => {
|
||||
// Région no-op : on alloue la resource pour que le client
|
||||
|
|
@ -946,6 +992,18 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
|
|||
let mut id_lock = data.id.lock().unwrap();
|
||||
if let Some(id) = id_lock.take() {
|
||||
state.registry.destroy(id);
|
||||
// Phase 7.4 : retirer du mapping inverse + clear focus
|
||||
// si c'était la surface focalisée (sinon des events seraient
|
||||
// envoyés à un wl_surface fantôme).
|
||||
if let Some(removed) = state.surfaces_by_id.remove(&id) {
|
||||
if state.focused_surface.as_ref() == Some(&removed) {
|
||||
state.focused_surface = None;
|
||||
}
|
||||
}
|
||||
// Si c'était le curseur custom, retomber sur le sprite par défaut.
|
||||
if state.cursor_surface_id == Some(id) {
|
||||
state.cursor_surface_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
|||
BIN
docs/phase7-4-1-initial-B-top.png
Normal file
BIN
docs/phase7-4-1-initial-B-top.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
docs/phase7-4-2-after-click-A-raised.png
Normal file
BIN
docs/phase7-4-2-after-click-A-raised.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
docs/phase7-4-3-after-click-B-raised.png
Normal file
BIN
docs/phase7-4-3-after-click-B-raised.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
199
docs/phase7-4-focus-raise.md
Normal file
199
docs/phase7-4-focus-raise.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# Phase 7.4 — Focus + raise on click
|
||||
|
||||
> Document produit le 2026-05-13 dans le cadre du plan directeur
|
||||
> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`.
|
||||
>
|
||||
> **Scope strict** :
|
||||
> - sur clic gauche, faire `registry.hit_test(cursor_x, cursor_y)`
|
||||
> - si la surface ciblée est non-cursor : `registry.raise(sid)` +
|
||||
> `set_focus(matching wl_surface)` (transfert du keyboard focus)
|
||||
> - ajouter un mapping inverse `SurfaceId → wl_surface::WlSurface` pour
|
||||
> pouvoir retrouver la WlSurface depuis le hit_test
|
||||
> - tester avec 2 fenêtres xdg_toplevel SHM en parallèle (fork)
|
||||
>
|
||||
> **Hors scope 7.4** : focus-follows-mouse, click-through, move/resize
|
||||
> interactifs (7.7), filtrage par client des events pointer (7.6),
|
||||
> validation des serials ack_configure (7.5).
|
||||
|
||||
## Verdict
|
||||
|
||||
**✅ Click-to-raise + focus transfer validés runtime, 2 clients SHM
|
||||
parallèles, 4 captures de preuve.**
|
||||
|
||||
| Capture | État |
|
||||
|---|---|
|
||||
| `phase7-4-1-initial-B-top.png` | B violet créée en dernier → au top du Z-order, A vert visible sous B. Curseur sur A (80, 80). |
|
||||
| `phase7-4-2-after-click-A-raised.png` | Après clic à (80, 80) sur A : **A passe au top**, B passe en-dessous. Curseur a migré sur B (cycle programmé). |
|
||||
| `phase7-4-3-after-click-B-raised.png` | Après clic à (400, 280) sur B : **B repasse au top**, A passe en-dessous. |
|
||||
|
||||
Trace `/tmp/comp.log` côté Redox (extraite via redoxfs après le run) :
|
||||
|
||||
```
|
||||
[comp] phase 0: cursor → A (80, 80)
|
||||
[frontend] focus change: None → Some(SurfaceId(0)) # A créée
|
||||
[frontend] focus change: Some(SurfaceId(0)) → Some(SurfaceId(1)) # B créée (commit auto-focus)
|
||||
[comp] 1 input events from inputd # mouse_button 1 = press
|
||||
[frontend] left-click @ (80, 80) → hit_test = Some(SurfaceId(0)) # A trouvée
|
||||
[frontend] focus change: Some(SurfaceId(1)) → Some(SurfaceId(0)) # focus → A
|
||||
[comp] 1 input events from inputd # mouse_button 0 = release
|
||||
[comp] phase 1: cursor → B (400, 280)
|
||||
[comp] 1 input events from inputd # 2e clic press
|
||||
[frontend] left-click @ (400, 280) → hit_test = Some(SurfaceId(1)) # B trouvée
|
||||
[frontend] focus change: Some(SurfaceId(0)) → Some(SurfaceId(1)) # focus → B
|
||||
[comp] 1 input events from inputd # release
|
||||
```
|
||||
|
||||
Le pipeline complet est validé end-to-end :
|
||||
`mouse_button QEMU → ps2d → inputd → InputBackend::poll →
|
||||
RedoxInputEvent::PointerButton → WaylandFrontend::forward_input →
|
||||
SurfaceRegistry::hit_test → registry.raise + set_focus →
|
||||
wl_keyboard.leave/enter broadcast`.
|
||||
|
||||
## Modifications apportées
|
||||
|
||||
### `redox-wl-wayland-frontend`
|
||||
|
||||
**`WaylandFrontend`** : nouveau champ
|
||||
```rust
|
||||
surfaces_by_id: HashMap<SurfaceId, wl_surface::WlSurface>,
|
||||
```
|
||||
peuplé dans le handler `wl_compositor::Request::CreateSurface` (en
|
||||
récupérant le retour de `data_init.init(id, ...)`), nettoyé dans
|
||||
`wl_surface::Request::Destroy` avec en plus le clear de
|
||||
`focused_surface` et `cursor_surface_id` si la surface détruite était
|
||||
ces références (évite les wl_surface fantômes dans les events suivants).
|
||||
|
||||
**`forward_input(PointerButton { left, .. })`** : avant l'envoi des
|
||||
events button aux pointers, si `left == true` :
|
||||
1. `self.registry.hit_test(cursor_x, cursor_y) -> Option<SurfaceId>`
|
||||
2. Si `Some(sid)` et la surface n'est pas un curseur
|
||||
(`is_cursor.load() == false`), récupérer la `wl_surface` dans
|
||||
`surfaces_by_id`, faire `registry.raise(sid)` puis
|
||||
`set_focus(Some(surf))`.
|
||||
|
||||
Le `set_focus` envoie déjà les `wl_keyboard.leave` / `wl_keyboard.enter`
|
||||
et `wl_pointer.leave` / `wl_pointer.enter` sur les bons wl_surface, et
|
||||
ses modifiers reset (cf phase 7.2). Pas de duplication de code.
|
||||
|
||||
**Instrumentation** : `println!` du frontend à chaque clic
|
||||
(`[frontend] left-click @ (...) → hit_test = ...`) et à chaque
|
||||
changement de focus (`[frontend] focus change: old_sid → new_sid`).
|
||||
Sort sur stdout du processus compositor ; le service init redirige
|
||||
vers `/tmp/comp.log` côté Redox pour analyse post-run.
|
||||
|
||||
### `redox-wl-test-client-shm-two` (nouveau crate)
|
||||
|
||||
Binaire qui `fork()` une fois. Parent et enfant exécutent chacun la
|
||||
même séquence de connexion Wayland + xdg-shell, mais avec :
|
||||
- des couleurs ARGB différentes (vert pastel pour A, magenta pour B)
|
||||
- un grand pictogramme central distinctif (pyramide tronquée "A" /
|
||||
deux disques empilés "B") pour identifier la fenêtre dans les
|
||||
screendumps
|
||||
- un sleep 800 ms côté enfant pour garantir l'ordre A créée → B créée
|
||||
- timeout 160 s aligné sur le compositor
|
||||
|
||||
Le compositor place automatiquement A à `(60, 60)` et B à `(120, 120)`
|
||||
grâce à son cascade offset 7.1, ce qui produit l'overlap voulu pour
|
||||
tester le raise.
|
||||
|
||||
Au début (avant tout clic) : B est au top car commit le plus récent
|
||||
→ politique 6.4 "dernière surface commitée = au-dessus" (à raffiner
|
||||
en phase 7.6/7.7).
|
||||
|
||||
## Méthode de validation runtime
|
||||
|
||||
Limitation runtime héritée 7.3 : Redox n'a pas de driver mouse
|
||||
fonctionnel sous QEMU + virtio-vga (PS/2 mouse_button OK, PS/2 et USB
|
||||
tablet mouse_move muets). Pour positionner le curseur dans une zone
|
||||
précise avant le clic, le compositor a embarqué temporairement un
|
||||
cycle de 4 phases qui appelaient `frontend.set_cursor_position(x, y)`
|
||||
toutes les 15 s entre `(80, 80)` (uniquement sur A) et `(400, 280)`
|
||||
(uniquement sur B).
|
||||
|
||||
Pendant chaque phase, le script de test envoie via QMP :
|
||||
```
|
||||
mouse_button 1 # press
|
||||
mouse_button 0 # release
|
||||
screendump /tmp/screen-N.ppm
|
||||
```
|
||||
|
||||
Le clic, lui, passe par le pipeline réel et déclenche le code
|
||||
production `forward_input → hit_test → raise + set_focus`. Le cycle
|
||||
de positionnement est retiré du binaire après validation (comme en
|
||||
7.3) ; seul le câblage production est commité.
|
||||
|
||||
Le service init Redox utilisé (purement temporaire) :
|
||||
|
||||
```
|
||||
# /usr/lib/init.d/40_phase74_focus
|
||||
requires_weak 30_console
|
||||
nowait /usr/bin/launch_phase74.sh
|
||||
|
||||
# /usr/bin/launch_phase74.sh
|
||||
#!/bin/sh
|
||||
sleep 2
|
||||
/usr/bin/redox-wl-compositor > /tmp/comp.log 2>&1 &
|
||||
sleep 4
|
||||
/usr/bin/redox-wl-test-client-shm-two > /tmp/client.log 2>&1
|
||||
```
|
||||
|
||||
À noter : l'init Redox ne sait pas parser correctement
|
||||
`nowait sh -c "..."` avec guillemets dans la définition de service —
|
||||
il faut passer par un script wrapper sur disque pour fork + redirection.
|
||||
|
||||
## Limitations connues (à traiter en sous-tickets ultérieurs)
|
||||
|
||||
- **Events pointer/keyboard broadcast** : le clic raise + focus est
|
||||
correct, mais les events button/key sont encore broadcast à tous
|
||||
les `wl_pointer` / `wl_keyboard` (pas filtrage par client focused).
|
||||
Reportable à 7.6 (multi-clients).
|
||||
- **Focus follows mouse non implémenté** : on doit cliquer pour
|
||||
changer le focus (modèle click-to-focus). Optionnel selon politique
|
||||
WM, pas dans le scope.
|
||||
- **Pas de transition press/release tracked** : à chaque event
|
||||
`PointerButton { left: true, .. }`, on rejoue le hit_test. En
|
||||
pratique inputd n'envoie qu'1 event par transition donc OK, mais à
|
||||
durcir en 7.5 si on observe des re-raises spurieux.
|
||||
- **Surface détruite tout en étant focused** : géré (focused_surface
|
||||
cleared, cursor_surface_id cleared), mais pas de re-focus auto sur
|
||||
la prochaine surface du Z-order. Le client suivant qui commit
|
||||
reprendra le focus via la politique 6.4 — acceptable pour 7.4.
|
||||
- **Pas encore de fenêtre "popup"** : focus-stealing par xdg_popup
|
||||
reportable.
|
||||
|
||||
## Critère de fin 7.4
|
||||
|
||||
> Un clic gauche dans la fenêtre cliquée la fait passer au top du
|
||||
> Z-order et lui transfère le keyboard focus, sans déstabiliser le
|
||||
> compositor, multi-clients OK.
|
||||
|
||||
**✅ Validé.** 2 clients SHM en parallèle, 2 transitions Z-order
|
||||
distinctes, traces frontend explicites confirmant le hit_test et
|
||||
les changements de focus.
|
||||
|
||||
## Code
|
||||
|
||||
```
|
||||
crates/redox-wl-wayland-frontend/ # +~30 lignes (HashMap mapping, hit_test branch, logs)
|
||||
crates/redox-wl-test-client-shm-two/ # nouveau crate (~330 lignes)
|
||||
```
|
||||
|
||||
## Suite phase 7.5
|
||||
|
||||
Robustesse paquet A : tests négatifs sur les protocoles. Clients qui :
|
||||
- ferment sans destroy propre
|
||||
- envoient ack_configure avec un mauvais serial
|
||||
- détruisent un buffer encore attaché
|
||||
- attachent un buffer avec width/height/stride invalides
|
||||
- détruisent un xdg_surface avant son toplevel
|
||||
- envoient des messages au mauvais moment du cycle de vie
|
||||
|
||||
But : le compositor doit dans tous les cas soit ignorer proprement,
|
||||
soit fermer la connexion du client fautif avec un `post_error`, mais
|
||||
JAMAIS paniquer.
|
||||
|
||||
Estimé : 1-2 sessions.
|
||||
|
||||
---
|
||||
|
||||
*Fin du document de phase 7.4.*
|
||||
Loading…
Add table
Add a link
Reference in a new issue