# 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.*