From 90bffec3f169c244946d37d6a2a6d02805e52973 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 21 May 2026 03:46:03 +0000 Subject: [PATCH] =?UTF-8?q?feat(mirada):=20mover/redimensionar=20ventanas?= =?UTF-8?q?=20con=20el=20rat=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- crates/apps/mirada-compositor/README.md | 4 +- .../apps/mirada-compositor/src/drm_backend.rs | 95 +++++++++++++++++-- crates/apps/mirada-compositor/src/main.rs | 24 +++++ crates/modules/mirada/SDD.md | 3 + .../mirada/mirada-brain/src/desktop.rs | 43 +++++++++ crates/modules/mirada/mirada-link/src/lib.rs | 2 + .../modules/mirada/mirada-protocol/src/lib.rs | 4 + vamos.txt | 2 + 8 files changed, 169 insertions(+), 8 deletions(-) diff --git a/crates/apps/mirada-compositor/README.md b/crates/apps/mirada-compositor/README.md index 5fe07c0..f515bb6 100644 --- a/crates/apps/mirada-compositor/README.md +++ b/crates/apps/mirada-compositor/README.md @@ -41,7 +41,9 @@ pantalla completa; sal con `Super+Shift+e` o `Ctrl+C`. Lleva teclado y ratón por `libinput`: el ratón mueve un cursor de software, el foco sigue al puntero y los clics y la rueda llegan a la -ventana que tienes debajo. +ventana que tienes debajo. **`Super`+arrastre** con el botón izquierdo +mueve una ventana, con el derecho la redimensiona — al arrastrarla, la +ventana pasa a flotar. - `MIRADA_STARTUP=` — lanza una app al arrancar (`MIRADA_STARTUP=foot`). - `MIRADA_DRM_TIMEOUT=` — cierra el compositor solo tras N segundos diff --git a/crates/apps/mirada-compositor/src/drm_backend.rs b/crates/apps/mirada-compositor/src/drm_backend.rs index 9912b53..2cd538c 100644 --- a/crates/apps/mirada-compositor/src/drm_backend.rs +++ b/crates/apps/mirada-compositor/src/drm_backend.rs @@ -26,7 +26,7 @@ use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter; use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent}; use smithay::backend::egl::{EGLContext, EGLDisplay}; use smithay::backend::input::{ - AbsolutePositionEvent, Axis, AxisSource, InputEvent, KeyState, KeyboardKeyEvent, + AbsolutePositionEvent, Axis, AxisSource, ButtonState, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, }; use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}; @@ -56,9 +56,9 @@ use smithay::utils::{ DeviceFd, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER, }; -use mirada_brain::{CtlReply, Keymap}; +use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect}; -use crate::{combo_string, send_frames_surface_tree, App, Brain, ClientState, Setup}; +use crate::{combo_string, send_frames_surface_tree, App, Brain, ClientState, DragGrab, DragMode, Setup}; /// El `DrmCompositor` concreto para la salida (un solo GPU). type Compositor = @@ -81,6 +81,13 @@ const CURSOR_SIZE: i32 = 12; /// Color del cursor — un cuadrado casi blanco, opaco. const CURSOR_COLOR: [f32; 4] = [0.95, 0.95, 0.97, 1.0]; +/// Lado mínimo de una ventana al redimensionarla con el ratón. +const MIN_WINDOW: i32 = 120; + +/// Códigos de botón de ``. +const BTN_LEFT: u32 = 0x110; +const BTN_RIGHT: u32 = 0x111; + /// El estado del bucle DRM — lo comparten todos los callbacks de `calloop`. struct DrmState { app: App, @@ -256,7 +263,9 @@ impl DrmState { x = (x + event.delta_x()).clamp(0.0, self.output_size.0); y = (y + event.delta_y()).clamp(0.0, self.output_size.1); self.app.pointer_loc = (x, y); - self.pointer_motion(time); + if !self.drag_update() { + self.pointer_motion(time); + } } // --- Puntero: movimiento absoluto (táctil, tableta) ---------- @@ -270,11 +279,55 @@ impl DrmState { pos.x.clamp(0.0, self.output_size.0), pos.y.clamp(0.0, self.output_size.1), ); - self.pointer_motion(time); + if !self.drag_update() { + self.pointer_motion(time); + } } - // --- Puntero: botones — se reenvían a la ventana enfocada ---- + // --- Puntero: botones ---------------------------------------- InputEvent::PointerButton { event } => { + let pressed = event.state() == ButtonState::Pressed; + let button = event.button_code(); + + // ¿Empieza un arrastre? `Super`+botón sobre una ventana: + // izquierdo mueve, derecho redimensiona. + if pressed && self.app.drag.is_none() { + let super_held = self + .app + .keyboard + .as_ref() + .is_some_and(|kb| kb.modifier_state().logo); + let mode = match button { + BTN_LEFT if super_held => Some(DragMode::Move), + BTN_RIGHT if super_held => Some(DragMode::Resize), + _ => None, + }; + if let Some(mode) = mode { + let (x, y) = self.app.pointer_loc; + if let Some(i) = self.window_at(x, y) { + let w = &self.app.windows[i]; + let grab = DragGrab { + id: w.id, + mode, + start_pointer: (x, y), + start_rect: (w.loc.0, w.loc.1, w.size.0, w.size.1), + }; + self.app.drag = Some(grab); + return; // el arrastre captura el botón + } + } + } + + // Durante un arrastre los botones no llegan al cliente; + // soltar cualquiera lo termina. + if self.app.drag.is_some() { + if !pressed { + self.app.drag = None; + } + return; + } + + // Botón normal: a la ventana bajo el puntero. let Some(pointer) = self.app.pointer.clone() else { return; }; @@ -283,7 +336,7 @@ impl DrmState { &ButtonEvent { serial: SERIAL_COUNTER.next_serial(), time, - button: event.button_code(), + button, state: event.state(), }, ); @@ -355,6 +408,34 @@ impl DrmState { } } + /// Si hay un arrastre en curso, recalcula el rectángulo de la ventana + /// y se lo manda al Cerebro (que la hace flotar ahí). Devuelve `true` + /// si consumió el movimiento — entonces el puntero no llega al cliente. + fn drag_update(&mut self) -> bool { + let Some(drag) = self.app.drag.as_ref() else { + return false; + }; + let mode = drag.mode; + let (spx, spy) = drag.start_pointer; + let (sx, sy, sw, sh) = drag.start_rect; + let id = drag.id; + + let (px, py) = self.app.pointer_loc; + let dx = (px - spx) as i32; + let dy = (py - spy) as i32; + let rect = match mode { + DragMode::Move => Rect::new(sx + dx, sy + dy, sw, sh), + DragMode::Resize => Rect::new( + sx, + sy, + (sw + dx).max(MIN_WINDOW), + (sh + dy).max(MIN_WINDOW), + ), + }; + self.app.brain_feed(BodyEvent::WindowFloatTo { id, rect }); + true + } + /// El índice de la ventana visible bajo el punto `(x, y)`, si la hay — /// en orden front-to-back (las flotantes ganan a las teseladas). fn window_at(&self, x: f64, y: f64) -> Option { diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs index 2d55b7e..9eaa2ff 100644 --- a/crates/apps/mirada-compositor/src/main.rs +++ b/crates/apps/mirada-compositor/src/main.rs @@ -101,6 +101,27 @@ struct ManagedWindow { floating: bool, } +/// Un arrastre de ratón en curso: mueve o redimensiona una ventana. +struct DragGrab { + /// La ventana que se arrastra. + id: u64, + /// Mover (`Super`+botón izquierdo) o redimensionar (`Super`+derecho). + mode: DragMode, + /// Posición del puntero al empezar el arrastre. + start_pointer: (f64, f64), + /// Rectángulo `(x, y, w, h)` de la ventana al empezar. + start_rect: (i32, i32, i32, i32), +} + +/// Qué le hace un arrastre a la ventana. +#[derive(Clone, Copy)] +enum DragMode { + /// Reubicar la ventana — la esquina la sigue al puntero. + Move, + /// Redimensionarla — la esquina inferior-derecha sigue al puntero. + Resize, +} + /// El estado global del compositor. struct App { compositor_state: CompositorState, @@ -113,6 +134,8 @@ struct App { pointer: Option>, /// Posición del puntero en coordenadas globales. pointer_loc: (f64, f64), + /// Arrastre de ventana en curso (mover o redimensionar con el ratón). + drag: Option, /// Ventanas gestionadas, en orden de aparición. windows: Vec, @@ -673,6 +696,7 @@ fn build_app() -> Result> { keyboard: None, pointer: None, pointer_loc: (0.0, 0.0), + drag: None, windows: Vec::new(), body: BodyState::new(), brain, diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md index 916eed3..0c910b2 100644 --- a/crates/modules/mirada/SDD.md +++ b/crates/modules/mirada/SDD.md @@ -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: diff --git a/crates/modules/mirada/mirada-brain/src/desktop.rs b/crates/modules/mirada/mirada-brain/src/desktop.rs index 1a284bc..b5193b0 100644 --- a/crates/modules/mirada/mirada-brain/src/desktop.rs +++ b/crates/modules/mirada/mirada-brain/src/desktop.rs @@ -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(); diff --git a/crates/modules/mirada/mirada-link/src/lib.rs b/crates/modules/mirada/mirada-link/src/lib.rs index 3e494cf..a8584f4 100644 --- a/crates/modules/mirada/mirada-link/src/lib.rs +++ b/crates/modules/mirada/mirada-link/src/lib.rs @@ -135,6 +135,8 @@ mod tests { rect: Rect::new(0, 0, 800, 600), visible: true, focused: true, + floating: false, + fullscreen: false, }]) } diff --git a/crates/modules/mirada/mirada-protocol/src/lib.rs b/crates/modules/mirada/mirada-protocol/src/lib.rs index b1a48bc..775468c 100644 --- a/crates/modules/mirada/mirada-protocol/src/lib.rs +++ b/crates/modules/mirada/mirada-protocol/src/lib.rs @@ -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`] diff --git a/vamos.txt b/vamos.txt index 24d791b..bf617f6 100644 --- a/vamos.txt +++ b/vamos.txt @@ -1002,6 +1002,8 @@ cargo run -p mirada-compositor -- --drm # nativo sobre TTY (MIRADA_STARTUP=foot lanza un cliente al arrancar) Habla wl_compositor/xdg_shell/wl_shm/wl_seat/wl_data_device; compone con GlesRenderer. Reusa mirada-body y mirada-link. En --drm el ratón pinta un cursor de software, el foco sigue al puntero y clics/rueda van a la ventana debajo. + Super+arrastre mueve la ventana (botón izq.) o la redimensiona (der.) — al arrastrarla pasa a flotar. + Fuerza xdg-decoration ServerSide y no dibuja marco: las ventanas teseladas van sin barra de título. Ver crates/apps/mirada-compositor/README.md.