redox-wayland-compositor/docs/phase7-9-polish.md
Votre Nom 1689b93d9b 🎉 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
2026-05-13 20:54:33 +02:00

10 KiB
Raw Permalink Blame History

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 — fenêtre client à sa taille initiale 320×200 cyan, curseur à (1100, 700) en phase 1 du cycle compositor.

Capture après clamp : clamp-min — 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

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

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

toplevels_by_id: HashMap<SurfaceId, xdg_toplevel::XdgToplevel>,

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 :

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::<Arc<XdgToplevelData>>() 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

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<XdgToplevelData> directement. Solution : xdg_toplevel_res.data::<Arc<XdgToplevelData>>() retourne Option<&Arc<XdgToplevelData>>. 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.