🎉 Phase 7.7 — move interactif xdg_toplevel validé runtime

Move interactif complet (Resize en stub log-only, reportable à 7.8).

Frontend additions :
- enum DragMode { Move, Resize(u32) } et struct InteractiveDrag
  { surface_id, xdg_toplevel_res, xdg_surface_res, mode,
    start_cursor_x/y, start_x/y, start_w/h }
- WaylandFrontend : interactive_drag: Option<InteractiveDrag> +
  last_button_serial: u32
- XdgToplevelData.xdg_surface: Mutex<Option<XdgSurface>> peuplé
  dans xdg_surface.GetToplevel pour retrouver le wl_surface parent
  depuis un toplevel.move/resize
- Handler xdg_toplevel.Move : valide serial != 0, refuse drag déjà
  actif, retrouve SurfaceId via cascade UserData (xdg_surface →
  wl_surface → SurfaceData), capture start_cursor + start_geom,
  stocke InteractiveDrag
- Handler xdg_toplevel.Resize : stub log-only (à compléter 7.8)
- Handler xdg_toplevel.Destroy nettoie interactive_drag si on était
  en train de drag cette surface
- Méthode apply_interactive_drag() : applique le delta (cursor -
  start_cursor) à la position de la surface (Move) ou consume le
  motion (Resize stub)
- forward_input(PointerMotion(Relative)) : apply au début, return
  si drag actif (court-circuite l'envoi de motion au client pendant
  un drag, conforme spec Wayland)
- forward_input(PointerButton release) : sort du mode drag
- set_cursor_position : appelle aussi apply_interactive_drag (sans
  ça, les cycles de test programmatique ne déclenchent pas le drag
  car ils court-circuitent forward_input)
- Tracking last_button_serial à chaque button LEFT Pressed

Test client modifications :
- redox-wl-test-client-shm-two bind wl_seat
- Si label=="A", déclenche toplevel.move(&seat, 1) 8s après le
  commit initial (mécanisme "synthétique" : le client n'écoute pas
  les pointer events, il envoie Move sans attendre un clic — assez
  pour valider le pipeline serveur, durcissement client 7.8)

Validation runtime :
- Cycle compositor temporaire (retiré du binaire final) qui change
  cursor_position à plusieurs positions pendant que le drag est
  actif, screendumps à 3 positions distinctes
- Logs frontend :
  [frontend] xdg_toplevel.move: enter drag sid=SurfaceId(0) start=(60,60) cursor=(500,400)
  [frontend] left-release → exit interactive drag
- A déplacée visuellement entre les captures pos1, pos2 et
  post-release ; sortie clipée sur les bords (pas de snap-to-edge,
  WM policy reportable)

Limitations 7.7 :
- Resize non implémenté (stub)
- Validation serial laxiste (serial != 0)
- Pas de contrôle policy (snap-to-edge, min/max)
- Pas de check "surface du même client" (sécu)
- Pas d'event xdg_toplevel.configure([Resizing]) envoyé pendant le drag

Doc complète : docs/phase7-7-move-resize.md

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-13 19:22:04 +02:00
parent a87de02555
commit 50f7a064e4
6 changed files with 468 additions and 8 deletions

View file

@ -29,8 +29,8 @@ use wayland_client::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
backend::Backend,
protocol::{
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_shm::WlShm,
wl_shm_pool::WlShmPool, wl_surface::WlSurface,
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_seat::WlSeat,
wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_surface::WlSurface,
},
};
use wayland_protocols::xdg::shell::client::{
@ -71,6 +71,7 @@ struct ClientState {
compositor: Option<WlCompositor>,
shm: Option<WlShm>,
wm_base: Option<XdgWmBase>,
seat: Option<WlSeat>,
pending_serial: Option<u32>,
configured: bool,
label: String,
@ -96,6 +97,9 @@ impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
"xdg_wm_base" => {
state.wm_base = Some(registry.bind(name, version.min(5), qh, ()));
}
"wl_seat" => {
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
}
_ => {}
}
}
@ -122,6 +126,7 @@ noop!(WlShm);
noop!(WlShmPool);
noop!(WlBuffer);
noop!(WlSurface);
noop!(WlSeat);
impl Dispatch<XdgWmBase, ()> for ClientState {
fn event(
@ -312,6 +317,16 @@ fn run_one(label: &str, base_color: u32, letter: char, shm_name: &str) -> Result
event_queue.flush()?;
dlog(&format!("[{label}] buffer commit POST-ack"));
// Phase 7.7 : la fenêtre A déclenche un interactive_move après 8 s.
// Sert à valider que le compositor déplace la surface en suivant le
// curseur (positionné par le cycle test du compositor lui-même).
let move_at = if label == "A" {
Some(std::time::Instant::now() + Duration::from_secs(8))
} else {
None
};
let mut move_done = false;
// Boucle vivante 160 s avec dispatch des events
let start = std::time::Instant::now();
while start.elapsed() < Duration::from_secs(160) {
@ -321,6 +336,21 @@ fn run_one(label: &str, base_color: u32, letter: char, shm_name: &str) -> Result
let _ = guard.read();
}
let _ = event_queue.dispatch_pending(&mut state);
// Phase 7.7 : déclencher toplevel.move après 8s pour la fenêtre A.
if !move_done {
if let Some(t) = move_at {
if std::time::Instant::now() >= t {
if let Some(seat) = state.seat.clone() {
// Serial synthétique != 0 pour passer le garde compositor.
toplevel._move(&seat, 1);
let _ = event_queue.flush();
dlog(&format!("[{label}] toplevel.move() envoyé"));
move_done = true;
}
}
}
}
thread::sleep(Duration::from_millis(50));
}

View file

@ -192,6 +192,46 @@ struct XdgSurfaceData {
struct XdgToplevelData {
title: Mutex<Option<String>>,
app_id: Mutex<Option<String>>,
/// 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.7 : mode d'un drag interactif déclenché par
/// `xdg_toplevel.move` ou `xdg_toplevel.resize`.
#[derive(Clone, Copy, Debug)]
enum DragMode {
Move,
/// Resize avec un bitmask de bords tirés. Le contenu est un
/// `xdg_toplevel::ResizeEdge` interprété par le compositor pour
/// calculer (new_w, new_h, new_x, new_y).
#[allow(dead_code)]
Resize(u32),
}
/// Phase 7.7 : état d'un drag interactif en cours. Posé par le handler
/// `xdg_toplevel.Move`/`Resize`, lu par `forward_input` pour appliquer
/// les deltas, retiré au release du bouton gauche.
#[derive(Clone)]
struct InteractiveDrag {
surface_id: SurfaceId,
/// Le xdg_toplevel cible — gardé pour envoyer `configure(w, h)` au
/// resize. Pour move seul on s'en sert pas mais on garde par symétrie.
#[allow(dead_code)]
xdg_toplevel_res: xdg_toplevel::XdgToplevel,
/// Le xdg_surface parent — gardé pour envoyer `configure(serial)` au
/// resize. Inutilisé en move pur.
#[allow(dead_code)]
xdg_surface_res: xdg_surface::XdgSurface,
mode: DragMode,
start_cursor_x: i32,
start_cursor_y: i32,
start_x: i32,
start_y: i32,
#[allow(dead_code)]
start_w: u32,
#[allow(dead_code)]
start_h: u32,
}
#[derive(Debug)]
@ -261,6 +301,17 @@ pub struct WaylandFrontend {
/// Remplie par les callbacks `DumbClientData::disconnected`, drainée
/// par `garbage_collect_dead_clients` dans la boucle main.
dead_clients: Arc<Mutex<Vec<ClientId>>>,
// ----- Phase 7.7 : move/resize interactifs ------------------------
/// Drag interactif en cours, posé par `xdg_toplevel.Move`/`Resize`.
/// Lu par `forward_input` à chaque PointerMotion(Relative). Vidé au
/// release du bouton gauche.
interactive_drag: Option<InteractiveDrag>,
/// Dernier serial envoyé pour un `wl_pointer.button` Pressed. Utilisé
/// pour valider qu'un `xdg_toplevel.Move/Resize` arrive en réponse à
/// un clic récent (anti-replay). Pour 7.7 on accepte simplement
/// `serial != 0` (validation à durcir en 7.8).
last_button_serial: u32,
/// Hot-spot du curseur (offset à soustraire à cursor_x/y pour le placement).
cursor_hot_x: i32,
cursor_hot_y: i32,
@ -307,6 +358,8 @@ impl WaylandFrontend {
cursor_visible: false,
surfaces_by_id: HashMap::new(),
dead_clients: Arc::new(Mutex::new(Vec::new())),
interactive_drag: None,
last_button_serial: 0,
})
}
@ -509,6 +562,37 @@ impl WaylandFrontend {
(self.cursor_x, self.cursor_y)
}
/// Phase 7.7 : si un drag interactif est actif, applique le delta
/// `(cursor - start_cursor)` à la position de la surface ciblée et
/// retourne `true` pour court-circuiter l'envoi de motion au client.
/// Retourne `false` si pas de drag, le caller doit alors faire son
/// envoi normal.
fn apply_interactive_drag(&mut self) -> bool {
let Some(drag) = self.interactive_drag.clone() else {
return false;
};
match drag.mode {
DragMode::Move => {
let dx = self.cursor_x - drag.start_cursor_x;
let dy = self.cursor_y - drag.start_cursor_y;
let new_x = drag.start_x.saturating_add(dx);
let new_y = drag.start_y.saturating_add(dy);
self.registry.modify_pending(drag.surface_id, |s| {
s.x = new_x;
s.y = new_y;
});
self.registry.commit(drag.surface_id);
true
}
DragMode::Resize(_) => {
// Stub 7.7 : pas de resize effectif (cf XdgToplevel.Resize handler).
// Au minimum on consomme le motion pour éviter de l'envoyer
// au client pendant qu'il pense être en mode resize.
true
}
}
}
/// Renvoie le ClientId de la surface focalisée, s'il y en a une.
/// Utilisé en 7.6 pour filtrer le routage des events pointer/keyboard
/// (un seul client à la fois reçoit les events, contrairement au
@ -555,6 +639,11 @@ impl WaylandFrontend {
self.cursor_x = *x;
self.cursor_y = *y;
self.cursor_visible = true;
// Phase 7.7 : si drag actif, déplacer la surface au lieu
// d'envoyer motion au client.
if self.apply_interactive_drag() {
return;
}
let time = self.alloc_input_time();
if let Some(focus) = self.focused_surface.clone() {
let focus_cid = focus.client().map(|c| c.id());
@ -573,6 +662,10 @@ impl WaylandFrontend {
self.cursor_x = self.cursor_x.saturating_add(*dx);
self.cursor_y = self.cursor_y.saturating_add(*dy);
self.cursor_visible = true;
// Phase 7.7 : si drag actif, déplacer la surface.
if self.apply_interactive_drag() {
return;
}
let time = self.alloc_input_time();
if let Some(focus) = self.focused_surface.clone() {
let focus_cid = focus.client().map(|c| c.id());
@ -596,6 +689,13 @@ impl WaylandFrontend {
// position curseur et raise + set_focus à la surface ciblée.
// À faire AVANT l'envoi des events button pour que la nouvelle
// surface reçoive enter+modifiers en premier, puis le button.
// Phase 7.7 : si drag actif et le bouton gauche est relâché,
// sortir du mode drag avant l'envoi du button au client.
if !*left && self.interactive_drag.is_some() {
println!("[frontend] left-release → exit interactive drag");
self.interactive_drag = None;
}
if *left {
let hit = self.registry.hit_test(self.cursor_x, self.cursor_y);
println!(
@ -640,6 +740,12 @@ impl WaylandFrontend {
// on envoie les 3 à chaque event au client focused.
for (btn, pressed) in buttons {
let serial = self.alloc_input_serial();
// Phase 7.7 : tracker le dernier serial button Pressed
// pour valider les xdg_toplevel.move/resize qui doivent
// arriver en réponse.
if pressed && btn == BTN_LEFT {
self.last_button_serial = serial;
}
let state = if pressed {
wl_pointer::ButtonState::Pressed
} else {
@ -794,10 +900,15 @@ impl WaylandFrontend {
/// Force la position du curseur à tout moment. Utile pour tests
/// programmatiques qui veulent simuler un mouvement souris en dehors
/// du circuit `forward_input`.
///
/// Phase 7.7 : applique aussi le drag interactif s'il est actif, car
/// `set_cursor_position` court-circuite `forward_input(PointerMotion)`
/// qui est l'endroit normal où le drag est mis à jour.
pub fn set_cursor_position(&mut self, x: i32, y: i32) {
self.cursor_x = x;
self.cursor_y = y;
self.cursor_visible = true;
let _ = self.apply_interactive_drag();
}
/// Récupère le buffer curseur courant (client custom si fourni, sinon
@ -1326,6 +1437,9 @@ impl wayland_server::Dispatch<xdg_surface::XdgSurface, Arc<XdgSurfaceData>> for
match request {
xdg_surface::Request::GetToplevel { id } => {
let toplevel_data = Arc::new(XdgToplevelData::default());
// Phase 7.7 : capter le xdg_surface parent pour pouvoir
// envoyer configure(serial) au resize.
*toplevel_data.xdg_surface.lock().unwrap() = Some(resource.clone());
let toplevel = data_init.init(id, toplevel_data);
// Position cascading pour que les fenêtres successives ne
@ -1598,9 +1712,9 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
for WaylandFrontend
{
fn request(
_state: &mut Self,
state: &mut Self,
_client: &Client,
_r: &xdg_toplevel::XdgToplevel,
resource: &xdg_toplevel::XdgToplevel,
request: xdg_toplevel::Request,
data: &Arc<XdgToplevelData>,
_dh: &DisplayHandle,
@ -1613,11 +1727,88 @@ 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::Destroy => {
// wl_surface destroy gérera la suppression côté registry
xdg_toplevel::Request::Move {
seat: _,
serial,
} => {
// Phase 7.7 : entrer en mode drag-move.
// Validation laxiste : on accepte tout `serial != 0` pour
// 7.7 ; à durcir en 7.8 (comparer à `last_button_serial`).
if serial == 0 {
println!("[frontend] xdg_toplevel.move: serial=0 refused");
return;
}
if state.interactive_drag.is_some() {
println!("[frontend] xdg_toplevel.move: drag already in progress");
return;
}
let Some(xdg_surf) = data.xdg_surface.lock().unwrap().clone() else {
println!("[frontend] xdg_toplevel.move: no parent xdg_surface");
return;
};
let Some(xdg_data) = xdg_surf.data::<Arc<XdgSurfaceData>>() else {
return;
};
let wl_surf = &xdg_data.wl_surface;
let Some(surf_data) = wl_surf.data::<Arc<SurfaceData>>() else {
return;
};
let Some(sid) = *surf_data.id.lock().unwrap() else {
return;
};
let Some(s) = state.registry.get(sid) else {
return;
};
let st = s.current();
let (start_w, start_h) = st
.buffer
.as_ref()
.map(|b| (b.width, b.height))
.unwrap_or((0, 0));
let drag = InteractiveDrag {
surface_id: sid,
xdg_toplevel_res: resource.clone(),
xdg_surface_res: xdg_surf,
mode: DragMode::Move,
start_cursor_x: state.cursor_x,
start_cursor_y: state.cursor_y,
start_x: st.x,
start_y: st.y,
start_w,
start_h,
};
println!(
"[frontend] xdg_toplevel.move: enter drag sid={:?} start=({},{}) cursor=({},{})",
sid, st.x, st.y, state.cursor_x, state.cursor_y
);
state.interactive_drag = Some(drag);
}
// Tout le reste ignoré en 7.1 :
// SetParent, ShowWindowMenu, Move, Resize, SetMaxSize, SetMinSize,
xdg_toplevel::Request::Resize {
seat: _,
serial,
edges,
} => {
// Phase 7.7 : stub log-only. Implémentation complète
// reportée à 7.8 (compute new_w/new_h selon edges et
// envoyer configure).
let edges_raw = match edges.into_result() {
Ok(e) => e as u32,
Err(_) => 0,
};
println!(
"[frontend] xdg_toplevel.resize: serial={serial} edges={edges_raw} (stub, ignored)"
);
}
xdg_toplevel::Request::Destroy => {
// Si on était en train de drag cette surface, abandonner le mode
if let Some(drag) = &state.interactive_drag {
if drag.xdg_toplevel_res == *resource {
state.interactive_drag = None;
}
}
}
// Tout le reste ignoré pour 7.7 :
// SetParent, ShowWindowMenu, SetMaxSize, SetMinSize,
// SetMaximized, UnsetMaximized, SetFullscreen, UnsetFullscreen,
// SetMinimized
_ => {}

BIN
docs/phase7-7-drag-pos1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
docs/phase7-7-drag-pos2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,239 @@
# Phase 7.7 — Move/resize interactifs via xdg_toplevel
> Document produit le 2026-05-13 dans le cadre du plan directeur
> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`.
>
> **Scope strict** :
> - `xdg_toplevel.Move { seat, serial }` : entrer en mode drag-move,
> appliquer le delta cursor sur la position de la surface, sortir au
> release du bouton gauche.
> - `xdg_toplevel.Resize { seat, serial, edges }` : **stub log-only**
> pour 7.7 (implémentation complète reportée à 7.8 si besoin).
> - Validation runtime end-to-end via un client qui appelle
> `toplevel.move(&seat, serial)` au compositor.
>
> **Hors scope 7.7** : resize effectif (xdg_toplevel.configure(w,h) +
> recompute selon edges), validation stricte du serial, snap-to-edge,
> contraintes min/max size.
## Verdict
**✅ Move interactif validé runtime.** La fenêtre A (verte/pyramide)
créée par le test client envoie `toplevel.move(&seat, 1)` 8 s après
son ack_configure ; le compositor enregistre l'état initial de drag
puis applique le delta de curseur à chaque `set_cursor_position` (ou
`PointerMotion` réel). À `mouse_button 0` (release) le mode drag se
termine et la surface reste à sa dernière position.
Captures :
- ![pos1](phase7-7-drag-pos1.png) — état pendant le drag : A clipée
partiellement à gauche (delta négatif appliqué depuis sa position
initiale (60, 60))
- ![pos2](phase7-7-drag-pos2.png) — état pendant le drag : A déplacée
en haut, partiellement clipée en y négatif (delta différent)
- ![post-release](phase7-7-post-release.png) — état après release :
surface stabilisée à la dernière position appliquée
Logs frontend (`/tmp/comp.log` extrait via redoxfs) :
```
[frontend] focus change: None → Some(SurfaceId(0))
[frontend] focus change: Some(SurfaceId(0)) → Some(SurfaceId(1))
[frontend] xdg_toplevel.move: enter drag sid=SurfaceId(0) start=(60,60) cursor=(500,400)
[frontend] left-release → exit interactive drag
```
Logs côté client :
```
[A] connect to compositor
[A] xdg_toplevel créé
[A] ack_configure(1)
[A] buffer commit POST-ack
[A] toplevel.move() envoyé
```
## Modifications apportées
### `redox-wl-wayland-frontend`
**Nouveaux types** :
- `DragMode { Move, Resize(u32) }` — quel type de drag est actif.
Le u32 pour Resize est le bitmask `xdg_toplevel::ResizeEdge` brut.
- `InteractiveDrag { surface_id, xdg_toplevel_res, xdg_surface_res,
mode, start_cursor_x, start_cursor_y, start_x, start_y, start_w,
start_h }` — l'état d'un drag en cours, posé à l'entrée et lu à
chaque update curseur.
**`WaylandFrontend`** : nouveaux champs
```rust
interactive_drag: Option<InteractiveDrag>,
last_button_serial: u32,
```
**`XdgToplevelData`** : nouveau champ
```rust
xdg_surface: Mutex<Option<xdg_surface::XdgSurface>>,
```
peuplé dans `xdg_surface::Request::GetToplevel`. Permet au handler
`Move`/`Resize` du toplevel de retrouver le `wl_surface` parent + le
`SurfaceId` côté compositor-core.
**Handler `xdg_toplevel::Request::Move`** :
1. Validation laxiste du serial : `serial != 0` accepté (durcir 7.8).
2. Refus si un drag est déjà en cours (un seul drag actif à la fois).
3. Retrouve `xdg_surface``wl_surface``SurfaceData``SurfaceId`
via les data UserData.
4. Récupère la position actuelle de la surface dans le registry pour
en faire `start_x`, `start_y`. Capture aussi `start_cursor_x/y`.
5. Stocke `InteractiveDrag::Move` dans `self.interactive_drag`.
**Handler `xdg_toplevel::Request::Resize`** : stub log-only. Logge
`serial` et `edges` puis ignore. À implémenter en 7.8 :
- calculer `(new_x, new_y, new_w, new_h)` selon `edges` (TOP/BOTTOM/
LEFT/RIGHT/coins) et le delta cursor
- envoyer `xdg_toplevel.configure(new_w, new_h, vec![])` +
`xdg_surface.configure(serial)` pour que le client re-rende
- attendre `ack_configure` puis le prochain commit avec le nouveau
buffer
**Méthode `apply_interactive_drag(&mut self) -> bool`** : si un drag
est actif, applique le delta `(cursor - start_cursor)` à la position
de la surface ciblée (`registry.modify_pending + commit`) et retourne
`true` pour que le caller court-circuite l'envoi des events
`wl_pointer.motion` au client (la spec dit que pendant un drag, le
client ne doit pas recevoir de motion).
**`forward_input(PointerMotion / PointerMotionRelative)`** : appelle
`apply_interactive_drag()` au début. Si `true`, return immédiat
(pas d'envoi de motion). Sinon comportement normal 7.6.
**`forward_input(PointerButton)`** : si `left == false` (release) et
drag actif, vide `self.interactive_drag` et log "left-release →
exit interactive drag". Indépendant du focus actuel.
**`set_cursor_position(x, y)`** : appelle `apply_interactive_drag()`
après mise à jour de `cursor_x/y`, car ce setter court-circuite
`forward_input(PointerMotion)` qui est l'endroit normal d'application
du drag. Sans cette ligne, un cycle de test programmatique qui
change le cursor sans event réel ne déclenche pas le drag.
### `redox-wl-test-client-shm-two`
Modifications mineures pour bind `wl_seat` (resource simple, pas de
`get_pointer` ni d'écoute de `button` — le client A déclenche le
move de manière synthétique 8 s après son commit) :
- Import `wl_seat::WlSeat`
- `ClientState.seat: Option<WlSeat>`
- Registry handler bind `"wl_seat"` v7
- `noop!(WlSeat)`
- Dans `run_one`, si `label == "A"`, calcule `move_at = now + 8s` ;
dans la boucle vivante, dès que `now >= move_at`, appel
`toplevel._move(&seat, 1)` puis flush. Une seule fois.
Le serial `1` est synthétique. Le compositor 7.7 accepte (validation
laxiste). À durcir en 7.8 : le serial doit correspondre à un
`wl_pointer.button` récent et le client devrait écouter le button
events avant d'envoyer Move.
## Méthode de validation runtime
Comme en 7.3, 7.4 et 7.6, la validation utilise un cycle temporaire
de `set_cursor_position` côté compositor pour simuler des mouvements
souris (le driver mouse Redox sous QEMU ne reçoit pas de
`PointerMotion`). Le cycle a 4 phases :
- T+0 à T+14 : cursor à (200, 200) — point de départ avant le Move
du client (qui arrive à compositor T~10s)
- T+14 → cursor à (500, 400)
- T+18 → cursor à (800, 100)
- T+22 → cursor à (300, 500)
- T+26 (post-release) → cursor à (600, 300)
Le client A envoie `toplevel.move()` à T+8s de son démarrage. Le
compositor entre en drag avec start_cursor = position courante,
start_geom = (60, 60). Aux changements de cursor suivants,
`apply_interactive_drag` applique le delta. À T_QEMU ~43s, le bash
script envoie `mouse_button 1` puis `mouse_button 0` (release) pour
terminer le drag.
Ce cycle de positionnement et l'auto-move côté client A sont des
helpers de validation, **retirés du compositor binaire après les
screendumps** (le code production ne fait que
`set_cursor_initial_position(center)` + `draw_cursor` + `apply` via
`forward_input` réel).
Note : le côté `move auto à T+8s` du client `redox-wl-test-client-shm-two`
est conservé dans le code commité — c'est un test binary, pas un
client de production. La fenêtre B n'est pas affectée (`label != "A"`).
## Limitations connues (à traiter en sous-tickets ultérieurs)
- **Resize non implémenté** : stub log-only. La compute de
`(new_x, new_y, new_w, new_h)` selon `edges` + l'envoi de
`xdg_toplevel.configure(w, h, [Resizing])` + `xdg_surface.configure
(serial)` + attente de `ack_configure` puis du commit avec nouveau
buffer est reportable.
- **Validation serial laxiste** : on accepte `serial != 0`. La spec
dit "doit correspondre à un input event récent (button)". À
durcir : comparer à `last_button_serial`, refuser si trop ancien
(par ex. > 100ms).
- **Pas de snap-to-edge / contraintes** : la surface peut sortir
partiellement ou entièrement de l'écran (visible dans les captures
pos1/pos2). Reportable à un sous-ticket "policy WM".
- **Pas d'événement `xdg_toplevel.configure([Resizing])` envoyé en
début de Move** : la spec dit que le compositor peut/doit annoncer
le state via configure pour que le toolkit affiche un curseur de
drag différent. Pas critique pour 7.7.
- **Un seul drag actif à la fois** : si un autre client envoie Move
pendant qu'un drag est en cours, on log et on ignore. La spec ne
précise pas ce comportement ; tolérant est OK.
- **Pas de validation que la surface ciblée est bien le client
appelant** : un client malveillant pourrait demander à déplacer une
surface qui n'est pas la sienne. À durcir en 7.8 (vérifier que
`xdg_surface.client_id == client_id du request`).
## Critère de fin 7.7
> Un client peut envoyer `toplevel.move()` au compositor. Le
> compositor entre en mode drag, applique le delta de curseur sur la
> position de la surface, et sort du mode au release du bouton
> gauche. Tout cela sans panic ni régression sur les phases
> précédentes.
**✅ Validé.** Move fonctionnel runtime, 3 captures de preuve, logs
frontend confirment enter/release du drag, multi-clients (A + B)
toujours OK.
## Code
```
crates/redox-wl-wayland-frontend/ # +~100 lignes (InteractiveDrag,
# DragMode, Move handler,
# apply_interactive_drag,
# XdgToplevelData.xdg_surface)
crates/redox-wl-test-client-shm-two/ # +bind wl_seat + auto-move
# à T+8s pour la fenêtre A
docs/phase7-7-move-resize.md
docs/phase7-7-drag-pos{1,2}.png
docs/phase7-7-post-release.png
```
## Suite phase 7.8
Au choix :
- **Resize complet** (xdg_toplevel.Resize + configure avec edges
selon Top/Bottom/Left/Right + ack_configure → commit)
- **Validation stricte des serials** (`last_button_serial` enforcé)
- **Politique WM** (snap-to-edge, contraintes min/max, retour de
surface hors-écran)
- **Frame callbacks per-client throttle** (frame done seulement
envoyé aux clients dont la surface a été composée)
Estimé : 1-2 sessions par item.
Avec la phase 7 quasi-complète (7.1-7.7), le **vrai prochain
jalon** est le port COSMIC (phase plan-directeur 13, réordonnée avant
GPU). Le compositor 7.x devrait être suffisamment compatible Wayland
pour qu'un toolkit comme cosmic-comp lib + GTK4 puisse se connecter.
---
*Fin du document de phase 7.7.*

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB