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

253 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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