From 58e72c3d0837a5f1515b274ccbd87be7755ee7e7 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 21 May 2026 04:16:44 +0000 Subject: [PATCH] feat(mirada): el cursor toma la forma que pide el cliente MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El cursor dejaba de ser un cuadrado fijo. Ahora honra `wl_pointer.set_cursor`: sobre el texto de una terminal sale la «I», sobre un enlace la mano, etc. — la forma la dibuja el cliente en una superficie y el compositor la compone. - `App` guarda un `cursor_status: CursorImageStatus`; el handler `SeatHandler::cursor_image` lo actualiza. - `render()` lo interpreta: `Surface` → compone el árbol de la superficie del cursor en `pointer_loc - hotspot` (helper `cursor_hotspot`, vía `CursorImageSurfaceData`); `Named` o sin tema → el cuadrado de siempre; `Hidden` → nada. - Sobre el escritorio pelado (sin cliente debajo) el cursor vuelve al de por defecto, para que no se quede con la «I» de la última ventana. - La superficie del cursor también recibe frame-callbacks (cursores animados). Co-Authored-By: Claude Opus 4.7 --- crates/apps/mirada-compositor/README.md | 13 ++-- .../apps/mirada-compositor/src/drm_backend.rs | 61 +++++++++++++++---- crates/apps/mirada-compositor/src/main.rs | 32 ++++++++-- crates/modules/mirada/SDD.md | 9 +-- vamos.txt | 2 +- 5 files changed, 87 insertions(+), 30 deletions(-) diff --git a/crates/apps/mirada-compositor/README.md b/crates/apps/mirada-compositor/README.md index 39a8d0d..87010bc 100644 --- a/crates/apps/mirada-compositor/README.md +++ b/crates/apps/mirada-compositor/README.md @@ -39,12 +39,13 @@ 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. **`Super`+arrastre** con el botón izquierdo -mueve una ventana, con el derecho la redimensiona — al arrastrarla, la -ventana pasa a flotar. Cada ventana lleva un marco fino: azul la que -tiene el foco, gris las demás. +Lleva teclado y ratón por `libinput`: el foco sigue al puntero y los +clics y la rueda llegan a la ventana que tienes debajo. El cursor toma +la forma que pide el cliente (la «I» sobre texto, una mano…) y cae a un +cuadrado por defecto sobre el escritorio. **`Super`+arrastre** con el +botón izquierdo mueve una ventana, con el derecho la redimensiona — al +arrastrarla, la ventana pasa a flotar. Cada ventana lleva un marco +fino: azul la que tiene el foco, gris las demás. - `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 cf94d9c..72ac07b 100644 --- a/crates/apps/mirada-compositor/src/drm_backend.rs +++ b/crates/apps/mirada-compositor/src/drm_backend.rs @@ -42,7 +42,7 @@ 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::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent}; use smithay::output::OutputModeSource; use smithay::reexports::calloop::generic::Generic; use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; @@ -53,7 +53,7 @@ use smithay::reexports::input::Libinput; use smithay::reexports::rustix::fs::OFlags; use smithay::reexports::wayland_server::{Display, ListeningSocket}; use smithay::utils::{ - DeviceFd, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER, + DeviceFd, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER, }; use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect}; @@ -165,18 +165,40 @@ impl DrmState { let elements: Vec> = { let mut out: Vec> = Vec::new(); + // El cursor — la superficie que pidió el cliente (la «I» del + // texto, una mano…), o el cuadrado por defecto si pidió un + // cursor con nombre y no hay tema. `Hidden` no pinta nada. let (cx, cy) = self.app.pointer_loc; - let cursor_rect = Rectangle::new( - Point::::from((cx.round() as i32, cy.round() as i32)), - Size::::from((CURSOR_SIZE, CURSOR_SIZE)), - ); - out.push(Frame::Solid(SolidColorRenderElement::new( - self.cursor_id.clone(), - cursor_rect, - CommitCounter::default(), - CURSOR_COLOR, - Kind::Cursor, - ))); + match &self.app.cursor_status { + CursorImageStatus::Hidden => {} + CursorImageStatus::Surface(surface) if surface.alive() => { + let (hx, hy) = crate::cursor_hotspot(surface); + let loc = (cx.round() as i32 - hx, cy.round() as i32 - hy); + for el in render_elements_from_surface_tree( + &mut self.renderer, + surface, + loc, + 1.0, + 1.0, + Kind::Cursor, + ) { + out.push(Frame::Window(el)); + } + } + _ => { + let cursor_rect = Rectangle::new( + Point::::from((cx.round() as i32, cy.round() as i32)), + Size::::from((CURSOR_SIZE, CURSOR_SIZE)), + ); + out.push(Frame::Solid(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); @@ -228,6 +250,12 @@ impl DrmState { for w in &self.app.windows { send_frames_surface_tree(&w.surface, time); } + // También a la superficie del cursor, por si es un cursor animado. + if let CursorImageStatus::Surface(surface) = &self.app.cursor_status { + if surface.alive() { + send_frames_surface_tree(surface, time); + } + } } /// Tarea periódica: Cerebro enlazado, recarga del keymap, API de @@ -452,6 +480,13 @@ impl DrmState { ); pointer.frame(&mut self.app); + // Sobre el escritorio pelado no manda ningún cliente: el cursor + // vuelve al de por defecto (si no, se queda con la «I» del texto + // de la última ventana). + if hit.is_none() { + self.app.cursor_status = CursorImageStatus::default_named(); + } + // 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 { diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs index 339b09b..c3712ef 100644 --- a/crates/apps/mirada-compositor/src/main.rs +++ b/crates/apps/mirada-compositor/src/main.rs @@ -33,7 +33,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::pointer::{CursorImageStatus, CursorImageSurfaceData, PointerHandle}; use smithay::input::{Seat, SeatHandler, SeatState}; use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; @@ -140,6 +140,9 @@ struct App { pointer: Option>, /// Posición del puntero en coordenadas globales. pointer_loc: (f64, f64), + /// Qué cursor pide el cliente enfocado — una superficie suya, un + /// cursor con nombre, u oculto. El backend lo pinta en consecuencia. + cursor_status: CursorImageStatus, /// Arrastre de ventana en curso (mover o redimensionar con el ratón). drag: Option, @@ -476,11 +479,11 @@ impl SeatHandler for App { } fn focus_changed(&mut self, _seat: &Seat, _focused: Option<&WlSurface>) {} - fn cursor_image( - &mut self, - _seat: &Seat, - _image: smithay::input::pointer::CursorImageStatus, - ) { + + /// El cliente enfocado pidió un cursor — guardamos su petición; el + /// backend la pinta (su superficie, o el cuadrado si es con nombre). + fn cursor_image(&mut self, _seat: &Seat, image: CursorImageStatus) { + self.cursor_status = image; } } @@ -609,6 +612,22 @@ fn surface_px_size(w: &ManagedWindow) -> Option<(i32, i32)> { .map(|s| (s.w, s.h)) } +/// El punto caliente (hotspot) de una superficie de cursor: el píxel de +/// la imagen que debe quedar bajo la posición real del puntero. `(0, 0)` +/// si el cliente no lo declaró. +fn cursor_hotspot(surface: &WlSurface) -> (i32, i32) { + with_states(surface, |states| { + states + .data_map + .get::() + .map(|m| { + let h = m.lock().unwrap().hotspot; + (h.x, h.y) + }) + .unwrap_or((0, 0)) + }) +} + /// Lanza un comando como proceso hijo, vía `sh -c`. El hijo hereda el /// entorno —`WAYLAND_DISPLAY` incluido—, así que el cliente que abra se /// conecta a este compositor. Lo usan la acción `spawn:…` del keymap y @@ -724,6 +743,7 @@ fn build_app() -> Result> { keyboard: None, pointer: None, pointer_loc: (0.0, 0.0), + cursor_status: CursorImageStatus::default_named(), drag: None, windows: Vec::new(), body: BodyState::new(), diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md index d9a0574..d879b55 100644 --- a/crates/modules/mirada/SDD.md +++ b/crates/modules/mirada/SDD.md @@ -204,10 +204,11 @@ Cerebro: **autónomo** (`Desktop` embebido) o **enlazado** (`MIRADA_SOCKET` anfitriona: `libseat` (sesión), `udev` (GPU), `DrmDevice` + GBM + EGL + `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. + salida limpia. El ratón: el cursor toma la superficie que pide el + cliente (`wl_pointer.set_cursor` → `cursor_image`) y cae a un cuadrado + (`SolidColorRenderElement` `Kind::Cursor`) por defecto; el foco sigue + al puntero (`BodyEvent::PointerEntered`) y clics y rueda van a la + ventana debajo. Todo en un enum `Frame` de elementos de render. `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`). Cada ventana lleva un marco diff --git a/vamos.txt b/vamos.txt index d128f1f..3520d9d 100644 --- a/vamos.txt +++ b/vamos.txt @@ -1001,7 +1001,7 @@ 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. + En --drm el foco sigue al puntero y clics/rueda van a la ventana debajo; el cursor toma la forma del cliente. Cada ventana lleva un marco fino: azul la enfocada, gris las demás. 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.