🎉 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:
Votre Nom 2026-05-13 19:47:53 +02:00
parent 50f7a064e4
commit a8898960f1
7 changed files with 838 additions and 21 deletions

View file

@ -0,0 +1,10 @@
[package]
name = "redox-wl-test-client-resize"
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"

View 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
}
}
}

View file

@ -203,12 +203,56 @@ struct XdgToplevelData {
enum DragMode {
Move,
/// Resize avec un bitmask de bords tirés. Le contenu est un
/// `xdg_toplevel::ResizeEdge` interprété par le compositor pour
/// calculer (new_w, new_h, new_x, new_y).
#[allow(dead_code)]
/// `xdg_toplevel::ResizeEdge` interprété par `compute_resize_geom`
/// pour calculer (new_x, new_y, new_w, new_h).
Resize(u32),
}
/// Phase 7.8 : taille minimale d'une surface pendant le resize.
/// Sous cette valeur, on clamp pour éviter w/h <= 0 qui ferait planter
/// les clients tout en validant leur buffer.
const MIN_RESIZE_DIM: u32 = 32;
/// Phase 7.8 : calcule la nouvelle géométrie d'une surface en cours de
/// resize, selon le bitmask `edges` xdg-shell et le delta cursor.
///
/// edges bitmask : Top=1, Bottom=2, Left=4, Right=8 (coins = combinaisons).
fn compute_resize_geom(
edges: u32,
start_x: i32,
start_y: i32,
start_w: u32,
start_h: u32,
dx: i32,
dy: i32,
) -> (i32, i32, u32, u32) {
let mut new_x = start_x;
let mut new_y = start_y;
let mut new_w = start_w as i32;
let mut new_h = start_h as i32;
// Right (8) : étend / contracte la droite
if edges & 8 != 0 {
new_w = (start_w as i32).saturating_add(dx);
}
// Left (4) : déplace l'origine et inverse le delta sur la largeur
if edges & 4 != 0 {
new_w = (start_w as i32).saturating_sub(dx);
new_x = start_x.saturating_add(dx);
}
// Bottom (2) : étend / contracte le bas
if edges & 2 != 0 {
new_h = (start_h as i32).saturating_add(dy);
}
// Top (1) : déplace l'origine et inverse le delta sur la hauteur
if edges & 1 != 0 {
new_h = (start_h as i32).saturating_sub(dy);
new_y = start_y.saturating_add(dy);
}
let new_w = new_w.max(MIN_RESIZE_DIM as i32) as u32;
let new_h = new_h.max(MIN_RESIZE_DIM as i32) as u32;
(new_x, new_y, new_w, new_h)
}
/// Phase 7.7 : état d'un drag interactif en cours. Posé par le handler
/// `xdg_toplevel.Move`/`Resize`, lu par `forward_input` pour appliquer
/// les deltas, retiré au release du bouton gauche.
@ -562,19 +606,21 @@ impl WaylandFrontend {
(self.cursor_x, self.cursor_y)
}
/// Phase 7.7 : si un drag interactif est actif, applique le delta
/// `(cursor - start_cursor)` à la position de la surface ciblée et
/// retourne `true` pour court-circuiter l'envoi de motion au client.
/// Retourne `false` si pas de drag, le caller doit alors faire son
/// envoi normal.
/// Phase 7.7/7.8 : si un drag interactif est actif, applique le delta
/// `(cursor - start_cursor)`.
/// - Move : déplace la surface.
/// - Resize : compute new geom, modifie x/y dans le registry,
/// envoie `toplevel.configure(w, h, [Resizing])` +
/// `xdg_surface.configure(serial)` pour que le client re-peigne.
/// Retourne `true` pour court-circuiter l'envoi de motion au client.
fn apply_interactive_drag(&mut self) -> bool {
let Some(drag) = self.interactive_drag.clone() else {
return false;
};
let dx = self.cursor_x - drag.start_cursor_x;
let dy = self.cursor_y - drag.start_cursor_y;
match drag.mode {
DragMode::Move => {
let dx = self.cursor_x - drag.start_cursor_x;
let dy = self.cursor_y - drag.start_cursor_y;
let new_x = drag.start_x.saturating_add(dx);
let new_y = drag.start_y.saturating_add(dy);
self.registry.modify_pending(drag.surface_id, |s| {
@ -584,10 +630,44 @@ impl WaylandFrontend {
self.registry.commit(drag.surface_id);
true
}
DragMode::Resize(_) => {
// Stub 7.7 : pas de resize effectif (cf XdgToplevel.Resize handler).
// Au minimum on consomme le motion pour éviter de l'envoyer
// au client pendant qu'il pense être en mode resize.
DragMode::Resize(edges) => {
let (new_x, new_y, new_w, new_h) = compute_resize_geom(
edges,
drag.start_x,
drag.start_y,
drag.start_w,
drag.start_h,
dx,
dy,
);
// Mettre à jour la position côté compositor immédiatement
// (la taille effective dépendra du prochain commit du client).
self.registry.modify_pending(drag.surface_id, |s| {
s.x = new_x;
s.y = new_y;
});
self.registry.commit(drag.surface_id);
// Annoncer la nouvelle taille au client. Le state
// `Resizing` (3) indique au toolkit qu'il est en train
// d'être redimensionné.
let resizing_state: Vec<u8> = vec![
(xdg_toplevel::State::Resizing as u32) as u8,
0,
0,
0,
];
drag.xdg_toplevel_res
.configure(new_w as i32, new_h as i32, resizing_state);
let serial = self.next_xdg_serial;
self.next_xdg_serial = self.next_xdg_serial.wrapping_add(1).max(1);
// Phase 7.8 : mettre à jour last_serial côté XdgSurfaceData
// pour que l'ack du client soit accepté (sinon ack_configure
// refuse car serial > last_sent qui n'avait été mis à jour
// qu'à l'initial configure).
if let Some(xdg_data) = drag.xdg_surface_res.data::<Arc<XdgSurfaceData>>() {
*xdg_data.last_serial.lock().unwrap() = serial;
}
drag.xdg_surface_res.configure(serial);
true
}
}
@ -1731,13 +1811,21 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
seat: _,
serial,
} => {
// Phase 7.7 : entrer en mode drag-move.
// Validation laxiste : on accepte tout `serial != 0` pour
// 7.7 ; à durcir en 7.8 (comparer à `last_button_serial`).
// Phase 7.7/7.8 : entrer en mode drag-move.
if serial == 0 {
println!("[frontend] xdg_toplevel.move: serial=0 refused");
return;
}
// Phase 7.8 : validation soft du serial — log warning si
// mismatch mais accept quand même (à durcir en 7.9 quand
// tous les clients utiliseront un vrai serial capté
// depuis pointer.button).
if state.last_button_serial != 0 && serial != state.last_button_serial {
println!(
"[frontend] xdg_toplevel.move: serial {serial} ≠ last_button {} (accepting)",
state.last_button_serial
);
}
if state.interactive_drag.is_some() {
println!("[frontend] xdg_toplevel.move: drag already in progress");
return;
@ -1753,6 +1841,15 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
let Some(surf_data) = wl_surf.data::<Arc<SurfaceData>>() else {
return;
};
// Phase 7.8 : check "surface du même client".
let caller_cid = resource.client().map(|c| c.id());
let owner_cid = surf_data.client_id.lock().unwrap().clone();
if caller_cid != owner_cid {
println!(
"[frontend] xdg_toplevel.move: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED"
);
return;
}
let Some(sid) = *surf_data.id.lock().unwrap() else {
return;
};
@ -1788,16 +1885,77 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
serial,
edges,
} => {
// Phase 7.7 : stub log-only. Implémentation complète
// reportée à 7.8 (compute new_w/new_h selon edges et
// envoyer configure).
// Phase 7.8 : Resize complet.
let edges_raw = match edges.into_result() {
Ok(e) => e as u32,
Err(_) => 0,
};
if serial == 0 {
println!("[frontend] xdg_toplevel.resize: serial=0 refused");
return;
}
if edges_raw == 0 {
println!("[frontend] xdg_toplevel.resize: edges=None, ignoring");
return;
}
if state.last_button_serial != 0 && serial != state.last_button_serial {
println!(
"[frontend] xdg_toplevel.resize: serial {serial} ≠ last_button {} (accepting, durcir 7.9)",
state.last_button_serial
);
}
if state.interactive_drag.is_some() {
println!("[frontend] xdg_toplevel.resize: drag already in progress");
return;
}
let Some(xdg_surf) = data.xdg_surface.lock().unwrap().clone() else {
return;
};
let Some(xdg_data) = xdg_surf.data::<Arc<XdgSurfaceData>>() else {
return;
};
let wl_surf = &xdg_data.wl_surface;
let Some(surf_data) = wl_surf.data::<Arc<SurfaceData>>() else {
return;
};
// Phase 7.8 : check "surface du même client"
let caller_cid = resource.client().map(|c| c.id());
let owner_cid = surf_data.client_id.lock().unwrap().clone();
if caller_cid != owner_cid {
println!(
"[frontend] xdg_toplevel.resize: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED"
);
return;
}
let Some(sid) = *surf_data.id.lock().unwrap() else {
return;
};
let Some(s) = state.registry.get(sid) else {
return;
};
let st = s.current();
let (start_w, start_h) = st
.buffer
.as_ref()
.map(|b| (b.width, b.height))
.unwrap_or((DEFAULT_TOPLEVEL_SIZE.0 as u32, DEFAULT_TOPLEVEL_SIZE.1 as u32));
let drag = InteractiveDrag {
surface_id: sid,
xdg_toplevel_res: resource.clone(),
xdg_surface_res: xdg_surf,
mode: DragMode::Resize(edges_raw),
start_cursor_x: state.cursor_x,
start_cursor_y: state.cursor_y,
start_x: st.x,
start_y: st.y,
start_w,
start_h,
};
println!(
"[frontend] xdg_toplevel.resize: serial={serial} edges={edges_raw} (stub, ignored)"
"[frontend] xdg_toplevel.resize: enter drag sid={sid:?} edges={edges_raw} start_geom=({},{},{},{}) cursor=({},{})",
st.x, st.y, start_w, start_h, state.cursor_x, state.cursor_y
);
state.interactive_drag = Some(drag);
}
xdg_toplevel::Request::Destroy => {
// Si on était en train de drag cette surface, abandonner le mode