gioser

tawasuyu/brahman

Rust 231TOML 70JSON 14HTML 12905 Sutichaykuna

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>

90bffec3f169c244946d37d6a2a6d02805e52973 · ruwaq sergio · (5 killa ñawpaq) · tayta ae8139985 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 14

crates/apps/mirada-compositor/src/drm_backend.rs

crates/apps/mirada-compositor/src/main.rs

crates/modules/mirada/mirada-brain/src/desktop.rs

crates/modules/mirada/mirada-link/src/lib.rs

  • ukhunpi t'ikrakurqan fn place

crates/modules/mirada/mirada-protocol/src/lib.rs

  • silq'uyninpi t'ikrakurqan enum BodyEvent pub

8 qillqakuna · +169 −8

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=<cmd>` — lanza una app al arrancar (`MIRADA_STARTUP=foot`).
 - `MIRADA_DRM_TIMEOUT=<s>` — 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 `<linux/input-event-codes.h>`.
+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<usize> {
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<PointerHandle<Self>>,
     /// 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<DragGrab>,
 
     /// Ventanas gestionadas, en orden de aparición.
     windows: Vec<ManagedWindow>,
@@ -673,6 +696,7 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
         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.