Sprint 0 — industrialisation : CI, tracing, tests xdg-shell, GPLv3
5 livrables d'industrialisation posés avant la phase 13 (client réel). - .gitlab-ci.yml : pipeline minimal (fmt-check + tests core + tests frontend nightly). Pas de cross-compile Redox dans le MVP. - rustfmt.toml + fmt.sh : config formatter racine + boucle sur les 23 crates ; fmt sweep appliqué (d'où les diffs cosmétiques). - tracing dans wayland-frontend : 22 println/eprintln remplacés par debug/info/warn/error selon sévérité. Préfixe "[frontend]" supprimé (le target tracing le fournit). - tracing-subscriber dans le compositor : TeeWriter écrit sur stdout + /scheme/debug, filtre via RUST_LOG (défaut info). DebugSink/dlog supprimés. - 15 tests unitaires xdg-shell après extraction de 2 helpers libres (clamp_to_min_max + should_throttle_configure). Couvre compute_resize_geom, contraintes min/max et throttling configure. - LICENSE (GPLv3 texte officiel FSF) + .editorconfig. Total tests : 27 → 42 automatisés (compositor-core + frontend). Leyoda 2026 – GPLv3
This commit is contained in:
parent
2150f31d82
commit
8795f39f08
30 changed files with 1373 additions and 298 deletions
|
|
@ -27,17 +27,19 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use redox_wl_compositor_core::{Framebuffer, SurfaceBuffer, SurfaceId, SurfaceRegistry};
|
||||
use wayland_protocols::xdg::shell::server::{
|
||||
xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base,
|
||||
};
|
||||
use wayland_server::{
|
||||
Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource,
|
||||
backend::{ClientData, ClientId, DisconnectReason},
|
||||
protocol::{
|
||||
wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_seat,
|
||||
wl_shm, wl_shm_pool, wl_surface,
|
||||
wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_seat, wl_shm,
|
||||
wl_shm_pool, wl_surface,
|
||||
},
|
||||
Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource,
|
||||
};
|
||||
|
||||
use redox_wl_input::InputEvent as RedoxInputEvent;
|
||||
|
|
@ -259,6 +261,40 @@ fn compute_resize_geom(
|
|||
(new_x, new_y, new_w, new_h)
|
||||
}
|
||||
|
||||
/// Phase 7.9 : applique les contraintes `set_min_size` / `set_max_size`.
|
||||
/// Convention spec wayland-protocols : `(0, 0)` = pas de contrainte
|
||||
/// active (axe par axe). Une dimension `min > 0` impose `w >= min`,
|
||||
/// une dimension `max > 0` impose `w <= max`.
|
||||
fn clamp_to_min_max(w: u32, h: u32, min: (i32, i32), max: (i32, i32)) -> (u32, u32) {
|
||||
let mut w = w;
|
||||
let mut h = h;
|
||||
if min.0 > 0 {
|
||||
w = w.max(min.0 as u32);
|
||||
}
|
||||
if min.1 > 0 {
|
||||
h = h.max(min.1 as u32);
|
||||
}
|
||||
if max.0 > 0 {
|
||||
w = w.min(max.0 as u32);
|
||||
}
|
||||
if max.1 > 0 {
|
||||
h = h.min(max.1 as u32);
|
||||
}
|
||||
(w, h)
|
||||
}
|
||||
|
||||
/// Phase 7.9 : décide si un `xdg_toplevel.configure` doit être envoyé
|
||||
/// pendant un drag de resize. Skip si la taille n'a pas bougé OU si
|
||||
/// le dernier configure date de moins de `min_interval` (~60 fps).
|
||||
fn should_throttle_configure(
|
||||
new_size: (u32, u32),
|
||||
last_size: (u32, u32),
|
||||
elapsed: Duration,
|
||||
min_interval: Duration,
|
||||
) -> bool {
|
||||
new_size == last_size || elapsed < min_interval
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
|
@ -432,10 +468,7 @@ impl WaylandFrontend {
|
|||
let cd = DumbClientData {
|
||||
dead_clients: Arc::clone(&self.dead_clients),
|
||||
};
|
||||
let _ = self
|
||||
.display
|
||||
.handle()
|
||||
.insert_client(stream, Arc::new(cd));
|
||||
let _ = self.display.handle().insert_client(stream, Arc::new(cd));
|
||||
}
|
||||
Ok(None) => break, // pas de client en attente
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
|
||||
|
|
@ -497,8 +530,8 @@ impl WaylandFrontend {
|
|||
// déjà déconnecté.
|
||||
self.pointers.retain(|p| p.client().is_some());
|
||||
self.keyboards.retain(|k| k.client().is_some());
|
||||
println!(
|
||||
"[frontend] garbage_collect: client {cid:?} → destroyed {} surfaces",
|
||||
info!(
|
||||
"garbage_collect: client {cid:?} → destroyed {} surfaces",
|
||||
to_destroy.len()
|
||||
);
|
||||
}
|
||||
|
|
@ -571,19 +604,12 @@ impl WaylandFrontend {
|
|||
.as_ref()
|
||||
.and_then(|s| s.data::<Arc<SurfaceData>>())
|
||||
.and_then(|d| *d.id.lock().unwrap());
|
||||
println!(
|
||||
"[frontend] focus change: {old_sid:?} → {new_sid:?}"
|
||||
);
|
||||
info!("focus change: {old_sid:?} → {new_sid:?}");
|
||||
// Phase 7.9 : envoyer configure(w, h, [Activated]) au nouveau
|
||||
// focused toplevel et configure(w, h, []) à l'ancien, pour que
|
||||
// les toolkits puissent styler le focus (titlebar active, etc.).
|
||||
let empty_state: Vec<u8> = vec![];
|
||||
let activated_state: Vec<u8> = vec![
|
||||
(xdg_toplevel::State::Activated as u32) as u8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
];
|
||||
let activated_state: Vec<u8> = vec![(xdg_toplevel::State::Activated as u32) as u8, 0, 0, 0];
|
||||
if let Some(sid) = old_sid {
|
||||
if let Some(tl) = self.toplevels_by_id.get(&sid).cloned() {
|
||||
self.send_focus_configure(&tl, sid, empty_state);
|
||||
|
|
@ -616,12 +642,7 @@ impl WaylandFrontend {
|
|||
// 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),
|
||||
);
|
||||
ptr.enter(serial_enter, new, fixed_from_int(sx), fixed_from_int(sy));
|
||||
if SEAT_VERSION >= 5 {
|
||||
ptr.frame();
|
||||
}
|
||||
|
|
@ -658,7 +679,12 @@ impl WaylandFrontend {
|
|||
let (w, h) = self
|
||||
.registry
|
||||
.get(sid)
|
||||
.and_then(|s| s.current().buffer.as_ref().map(|b| (b.width as i32, b.height as i32)))
|
||||
.and_then(|s| {
|
||||
s.current()
|
||||
.buffer
|
||||
.as_ref()
|
||||
.map(|b| (b.width as i32, b.height as i32))
|
||||
})
|
||||
.unwrap_or(DEFAULT_TOPLEVEL_SIZE);
|
||||
toplevel.configure(w, h, states);
|
||||
if let Some(td) = toplevel.data::<Arc<XdgToplevelData>>() {
|
||||
|
|
@ -709,22 +735,12 @@ impl WaylandFrontend {
|
|||
);
|
||||
// Phase 7.9 : clamp aux contraintes min/max envoyées
|
||||
// par le client via xdg_toplevel.set_min/max_size.
|
||||
// (0, 0) = pas de contrainte.
|
||||
if let Some(td) = drag.xdg_toplevel_res.data::<Arc<XdgToplevelData>>() {
|
||||
let (min_w, min_h) = *td.min_size.lock().unwrap();
|
||||
let (max_w, max_h) = *td.max_size.lock().unwrap();
|
||||
if min_w > 0 {
|
||||
new_w = new_w.max(min_w as u32);
|
||||
}
|
||||
if min_h > 0 {
|
||||
new_h = new_h.max(min_h as u32);
|
||||
}
|
||||
if max_w > 0 {
|
||||
new_w = new_w.min(max_w as u32);
|
||||
}
|
||||
if max_h > 0 {
|
||||
new_h = new_h.min(max_h as u32);
|
||||
}
|
||||
let min = *td.min_size.lock().unwrap();
|
||||
let max = *td.max_size.lock().unwrap();
|
||||
let clamped = clamp_to_min_max(new_w, new_h, min, max);
|
||||
new_w = clamped.0;
|
||||
new_h = clamped.1;
|
||||
}
|
||||
// Mettre à jour la position côté compositor immédiatement
|
||||
// (la taille effective dépendra du prochain commit du client).
|
||||
|
|
@ -738,22 +754,20 @@ impl WaylandFrontend {
|
|||
// (re)allocations côté client : skip si la taille n'a
|
||||
// pas changé OU si moins de 16ms depuis le dernier
|
||||
// configure (~60fps max).
|
||||
let same_size = (new_w, new_h) == drag.last_configure_size;
|
||||
let too_soon =
|
||||
drag.last_configure_at.elapsed() < Duration::from_millis(16);
|
||||
if same_size || too_soon {
|
||||
if should_throttle_configure(
|
||||
(new_w, new_h),
|
||||
drag.last_configure_size,
|
||||
drag.last_configure_at.elapsed(),
|
||||
Duration::from_millis(16),
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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,
|
||||
];
|
||||
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;
|
||||
|
|
@ -876,14 +890,14 @@ impl WaylandFrontend {
|
|||
// Phase 7.7 : si drag actif et le bouton gauche est relâché,
|
||||
// sortir du mode drag avant l'envoi du button au client.
|
||||
if !*left && self.interactive_drag.is_some() {
|
||||
println!("[frontend] left-release → exit interactive drag");
|
||||
info!("left-release → exit interactive drag");
|
||||
self.interactive_drag = None;
|
||||
}
|
||||
|
||||
if *left {
|
||||
let hit = self.registry.hit_test(self.cursor_x, self.cursor_y);
|
||||
println!(
|
||||
"[frontend] left-click @ ({}, {}) → hit_test = {:?}",
|
||||
debug!(
|
||||
"left-click @ ({}, {}) → hit_test = {:?}",
|
||||
self.cursor_x, self.cursor_y, hit
|
||||
);
|
||||
if let Some(sid) = hit {
|
||||
|
|
@ -959,11 +973,7 @@ impl WaylandFrontend {
|
|||
continue;
|
||||
}
|
||||
if *dy != 0 {
|
||||
ptr.axis(
|
||||
time,
|
||||
wl_pointer::Axis::VerticalScroll,
|
||||
fixed_from_int(*dy),
|
||||
);
|
||||
ptr.axis(time, wl_pointer::Axis::VerticalScroll, fixed_from_int(*dy));
|
||||
}
|
||||
if *dx != 0 {
|
||||
ptr.axis(
|
||||
|
|
@ -1271,7 +1281,7 @@ impl wayland_server::Dispatch<wl_shm::WlShm, ()> for WaylandFrontend {
|
|||
data_init.init(id, Arc::new(Mutex::new(p)));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[frontend] shm CreatePool mmap failed: {e}");
|
||||
error!("shm CreatePool mmap failed: {e}");
|
||||
// En cas d'échec on ne peut pas init le pool. Le client va
|
||||
// probablement avoir un fd resource leaked, mais c'est
|
||||
// moins grave qu'un crash. Implementation simple pour 6.4.
|
||||
|
|
@ -1321,7 +1331,8 @@ impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, Arc<Mutex<ShmPool>>> for W
|
|||
if valid {
|
||||
let pool_size = pool.lock().unwrap().size as i64;
|
||||
let needed = (offset as i64).saturating_add(
|
||||
(stride as i64).saturating_mul((height as i64).saturating_sub(1))
|
||||
(stride as i64)
|
||||
.saturating_mul((height as i64).saturating_sub(1))
|
||||
.saturating_add((width as i64).saturating_mul(4)),
|
||||
);
|
||||
if needed > pool_size {
|
||||
|
|
@ -1329,8 +1340,8 @@ impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, Arc<Mutex<ShmPool>>> for W
|
|||
}
|
||||
}
|
||||
if !valid {
|
||||
println!(
|
||||
"[frontend] wl_shm_pool.create_buffer rejected: \
|
||||
warn!(
|
||||
"wl_shm_pool.create_buffer rejected: \
|
||||
offset={offset} width={width} height={height} stride={stride}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1431,17 +1442,11 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
|
|||
} else {
|
||||
let pool = bd.pool.lock().unwrap();
|
||||
let pixels_opt = unsafe {
|
||||
pool.read_argb(
|
||||
bd.offset as usize,
|
||||
bd.width,
|
||||
bd.height,
|
||||
bd.stride,
|
||||
)
|
||||
pool.read_argb(bd.offset as usize, bd.width, bd.height, bd.stride)
|
||||
};
|
||||
drop(pool);
|
||||
if let Some(pixels) = pixels_opt {
|
||||
let sb =
|
||||
SurfaceBuffer::from_pixels(bd.width, bd.height, pixels);
|
||||
let sb = SurfaceBuffer::from_pixels(bd.width, bd.height, pixels);
|
||||
// Pour une surface curseur, on stocke le buffer
|
||||
// mais visible=false (cf draw_cursor).
|
||||
state.registry.modify_pending(id, |s| {
|
||||
|
|
@ -1449,9 +1454,7 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
|
|||
s.visible = !is_cursor;
|
||||
});
|
||||
} else {
|
||||
println!(
|
||||
"[frontend] commit: read_argb refused buffer (overrun guard)"
|
||||
);
|
||||
warn!("commit: read_argb refused buffer (overrun guard)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1683,8 +1686,8 @@ impl wayland_server::Dispatch<xdg_surface::XdgSurface, Arc<XdgSurfaceData>> for
|
|||
// abus délibérés.
|
||||
let last = *data.last_serial.lock().unwrap();
|
||||
if serial > last {
|
||||
println!(
|
||||
"[frontend] xdg_surface.ack_configure: serial {serial} > last_sent {last}, ignoring"
|
||||
warn!(
|
||||
"xdg_surface.ack_configure: serial {serial} > last_sent {last}, ignoring"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1901,9 +1904,7 @@ fn send_keymap_no_keymap(kb: &wl_keyboard::WlKeyboard) {
|
|||
use std::os::fd::AsFd;
|
||||
|
||||
// ---- xdg_toplevel ----
|
||||
impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
||||
for WaylandFrontend
|
||||
{
|
||||
impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>> for WaylandFrontend {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
|
|
@ -1922,19 +1923,16 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
|||
}
|
||||
xdg_toplevel::Request::SetMinSize { width, height } => {
|
||||
*data.min_size.lock().unwrap() = (width, height);
|
||||
println!("[frontend] xdg_toplevel.set_min_size({width}, {height})");
|
||||
debug!("xdg_toplevel.set_min_size({width}, {height})");
|
||||
}
|
||||
xdg_toplevel::Request::SetMaxSize { width, height } => {
|
||||
*data.max_size.lock().unwrap() = (width, height);
|
||||
println!("[frontend] xdg_toplevel.set_max_size({width}, {height})");
|
||||
debug!("xdg_toplevel.set_max_size({width}, {height})");
|
||||
}
|
||||
xdg_toplevel::Request::Move {
|
||||
seat: _,
|
||||
serial,
|
||||
} => {
|
||||
xdg_toplevel::Request::Move { seat: _, serial } => {
|
||||
// Phase 7.7/7.8 : entrer en mode drag-move.
|
||||
if serial == 0 {
|
||||
println!("[frontend] xdg_toplevel.move: serial=0 refused");
|
||||
warn!("xdg_toplevel.move: serial=0 refused");
|
||||
return;
|
||||
}
|
||||
// Phase 7.8 : validation soft du serial — log warning si
|
||||
|
|
@ -1942,17 +1940,17 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
|||
// 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)",
|
||||
debug!(
|
||||
"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");
|
||||
warn!("xdg_toplevel.move: drag already in progress");
|
||||
return;
|
||||
}
|
||||
let Some(xdg_surf) = data.xdg_surface.lock().unwrap().clone() else {
|
||||
println!("[frontend] xdg_toplevel.move: no parent xdg_surface");
|
||||
warn!("xdg_toplevel.move: no parent xdg_surface");
|
||||
return;
|
||||
};
|
||||
let Some(xdg_data) = xdg_surf.data::<Arc<XdgSurfaceData>>() else {
|
||||
|
|
@ -1966,8 +1964,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
|||
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"
|
||||
warn!(
|
||||
"xdg_toplevel.move: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1997,8 +1995,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
|||
last_configure_size: (start_w, start_h),
|
||||
last_configure_at: Instant::now(),
|
||||
};
|
||||
println!(
|
||||
"[frontend] xdg_toplevel.move: enter drag sid={:?} start=({},{}) cursor=({},{})",
|
||||
info!(
|
||||
"xdg_toplevel.move: enter drag sid={:?} start=({},{}) cursor=({},{})",
|
||||
sid, st.x, st.y, state.cursor_x, state.cursor_y
|
||||
);
|
||||
state.interactive_drag = Some(drag);
|
||||
|
|
@ -2014,21 +2012,21 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
|||
Err(_) => 0,
|
||||
};
|
||||
if serial == 0 {
|
||||
println!("[frontend] xdg_toplevel.resize: serial=0 refused");
|
||||
warn!("xdg_toplevel.resize: serial=0 refused");
|
||||
return;
|
||||
}
|
||||
if edges_raw == 0 {
|
||||
println!("[frontend] xdg_toplevel.resize: edges=None, ignoring");
|
||||
warn!("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)",
|
||||
debug!(
|
||||
"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");
|
||||
warn!("xdg_toplevel.resize: drag already in progress");
|
||||
return;
|
||||
}
|
||||
let Some(xdg_surf) = data.xdg_surface.lock().unwrap().clone() else {
|
||||
|
|
@ -2045,8 +2043,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
|||
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"
|
||||
warn!(
|
||||
"xdg_toplevel.resize: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -2057,11 +2055,11 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
|||
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 (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(),
|
||||
|
|
@ -2076,8 +2074,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
|||
last_configure_size: (start_w, start_h),
|
||||
last_configure_at: Instant::now(),
|
||||
};
|
||||
println!(
|
||||
"[frontend] xdg_toplevel.resize: enter drag sid={sid:?} edges={edges_raw} start_geom=({},{},{},{}) cursor=({},{})",
|
||||
info!(
|
||||
"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);
|
||||
|
|
@ -2098,3 +2096,151 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests unitaires xdg-shell (sprint 0 point 4).
|
||||
//
|
||||
// Couverture : la logique pure extraite des handlers Dispatch — c.-à-d.
|
||||
// `compute_resize_geom`, `clamp_to_min_max`, `should_throttle_configure`.
|
||||
// Pas de Wayland-server runtime ici : ces fns sont libres et déterministes,
|
||||
// donc isolables. Les tests "protocole" (ack_configure serial, set_focus
|
||||
// configure) nécessitent un vrai client + Display et sont reportés à une
|
||||
// future phase d'intégration.
|
||||
// ---------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ---- compute_resize_geom : bord Right (8) ----
|
||||
#[test]
|
||||
fn resize_right_grows_width_keeps_origin() {
|
||||
let (x, y, w, h) = compute_resize_geom(8, 100, 200, 300, 150, 50, 0);
|
||||
assert_eq!((x, y), (100, 200));
|
||||
assert_eq!((w, h), (350, 150));
|
||||
}
|
||||
|
||||
// ---- compute_resize_geom : bord Left (4) ----
|
||||
#[test]
|
||||
fn resize_left_moves_origin_and_shrinks_on_positive_dx() {
|
||||
let (x, y, w, h) = compute_resize_geom(4, 100, 200, 300, 150, 40, 0);
|
||||
assert_eq!((x, y), (140, 200));
|
||||
assert_eq!((w, h), (260, 150));
|
||||
}
|
||||
|
||||
// ---- compute_resize_geom : bord Bottom (2) ----
|
||||
#[test]
|
||||
fn resize_bottom_grows_height_keeps_origin() {
|
||||
let (x, y, w, h) = compute_resize_geom(2, 100, 200, 300, 150, 0, 70);
|
||||
assert_eq!((x, y), (100, 200));
|
||||
assert_eq!((w, h), (300, 220));
|
||||
}
|
||||
|
||||
// ---- compute_resize_geom : bord Top (1) ----
|
||||
#[test]
|
||||
fn resize_top_moves_origin_and_shrinks_on_positive_dy() {
|
||||
let (x, y, w, h) = compute_resize_geom(1, 100, 200, 300, 150, 0, 60);
|
||||
assert_eq!((x, y), (100, 260));
|
||||
assert_eq!((w, h), (300, 90));
|
||||
}
|
||||
|
||||
// ---- compute_resize_geom : coin BottomRight (Bottom|Right = 10) ----
|
||||
#[test]
|
||||
fn resize_bottom_right_corner_grows_both_axes() {
|
||||
let (x, y, w, h) = compute_resize_geom(10, 100, 200, 300, 150, 25, 35);
|
||||
assert_eq!((x, y), (100, 200));
|
||||
assert_eq!((w, h), (325, 185));
|
||||
}
|
||||
|
||||
// ---- compute_resize_geom : delta trop négatif clamp à MIN_RESIZE_DIM ----
|
||||
#[test]
|
||||
fn resize_clamps_to_min_resize_dim_on_extreme_shrink() {
|
||||
let (_x, _y, w, h) = compute_resize_geom(10, 0, 0, 300, 150, -10_000, -10_000);
|
||||
assert_eq!((w, h), (MIN_RESIZE_DIM, MIN_RESIZE_DIM));
|
||||
}
|
||||
|
||||
// ---- compute_resize_geom : edges=0 (aucun bord) → géom inchangée ----
|
||||
#[test]
|
||||
fn resize_with_no_edges_returns_starting_geometry() {
|
||||
let (x, y, w, h) = compute_resize_geom(0, 50, 60, 200, 100, 999, 999);
|
||||
assert_eq!((x, y, w, h), (50, 60, 200, 100));
|
||||
}
|
||||
|
||||
// ---- clamp_to_min_max : (0, 0) = pas de contrainte ----
|
||||
#[test]
|
||||
fn clamp_with_zero_constraints_is_identity() {
|
||||
let (w, h) = clamp_to_min_max(320, 200, (0, 0), (0, 0));
|
||||
assert_eq!((w, h), (320, 200));
|
||||
}
|
||||
|
||||
// ---- clamp_to_min_max : min seul force le plancher ----
|
||||
#[test]
|
||||
fn clamp_applies_min_floor() {
|
||||
let (w, h) = clamp_to_min_max(50, 30, (150, 80), (0, 0));
|
||||
assert_eq!((w, h), (150, 80));
|
||||
}
|
||||
|
||||
// ---- clamp_to_min_max : max seul force le plafond ----
|
||||
#[test]
|
||||
fn clamp_applies_max_ceiling() {
|
||||
let (w, h) = clamp_to_min_max(900, 500, (0, 0), (600, 400));
|
||||
assert_eq!((w, h), (600, 400));
|
||||
}
|
||||
|
||||
// ---- clamp_to_min_max : min + max coexistent ----
|
||||
#[test]
|
||||
fn clamp_applies_both_min_and_max() {
|
||||
// size dans la fenêtre → identique
|
||||
let (w, h) = clamp_to_min_max(320, 200, (150, 80), (600, 400));
|
||||
assert_eq!((w, h), (320, 200));
|
||||
// size sous le plancher → remontée
|
||||
let (w, h) = clamp_to_min_max(10, 10, (150, 80), (600, 400));
|
||||
assert_eq!((w, h), (150, 80));
|
||||
// size au-dessus du plafond → descente
|
||||
let (w, h) = clamp_to_min_max(9999, 9999, (150, 80), (600, 400));
|
||||
assert_eq!((w, h), (600, 400));
|
||||
}
|
||||
|
||||
// ---- clamp_to_min_max : axes indépendants ----
|
||||
#[test]
|
||||
fn clamp_constraints_are_independent_per_axis() {
|
||||
// min sur width seulement
|
||||
let (w, h) = clamp_to_min_max(10, 200, (150, 0), (0, 0));
|
||||
assert_eq!((w, h), (150, 200));
|
||||
// max sur height seulement
|
||||
let (w, h) = clamp_to_min_max(300, 9999, (0, 0), (0, 400));
|
||||
assert_eq!((w, h), (300, 400));
|
||||
}
|
||||
|
||||
// ---- should_throttle_configure : même taille → skip ----
|
||||
#[test]
|
||||
fn throttle_skips_when_size_unchanged() {
|
||||
assert!(should_throttle_configure(
|
||||
(320, 200),
|
||||
(320, 200),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_millis(16),
|
||||
));
|
||||
}
|
||||
|
||||
// ---- should_throttle_configure : trop tôt → skip ----
|
||||
#[test]
|
||||
fn throttle_skips_when_elapsed_below_min_interval() {
|
||||
assert!(should_throttle_configure(
|
||||
(321, 200),
|
||||
(320, 200),
|
||||
Duration::from_millis(8),
|
||||
Duration::from_millis(16),
|
||||
));
|
||||
}
|
||||
|
||||
// ---- should_throttle_configure : taille différente + délai OK → envoie ----
|
||||
#[test]
|
||||
fn throttle_allows_configure_when_size_changed_and_interval_elapsed() {
|
||||
assert!(!should_throttle_configure(
|
||||
(321, 200),
|
||||
(320, 200),
|
||||
Duration::from_millis(20),
|
||||
Duration::from_millis(16),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue