🎉 Phase 7.9 — polish : min/max size + Activated state + throttling

3 livrables qui polissent le compositor pour des toolkits Wayland
réels.

Frontend additions :

1. set_min_size / set_max_size
   - XdgToplevelData : min_size/max_size: Mutex<(i32, i32)>
   - Handlers SetMinSize/SetMaxSize stockent
   - apply_interactive_drag(Resize) clamp new_w/new_h aux contraintes
     après compute_resize_geom

2. Activated state au focus change
   - WaylandFrontend.toplevels_by_id: HashMap<SurfaceId, XdgToplevel>
     peuplé à xdg_surface.GetToplevel, nettoyé à wl_surface.Destroy
     et garbage_collect_dead_clients
   - Méthode send_focus_configure(toplevel, sid, states) : envoie
     configure(w, h, states) avec taille du buffer courant +
     xdg_surface.configure(serial) + update last_serial
   - set_focus envoie configure([Activated]) à la nouvelle focused
     surface et configure([]) à l'ancienne

3. Throttling configure pendant resize
   - InteractiveDrag : last_configure_size + last_configure_at
   - apply_interactive_drag(Resize) skip l'envoi si taille inchangée
     OU elapsed < 16ms (max ~60fps), mais applique quand même le
     delta de position pour que la fenêtre suive le curseur

Test client : set_min_size(150, 80) + set_max_size(600, 400)

Validation runtime :
- [frontend] xdg_toplevel.set_min_size(150, 80) + set_max_size logs
- focus change → configure(320x200) reçu côté client = Activated OK
- Configure pendant resize : un seul w=150 h=80 envoyé pour les 4
  phases de cursor (clamp + throttling = même résultat → skip)

Capture phase7-9-2-clamp-min.png montre la fenêtre clampée à 150x80
malgré des cursor moves qui auraient produit des tailles négatives.

Bilan phase 7 (1-9) close. Compositor 7.x suffisamment poli pour
toolkits Wayland réels : xdg-shell complet, focus avec Activated,
input filtré, cursor, move/resize avec contraintes, robustesse,
multi-clients avec cleanup.

Prochain jalon : port COSMIC (phase 13 plan-directeur, réordonnée
avant GPU).

Doc complète : docs/phase7-9-polish.md

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-13 20:54:33 +02:00
parent a8898960f1
commit 1689b93d9b
5 changed files with 386 additions and 4 deletions

View file

@ -241,8 +241,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
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_title("Phase 7.9 resize w/ min-max".to_string());
toplevel.set_app_id("redox.wl.test.resize".to_string());
// Phase 7.9 : annoncer min/max au compositor. Le compositor
// clamp les configures de resize entre ces bornes.
toplevel.set_min_size(150, 80);
toplevel.set_max_size(600, 400);
surface.commit();
dlog("[resize] toplevel créé");

View file

@ -25,6 +25,7 @@ use std::os::fd::{AsRawFd, OwnedFd};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use redox_wl_compositor_core::{Framebuffer, SurfaceBuffer, SurfaceId, SurfaceRegistry};
use wayland_protocols::xdg::shell::server::{
@ -195,6 +196,11 @@ struct XdgToplevelData {
/// Phase 7.7 : référence vers le `xdg_surface` parent (pour envoyer
/// les configure de resize). Peuplé dans `xdg_surface.GetToplevel`.
xdg_surface: Mutex<Option<xdg_surface::XdgSurface>>,
/// Phase 7.9 : contraintes min/max envoyées par le client via
/// `set_min_size` / `set_max_size`. (0, 0) = pas de contrainte.
/// Appliquées comme clamp dans `apply_interactive_drag(Resize)`.
min_size: Mutex<(i32, i32)>,
max_size: Mutex<(i32, i32)>,
}
/// Phase 7.7 : mode d'un drag interactif déclenché par
@ -272,10 +278,15 @@ struct InteractiveDrag {
start_cursor_y: i32,
start_x: i32,
start_y: i32,
#[allow(dead_code)]
start_w: u32,
#[allow(dead_code)]
start_h: u32,
/// Phase 7.9 : dernière taille envoyée via configure(w, h) pendant
/// ce drag. Permet d'éviter le burst de configures si la taille
/// n'a pas changé entre deux frames.
last_configure_size: (u32, u32),
/// Phase 7.9 : timestamp du dernier configure envoyé. Permet de
/// throttler à ~60 fps max (1 configure / 16ms).
last_configure_at: Instant,
}
#[derive(Debug)]
@ -341,6 +352,10 @@ pub struct WaylandFrontend {
/// 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>,
/// Phase 7.9 : mapping SurfaceId → xdg_toplevel, pour envoyer
/// `configure([Activated])` au focus change. Peuplé au
/// `xdg_surface.GetToplevel`, nettoyé au `wl_surface.destroy`.
toplevels_by_id: HashMap<SurfaceId, xdg_toplevel::XdgToplevel>,
/// Phase 7.6 : queue partagée de ClientId qui se sont déconnectés.
/// Remplie par les callbacks `DumbClientData::disconnected`, drainée
/// par `garbage_collect_dead_clients` dans la boucle main.
@ -401,6 +416,7 @@ impl WaylandFrontend {
cursor_hot_y: 0,
cursor_visible: false,
surfaces_by_id: HashMap::new(),
toplevels_by_id: HashMap::new(),
dead_clients: Arc::new(Mutex::new(Vec::new())),
interactive_drag: None,
last_button_serial: 0,
@ -470,6 +486,8 @@ impl WaylandFrontend {
self.focused_surface = None;
}
}
// Phase 7.9 : nettoyer aussi le mapping toplevels.
self.toplevels_by_id.remove(sid);
if self.cursor_surface_id == Some(*sid) {
self.cursor_surface_id = None;
}
@ -556,6 +574,26 @@ impl WaylandFrontend {
println!(
"[frontend] 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,
];
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);
}
}
if let Some(sid) = new_sid {
if let Some(tl) = self.toplevels_by_id.get(&sid).cloned() {
self.send_focus_configure(&tl, sid, activated_state);
}
}
let serial_leave = self.alloc_input_serial();
let serial_enter = self.alloc_input_serial();
@ -606,6 +644,35 @@ impl WaylandFrontend {
(self.cursor_x, self.cursor_y)
}
/// Phase 7.9 : envoie `xdg_toplevel.configure(w, h, states)` +
/// `xdg_surface.configure(serial)` à un toplevel pour signaler un
/// changement d'état (typiquement Activated/Inactive au focus change).
/// La taille est celle du buffer courant ; si pas de buffer, on
/// utilise la taille par défaut.
fn send_focus_configure(
&mut self,
toplevel: &xdg_toplevel::XdgToplevel,
sid: SurfaceId,
states: Vec<u8>,
) {
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)))
.unwrap_or(DEFAULT_TOPLEVEL_SIZE);
toplevel.configure(w, h, states);
if let Some(td) = toplevel.data::<Arc<XdgToplevelData>>() {
if let Some(xdg_surf) = td.xdg_surface.lock().unwrap().clone() {
let serial = self.next_xdg_serial;
self.next_xdg_serial = self.next_xdg_serial.wrapping_add(1).max(1);
if let Some(xs_data) = xdg_surf.data::<Arc<XdgSurfaceData>>() {
*xs_data.last_serial.lock().unwrap() = serial;
}
xdg_surf.configure(serial);
}
}
}
/// Phase 7.7/7.8 : si un drag interactif est actif, applique le delta
/// `(cursor - start_cursor)`.
/// - Move : déplace la surface.
@ -631,7 +698,7 @@ impl WaylandFrontend {
true
}
DragMode::Resize(edges) => {
let (new_x, new_y, new_w, new_h) = compute_resize_geom(
let (new_x, new_y, mut new_w, mut new_h) = compute_resize_geom(
edges,
drag.start_x,
drag.start_y,
@ -640,6 +707,25 @@ impl WaylandFrontend {
dx,
dy,
);
// 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);
}
}
// 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| {
@ -647,6 +733,18 @@ impl WaylandFrontend {
s.y = new_y;
});
self.registry.commit(drag.surface_id);
// Phase 7.9 : throttling configure. Évite le burst de
// (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 {
return true;
}
// Annoncer la nouvelle taille au client. Le state
// `Resizing` (3) indique au toolkit qu'il est en train
// d'être redimensionné.
@ -668,6 +766,12 @@ impl WaylandFrontend {
*xdg_data.last_serial.lock().unwrap() = serial;
}
drag.xdg_surface_res.configure(serial);
// Phase 7.9 : mémoriser le configure envoyé pour le
// throttling au prochain tick.
if let Some(d) = self.interactive_drag.as_mut() {
d.last_configure_size = (new_w, new_h);
d.last_configure_at = Instant::now();
}
true
}
}
@ -1389,6 +1493,8 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
state.focused_surface = None;
}
}
// Phase 7.9 : nettoyer le mapping toplevels.
state.toplevels_by_id.remove(&id);
// 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;
@ -1521,6 +1627,13 @@ impl wayland_server::Dispatch<xdg_surface::XdgSurface, Arc<XdgSurfaceData>> for
// envoyer configure(serial) au resize.
*toplevel_data.xdg_surface.lock().unwrap() = Some(resource.clone());
let toplevel = data_init.init(id, toplevel_data);
// Phase 7.9 : indexer le toplevel par SurfaceId pour
// envoyer Activated au focus change.
if let Some(sd) = data.wl_surface.data::<Arc<SurfaceData>>() {
if let Some(sid) = *sd.id.lock().unwrap() {
state.toplevels_by_id.insert(sid, toplevel.clone());
}
}
// Position cascading pour que les fenêtres successives ne
// s'empilent pas toutes à (0, 0).
@ -1807,6 +1920,14 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
xdg_toplevel::Request::SetAppId { app_id } => {
*data.app_id.lock().unwrap() = Some(app_id);
}
xdg_toplevel::Request::SetMinSize { width, height } => {
*data.min_size.lock().unwrap() = (width, height);
println!("[frontend] 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})");
}
xdg_toplevel::Request::Move {
seat: _,
serial,
@ -1873,6 +1994,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
start_y: st.y,
start_w,
start_h,
last_configure_size: (start_w, start_h),
last_configure_at: Instant::now(),
};
println!(
"[frontend] xdg_toplevel.move: enter drag sid={:?} start=({},{}) cursor=({},{})",
@ -1950,6 +2073,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
start_y: st.y,
start_w,
start_h,
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=({},{})",