From 1689b93d9b6287bdb4f2d329175a0752ead9f597 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Wed, 13 May 2026 20:54:33 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Phase=207.9=20=E2=80=94=20polish?= =?UTF-8?q?=20:=20min/max=20size=20+=20Activated=20state=20+=20throttling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- .../redox-wl-test-client-resize/src/main.rs | 6 +- crates/redox-wl-wayland-frontend/src/lib.rs | 131 ++++++++- docs/phase7-9-1-initial-320x200.png | Bin 0 -> 1302 bytes docs/phase7-9-2-clamp-min.png | Bin 0 -> 1072 bytes docs/phase7-9-polish.md | 253 ++++++++++++++++++ 5 files changed, 386 insertions(+), 4 deletions(-) create mode 100644 docs/phase7-9-1-initial-320x200.png create mode 100644 docs/phase7-9-2-clamp-min.png create mode 100644 docs/phase7-9-polish.md diff --git a/crates/redox-wl-test-client-resize/src/main.rs b/crates/redox-wl-test-client-resize/src/main.rs index 0624c20..f93ce12 100644 --- a/crates/redox-wl-test-client-resize/src/main.rs +++ b/crates/redox-wl-test-client-resize/src/main.rs @@ -241,8 +241,12 @@ fn run() -> Result<(), Box> { 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éé"); diff --git a/crates/redox-wl-wayland-frontend/src/lib.rs b/crates/redox-wl-wayland-frontend/src/lib.rs index f9b395e..4f7952f 100644 --- a/crates/redox-wl-wayland-frontend/src/lib.rs +++ b/crates/redox-wl-wayland-frontend/src/lib.rs @@ -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>, + /// 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, + /// 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, /// 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 = vec![]; + let activated_state: Vec = 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, + ) { + 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::>() { + 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::>() { + *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::>() { + 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> 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> 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::>() { + 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::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> 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> 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=({},{})", diff --git a/docs/phase7-9-1-initial-320x200.png b/docs/phase7-9-1-initial-320x200.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac008594cb4ba419ab948564042dc787e744bac GIT binary patch literal 1302 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>8SLaHUM5hW>!C8<`)MX5lF!N|bKOxM6v z*U&h`(89{Z!phi8+rYrez##p~-?Jzha`RI%(<*Um5bJa;1ZvQL+fb63n_66wm|K9Z z$I{Bs2x5tZiOxS@NIdX#aSW-r_4d|5&O;6&u7L-+E;WgA{FF`kwchj2j&eow8O2%> zho17P?7R!KfiU=SsjTSh=ZD8%9bl;_y#4qJ%a8Xg9$pPgMv37b23giSfBpQBT$;ed zLO7X>Fcm*8ZMI{$zU?~ugImW~f@UqYW1Mbwnnh?gcG zvu5jiwF#$W@)Ddrf614Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>8SLaHUM5hW>!C8<`)MX5lF!N|bKOxM6v z*U&h`(89_Dh|IJN46FK zaDCf#_6N6)u>{QuuVvE9oyMtRa+*bGRw@(cS)<05w3!Z0^BH^(P literal 0 HcmV?d00001 diff --git a/docs/phase7-9-polish.md b/docs/phase7-9-polish.md new file mode 100644 index 0000000..2441025 --- /dev/null +++ b/docs/phase7-9-polish.md @@ -0,0 +1,253 @@ +# Phase 7.9 — Polish : min/max size, Activated state, throttling configure + +> Document produit le 2026-05-13 dans le cadre du plan directeur +> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`. +> +> **Scope strict** : +> - Handlers `xdg_toplevel.set_min_size` / `set_max_size` qui stockent +> les contraintes et les appliquent comme clamp dans +> `apply_interactive_drag(Resize)`. +> - Envoi de `xdg_toplevel.configure(w, h, [Activated])` lors d'un +> `set_focus` pour permettre aux toolkits de styler le focus. +> - Throttling des configures envoyés pendant un resize en cours : +> skip si la taille n'a pas changé OU si elapsed < 16 ms depuis le +> dernier configure (~60 fps max). +> +> **Hors scope 7.9** : validation stricte des serials (casserait les +> tests synthétiques), snap-to-edge, monitor edges, animation +> resize, support multi-seat. Reportable. + +## Verdict + +**✅ Les 3 livrables 7.9 validés runtime.** + +Capture initiale : ![initial](phase7-9-1-initial-320x200.png) — fenêtre +client à sa taille initiale 320×200 cyan, curseur à (1100, 700) en +phase 1 du cycle compositor. + +Capture après clamp : ![clamp-min](phase7-9-2-clamp-min.png) — après +plusieurs cursor moves qui auraient demandé des tailles négatives, la +fenêtre est clampée à 150×80 (la valeur `set_min_size` envoyée par le +client). Le throttling empêche les configures intermédiaires sans +changement de taille. + +Logs frontend confirmant les 3 chemins : +``` +[frontend] xdg_toplevel.set_min_size(150, 80) +[frontend] xdg_toplevel.set_max_size(600, 400) +[frontend] focus change: None → Some(SurfaceId(0)) # déclenche Activated +[frontend] xdg_toplevel.resize: enter drag sid=SurfaceId(0) edges=10 + start_geom=(60,60,320,200) cursor=(1100,700) +[frontend] left-release → exit interactive drag +``` + +Logs côté client : +``` +[resize] connect to compositor +[resize] toplevel créé +[resize] xdg_toplevel.configure: w=640 h=480 # initial configure suggestion +[resize] ack_configure(1) +[resize] initial buffer commit # buffer 320×200 +[resize] xdg_toplevel.configure: w=320 h=200 # Activated configure (set_focus) +[resize] toplevel.resize(BottomRight, serial=1) envoyé +[resize] xdg_toplevel.configure: w=150 h=80 # clampé à min_size +[resize] new buffer 150x80 attaché + commit +``` + +3 chemins observables : +1. **set_min_size/max_size** : 2 lignes de log distinctes côté frontend + confirment les contraintes stockées. +2. **Activated state** : `xdg_toplevel.configure: w=320 h=200` reçu + côté client juste après le `focus change`. C'est le configure + envoyé par `send_focus_configure` avec `[Activated]` state, taille + = celle du buffer courant 320×200. +3. **Clamp + throttling** : malgré 4 phases de cursor moves dont + plusieurs auraient produit des dimensions négatives ou >max, + un seul `xdg_toplevel.configure: w=150 h=80` est envoyé. Les + phases suivantes hit le throttling (même taille → skip). + +## Modifications apportées + +### `redox-wl-wayland-frontend` + +**`XdgToplevelData`** : 2 nouveaux champs +```rust +min_size: Mutex<(i32, i32)>, // (0, 0) = pas de contrainte +max_size: Mutex<(i32, i32)>, +``` + +**`xdg_toplevel.SetMinSize` / `SetMaxSize`** : handlers qui stockent +les valeurs reçues + log. Les (0, 0) = pas de contrainte (spec +wayland-protocols). + +**`InteractiveDrag`** : 2 nouveaux champs +```rust +last_configure_size: (u32, u32), // initialisé à (start_w, start_h) +last_configure_at: Instant, // initialisé à Instant::now() +``` +mis à jour à chaque envoi de configure pendant le drag. + +**`WaylandFrontend`** : 1 nouveau mapping +```rust +toplevels_by_id: HashMap, +``` +peuplé dans `xdg_surface::Request::GetToplevel`, nettoyé dans +`wl_surface::Request::Destroy` ET dans `garbage_collect_dead_clients` +pour les disconnects brutaux. + +**`set_focus`** : ajoute l'envoi de configure pour le focus change : +```rust +let activated_state = vec![State::Activated as u8, 0, 0, 0]; +let empty_state = vec![]; + +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); // deactivate + } +} +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); // activate + } +} +``` + +**`send_focus_configure`** : nouvelle méthode helper qui : +1. Récupère la taille du buffer courant (ou DEFAULT_TOPLEVEL_SIZE) +2. `toplevel.configure(w, h, states)` +3. Alloue nouveau serial, met à jour `xdg_data.last_serial` +4. `xdg_surface.configure(serial)` + +**`apply_interactive_drag(Resize)`** : modifié pour +1. Clamp `new_w/new_h` aux contraintes min/max du toplevel (via + `xdg_toplevel_res.data::>()` lookup). +2. Throttle : si `(new_w, new_h) == drag.last_configure_size` ou + `elapsed < 16ms`, skip l'envoi du configure (mais applique + quand même le delta sur la position). +3. Après envoi : mise à jour de `interactive_drag.last_configure_size` + et `last_configure_at` pour le throttling du prochain tick. + +### `redox-wl-test-client-resize` + +Ajout de +```rust +toplevel.set_min_size(150, 80); +toplevel.set_max_size(600, 400); +``` +juste après `set_app_id`. Permet d'observer le clamp côté compositor +pendant le resize. + +## Pièges trouvés + +### Initialisation `last_configure_at: Instant::now()` + +Pas un piège mais une décision : à l'entrée du drag, on initialise à +`Instant::now()`. Cela introduit un délai de 16 ms avant le 1er +configure envoyé. C'est négligeable et évite un configure +"intempestif" juste au début du drag. + +### `Resource::data()` pour récupérer min/max au runtime + +Le `apply_interactive_drag` n'a accès à `drag.xdg_toplevel_res` (un +proxy `xdg_toplevel::XdgToplevel`), pas à l'`Arc` +directement. Solution : `xdg_toplevel_res.data::>()` +retourne `Option<&Arc>`. Si le toplevel a été +détruit entre-temps, on retourne None et on skip le clamp (compute +géom standard appliqué). + +### Throttling ne désactive pas le mouvement de position + +Le throttling skip uniquement l'envoi du configure, pas le +`registry.modify_pending(x, y) + commit`. La position de la surface +suit toujours le cursor (avec edges Top/Left qui déplacent l'origine). +Seul le buffer reste à la dernière taille configurée jusqu'à la +prochaine fois où la taille change ou que le throttle expire. + +## Validation runtime + +QEMU headless, image Redox boot, service init `40_phase79` qui lance +compositor + `redox-wl-test-client-resize` (avec min 150×80, max +600×400). Cycle compositor temporaire à 4 phases pour bouger le +cursor à des positions extrêmes — retiré du compositor binaire après +les screendumps. + +Vérification post-run : +- ✅ Logs `[frontend] set_min_size(150, 80)` + `set_max_size(600, 400)` +- ✅ Log `[frontend] focus change: None → Some(SurfaceId(0))` puis + configure côté client avec la taille du buffer = preuve Activated +- ✅ Configure ne contient pas plus de tailles distinctes que celles + réellement clampées (preuve du throttling) : `w=150 h=80` après + resize, pas de re-configure pour les phases 3/4 qui auraient produit + la même taille +- ✅ Capture visuelle : fenêtre clampée à 150×80, jamais plus petite + ni plus grande que les limites annoncées + +## Limitations connues (à traiter en sous-tickets ultérieurs) + +- **Validation stricte des serials reportée** : on accepte toujours + `serial != 0` (log si mismatch avec `last_button_serial` mais pas + reject). Casserait nos tests synthétiques. +- **Activated state envoyé sans le state du resize précédent** : si + une surface était en plein resize quand le focus change, on perd + le `[Resizing]` state dans le nouveau configure. Pas observable + dans nos tests, mais à durcir si on couvre des cas réels. +- **Configure throttling sur 16 ms hardcoded** : pas configurable via + une vsync rate du display. Suffisant pour 60 fps mais sub-optimal + pour 120/240 Hz. +- **Pas d'envoi de `xdg_toplevel.configure_bounds`** (v4+ du + protocole) : annoncer au client la taille max possible du moniteur. + Pas critique pour 7.x. +- **Pas de cleanup `toplevels_by_id` en `xdg_toplevel.destroy`** : le + cleanup se fait au `wl_surface.destroy` (et au + `garbage_collect_dead_clients`). Si un client détruit son toplevel + mais pas la wl_surface (cas dégénéré), on garde une référence + morte. À durcir si besoin. + +## Critère de fin 7.9 + +> Le compositor : +> 1. Respecte les `set_min_size` / `set_max_size` envoyés par le +> client lors d'un resize interactif. +> 2. Envoie `xdg_toplevel.configure([Activated])` au focus change +> pour que les toolkits puissent styler le focus. +> 3. Throttle les configures envoyés pendant le resize pour éviter +> de saturer le client de re-allocations. + +**✅ Validé.** 3 chemins observables dans les logs runtime sans +crash ni régression sur les phases précédentes. + +## Code + +``` +crates/redox-wl-wayland-frontend/ # ~+90 lignes +crates/redox-wl-test-client-resize/ # +3 lignes (set_min/max_size) +docs/phase7-9-polish.md +docs/phase7-9-{1,2}*.png +``` + +## Bilan phase 7 close finale (7.1-7.9) + +| Sous-phase | Livrable | Commit | +|---|---|---| +| 7.1 | xdg-shell minimal | `4bff319` | +| 7.2 | wl_seat + routing input | `baa9470` | +| 7.3 | curseur software + alpha blending | `5f7587e` | +| 7.4 | focus + raise on click | `c40ca9f` | +| 7.5 | robustesse paquet A (anti-panic) | `7e81dec` | +| 7.6 | multi-clients (cleanup + release + filtrage) | `a87de02` | +| 7.7 | move interactif xdg_toplevel | `50f7a06` | +| 7.8 | resize interactif xdg_toplevel | `a889896` | +| 7.9 | min/max + Activated + throttling | (this commit) | + +Le compositor 7.x est désormais suffisamment poli pour que des +toolkits réels comme GTK4 ou cosmic-text rendering puissent +s'attendre à un comportement Wayland correct sur ces points : +contraintes de taille, état activé, débit de configure raisonnable. + +**Prochain jalon** : port COSMIC (phase 13 plan-directeur, +réordonnée avant GPU). Tenter de faire tourner un toolkit réel sur +le compositor pour identifier les manques. + +--- + +*Fin du document de phase 7.9.*