feat(mirada): mover/redimensionar ventanas con el ratón

`Super`+arrastre interactivo en el backend DRM: botón izquierdo mueve
la ventana, botón derecho la redimensiona. Al arrastrarla, la ventana
pasa a flotar — comportamiento estilo dwm.

La verdad geométrica vive en el Cerebro, así que el arrastre viaja
hasta él:

- `mirada-protocol`: nuevo `BodyEvent::WindowFloatTo { id, rect }`.
- `mirada-brain`: `Desktop::on_event` lo atiende — busca el escritorio
  de la ventana y la hace flotar en ese rectángulo
  (`Workspace::set_floating`). Dos tests nuevos.
- `mirada-compositor`: `DragGrab`/`DragMode` en `App`; `handle_input`
  arranca el arrastre con `Super`+botón sobre una ventana
  (`keyboard.modifier_state().logo`), traga los botones mientras dura y
  lo cierra al soltar. `drag_update` recalcula el rectángulo (mover =
  esquina sigue al puntero; redimensionar = esquina inferior-derecha,
  con un mínimo de 120 px) y emite `WindowFloatTo`. Durante el arrastre
  el puntero no llega al cliente.

De paso, arregla un test de `mirada-link` que construía un
`WindowPlacement` sin los campos `floating`/`fullscreen`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 03:46:03 +00:00
parent ae81399857
commit 90bffec3f1
8 changed files with 169 additions and 8 deletions
+3
View File
@@ -203,6 +203,9 @@ Cerebro: **autónomo** (`Desktop` embebido) o **enlazado** (`MIRADA_SOCKET`
`SolidColorRenderElement` marcado `Kind::Cursor`, encima de todo en un
enum `Frame` de elementos de render); el foco sigue al puntero
(`BodyEvent::PointerEntered`) y clics y rueda van a la ventana debajo.
`Super`+arrastre mueve/redimensiona: el Cuerpo calcula el rectángulo y
emite `BodyEvent::WindowFloatTo { id, rect }`; el Cerebro hace flotar
la ventana ahí (`Workspace::set_floating`).
**Pendiente** — refinamientos del Cuerpo:
@@ -209,6 +209,23 @@ impl Desktop {
Vec::new()
}
}
BodyEvent::WindowFloatTo { id, rect } => {
// Arrastre interactivo: la ventana pasa a flotar en el
// rectángulo dado, en el escritorio donde viva.
let mut changed = false;
for ws in &mut self.workspaces {
if ws.windows().contains(&id) {
ws.set_floating(id, Some(rect));
changed = true;
break;
}
}
if changed {
self.relayout()
} else {
Vec::new()
}
}
}
}
@@ -844,6 +861,32 @@ mod tests {
assert_eq!(d.focused_window(), Some(1));
}
#[test]
fn dragging_floats_a_window_at_the_given_rect() {
let mut d = desktop_with_screen();
open(&mut d, 1);
open(&mut d, 2);
assert!(!d.active_workspace().is_floating(2));
let target = Rect::new(300, 200, 640, 480);
let cmds = d.on_event(BodyEvent::WindowFloatTo { id: 2, rect: target });
// La 2 ahora flota exactamente en el rectángulo pedido.
assert!(d.active_workspace().is_floating(2));
let p = places(&cmds).iter().find(|p| p.id == 2).unwrap();
assert!(p.floating);
assert_eq!(p.rect, target);
}
#[test]
fn dragging_an_unknown_window_does_nothing() {
let mut d = desktop_with_screen();
open(&mut d, 1);
let cmds = d.on_event(BodyEvent::WindowFloatTo {
id: 99,
rect: Rect::new(0, 0, 100, 100),
});
assert!(cmds.is_empty());
}
#[test]
fn retitling_updates_the_registry_without_relayout() {
let mut d = desktop_with_screen();
@@ -135,6 +135,8 @@ mod tests {
rect: Rect::new(0, 0, 800, 600),
visible: true,
focused: true,
floating: false,
fullscreen: false,
}])
}
@@ -91,6 +91,10 @@ pub enum BodyEvent {
/// Un cliente pidió pantalla completa para su ventana (`true`), o la
/// soltó (`false`) — `xdg_toplevel.set_fullscreen`.
FullscreenRequest { id: WindowId, fullscreen: bool },
/// El usuario arrastró una ventana con el ratón a un rectángulo nuevo
/// (mover o redimensionar interactivos). El Cerebro la hace flotar
/// ahí; si estaba teselada, deja de estarlo.
WindowFloatTo { id: WindowId, rect: Rect },
}
/// Tamaño máximo de un marco, en bytes. Acota el búfer de [`read_frame`]