gioser

tawasuyu/brahman

Rust 496TOML 195Markdown 55JSON 197001 Definiciones

feat(mirada): puntero/ratón en el backend DRM del compositor

El backend DRM del Cuerpo deja de ser sólo-teclado: `libinput` ahora
mueve un cursor de software y reenvía clics y rueda a los clientes.

- Enum `Frame` (vía `render_elements!`) que mezcla superficies de
  cliente y un `SolidColorRenderElement` para el cursor, marcado
  `Kind::Cursor` y compuesto encima de todo.
- `handle_input` atiende `PointerMotion`/`PointerMotionAbsolute`/
  `PointerButton`/`PointerAxis`; el puntero se acota a la salida.
- Foco-sigue-ratón: `window_at` hace el test de impacto (flotantes
  sobre teseladas, contra el rectángulo real de la superficie) y, al
  cambiar de ventana, emite `BodyEvent::PointerEntered`.
- `surface_px_size` en main.rs — tamaño presentado de una superficie,
  reusado por el test de impacto.

Compila + clippy limpio; pendiente de verificar en hardware.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

b4ddab9c065839ba943c5192de91c6e32f809aee · por sergio · (hace 5 meses) · padre 5230d42b1 · ver el árbol en este commit

Delta semántico · 11

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

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

5 archivos · +255 −56

diff --git a/crates/apps/mirada-compositor/README.md b/crates/apps/mirada-compositor/README.md
index a7d68ae..90d9d08 100644
--- a/crates/apps/mirada-compositor/README.md
+++ b/crates/apps/mirada-compositor/README.md
@@ -39,6 +39,10 @@ Corre directo sobre el hardware. Requiere una **TTY** (`Ctrl+Alt+F3`),
 una GPU con `/dev/dri`, y `seatd` o `logind` para la sesión. Toma la
 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.
+
 - `MIRADA_STARTUP=<cmd>` — lanza una app al arrancar (`MIRADA_STARTUP=foot`).
 - `MIRADA_DRM_TIMEOUT=<s>` — cierra el compositor solo tras N segundos
   (0 o sin definir = sin tope).
@@ -112,9 +116,9 @@ En modo enlazado el socket de control lo abre el Cerebro (la app
 ## Qué implementa
 
 `wl_compositor`, `xdg_shell` (toplevels y popups), `wl_shm`, `wl_seat`
-(teclado), `wl_output` y `wl_data_device` (selección). Composición con
-`GlesRenderer` — en `winit` sobre la ventana, en `drm` con un
-`DrmCompositor` por salida.
+(teclado, y ratón en el backend DRM), `wl_output` y `wl_data_device`
+(selección). Composición con `GlesRenderer` — en `winit` sobre la
+ventana, en `drm` con un `DrmCompositor` por salida.
 
 Reusa `mirada-body` para la contabilidad de salidas y superficies, y
 `mirada-link` para el cable hacia un Cerebro externo. Toda la lógica
@@ -123,7 +127,7 @@ espacial es agnóstica de Wayland y vive en los crates de
 
 ## Pendiente
 
-Del backend DRM: puntero/ratón (hoy sólo teclado), conmutación de VT,
-hotplug de monitores. Aislamiento de clientes. Ver el SDD.
+Del backend DRM: conmutación de VT, hotplug de monitores, multi-GPU.
+Puntero en el backend `winit`. Aislamiento de clientes. Ver el SDD.
 
 [`smithay`]: https://github.com/Smithay/smithay
diff --git a/crates/apps/mirada-compositor/src/drm_backend.rs b/crates/apps/mirada-compositor/src/drm_backend.rs
index e0bc467..9912b53 100644
--- a/crates/apps/mirada-compositor/src/drm_backend.rs
+++ b/crates/apps/mirada-compositor/src/drm_backend.rs
@@ -25,18 +25,24 @@ use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags};
 use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
 use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent};
 use smithay::backend::egl::{EGLContext, EGLDisplay};
-use smithay::backend::input::{InputEvent, KeyState, KeyboardKeyEvent};
+use smithay::backend::input::{
+    AbsolutePositionEvent, Axis, AxisSource, InputEvent, KeyState, KeyboardKeyEvent,
+    PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
+};
 use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
+use smithay::backend::renderer::element::solid::SolidColorRenderElement;
 use smithay::backend::renderer::element::surface::{
     render_elements_from_surface_tree, WaylandSurfaceRenderElement,
 };
-use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::element::{render_elements, Id, Kind};
 use smithay::backend::renderer::gles::GlesRenderer;
-use smithay::backend::renderer::ImportDma;
+use smithay::backend::renderer::utils::CommitCounter;
+use smithay::backend::renderer::{ImportAll, ImportDma};
 use smithay::backend::session::libseat::LibSeatSession;
 use smithay::backend::session::{Event as SessionEvent, Session};
 use smithay::backend::udev;
 use smithay::input::keyboard::FilterResult;
+use smithay::input::pointer::{AxisFrame, ButtonEvent, MotionEvent};
 use smithay::output::OutputModeSource;
 use smithay::reexports::calloop::generic::Generic;
 use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
@@ -46,7 +52,9 @@ use smithay::reexports::drm::control::{Device as ControlDevice, ModeTypeFlags};
 use smithay::reexports::input::Libinput;
 use smithay::reexports::rustix::fs::OFlags;
 use smithay::reexports::wayland_server::{Display, ListeningSocket};
-use smithay::utils::{DeviceFd, Scale, Size, Transform, SERIAL_COUNTER};
+use smithay::utils::{
+    DeviceFd, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
+};
 
 use mirada_brain::{CtlReply, Keymap};
 
@@ -56,9 +64,23 @@ use crate::{combo_string, send_frames_surface_tree, App, Brain, ClientState, Set
 type Compositor =
     DrmCompositor<GbmAllocator<DrmDeviceFd>, GbmFramebufferExporter<DrmDeviceFd>, (), DrmDeviceFd>;
 
+render_elements! {
+    /// Lo que el backend DRM compone en un cuadro: las superficies de los
+    /// clientes y, encima de todo, el cursor de software.
+    Frame<R> where R: ImportAll;
+    Window = WaylandSurfaceRenderElement<R>,
+    Cursor = SolidColorRenderElement,
+}
+
 /// Color de fondo del escritorio cuando no hay nada que lo tape.
 const CLEAR_COLOR: [f32; 4] = [0.05, 0.05, 0.08, 1.0];
 
+/// Lado del cursor de software, en píxeles.
+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];
+
 /// El estado del bucle DRM — lo comparten todos los callbacks de `calloop`.
 struct DrmState {
     app: App,
@@ -74,31 +96,54 @@ struct DrmState {
     start: Instant,
     /// Nº de ventanas en el último `tick` — para registrar los cambios.
     last_windows: usize,
+    /// Identidad estable del cursor de software — el seguimiento de daño
+    /// la usa para no recomponer todo cuando el cursor sólo se mueve.
+    cursor_id: Id,
+    /// Ventana sobre la que estaba el puntero — para el foco-sigue-ratón.
+    last_pointer_window: Option<u64>,
+    /// Tamaño de la salida, en píxeles — los topes del puntero.
+    output_size: (f64, f64),
 }
 
 impl DrmState {
-    /// Compone las ventanas y, si hubo cambios, encola el cuadro.
+    /// Compone el cursor y las ventanas y, si hubo cambios, encola el cuadro.
     fn render(&mut self) {
         if self.pending_flip {
             return; // aún esperamos el VBlank del cuadro anterior
         }
-        // Elementos a pintar: las flotantes primero (lista front-to-back).
-        let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = {
+        // Elementos a pintar — lista front-to-back (índice 0 = encima):
+        // primero el cursor, luego las flotantes, luego las teseladas.
+        let elements: Vec<Frame<GlesRenderer>> = {
+            let mut out: Vec<Frame<GlesRenderer>> = Vec::new();
+
+            let (cx, cy) = self.app.pointer_loc;
+            let cursor_rect = Rectangle::new(
+                Point::<i32, Physical>::from((cx.round() as i32, cy.round() as i32)),
+                Size::<i32, Physical>::from((CURSOR_SIZE, CURSOR_SIZE)),
+            );
+            out.push(Frame::Cursor(SolidColorRenderElement::new(
+                self.cursor_id.clone(),
+                cursor_rect,
+                CommitCounter::default(),
+                CURSOR_COLOR,
+                Kind::Cursor,
+            )));
+
             let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
             shown.sort_by_key(|w| !w.floating);
-            shown
-                .iter()
-                .flat_map(|w| {
-                    render_elements_from_surface_tree(
-                        &mut self.renderer,
-                        &w.surface,
-                        crate::render_loc(w),
-                        1.0,
-                        1.0,
-                        Kind::Unspecified,
-                    )
-                })
-                .collect()
+            for w in &shown {
+                for el in render_elements_from_surface_tree(
+                    &mut self.renderer,
+                    &w.surface,
+                    crate::render_loc(w),
+                    1.0,
+                    1.0,
+                    Kind::Unspecified,
+                ) {
+                    out.push(Frame::Window(el));
+                }
+            }
+            out
         };
         match self.compositor.render_frame::<_, _>(
             &mut self.renderer,
@@ -168,42 +213,162 @@ impl DrmState {
         let _ = self.display.flush_clients();
     }
 
-    /// Procesa un evento de `libinput` — por ahora, sólo el teclado.
+    /// Procesa un evento de `libinput`: teclado y puntero.
     fn handle_input(&mut self, event: InputEvent<LibinputInputBackend>) {
-        let InputEvent::Keyboard { event } = event else {
-            return; // dispositivos, puntero, táctil: aún no
-        };
-        let Some(keyboard) = self.app.keyboard.clone() else {
-            return;
-        };
-        let code = event.key_code();
-        let key_state = event.state();
-        let pressed = key_state == KeyState::Pressed;
         let time = self.start.elapsed().as_millis() as u32;
-        keyboard.input::<(), _>(
-            &mut self.app,
-            code,
-            key_state,
-            SERIAL_COUNTER.next_serial(),
-            time,
-            |st, mods, handle| {
-                if !pressed {
-                    return FilterResult::Forward;
+        match event {
+            // --- Teclado: intercepta los atajos del Cerebro --------------
+            InputEvent::Keyboard { event } => {
+                let Some(keyboard) = self.app.keyboard.clone() else {
+                    return;
+                };
+                let code = event.key_code();
+                let key_state = event.state();
+                let pressed = key_state == KeyState::Pressed;
+                keyboard.input::<(), _>(
+                    &mut self.app,
+                    code,
+                    key_state,
+                    SERIAL_COUNTER.next_serial(),
+                    time,
+                    |st, mods, handle| {
+                        if !pressed {
+                            return FilterResult::Forward;
+                        }
+                        if let Some(combo) = combo_string(mods, handle.modified_sym()) {
+                            if st.grabs.contains(&combo) {
+                                st.pending_keybind = Some(combo);
+                                return FilterResult::Intercept(());
+                            }
+                        }
+                        FilterResult::Forward
+                    },
+                );
+                if let Some(combo) = self.app.pending_keybind.take() {
+                    let ev = self.app.body.keybind(combo);
+                    self.app.brain_feed(ev);
                 }
-                if let Some(combo) = combo_string(mods, handle.modified_sym()) {
-                    if st.grabs.contains(&combo) {
-                        st.pending_keybind = Some(combo);
-                        return FilterResult::Intercept(());
+            }
+
+            // --- Puntero: movimiento relativo (ratón, touchpad) ----------
+            InputEvent::PointerMotion { event } => {
+                let (mut x, mut y) = self.app.pointer_loc;
+                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);
+            }
+
+            // --- Puntero: movimiento absoluto (táctil, tableta) ----------
+            InputEvent::PointerMotionAbsolute { event } => {
+                let space = Size::<i32, Logical>::from((
+                    self.output_size.0 as i32,
+                    self.output_size.1 as i32,
+                ));
+                let pos = event.position_transformed(space);
+                self.app.pointer_loc = (
+                    pos.x.clamp(0.0, self.output_size.0),
+                    pos.y.clamp(0.0, self.output_size.1),
+                );
+                self.pointer_motion(time);
+            }
+
+            // --- Puntero: botones — se reenvían a la ventana enfocada ----
+            InputEvent::PointerButton { event } => {
+                let Some(pointer) = self.app.pointer.clone() else {
+                    return;
+                };
+                pointer.button(
+                    &mut self.app,
+                    &ButtonEvent {
+                        serial: SERIAL_COUNTER.next_serial(),
+                        time,
+                        button: event.button_code(),
+                        state: event.state(),
+                    },
+                );
+                pointer.frame(&mut self.app);
+            }
+
+            // --- Puntero: rueda / desplazamiento -------------------------
+            InputEvent::PointerAxis { event } => {
+                let Some(pointer) = self.app.pointer.clone() else {
+                    return;
+                };
+                let source = event.source();
+                let mut frame = AxisFrame::new(time).source(source);
+                for axis in [Axis::Horizontal, Axis::Vertical] {
+                    match event.amount(axis) {
+                        Some(v) if v != 0.0 => frame = frame.value(axis, v),
+                        Some(_) if source == AxisSource::Finger => {
+                            frame = frame.stop(axis);
+                        }
+                        _ => {}
+                    }
+                    if let Some(d) = event.amount_v120(axis) {
+                        frame = frame.v120(axis, d as i32);
                     }
                 }
-                FilterResult::Forward
+                pointer.axis(&mut self.app, frame);
+                pointer.frame(&mut self.app);
+            }
+
+            _ => {} // otros dispositivos: aún no
+        }
+    }
+
+    /// Reenvía el puntero a la ventana que tiene debajo y, si esa ventana
+    /// cambió, aplica el foco-sigue-ratón avisando al Cerebro.
+    fn pointer_motion(&mut self, time: u32) {
+        let Some(pointer) = self.app.pointer.clone() else {
+            return;
+        };
+        let (x, y) = self.app.pointer_loc;
+        let hit = self.window_at(x, y);
+        let focus = hit.map(|i| {
+            let w = &self.app.windows[i];
+            let (lx, ly) = crate::render_loc(w);
+            (
+                w.surface.clone(),
+                Point::<f64, Logical>::from((lx as f64, ly as f64)),
+            )
+        });
+        pointer.motion(
+            &mut self.app,
+            focus,
+            &MotionEvent {
+                location: Point::from((x, y)),
+                serial: SERIAL_COUNTER.next_serial(),
+                time,
             },
         );
-        if let Some(combo) = self.app.pending_keybind.take() {
-            let ev = self.app.body.keybind(combo);
-            self.app.brain_feed(ev);
+        pointer.frame(&mut self.app);
+
+        // Foco-sigue-ratón: al pasar a otra ventana, que el Cerebro la enfoque.
+        let hovered = hit.map(|i| self.app.windows[i].id);
+        if hovered != self.last_pointer_window {
+            self.last_pointer_window = hovered;
+            if let Some(id) = hovered {
+                let ev = self.app.body.pointer_enter(id);
+                self.app.brain_feed(ev);
+            }
         }
     }
+
+    /// 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> {
+        let mut idx: Vec<usize> = (0..self.app.windows.len())
+            .filter(|&i| self.app.windows[i].visible)
+            .collect();
+        idx.sort_by_key(|&i| !self.app.windows[i].floating);
+        idx.into_iter().find(|&i| {
+            let w = &self.app.windows[i];
+            let (lx, ly) = crate::render_loc(w);
+            let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
+            x >= lx as f64 && y >= ly as f64 && x < (lx + sw) as f64 && y < (ly + sh) as f64
+        })
+    }
 }
 
 /// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b.
@@ -339,6 +504,8 @@ pub fn run() -> Result<(), Box<dyn Error>> {
     // La salida del Cerebro = el modo del monitor.
     let ev = app.body.add_output(0, mode_w as i32, mode_h as i32);
     app.brain_feed(ev);
+    // El puntero arranca en el centro de la pantalla.
+    app.pointer_loc = (mode_w as f64 / 2.0, mode_h as f64 / 2.0);
     // Anuncia el monitor en el protocolo Wayland — los clientes lo exigen.
     let _wl_output = crate::announce_output(
         &display.handle(),
@@ -475,6 +642,9 @@ pub fn run() -> Result<(), Box<dyn Error>> {
         ctl,
         start: Instant::now(),
         last_windows: 0,
+        cursor_id: Id::new(),
+        last_pointer_window: None,
+        output_size: (mode_w as f64, mode_h as f64),
     };
 
     let signal = event_loop.get_signal();
diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs
index d2c11f1..d8d1100 100644
--- a/crates/apps/mirada-compositor/src/main.rs
+++ b/crates/apps/mirada-compositor/src/main.rs
@@ -32,6 +32,7 @@ use smithay::backend::renderer::utils::{
 use smithay::backend::renderer::{Color32F, Frame, Renderer};
 use smithay::backend::winit::{self, WinitEvent};
 use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState};
+use smithay::input::pointer::PointerHandle;
 use smithay::input::{Seat, SeatHandler, SeatState};
 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
 use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
@@ -107,6 +108,9 @@ struct App {
     data_device_state: DataDeviceState,
     seat: Seat<Self>,
     keyboard: Option<KeyboardHandle<Self>>,
+    pointer: Option<PointerHandle<Self>>,
+    /// Posición del puntero en coordenadas globales.
+    pointer_loc: (f64, f64),
 
     /// Ventanas gestionadas, en orden de aparición.
     windows: Vec<ManagedWindow>,
@@ -534,6 +538,16 @@ fn render_loc(w: &ManagedWindow) -> (i32, i32) {
     }
 }
 
+/// El tamaño en píxeles de la superficie de una ventana, si el cliente
+/// ya presentó un buffer. `None` mientras no haya dibujado nada — la usa
+/// el backend DRM para acertar el rectángulo en el test de impacto del
+/// puntero.
+fn surface_px_size(w: &ManagedWindow) -> Option<(i32, i32)> {
+    with_renderer_surface_state(&w.surface, |s| s.surface_size())
+        .flatten()
+        .map(|s| (s.w, s.h))
+}
+
 /// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
 fn load_user_rules() -> Rules {
     match Rules::default_path() {
@@ -627,6 +641,8 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
         data_device_state: DataDeviceState::new::<App>(&dh),
         seat,
         keyboard: None,
+        pointer: None,
+        pointer_loc: (0.0, 0.0),
         windows: Vec::new(),
         body: BodyState::new(),
         brain,
@@ -638,6 +654,7 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
 
     let keyboard = app.seat.add_keyboard(Default::default(), 200, 25)?;
     app.keyboard = Some(keyboard);
+    app.pointer = Some(app.seat.add_pointer());
 
     // En modo embebido, el propio Desktop dicta los atajos a interceptar.
     if let Brain::Embedded(desktop) = &app.brain {
diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md
index 4b843c6..5180be1 100644
--- a/crates/modules/mirada/SDD.md
+++ b/crates/modules/mirada/SDD.md
@@ -195,14 +195,18 @@ Cerebro: **autónomo** (`Desktop` embebido) o **enlazado** (`MIRADA_SOCKET`
 - **`winit`** — corre anidado, una ventana en la sesión gráfica actual.
 - **`drm`** (`drm_backend.rs`) — corre nativo sobre una TTY, sin sesión
   anfitriona: `libseat` (sesión), `udev` (GPU), `DrmDevice` + GBM + EGL +
-  `DrmCompositor`, `libinput` (teclado), bucle `calloop`. Verificado en
-  hardware: sesión, render, teclado, atajos, clientes, salida limpia.
+  `DrmCompositor`, `libinput` (teclado y ratón), bucle `calloop`.
+  Verificado en hardware: sesión, render, teclado, atajos, clientes,
+  salida limpia. El ratón pinta un cursor de software (un
+  `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.
 
 **Pendiente** — refinamientos del Cuerpo:
 
 | capa pendiente   | rol                                                          |
 | ---------------- | ------------------------------------------------------------ |
-| puntero en `drm` | ratón/touchpad por `libinput` (hoy el backend DRM sólo teclado) |
+| puntero en `winit` | ratón en el backend anidado (hoy sólo el backend DRM)      |
 | `mirada-input`   | repetición de teclas, gestos; conmutación de VT, hotplug     |
 | `mirada-sandbox` | aislamiento de clientes sobre `arje-incarnate`               |
 
diff --git a/vamos.txt b/vamos.txt
index abe0851..24d791b 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -993,11 +993,15 @@
 
 
   El Cuerpo de verdad — mirada-compositor:
-  Compositor Wayland teselante real sobre smithay, backend winit (corre anidado, una ventana dentro de tu sesión X11/Wayland).
+  Compositor Wayland teselante real sobre smithay, con dos backends gráficos (main() elige; --winit/--drm lo fuerzan):
+   · winit — corre anidado, una ventana dentro de tu sesión X11/Wayland (para desarrollar sin dejar el escritorio).
+   · drm   — corre nativo sobre una TTY, sin sesión anfitriona: toma GPU (DRM/KMS/GBM/EGL), teclado y ratón (libinput).
   cargo run -p mirada-compositor                                   # autónomo: Cerebro Desktop embebido, un solo proceso
   WAYLAND_DISPLAY=wayland-1 foot                                    # lanza clientes contra él (imprime su WAYLAND_DISPLAY al arrancar)
   MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada-compositor     # enlazado: la app mirada (Cerebro GPUI) decide la geometría
+  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.
   Ver crates/apps/mirada-compositor/README.md.