//! Phase 13.2.b.3 — Client de test visuel pour wl_subcompositor. //! //! Crée un toplevel parent 300×200 bleu uni avec bordure noire, et lui //! attache une subsurface 60×60 rouge à l'offset (50, 50). Si le rendering //! côté compositor (phase 13.2.b.2) est correct, on doit voir à l'écran : //! //! - Fenêtre bleue en haut-gauche (selon cascading compositor) //! - Un carré rouge dedans, en haut-gauche de la fenêtre bleue, à 50px //! du coin haut-gauche du parent //! //! Le client tourne ~60s puis exit. ESC ferme proprement avant timeout //! (path keyboard via wl_keyboard.enter envoyé au focus = parent toplevel). //! //! NB : sans cascade sync ni propagation parent-move (limitations 13.2.b.2), //! la subsurface est figée à la position de création. Acceptable pour //! validation visuelle de base. 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::{ backend::Backend, protocol::{ wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_keyboard, wl_registry, wl_seat::WlSeat, wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_subcompositor::WlSubcompositor, wl_subsurface::WlSubsurface, wl_surface::WlSurface, }, Connection, Dispatch, EventQueue, Proxy, QueueHandle, }; 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"; // Parent : 300×200 bleu nuit const PW: i32 = 300; const PH: i32 = 200; const PSTRIDE: i32 = PW * 4; const PSIZE: i32 = PSTRIDE * PH; const PCOLOR: u32 = 0xFF_22_44_AA; // Subsurface : 60×60 rouge, offset (50, 50) const SW: i32 = 60; const SH: i32 = 60; const SSTRIDE: i32 = SW * 4; const SSIZE: i32 = SSTRIDE * SH; const SCOLOR: u32 = 0xFF_CC_30_30; const SOFF_X: i32 = 50; const SOFF_Y: i32 = 50; struct DebugSink(Mutex>); impl DebugSink { fn new() -> Self { Self(Mutex::new( OpenOptions::new().write(true).open("/scheme/debug").ok(), )) } fn writeln(&self, s: &str) { println!("{s}"); if let Ok(mut g) = self.0.lock() { if let Some(f) = g.as_mut() { let _ = writeln!(f, "{s}"); } } } } fn dlog(s: &str) { static SINK: OnceLock = OnceLock::new(); SINK.get_or_init(DebugSink::new).writeln(s); } #[derive(Default)] struct State { compositor: Option, shm: Option, wm_base: Option, seat: Option, subcompositor: Option, pending_serial: Option, configured: bool, running: bool, } impl Dispatch for State { fn event( state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _conn: &Connection, qh: &QueueHandle, ) { if let wl_registry::Event::Global { name, interface, version, } = event { match interface.as_str() { "wl_compositor" => { state.compositor = Some(registry.bind(name, version.min(5), qh, ())); } "wl_shm" => { state.shm = Some(registry.bind(name, 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, ())); } "wl_subcompositor" => { state.subcompositor = Some(registry.bind(name, 1, qh, ())); } _ => {} } } } } macro_rules! noop { ($ty:ty) => { impl Dispatch<$ty, ()> for State { fn event( _: &mut Self, _: &$ty, _: <$ty as Proxy>::Event, _: &(), _: &Connection, _: &QueueHandle, ) { } } }; } noop!(WlCompositor); noop!(WlShm); noop!(WlShmPool); noop!(WlBuffer); noop!(WlSurface); noop!(WlSeat); noop!(WlSubcompositor); noop!(WlSubsurface); impl Dispatch for State { fn event( _: &mut Self, wm_base: &XdgWmBase, event: xdg_wm_base::Event, _: &(), _: &Connection, _: &QueueHandle, ) { if let xdg_wm_base::Event::Ping { serial } = event { wm_base.pong(serial); } } } impl Dispatch for State { fn event( state: &mut Self, _: &XdgSurface, event: xdg_surface::Event, _: &(), _: &Connection, _: &QueueHandle, ) { if let xdg_surface::Event::Configure { serial } = event { state.pending_serial = Some(serial); state.configured = true; } } } impl Dispatch for State { fn event( state: &mut Self, _: &XdgToplevel, event: xdg_toplevel::Event, _: &(), _: &Connection, _: &QueueHandle, ) { if matches!(event, xdg_toplevel::Event::Close) { state.running = false; } } } // ESC → exit. Même logique qu'en 13.1.b sur le client simple_window. impl Dispatch for State { fn event( state: &mut Self, _: &wl_keyboard::WlKeyboard, event: wl_keyboard::Event, _: &(), _: &Connection, _: &QueueHandle, ) { if let wl_keyboard::Event::Key { key, .. } = event { if key == 1 { dlog("[sub-client] ESC → exit"); state.running = false; } } } } unsafe fn create_shm(name: &str, size: i32, w: i32, h: i32, color: u32) -> Result { let cname = CString::new(name).unwrap(); let _ = libc::shm_unlink(cname.as_ptr()); let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600); if fd < 0 { return Err("shm_open 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); // Remplissage uni avec bordure noire 2px 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 { color }; } } libc::munmap(p, size as usize); Ok(OwnedFd::from_raw_fd(fd)) } fn run() -> Result<(), Box> { dlog("[sub-client] start (Phase 13.2.b.3 — visual subsurface)"); for i in 0..50 { if std::path::Path::new(SOCKET_PATH).exists() { break; } if i == 49 { return Err("compositor socket missing after 5s".into()); } thread::sleep(Duration::from_millis(100)); } let stream = UnixStream::connect(SOCKET_PATH)?; let backend = Backend::connect(stream)?; let conn = Connection::from_backend(backend); let mut event_queue: EventQueue = conn.new_event_queue(); let qh = event_queue.handle(); let _registry = conn.display().get_registry(&qh, ()); let mut state = State { running: true, ..State::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 subcomp = state .subcompositor .clone() .ok_or("no wl_subcompositor (compositor patché 13.2.b.1 ?)")?; let seat = state.seat.clone(); // Bind keyboard pour gérer ESC if let Some(s) = &seat { let _kb = s.get_keyboard(&qh, ()); } // --- Parent toplevel --- let parent_surface = compositor.create_surface(&qh, ()); let xdg_surface = wm_base.get_xdg_surface(&parent_surface, &qh, ()); let toplevel = xdg_surface.get_toplevel(&qh, ()); toplevel.set_title("Phase 13.2.b.3 — subcompositor visual".into()); toplevel.set_app_id("redox.wl.test.client.subcompositor".into()); parent_surface.commit(); dlog("[sub-client] parent 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!("[sub-client] ack_configure({serial})")); // Parent buffer + attach + commit let parent_fd = unsafe { create_shm("redox-sub-client-parent", PSIZE, PW, PH, PCOLOR) }?; let parent_pool = shm.create_pool(parent_fd.as_fd(), PSIZE, &qh, ()); let parent_buffer = parent_pool.create_buffer( 0, PW, PH, PSTRIDE, wayland_client::protocol::wl_shm::Format::Argb8888, &qh, (), ); parent_surface.attach(Some(&parent_buffer), 0, 0); parent_surface.commit(); dlog("[sub-client] parent buffer attached + commit"); // --- Subsurface --- let child_surface = compositor.create_surface(&qh, ()); let subsurface: WlSubsurface = subcomp.get_subsurface(&child_surface, &parent_surface, &qh, ()); subsurface.set_position(SOFF_X, SOFF_Y); subsurface.set_desync(); // mode desync → commit du child suffit pour appliquer dlog(&format!( "[sub-client] subsurface créée + set_position({SOFF_X}, {SOFF_Y}) + set_desync" )); let child_fd = unsafe { create_shm("redox-sub-client-child", SSIZE, SW, SH, SCOLOR) }?; let child_pool = shm.create_pool(child_fd.as_fd(), SSIZE, &qh, ()); let child_buffer = child_pool.create_buffer( 0, SW, SH, SSTRIDE, wayland_client::protocol::wl_shm::Format::Argb8888, &qh, (), ); child_surface.attach(Some(&child_buffer), 0, 0); child_surface.commit(); dlog("[sub-client] child buffer attached + commit"); // Re-commit du parent pour rendre les changements visibles en sync. // (En desync ce n'est pas strictement nécessaire mais ça force un redraw.) parent_surface.commit(); dlog("[sub-client] entering event loop"); let deadline = std::time::Instant::now() + Duration::from_secs(60); while state.running && std::time::Instant::now() < deadline { match event_queue.blocking_dispatch(&mut state) { Ok(_) => {} Err(e) => { let s = format!("{e}"); if s.contains("Broken pipe") || s.contains("Connection reset") { dlog("[sub-client] compositor disconnected → exit cleanly"); break; } return Err(e.into()); } } } dlog("[sub-client] loop exited cleanly"); Ok(()) } fn main() -> ExitCode { match run() { Ok(()) => { dlog("[sub-client] PASS"); ExitCode::SUCCESS } Err(e) => { dlog(&format!("[sub-client] FAIL: {e}")); ExitCode::FAILURE } } }