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=` — lanza una app al arrancar (`MIRADA_STARTUP=foot`). - `MIRADA_DRM_TIMEOUT=` — 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, GbmFramebufferExporter, (), 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 where R: ImportAll; + Window = WaylandSurfaceRenderElement, + 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, + /// 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> = { + // Elementos a pintar — lista front-to-back (índice 0 = encima): + // primero el cursor, luego las flotantes, luego las teseladas. + let elements: Vec> = { + let mut out: Vec> = Vec::new(); + + 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::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) { - 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::::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::::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 { + let mut idx: Vec = (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> { // 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> { 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, keyboard: Option>, + pointer: Option>, + /// Posición del puntero en coordenadas globales. + pointer_loc: (f64, f64), /// Ventanas gestionadas, en orden de aparición. windows: Vec, @@ -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> { data_device_state: DataDeviceState::new::(&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> { 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.