🎉 Phase 7.8 — resize interactif xdg_toplevel + durcissements
Resize complet implémenté (compute selon edges + configure cycle) + durcissements sécu Move/Resize. Frontend additions : - Helper compute_resize_geom(edges, start_x/y/w/h, dx, dy) qui interprète le bitmask xdg_toplevel::ResizeEdge : Top(1)/Bottom(2)/Left(4)/Right(8). Coins = combinaisons. Clamp w/h à MIN_RESIZE_DIM=32. - Handler xdg_toplevel.Resize complet : valide serial!=0, validation soft du serial vs last_button_serial (log warning, accept), refuse si drag déjà actif, check client_id (caller == owner de la surface — sécu défense en profondeur). Capture start_geom (x,y,w,h) + start_cursor, stocke InteractiveDrag:: Resize(edges). - apply_interactive_drag(Resize) : compute new geom, modifie registry x/y, envoie xdg_toplevel.configure(w, h, [Resizing]) + xdg_surface.configure(serial). Met à jour XdgSurfaceData.last_serial = serial pour que l'ack du client soit accepté (sinon refusé par garde 7.5). - Move handler : check client_id ajouté en mode défense en profondeur (un client ne peut pas drag une surface d'un autre). Nouveau crate : redox-wl-test-client-resize (~285 lignes) - Bind wl_seat - 1 fenêtre 320x200 cyan + bordure noire - À T+8s : toplevel.resize(seat, 1, BottomRight) - Écoute xdg_toplevel.Configure : à chaque new size, ack + nouveau shm pool + nouveau buffer + attach + commit - Vec<WlShmPool>/Vec<WlBuffer> pour ne pas drop les anciens pendant que le serveur peut encore les utiliser Pièges trouvés : - last_serial doit être mis à jour à chaque configure envoyé pendant le resize, sinon l'ack du client est refusé par le garde 7.5 (serial > last_sent → ignoring). - Le client peut ignorer la taille initiale suggérée par le configure du compositor (legal selon spec). Le compositor compose à la taille du buffer attaché. - À chaque resize, garder les anciens pool+buffer en mémoire pour ne pas crasher le serveur qui mmap dessus. Validation runtime : 3 captures à 3 tailles distinctes (320x200 initial, ~520x300 agrandi, ~70x25 rétréci) confirment le pipeline end-to-end. Logs symétriques côté client et compositor. Bilan phase 7 (1-8) : - 7.1 xdg-shell, 7.2 wl_seat, 7.3 cursor, 7.4 focus/raise, 7.5 robustesse, 7.6 multi-clients, 7.7 move, 7.8 resize - Compositor 7.x désormais utilisable pour un toolkit Wayland basique. Prochain jalon : port COSMIC (phase 13 plan-directeur, réordonnée avant GPU). Doc complète : docs/phase7-8-resize.md Leyoda 2026 – GPLv3
This commit is contained in:
parent
50f7a064e4
commit
a8898960f1
7 changed files with 838 additions and 21 deletions
377
crates/redox-wl-test-client-resize/src/main.rs
Normal file
377
crates/redox-wl-test-client-resize/src/main.rs
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
//! Phase 7.8 — Client de validation Resize interactif.
|
||||
//!
|
||||
//! Une seule fenêtre 320x200 cyan + bordure noire. À T+8s, envoie
|
||||
//! `xdg_toplevel.resize(seat, 1, BottomRight)`. Écoute les events
|
||||
//! `xdg_toplevel.configure { width, height, states }` : à chaque
|
||||
//! nouvelle taille reçue, recrée le shm pool + buffer + attach +
|
||||
//! commit. Cela permet de voir visuellement la fenêtre changer de
|
||||
//! taille pendant qu'un cycle compositor déplace le curseur.
|
||||
|
||||
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_seat::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 INITIAL_W: i32 = 320;
|
||||
const INITIAL_H: i32 = 200;
|
||||
/// BottomRight edges = Right(8) | Bottom(2) = 10
|
||||
const RESIZE_EDGES: u32 = 10;
|
||||
|
||||
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_once: bool,
|
||||
/// Dernière taille suggérée par xdg_toplevel.configure. (0, 0) =
|
||||
/// "client choisit", on garde alors la taille courante.
|
||||
pending_w: i32,
|
||||
pending_h: i32,
|
||||
}
|
||||
|
||||
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);
|
||||
noop!(WlSeat);
|
||||
|
||||
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_once = 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 {
|
||||
// (0, 0) = "client decides" → on garde l'ancienne taille
|
||||
state.pending_w = width;
|
||||
state.pending_h = height;
|
||||
dlog(&format!(
|
||||
"[resize] xdg_toplevel.configure: w={width} h={height}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un shm fd + buffer 32-bit ARGB rempli en `color` + bordure noire 2px.
|
||||
unsafe fn alloc_buffer(
|
||||
name: &str,
|
||||
w: i32,
|
||||
h: i32,
|
||||
color: u32,
|
||||
) -> Result<(OwnedFd, i32), String> {
|
||||
let stride = w * 4;
|
||||
let size = stride * h;
|
||||
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 { color };
|
||||
}
|
||||
}
|
||||
libc::munmap(p, size as usize);
|
||||
Ok((OwnedFd::from_raw_fd(fd), size))
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dlog("[resize] connect to compositor");
|
||||
for _ in 0..50 {
|
||||
if std::path::Path::new(SOCKET_PATH).exists() {
|
||||
break;
|
||||
}
|
||||
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)?;
|
||||
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("Phase 7.8 resize".to_string());
|
||||
toplevel.set_app_id("redox.wl.test.resize".to_string());
|
||||
surface.commit();
|
||||
dlog("[resize] toplevel créé");
|
||||
|
||||
// Attendre initial configure
|
||||
let start = std::time::Instant::now();
|
||||
while !state.configured_once && start.elapsed() < Duration::from_secs(5) {
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
if let Some(s) = state.pending_serial.take() {
|
||||
xdg_surface.ack_configure(s);
|
||||
dlog(&format!("[resize] ack_configure({s})"));
|
||||
}
|
||||
|
||||
// Premier buffer
|
||||
let mut cur_w = INITIAL_W;
|
||||
let mut cur_h = INITIAL_H;
|
||||
let color: u32 = 0xFF_45_8B_E0; // cyan/bleu
|
||||
let (fd, size) = unsafe { alloc_buffer("/redox-wl-resize-0", cur_w, cur_h, color) }?;
|
||||
let pool = shm.create_pool(fd.as_fd(), size, &qh, ());
|
||||
let buffer = pool.create_buffer(
|
||||
0,
|
||||
cur_w,
|
||||
cur_h,
|
||||
cur_w * 4,
|
||||
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
surface.damage_buffer(0, 0, cur_w, cur_h);
|
||||
surface.commit();
|
||||
let _ = event_queue.flush();
|
||||
dlog("[resize] initial buffer commit");
|
||||
|
||||
// Garder le pool et buffer en vie via Vec (sinon drop = destroy)
|
||||
let mut held_pools: Vec<WlShmPool> = vec![pool];
|
||||
let mut held_buffers: Vec<WlBuffer> = vec![buffer];
|
||||
|
||||
let resize_at = std::time::Instant::now() + Duration::from_secs(8);
|
||||
let mut resize_requested = false;
|
||||
let mut next_shm_name: u32 = 1;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < Duration::from_secs(160) {
|
||||
let _ = event_queue.flush();
|
||||
if let Some(guard) = event_queue.prepare_read() {
|
||||
let _ = guard.read();
|
||||
}
|
||||
let _ = event_queue.dispatch_pending(&mut state);
|
||||
|
||||
// Déclencher resize une fois après T+8s
|
||||
if !resize_requested && std::time::Instant::now() >= resize_at {
|
||||
if let Some(seat) = state.seat.clone() {
|
||||
toplevel.resize(&seat, 1, xdg_toplevel::ResizeEdge::BottomRight);
|
||||
let _ = event_queue.flush();
|
||||
dlog(&format!(
|
||||
"[resize] toplevel.resize(BottomRight, serial=1) envoyé"
|
||||
));
|
||||
resize_requested = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Si configure avec nouvelle taille reçu, ack + nouveau buffer
|
||||
if let Some(serial) = state.pending_serial.take() {
|
||||
xdg_surface.ack_configure(serial);
|
||||
let w = if state.pending_w > 0 {
|
||||
state.pending_w
|
||||
} else {
|
||||
cur_w
|
||||
};
|
||||
let h = if state.pending_h > 0 {
|
||||
state.pending_h
|
||||
} else {
|
||||
cur_h
|
||||
};
|
||||
if w != cur_w || h != cur_h {
|
||||
cur_w = w;
|
||||
cur_h = h;
|
||||
let name = format!("/redox-wl-resize-{next_shm_name}");
|
||||
next_shm_name += 1;
|
||||
match unsafe { alloc_buffer(&name, cur_w, cur_h, color) } {
|
||||
Ok((fd2, sz2)) => {
|
||||
let pool2 = shm.create_pool(fd2.as_fd(), sz2, &qh, ());
|
||||
let buf2 = pool2.create_buffer(
|
||||
0,
|
||||
cur_w,
|
||||
cur_h,
|
||||
cur_w * 4,
|
||||
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
surface.attach(Some(&buf2), 0, 0);
|
||||
surface.damage_buffer(0, 0, cur_w, cur_h);
|
||||
surface.commit();
|
||||
let _ = event_queue.flush();
|
||||
dlog(&format!(
|
||||
"[resize] new buffer {cur_w}x{cur_h} attaché + commit"
|
||||
));
|
||||
held_pools.push(pool2);
|
||||
held_buffers.push(buf2);
|
||||
}
|
||||
Err(e) => {
|
||||
dlog(&format!("[resize] alloc_buffer error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
|
||||
dlog("[resize] destroy");
|
||||
toplevel.destroy();
|
||||
xdg_surface.destroy();
|
||||
surface.destroy();
|
||||
let _ = event_queue.flush();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
dlog("[resize] PASS");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
dlog(&format!("[resize] FAIL: {e}"));
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue