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:
@@ -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
|
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
|
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_STARTUP=<cmd>` — lanza una app al arrancar (`MIRADA_STARTUP=foot`).
|
||||||
- `MIRADA_DRM_TIMEOUT=<s>` — cierra el compositor solo tras N segundos
|
- `MIRADA_DRM_TIMEOUT=<s>` — cierra el compositor solo tras N segundos
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
|||||||
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent};
|
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent};
|
||||||
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
||||||
use smithay::backend::input::{
|
use smithay::backend::input::{
|
||||||
AbsolutePositionEvent, Axis, AxisSource, InputEvent, KeyState, KeyboardKeyEvent,
|
AbsolutePositionEvent, Axis, AxisSource, ButtonState, InputEvent, KeyState, KeyboardKeyEvent,
|
||||||
PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
||||||
};
|
};
|
||||||
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||||
@@ -56,9 +56,9 @@ use smithay::utils::{
|
|||||||
DeviceFd, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
|
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).
|
/// El `DrmCompositor` concreto para la salida (un solo GPU).
|
||||||
type Compositor =
|
type Compositor =
|
||||||
@@ -81,6 +81,13 @@ const CURSOR_SIZE: i32 = 12;
|
|||||||
/// Color del cursor — un cuadrado casi blanco, opaco.
|
/// Color del cursor — un cuadrado casi blanco, opaco.
|
||||||
const CURSOR_COLOR: [f32; 4] = [0.95, 0.95, 0.97, 1.0];
|
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`.
|
/// El estado del bucle DRM — lo comparten todos los callbacks de `calloop`.
|
||||||
struct DrmState {
|
struct DrmState {
|
||||||
app: App,
|
app: App,
|
||||||
@@ -256,8 +263,10 @@ impl DrmState {
|
|||||||
x = (x + event.delta_x()).clamp(0.0, self.output_size.0);
|
x = (x + event.delta_x()).clamp(0.0, self.output_size.0);
|
||||||
y = (y + event.delta_y()).clamp(0.0, self.output_size.1);
|
y = (y + event.delta_y()).clamp(0.0, self.output_size.1);
|
||||||
self.app.pointer_loc = (x, y);
|
self.app.pointer_loc = (x, y);
|
||||||
|
if !self.drag_update() {
|
||||||
self.pointer_motion(time);
|
self.pointer_motion(time);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Puntero: movimiento absoluto (táctil, tableta) ----------
|
// --- Puntero: movimiento absoluto (táctil, tableta) ----------
|
||||||
InputEvent::PointerMotionAbsolute { event } => {
|
InputEvent::PointerMotionAbsolute { event } => {
|
||||||
@@ -270,11 +279,55 @@ impl DrmState {
|
|||||||
pos.x.clamp(0.0, self.output_size.0),
|
pos.x.clamp(0.0, self.output_size.0),
|
||||||
pos.y.clamp(0.0, self.output_size.1),
|
pos.y.clamp(0.0, self.output_size.1),
|
||||||
);
|
);
|
||||||
|
if !self.drag_update() {
|
||||||
self.pointer_motion(time);
|
self.pointer_motion(time);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Puntero: botones — se reenvían a la ventana enfocada ----
|
// --- Puntero: botones ----------------------------------------
|
||||||
InputEvent::PointerButton { event } => {
|
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 {
|
let Some(pointer) = self.app.pointer.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -283,7 +336,7 @@ impl DrmState {
|
|||||||
&ButtonEvent {
|
&ButtonEvent {
|
||||||
serial: SERIAL_COUNTER.next_serial(),
|
serial: SERIAL_COUNTER.next_serial(),
|
||||||
time,
|
time,
|
||||||
button: event.button_code(),
|
button,
|
||||||
state: event.state(),
|
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 —
|
/// 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).
|
/// en orden front-to-back (las flotantes ganan a las teseladas).
|
||||||
fn window_at(&self, x: f64, y: f64) -> Option<usize> {
|
fn window_at(&self, x: f64, y: f64) -> Option<usize> {
|
||||||
|
|||||||
@@ -101,6 +101,27 @@ struct ManagedWindow {
|
|||||||
floating: bool,
|
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.
|
/// El estado global del compositor.
|
||||||
struct App {
|
struct App {
|
||||||
compositor_state: CompositorState,
|
compositor_state: CompositorState,
|
||||||
@@ -113,6 +134,8 @@ struct App {
|
|||||||
pointer: Option<PointerHandle<Self>>,
|
pointer: Option<PointerHandle<Self>>,
|
||||||
/// Posición del puntero en coordenadas globales.
|
/// Posición del puntero en coordenadas globales.
|
||||||
pointer_loc: (f64, f64),
|
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.
|
/// Ventanas gestionadas, en orden de aparición.
|
||||||
windows: Vec<ManagedWindow>,
|
windows: Vec<ManagedWindow>,
|
||||||
@@ -673,6 +696,7 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
|
|||||||
keyboard: None,
|
keyboard: None,
|
||||||
pointer: None,
|
pointer: None,
|
||||||
pointer_loc: (0.0, 0.0),
|
pointer_loc: (0.0, 0.0),
|
||||||
|
drag: None,
|
||||||
windows: Vec::new(),
|
windows: Vec::new(),
|
||||||
body: BodyState::new(),
|
body: BodyState::new(),
|
||||||
brain,
|
brain,
|
||||||
|
|||||||
@@ -203,6 +203,9 @@ Cerebro: **autónomo** (`Desktop` embebido) o **enlazado** (`MIRADA_SOCKET`
|
|||||||
`SolidColorRenderElement` marcado `Kind::Cursor`, encima de todo en un
|
`SolidColorRenderElement` marcado `Kind::Cursor`, encima de todo en un
|
||||||
enum `Frame` de elementos de render); el foco sigue al puntero
|
enum `Frame` de elementos de render); el foco sigue al puntero
|
||||||
(`BodyEvent::PointerEntered`) y clics y rueda van a la ventana debajo.
|
(`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:
|
**Pendiente** — refinamientos del Cuerpo:
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,23 @@ impl Desktop {
|
|||||||
Vec::new()
|
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));
|
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]
|
#[test]
|
||||||
fn retitling_updates_the_registry_without_relayout() {
|
fn retitling_updates_the_registry_without_relayout() {
|
||||||
let mut d = desktop_with_screen();
|
let mut d = desktop_with_screen();
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ mod tests {
|
|||||||
rect: Rect::new(0, 0, 800, 600),
|
rect: Rect::new(0, 0, 800, 600),
|
||||||
visible: true,
|
visible: true,
|
||||||
focused: 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
|
/// Un cliente pidió pantalla completa para su ventana (`true`), o la
|
||||||
/// soltó (`false`) — `xdg_toplevel.set_fullscreen`.
|
/// soltó (`false`) — `xdg_toplevel.set_fullscreen`.
|
||||||
FullscreenRequest { id: WindowId, fullscreen: bool },
|
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`]
|
/// Tamaño máximo de un marco, en bytes. Acota el búfer de [`read_frame`]
|
||||||
|
|||||||
@@ -1002,6 +1002,8 @@
|
|||||||
cargo run -p mirada-compositor -- --drm # nativo sobre TTY (MIRADA_STARTUP=foot lanza un cliente al arrancar)
|
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.
|
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.
|
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.
|
Ver crates/apps/mirada-compositor/README.md.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user