🎉 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:
parent
a87de02555
commit
50f7a064e4
6 changed files with 468 additions and 8 deletions
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
_ => {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue