//! La mitad Wayland de `llimphi-layer`: el runner wlr-layer-shell de verdad. //! //! Vive en su propio archivo desde el 2026-08-29, cuando la app que hospeda este //! runner —`shuma`— empezó a querer compilar fuera de Linux. `wlr-layer-shell` //! es un protocolo de Wayland: no tiene equivalente en Windows ni en macOS, y no //! se trata de emularlo. Lo que se separa es **la declaración**: los tipos de //! configuración (`LayerConfig`, `Edge`, `Corner`…) son datos y viajan a //! cualquier lado; esto de acá sólo se compila donde el protocolo existe, y //! `crate::run` contesta un error honesto en el resto. //! //! El código es el mismo de siempre, movido tal cual. use std::error::Error; use std::ffi::c_void; use std::ptr::NonNull; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, Receiver}; use std::sync::{Arc, Mutex}; use raw_window_handle::{ RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, }; use smithay_client_toolkit::{ compositor::{CompositorHandler, CompositorState}, delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer, delegate_registry, delegate_seat, delegate_shm, output::{OutputHandler, OutputState}, registry::{ProvidesRegistryState, RegistryState}, registry_handlers, seat::{ keyboard::{KeyboardHandler, Keysym, Modifiers}, pointer::{ CursorIcon, PointerEvent, PointerEventKind, PointerHandler, ThemeSpec, ThemedPointer, BTN_LEFT, BTN_RIGHT, }, Capability, SeatHandler, SeatState, }, shm::{Shm, ShmHandler}, shell::{ wlr_layer::{ Anchor as LayerAnchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface, LayerSurfaceConfigure, }, WaylandSurface, }, }; use wayland_client::{ globals::registry_queue_init, protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface}, Connection, Proxy, QueueHandle, }; use llimphi_ui::llimphi_compositor::{ consume_scroll_chain, hit_test_click, hit_test_hover, hit_test_scroll_chain, measure_text_node, mount, paint, paint_range, AnimRegistry, DragFn, DragPhase, Instant as AnimInstant, Mounted, }; use llimphi_ui::llimphi_hal::{wgpu, Hal, RawSurface, Surface as _}; use llimphi_ui::llimphi_layout::{taffy, ComputedLayout, LayoutTree}; use llimphi_ui::llimphi_raster::{peniko::color::palette, vello, Renderer}; use llimphi_ui::llimphi_text::Typesetter; use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, Modifiers as LMods, NamedKey}; use crate::{Corner, Edge, Keyboard, LayerConfig, LayerKind, Margins}; /// Levanta el backend layer-shell y corre `A` hasta que la superficie se cierra. /// Devuelve error si no hay sesión Wayland o el compositor no expone /// `wlr-layer-shell`. pub fn run(cfg: LayerConfig) -> Result<(), Box> { // El sentido de la rueda, igual que en el runner de ventana: una barra no // puede scrollear al revés que las ventanas de la misma sesión. llimphi_ui::sembrar_politica_rueda(); let conn = Connection::connect_to_env()?; let (globals, mut event_queue) = registry_queue_init(&conn)?; let qh: QueueHandle> = event_queue.handle(); let compositor = CompositorState::bind(&globals, &qh)?; let layer_shell = LayerShell::bind(&globals, &qh)?; // `wl_shm` para cargar los pixmaps del tema de cursores (el `ThemedPointer` // los necesita). El render de la app va por wgpu; esto es sólo para el cursor. let shm = Shm::bind(&globals, &qh)?; // Handle channel-backed: el `update`/efectos de la app usan su propio `Msg`; // cada dispatch/spawn/spawn_periodic cae en `rx`, que `draw` drena por frame. // El truco (igual que en pata): el `for_test().lift(f)` ejecuta `f` —el // `send`— antes del dispatch no-op del handle de test, así el canal recibe. let (tx, rx) = channel::(); let tx = Mutex::new(tx); // La bandera que atiende `Handle::quit()`: este runner no es el event loop // de winit, así que el `UserEvent::Quit` que manda `quit()` no lo ve nadie. // Sin esto una app que se cierra a sí misma —un runner que ejecuta y se // va— anda como ventana y queda colgada como capa. let salir = Arc::new(AtomicBool::new(false)); let handle: Handle = Handle::<()>::for_test() .lift(move |m: A::Msg| { let _ = tx.lock().expect("tx layer").send(m); }) .con_salida(salir.clone()); let model = A::init(&handle); let resolved = resolve_anchor(&cfg); let wl_surface = compositor.create_surface(&qh); let layer = layer_shell.create_layer_surface( &qh, wl_surface, cfg.layer.into(), Some(cfg.namespace.clone()), None, ); layer.set_anchor(resolved.anchor); layer.set_size(resolved.size.0, resolved.size.1); layer.set_exclusive_zone(resolved.exclusive_zone); if let Some(m) = resolved.margins { layer.set_margin(m.top, m.right, m.bottom, m.left); } layer.set_keyboard_interactivity(cfg.keyboard.into()); layer.commit(); let size = resolved.size; let mut runner = Runner:: { registry_state: RegistryState::new(&globals), output_state: OutputState::new(&globals, &qh), seat_state: SeatState::new(&globals, &qh), conn, hal: None, keyboard: None, pointer: None, themed_pointer: None, shm, cursor_surface: None, seat: None, layer, compositor, layer_shell, cfg, width: size.0.max(1), height: size.1.max(1), gpu: None, cache: None, hover_idx: None, anim_registry: AnimRegistry::new(), anim_viva: false, dirty: true, frame_pending: false, drag: None, ultimo_tap: None, clic_retenido: None, ptr: (0.0, 0.0), mods: Modifiers::default(), model: Some(model), handle, rx, recreate_count: 0, output_actual: None, exit: false, }; // Nacer en un monitor concreto necesita saber cuáles anunció el compositor, // y eso recién se sabe tras un roundtrip — la superficie de arriba ya nació // donde él quiso. Si el conector pedido existe, la re-creamos allá antes de // pintar el primer cuadro; si no está, se queda donde nació. if let Some(nombre) = runner.cfg.output.clone() { event_queue.roundtrip(&mut runner)?; if runner.resolver_output(&nombre).is_some() { let qh2 = event_queue.handle(); runner.recreate_surface(&qh2); } } // Diagnóstico del cierre: con `LLIMPHI_LAYER_DIAG=1` cada vuelta del bucle // cuenta por dónde va. Existe porque un runner que se cierra a sí mismo // quedaba vivo y no había forma de saber si el bucle no salía o si el // proceso no moría después — dos causas distintas con el mismo síntoma. let diag = std::env::var("LLIMPHI_LAYER_DIAG").is_ok(); let mut vueltas: u64 = 0; while !runner.exit && !salir.load(Ordering::SeqCst) { vueltas += 1; if diag { eprintln!("layer-diag: vuelta {vueltas} — entro a blocking_dispatch"); } event_queue.blocking_dispatch(&mut runner)?; if diag { eprintln!( "layer-diag: vuelta {vueltas} — volvi; exit={} salir={}", runner.exit, salir.load(Ordering::SeqCst) ); } } if diag { eprintln!("layer-diag: SALI del bucle tras {vueltas} vueltas"); } drop(runner); if diag { eprintln!("layer-diag: runner dropeado"); } drop(event_queue); if diag { eprintln!("layer-diag: event_queue dropeada"); } Ok(()) } // ── Interno ───────────────────────────────────────────────────────────────── /// Tamaño por defecto de una caja de esquina cuando `cfg.size` es `None`. const DEFAULT_CORNER_SIZE: (u32, u32) = (360, 280); /// El anclaje resuelto para la superficie: anclas sctk, tamaño pedido, zona /// exclusiva y márgenes opcionales. Unifica los dos modos (barra/caja). struct Resolved { anchor: LayerAnchor, size: (u32, u32), exclusive_zone: i32, margins: Option, } /// Resuelve el anclaje según el modo de `cfg`: caja de esquina si `corner` es /// `Some`, barra de borde en caso contrario. fn resolve_anchor(cfg: &LayerConfig) -> Resolved { // Pantalla completa gana sobre los otros dos modos: los cuatro anclajes y // tamaño 0 en ambos ejes = "estirame a la salida". if cfg.fullscreen { return Resolved { anchor: LayerAnchor::TOP | LayerAnchor::BOTTOM | LayerAnchor::LEFT | LayerAnchor::RIGHT, size: (0, 0), exclusive_zone: 0, margins: None, }; } match cfg.corner { Some(corner) => { let size = cfg.size.unwrap_or(DEFAULT_CORNER_SIZE); let anchor = match corner { Corner::TopLeft => LayerAnchor::TOP | LayerAnchor::LEFT, Corner::TopRight => LayerAnchor::TOP | LayerAnchor::RIGHT, Corner::BottomLeft => LayerAnchor::BOTTOM | LayerAnchor::LEFT, Corner::BottomRight => LayerAnchor::BOTTOM | LayerAnchor::RIGHT, }; // Una caja flota siempre: no tiene sentido reservar zona exclusiva. Resolved { anchor, size, exclusive_zone: 0, margins: Some(cfg.margins) } } None => { let (anchor, size) = anchor_and_size(cfg.edge, cfg.thickness); let exclusive_zone = if cfg.exclusive { cfg.thickness as i32 } else { 0 }; Resolved { anchor, size, exclusive_zone, margins: None } } } } /// El anclaje sctk + el tamaño `(w, h)` pedido para un borde y grosor. El eje /// paralelo al borde va en 0 → el compositor lo estira a la salida completa. fn anchor_and_size(edge: Edge, thickness: u32) -> (LayerAnchor, (u32, u32)) { match edge { Edge::Top => ( LayerAnchor::TOP | LayerAnchor::LEFT | LayerAnchor::RIGHT, (0, thickness), ), Edge::Bottom => ( LayerAnchor::BOTTOM | LayerAnchor::LEFT | LayerAnchor::RIGHT, (0, thickness), ), Edge::Left => ( LayerAnchor::LEFT | LayerAnchor::TOP | LayerAnchor::BOTTOM, (thickness, 0), ), Edge::Right => ( LayerAnchor::RIGHT | LayerAnchor::TOP | LayerAnchor::BOTTOM, (thickness, 0), ), } } impl From for Layer { fn from(k: LayerKind) -> Self { match k { LayerKind::Background => Layer::Background, LayerKind::Bottom => Layer::Bottom, LayerKind::Top => Layer::Top, LayerKind::Overlay => Layer::Overlay, } } } impl From for KeyboardInteractivity { fn from(k: Keyboard) -> Self { match k { Keyboard::None => KeyboardInteractivity::None, Keyboard::OnDemand => KeyboardInteractivity::OnDemand, Keyboard::Exclusive => KeyboardInteractivity::Exclusive, } } } /// El estado wgpu de la superficie. struct PanelGpu { surface: RawSurface, renderer: Renderer, typesetter: Typesetter, scene: vello::Scene, layout: LayoutTree, } /// El árbol pintado en el último frame, para hit-test entre frames. struct RenderCache { mounted: Mounted, computed: ComputedLayout, } /// El cliente Wayland que corre un `App` como una sola layer surface. struct Runner { registry_state: RegistryState, output_state: OutputState, seat_state: SeatState, conn: Connection, hal: Option, keyboard: Option, pointer: Option, /// Puntero con tema de cursores: traduce el `Cursor` del nodo bajo el /// puntero a una forma de cursor del sistema (I-beam sobre texto, mano sobre /// links, etc). El backend winit ya lo hacía; el layer-shell no tenía NADA. themed_pointer: Option, /// `wl_shm` retenido para el tema de cursores. shm: Shm, /// Superficie dedicada donde el compositor pinta el cursor (la usa el /// `ThemedPointer`). cursor_surface: Option, seat: Option, layer: LayerSurface, /// Compositor + shell + config retenidos para **re-crear** la layer-surface /// si el compositor nos manda `closed` (reset/quita del output) en vez de /// morir. Ver [`Runner::recreate_surface`] y el handler `closed`. compositor: CompositorState, layer_shell: LayerShell, cfg: LayerConfig, width: u32, height: u32, gpu: Option, cache: Option>, hover_idx: Option, /// Registro de **animaciones implícitas** (`View::animated`, /// `animated_enter/_exit/_inout/_switch`) retenido entre cuadros. /// /// Hasta el 2026-09-11 este camino —el de layer-shell, por donde pintan el /// cajón de shuma, las barras, los rails y todos los flyouts de pata— hacía /// `mount → compute → paint` **sin registro**: las animaciones implícitas /// compilaban, se declaraban y eran **inertes**. Sólo corrían en el bucle /// winit (`llimphi-ui`), o sea únicamente en las apps con ventana propia. /// Eso se comió, entre otras, la paginación del lienzo de shuma al cambiar /// de pestaña (`b145050a9`): andaba en la shuma con ventana y no en la del /// cajón, que es la que se usa. anim_registry: AnimRegistry, /// Alguna animación implícita sigue viva: el próximo frame-callback tiene /// que **repintar** aunque el modelo no haya cambiado. Es el equivalente del /// `request_redraw` que el bucle winit dispara mientras `animating`; acá el /// latido de frame-callbacks ya existe, así que alcanza con volver a marcar /// sucio. Se autodetiene: cuando la última animación se asienta, el /// `reconcile` devuelve `false` y volvemos a dormir. anim_viva: bool, /// Hay algo nuevo que pintar. Arranca en `true` (nunca se pintó nada) y lo /// levantan `apply`, los cambios de hover/drag, `configure` y la re-creación /// de la surface; lo baja `draw` al presentar. Ver [`Runner::draw`]. dirty: bool, /// Ya hay un frame-callback pedido y sin responder. Evita encadenar uno por /// cada `draw`: `configure` también llama a `draw`, así que sin esto cada /// configure suma un latido paralelo y quedan N en vuelo — el commit-storm /// que le clava un core al compositor. Es la misma guarda que pata tiene /// por panel (`frame_pending`); `llimphi-layer` se extrajo de pata pero se /// quedó sin ella. Se limpia al llegar el callback. frame_pending: bool, /// Arrastre en curso: handler del nodo + última posición del puntero. drag: Option<(DragFn, (f32, f32))>, /// Último press izquierdo sobre un nodo con doble-tap: cuándo y dónde. /// /// Es la mitad que le faltaba a este bucle. El de ventana tiene su arena de /// gestos desde hace rato; acá no había ninguna, así que `on_double_tap` /// —puesto en una fila del árbol del IDE, por ejemplo— andaba en la app /// suelta y **no hacía nada en el cajón**, sin error ni aviso. ultimo_tap: Option<(std::time::Instant, (f32, f32))>, /// Click **retenido** por un nodo con doble-tap exclusivo /// (`View::double_tap_exclusive`): `(msg, origen, vence, soltado)`. /// /// Acá el click sale normalmente en el PRESS. Con el gesto arbitrado no /// puede: hasta que no venza la ventana del doble no se sabe si el toque fue /// uno o el primero de dos. Se guarda, y lo dispara `draw` al vencer (el /// latido de frame-callbacks nos despierta igual) o lo mata el segundo tap. /// Es lo que hace que un doble-click en un diente fije el espacio **sin** /// desplegar y replegar su panel de paso. clic_retenido: Option<(A::Msg, (f32, f32), std::time::Instant, bool)>, /// Dónde está el puntero, en coordenadas de la superficie. /// /// Hace falta porque el evento de **rueda** de Wayland no trae posición: la /// posición viaja en los eventos de movimiento, y el de rueda llega con lo /// que haya quedado — se lo vio llegar con `y = 0`, así que el hit-test /// caía fuera de la lista y la rueda no scrolleaba nada. Medido en metal el /// 2026-08-25 con `LLIMPHI_LAYER_DIAG=1`. ptr: (f32, f32), mods: Modifiers, model: Option, handle: Handle, rx: Receiver, /// Cierres consecutivos sin lograr pintar un frame. Resetea al presentar; si /// pasa el tope, nos rendimos (`exit`) para que el supervisor reinicie con /// estado limpio (output realmente ausente, no un reset transitorio). recreate_count: u32, /// Nombre del conector al que está atada la superficie **ahora**. `None` /// mientras la eligió el compositor y nadie pidió mudarse. Es contra esto /// que se compara [`App::output_deseado`] por cuadro. output_actual: Option, exit: bool, } impl Runner { /// Aplica un `Msg` a la app (transición pura del modelo) y marca para /// repintar invalidando la caché de hit-test. fn apply(&mut self, msg: A::Msg) { if let Some(model) = self.model.take() { self.model = Some(A::update(model, msg, &self.handle)); } self.cache = None; self.dirty = true; } /// Traduce un evento de teclado sctk al `KeyEvent` de Llimphi. fn keysym_to_keyevent( &self, event: &smithay_client_toolkit::seat::keyboard::KeyEvent, ) -> Option { use smithay_client_toolkit::seat::keyboard::Keysym as K; let named = match event.keysym { K::Return | K::KP_Enter => Some(NamedKey::Enter), K::BackSpace => Some(NamedKey::Backspace), K::Tab | K::ISO_Left_Tab => Some(NamedKey::Tab), K::Escape => Some(NamedKey::Escape), K::Up => Some(NamedKey::ArrowUp), K::Down => Some(NamedKey::ArrowDown), K::Right => Some(NamedKey::ArrowRight), K::Left => Some(NamedKey::ArrowLeft), K::Home => Some(NamedKey::Home), K::End => Some(NamedKey::End), K::Page_Up => Some(NamedKey::PageUp), K::Page_Down => Some(NamedKey::PageDown), K::Delete => Some(NamedKey::Delete), K::Insert => Some(NamedKey::Insert), K::F1 => Some(NamedKey::F1), K::F2 => Some(NamedKey::F2), K::F3 => Some(NamedKey::F3), K::F4 => Some(NamedKey::F4), K::F5 => Some(NamedKey::F5), K::F6 => Some(NamedKey::F6), K::F7 => Some(NamedKey::F7), K::F8 => Some(NamedKey::F8), K::F9 => Some(NamedKey::F9), K::F10 => Some(NamedKey::F10), K::F11 => Some(NamedKey::F11), K::F12 => Some(NamedKey::F12), _ => None, }; let modifiers = LMods { shift: self.mods.shift, ctrl: self.mods.ctrl, alt: self.mods.alt, meta: self.mods.logo, }; let (key, text) = if let Some(n) = named { (Key::Named(n), None) } else { let txt = match event.utf8.as_deref() { Some(s) if !s.is_empty() && !s.chars().all(char::is_control) => s.to_string(), _ => event.keysym.key_char()?.to_string(), }; (Key::Character(txt.as_str().into()), Some(txt)) }; Some(KeyEvent { key, state: KeyState::Pressed, text, modifiers, repeat: false, }) } /// Crea el estado wgpu de la superficie (idempotente). fn ensure_gpu(&mut self) { if self.gpu.is_some() { return; } let display_ptr = self.conn.backend().display_ptr() as *mut c_void; let surface_ptr = self.layer.wl_surface().id().as_ptr() as *mut c_void; let (w, h) = (self.width, self.height); let display_handle = RawDisplayHandle::Wayland(WaylandDisplayHandle::new( NonNull::new(display_ptr).expect("wl_display ptr"), )); let window_handle = RawWindowHandle::Wayland(WaylandWindowHandle::new( NonNull::new(surface_ptr).expect("wl_surface ptr"), )); // SAFETY: los handles apuntan a objetos Wayland que `self` mantiene vivos. let make_target = || wgpu::SurfaceTargetUnsafe::RawHandle { // wgpu 29 volvió opcional el display del target. raw_display_handle: Some(display_handle), raw_window_handle: window_handle, }; let surface = if self.hal.is_none() { match pollster::block_on(unsafe { Hal::new_for_raw_surface(make_target, w, h) }) { Ok((hal, surface)) => { self.hal = Some(hal); surface } Err(e) => { eprintln!("llimphi-layer · sin gpu: {e}"); return; } } } else { let hal = self.hal.as_ref().expect("hal"); let wgpu_surface = match unsafe { hal.instance.create_surface_unsafe(make_target()) } { Ok(s) => s, Err(e) => { eprintln!("llimphi-layer · sin gpu: {e}"); return; } }; match RawSurface::from_surface(hal, wgpu_surface, display_handle, window_handle, w, h) { Ok(s) => s, Err(e) => { eprintln!("llimphi-layer · sin gpu: {e}"); return; } } }; let hal = self.hal.as_ref().expect("hal"); let renderer = Renderer::new(hal).expect("renderer"); self.gpu = Some(PanelGpu { surface, renderer, typesetter: Typesetter::new(), scene: vello::Scene::new(), layout: LayoutTree::new(), }); } /// Traduce el nombre de un conector (`"DP-1"`) al `wl_output` que el /// compositor anunció. `None` si ese monitor no está (desenchufado, o un /// nombre que no existe) — el caller cae a «que elija el compositor». fn resolver_output(&self, nombre: &str) -> Option { self.output_state.outputs().find(|o| { self.output_state .info(o) .and_then(|i| i.name) .is_some_and(|n| n == nombre) }) } /// ¿La app pidió mudarse de monitor? Compara [`App::output_deseado`] con la /// salida a la que está atada la superficie y, si difieren, la re-crea allá. /// /// Devuelve `true` si mudó — el caller debe **abandonar el cuadro**: la /// superficie nueva todavía no tiene `configure`, y el `configure` que /// llegue vuelve a llamar a `draw` con la GPU rehecha. /// /// Se re-crea porque no hay alternativa: en wlr-layer-shell la salida se /// elige en `get_layer_surface` y después es inmutable. Lo que se paga es /// la superficie y su swapchain; el proceso, el modelo (mayúsculas, capa /// activa) y el dispositivo `wgpu` sobreviven intactos. fn seguir_output(&mut self, qh: &QueueHandle) -> bool { let Some(model) = self.model.as_ref() else { return false; }; let Some(quiere) = A::output_deseado(model).filter(|n| !n.is_empty()) else { return false; }; if self.output_actual.as_deref() == Some(quiere.as_str()) { return false; } if self.resolver_output(&quiere).is_none() { // El monitor pedido no está anunciado (todavía, o ya no). Quedarse // donde está es lo correcto; reintenta solo en el próximo cuadro. return false; } self.cfg.output = Some(quiere); self.recreate_surface(qh); true } /// Re-crea la layer-surface tras un `closed` del compositor (reset/quita del /// output: churn DRM, DPMS, hotplug). Suelta la surface GPU vieja —atada al /// `wl_surface` ya muerto; el `hal`/device **sobrevive**— y crea una nueva /// con la misma config. El próximo `configure` que mande el compositor /// reconstruye la GPU vía [`ensure_gpu`](Self::ensure_gpu) y `draw` re-arma /// el latido. Así la barra sobrevive a un reset de output sin morir. fn recreate_surface(&mut self, qh: &QueueHandle) { self.gpu = None; self.cache = None; self.dirty = true; // La surface vieja murió: su callback no va a llegar nunca. Si no // liberamos el slot, el latido queda trabado para siempre. self.frame_pending = false; let resolved = resolve_anchor(&self.cfg); // La salida pedida, si el compositor la anuncia. `None` = que elija él // (el camino de siempre, y el fallback si el conector no existe: mejor // una barra en el monitor equivocado que ninguna barra). let salida = self .cfg .output .as_deref() .and_then(|n| self.resolver_output(n)); self.output_actual = salida.as_ref().and(self.cfg.output.clone()); let wl_surface = self.compositor.create_surface(qh); let layer = self.layer_shell.create_layer_surface( qh, wl_surface, self.cfg.layer.into(), Some(self.cfg.namespace.clone()), salida.as_ref(), ); layer.set_anchor(resolved.anchor); layer.set_size(resolved.size.0, resolved.size.1); layer.set_exclusive_zone(resolved.exclusive_zone); if let Some(m) = resolved.margins { layer.set_margin(m.top, m.right, m.bottom, m.left); } layer.set_keyboard_interactivity(self.cfg.keyboard.into()); layer.commit(); self.layer = layer; // dropea la LayerSurface cerrada } /// Mantiene vivo el latido: pide el siguiente frame-callback. El loop de /// frames se auto-sostiene así (cada `frame` re-pide el próximo), lo que /// permite drenar el canal de la app por frame sin un timer aparte. fn latido(&mut self, qh: &QueueHandle) { if self.frame_pending { return; // ya hay uno pedido; encadenar otro es el commit-storm } self.frame_pending = true; let surface = self.layer.wl_surface(); surface.frame(qh, surface.clone()); surface.commit(); } /// Avanza un frame: drena el canal, construye la vista, layout, pinta y /// presenta. Cachea el árbol para el hit-test del próximo evento. /// /// **Sale temprano si nada cambió.** El latido de frame-callbacks se /// mantiene siempre (es lo que nos despierta para drenar el canal), pero /// construir vista + layout + pintar + presentar sólo se paga cuando hay /// algo nuevo: un `Msg` aplicado, un cambio de hover/drag, un `configure` o /// una re-creación de la surface. Antes se re-renderizaba en CADA frame /// aunque la pantalla fuera idéntica — medido en metal 2026-07-21, /// `pata-notify` quemaba 2.9% de un core sin un solo toast en pantalla. /// Es además el criterio que ya usa el backend de ventanas (`llimphi-ui` /// duerme en `ControlFlow::Wait` y pide `request_redraw` a demanda). /// /// Las apps que animan ya empujan `Msg` por [`Handle::spawn_periodic`] /// (pata: reloj 1 s, cava y dientes 100 ms), así que siguen andando igual. /// Lo que NO se sostiene solo es una `view()` que dependa del reloj sin /// emitir un `Msg`: eso hay que despertarlo con un tick, como corresponde. fn draw(&mut self, qh: &QueueHandle) { // Click retenido que ya venció con el botón arriba: era un click simple // y recién ahora se sabe (ver `clic_retenido`). El latido de // frame-callbacks es el que nos trae acá a tiempo. if self .clic_retenido .as_ref() .is_some_and(|(_, _, vence, soltado)| *soltado && std::time::Instant::now() >= *vence) { let (msg, _, _, _) = self.clic_retenido.take().expect("retenido"); self.apply(msg); } // Drenar los Msg que la app empujó al canal (ticks/async/follow-ups). // `apply` marca sucio, así que esto decide el early-return de abajo. while let Ok(m) = self.rx.try_recv() { self.apply(m); } // ¿La app quiere estar en otro monitor? Se muda ANTES de pintar: la // superficie que sobrevive a esto es otra, y pintar la vieja sería tirar // el cuadro. El `configure` de la nueva vuelve a entrar acá. if self.seguir_output(qh) { return; } // Nada nuevo y ya hay un frame presentado: mantené el latido y volvé. // `anim_viva` cuenta como «hay algo nuevo»: una animación implícita // cambia lo que se ve sin que nadie mande un `Msg`, así que es el único // caso en que hay que repintar con el modelo quieto. if !self.dirty && !self.anim_viva && self.cache.is_some() { self.latido(qh); return; } self.ensure_gpu(); let (w, h) = (self.width, self.height); let view = A::view(self.model.as_ref().expect("model")); let hover_idx = self.hover_idx; let hal = match self.hal.as_ref() { Some(h) => h, None => { self.latido(qh); return; } }; let gpu = match self.gpu.as_mut() { Some(g) => g, None => { self.latido(qh); return; } }; gpu.surface.resize(w, h); let frame = match gpu.surface.acquire() { Ok(f) => f, Err(_) => { self.latido(qh); return; } }; gpu.layout.clear(); let mounted = mount(&mut gpu.layout, view); let computed = { let ts = &mut gpu.typesetter; let tmap = &mounted.text_measures; gpu.layout .compute_with_measure(mounted.root, (w as f32, h as f32), |nid, known, avail| { match tmap.get(&nid) { Some(tm) => measure_text_node(ts, tm, known, avail), None => taffy::Size::ZERO, } }) .expect("layout") }; // Input-region recortada al contenido interactivo (opt-in): la caja fija // del overlay (un toast de 352×420, p. ej.) se traga todos los clicks de // la ventana debajo aunque esté vacía o transparente. Recortando la // región a la unión de los rects de nodos con handler (o VACÍA si no hay // ninguno), las zonas ociosas quedan click-through. Se aplica antes del // `present` (que commitea la surface) para que entre en el mismo frame. if self.cfg.input_from_content { use smithay_client_toolkit::compositor::Region; if let Ok(region) = Region::new(&self.compositor) { for n in &mounted.nodes { let interactivo = n.on_click.is_some() || n.on_click_at.is_some() || n.on_click_screen.is_some() || n.drag.is_some() || n.drag_at.is_some() || n.drag_velocity.is_some() || n.on_right_click.is_some() || n.on_right_click_at.is_some() || n.on_middle_click.is_some() || n.hover_fill.is_some(); if !interactivo { continue; } if let Some(r) = computed.get(n.id) { let (x, y, w, h) = (r.x as i32, r.y as i32, r.w.ceil() as i32, r.h.ceil() as i32); if w > 0 && h > 0 { region.add(x, y, w, h); } } } // Región (posiblemente vacía = todo click-through) → la surface. self.layer .wl_surface() .set_input_region(Some(region.wl_region())); } } // **Animaciones implícitas.** El mismo orden que el bucle winit // (`llimphi-ui/src/eventloop/redraw.rs`): reconciliar DESPUÉS del layout // y ANTES del paint —ahí se interpolan fill/radius/alpha/transform— y // hornear los underlays (el contenido viejo de un `animated_switch` con // `exit_under`, que el paint apendea debajo del nuevo en su posición z). let ahora = AnimInstant::now(); let mut mounted = mounted; let anim_activa = self.anim_registry.reconcile(&mut mounted, ahora); self.anim_registry.prepare_underlays(&mut mounted, ahora); gpu.scene.reset(); paint(&mut gpu.scene, &mounted, &computed, &mut gpu.typesetter, hover_idx, None); // Salidas y cross-fades. 1) Guardar la subescena de cada nodo `exit` // mientras todavía está vivo (cuando desaparezca ya no hay de dónde // sacarla). 2) Reproducir los fantasmas de los que se fueron, encima del // contenido. Cuesta cero si ningún nodo declara salida. for (idx, end, key) in self.anim_registry.live_exit_nodes(&mounted, ahora) { let a = mounted.nodes[idx].anim.expect("nodo exit lleva anim"); let centro = computed .get(mounted.nodes[idx].id) .map(|r| (r.x + r.w * 0.5, r.y + r.h * 0.5)) .unwrap_or((0.0, 0.0)); // El fantasma ANTERIOR no entra a la captura del contenido vivo: si // entrara, el próximo fantasma arrastraría al previo. let underlay_guardado = mounted.nodes[idx].underlay.take(); let mut sub = vello::Scene::new(); paint_range( &mut sub, &mounted, &computed, &mut gpu.typesetter, None, None, idx, end, vello::kurbo::Affine::IDENTITY, ); mounted.nodes[idx].underlay = underlay_guardado; self.anim_registry.store_live_exit_en(key, sub, centro, &a, ahora); } let fantasmas = self .anim_registry .replay_ghosts(&mut gpu.scene, ahora, w as f32, h as f32); self.anim_viva = anim_activa || fantasmas; if let Err(e) = gpu.renderer.render(hal, &gpu.scene, &frame, palette::css::TRANSPARENT) { eprintln!("llimphi-layer · render: {e}"); } gpu.surface.present(frame, hal); // Pintamos un frame con éxito: el ciclo de re-creación (si lo hubo) se // recuperó del todo. Resetea el backoff de `closed`. self.recreate_count = 0; self.cache = Some(RenderCache { mounted, computed }); // Frame presentado: hasta que algo vuelva a cambiar, los próximos // callbacks sólo re-arman el latido. self.dirty = false; self.latido(qh); } } // ── Handlers sctk ─────────────────────────────────────────────────────────── impl CompositorHandler for Runner { fn scale_factor_changed( &mut self, _: &Connection, _: &QueueHandle, _: &wl_surface::WlSurface, _: i32, ) { } fn transform_changed( &mut self, _: &Connection, _: &QueueHandle, _: &wl_surface::WlSurface, _: wl_output::Transform, ) { } fn frame( &mut self, _: &Connection, qh: &QueueHandle, _: &wl_surface::WlSurface, _: u32, ) { // Llegó el callback que pidió `latido`: liberamos el slot para que el // próximo `latido` pueda pedir el siguiente. self.frame_pending = false; self.draw(qh); } fn surface_enter( &mut self, _: &Connection, _: &QueueHandle, _: &wl_surface::WlSurface, _: &wl_output::WlOutput, ) { } fn surface_leave( &mut self, _: &Connection, _: &QueueHandle, _: &wl_surface::WlSurface, _: &wl_output::WlOutput, ) { } } impl LayerShellHandler for Runner { fn closed(&mut self, _: &Connection, qh: &QueueHandle, _: &LayerSurface) { // El compositor cerró nuestra superficie — típicamente al resetear o // quitar el output al que estábamos anclados (churn DRM, DPMS, hotplug). // Antes esto mataba el proceso (`exit = true`) y la barra quedaba muerta // hasta un respawn externo. Ahora re-creamos la layer-surface sobre el // output vigente y seguimos vivos. Con tope: si nos cierran repetido sin // lograr pintar (output realmente ausente, no un reset transitorio), nos // rendimos y salimos para que el supervisor reinicie con estado limpio. const MAX_RECREATES: u32 = 16; self.recreate_count += 1; if self.recreate_count > MAX_RECREATES { eprintln!( "llimphi-layer · cerrados {} veces sin recuperar; salgo (que reinicie el supervisor).", self.recreate_count ); self.exit = true; return; } eprintln!( "llimphi-layer · el compositor cerró la superficie; re-creo (intento {}).", self.recreate_count ); self.recreate_surface(qh); } fn configure( &mut self, _: &Connection, qh: &QueueHandle, _: &LayerSurface, configure: LayerSurfaceConfigure, _: u32, ) { let (cw, ch) = configure.new_size; const MAX_DIM: u32 = 16384; if (1..=MAX_DIM).contains(&cw) { self.width = cw; } if (1..=MAX_DIM).contains(&ch) { self.height = ch; } // Un configure siempre exige repintar: cambió el tamaño, o el compositor // nos re-mapeó y el frame anterior ya no vale. self.dirty = true; // La app puede necesitar su tamaño (p. ej. para calcular el viewport de // un scroll). El compositor estira el eje paralelo al borde, así que es // aquí —no en `run`— donde se conoce el alto/ancho real. let resize_msg = self .model .as_ref() .and_then(|m| A::on_resize(m, self.width, self.height)); if let Some(msg) = resize_msg { self.apply(msg); } self.draw(qh); } } impl OutputHandler for Runner { fn output_state(&mut self) -> &mut OutputState { &mut self.output_state } fn new_output(&mut self, _: &Connection, _: &QueueHandle, _: wl_output::WlOutput) {} fn update_output(&mut self, _: &Connection, _: &QueueHandle, _: wl_output::WlOutput) {} fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, _: wl_output::WlOutput) {} } impl SeatHandler for Runner { fn seat_state(&mut self) -> &mut SeatState { &mut self.seat_state } fn new_seat(&mut self, _: &Connection, _: &QueueHandle, seat: wl_seat::WlSeat) { if self.seat.is_none() { self.seat = Some(seat); } } fn new_capability( &mut self, _: &Connection, qh: &QueueHandle, seat: wl_seat::WlSeat, capability: Capability, ) { match capability { Capability::Keyboard if self.keyboard.is_none() => { if let Ok(kbd) = self.seat_state.get_keyboard(qh, &seat, None) { self.keyboard = Some(kbd); } } Capability::Pointer if self.pointer.is_none() => { // Puntero CON tema de cursores: una superficie dedicada para el // cursor + el tema del sistema. Si algo falla (raro), caemos al // puntero pelado (sin cambio de forma, como estaba antes). let surface = self.compositor.create_surface(qh); match self.seat_state.get_pointer_with_theme( qh, &seat, self.shm.wl_shm(), surface.clone(), ThemeSpec::default(), ) { Ok(themed) => { self.pointer = Some(themed.pointer().clone()); self.cursor_surface = Some(surface); self.themed_pointer = Some(themed); } Err(_) => { if let Ok(ptr) = self.seat_state.get_pointer(qh, &seat) { self.pointer = Some(ptr); } } } } _ => {} } } fn remove_capability( &mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat, capability: Capability, ) { match capability { Capability::Keyboard => { if let Some(k) = self.keyboard.take() { k.release(); } } Capability::Pointer => { self.themed_pointer = None; self.cursor_surface = None; if let Some(p) = self.pointer.take() { p.release(); } } _ => {} } } fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} } impl KeyboardHandler for Runner { fn enter( &mut self, _: &Connection, _: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: &wl_surface::WlSurface, _: u32, _: &[u32], _: &[Keysym], ) { } fn leave( &mut self, _: &Connection, _: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: &wl_surface::WlSurface, _: u32, ) { } fn press_key( &mut self, _: &Connection, _: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: u32, event: smithay_client_toolkit::seat::keyboard::KeyEvent, ) { if let Some(ke) = self.keysym_to_keyevent(&event) { let msg = A::on_key(self.model.as_ref().expect("model"), &ke); if let Some(msg) = msg { self.apply(msg); } } } fn release_key( &mut self, _: &Connection, _: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: u32, _: smithay_client_toolkit::seat::keyboard::KeyEvent, ) { } fn update_modifiers( &mut self, _: &Connection, _: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: u32, modifiers: Modifiers, _: u32, ) { self.mods = modifiers; } } /// Traduce el [`Cursor`](llimphi_ui::llimphi_compositor::Cursor) del nodo bajo /// el puntero a la forma de cursor del sistema. Espeja `to_winit_cursor` del /// backend winit — la misma cobertura de variantes. `None` → flecha default. fn cursor_to_icon(c: Option) -> CursorIcon { use llimphi_ui::llimphi_compositor::Cursor as C; match c { None | Some(C::Default) => CursorIcon::Default, Some(C::Pointer) => CursorIcon::Pointer, Some(C::Text) => CursorIcon::Text, Some(C::Crosshair) => CursorIcon::Crosshair, Some(C::Move) => CursorIcon::Move, Some(C::Grab) => CursorIcon::Grab, Some(C::Grabbing) => CursorIcon::Grabbing, Some(C::NotAllowed) => CursorIcon::NotAllowed, Some(C::Copy) => CursorIcon::Copy, Some(C::NoDrop) => CursorIcon::NoDrop, Some(C::Wait) => CursorIcon::Wait, Some(C::Progress) => CursorIcon::Progress, Some(C::Help) => CursorIcon::Help, Some(C::ColResize) => CursorIcon::ColResize, Some(C::RowResize) => CursorIcon::RowResize, Some(C::EwResize) => CursorIcon::EwResize, Some(C::NsResize) => CursorIcon::NsResize, Some(C::NeswResize) => CursorIcon::NeswResize, Some(C::NwseResize) => CursorIcon::NwseResize, Some(C::ZoomIn) => CursorIcon::ZoomIn, Some(C::ZoomOut) => CursorIcon::ZoomOut, } } impl Runner { /// Aplica al puntero la forma de cursor del nodo `hover_idx` actual. No-op si /// no hay `ThemedPointer` (el compositor no dio la capacidad temada). fn aplicar_cursor(&mut self, conn: &Connection) { let Some(themed) = self.themed_pointer.as_ref() else { return }; let cur = self .hover_idx .and_then(|i| self.cache.as_ref()?.mounted.nodes.get(i)) .and_then(|n| n.cursor); let _ = themed.set_cursor(conn, cursor_to_icon(cur)); } } impl PointerHandler for Runner { fn pointer_frame( &mut self, conn: &Connection, _: &QueueHandle, _: &wl_pointer::WlPointer, events: &[PointerEvent], ) { for e in events { match e.kind { PointerEventKind::Enter { .. } => { // Al entrar, fijar el cursor según el nodo bajo el puntero // (con el serial de Enter que el ThemedPointer ya rastrea). let (px, py) = (e.position.0 as f32, e.position.1 as f32); let nuevo = self .cache .as_ref() .and_then(|c| hit_test_hover(&c.mounted, &c.computed, px, py)); self.ptr = (px, py); if self.hover_idx != nuevo { self.hover_idx = nuevo; self.dirty = true; // el hover se PINTA (hover_fill) } self.aplicar_cursor(conn); continue; } PointerEventKind::Motion { .. } => { let (px, py) = (e.position.0 as f32, e.position.1 as f32); self.ptr = (px, py); // Alejarse del origen mata el click retenido: fue arrastre. if let Some((_, (ox, oy), _, _)) = self.clic_retenido.as_ref() { if ((px - ox).powi(2) + (py - oy).powi(2)).sqrt() as f64 > llimphi_ui::CLICK_MOVE_CANCEL { self.clic_retenido = None; } } // Drag en curso: el delta va al handler del nodo. if let Some((handler, last)) = self.drag.as_ref().map(|(h, l)| (h.clone(), *l)) { if let Some(d) = self.drag.as_mut() { d.1 = (px, py); } if let Some(msg) = (handler)(DragPhase::Move, px - last.0, py - last.1) { self.apply(msg); } continue; } let nuevo = self .cache .as_ref() .and_then(|c| hit_test_hover(&c.mounted, &c.computed, px, py)); if self.hover_idx != nuevo { self.hover_idx = nuevo; self.dirty = true; // el hover se PINTA (hover_fill) // Cambió el nodo bajo el puntero → actualizar la forma. self.aplicar_cursor(conn); } continue; } PointerEventKind::Leave { .. } => { if self.hover_idx.is_some() { self.hover_idx = None; self.dirty = true; // se va el resaltado } continue; } _ => {} } if let PointerEventKind::Axis { vertical, .. } = e.kind { // Convención de Llimphi: el delta va en **líneas**. Wayland da // los clicks de rueda en `discrete` y el desplazamiento suave // (touchpad) en `absolute`, en px — la misma división por 20 // que hace el runner de ventana con `PixelDelta`. // // Esa misma división dice la CLASE de dispositivo, que es lo // que decide el sentido: `discrete` sólo lo manda una rueda // física; un pad entrega nada más el continuo. let (dy, fuente) = if vertical.discrete != 0 { (vertical.discrete as f32, llimphi_ui::FuenteRueda::Raton) } else { ( vertical.absolute as f32 / 20.0, llimphi_ui::FuenteRueda::Touchpad, ) }; let dy = llimphi_ui::politica_rueda() .aplicar(llimphi_ui::WheelDelta { x: 0.0, y: dy }, fuente) .y; if dy != 0.0 { // La posición sale de `self.ptr`, no del evento: el evento // de rueda de Wayland no trae posición (ver el campo). let (px, py) = self.ptr; // La **cadena** de nodos scrollables bajo el puntero, no el // primero: un área de scroll consume hasta su tope y deja // el sobrante al ancestro, y eso puede dar varios Msg por // evento. Es lo mismo que hace el runner de ventana. // // Esto estuvo roto desde siempre en el runner de capa: // miraba el `on_scroll` viejo, y el widget de scroll publica // la rueda por `on_scroll_take` (el reparto parcial). O sea // que NINGUNA app de capa scrolleaba con la rueda. Cazado en // metal el 2026-08-25 con `LLIMPHI_LAYER_DIAG=1`, que mostró // el hit-test funcionando (`hover=Some(36)`) y la lista de // nodos con rueda **vacía**. let msgs: Vec = match self.cache.as_ref() { Some(c) => { let chain = hit_test_scroll_chain(&c.mounted, &c.computed, px, py); consume_scroll_chain(&c.mounted, &chain, 0.0, dy) } None => Vec::new(), }; if std::env::var("LLIMPHI_LAYER_DIAG").is_ok() { eprintln!( "layer-diag: rueda dy={dy} en ({px},{py}) msgs={}", msgs.len() ); } for msg in msgs { self.apply(msg); } } continue; } if let PointerEventKind::Release { button, .. } = e.kind { if button == BTN_LEFT { if let Some((handler, _)) = self.drag.take() { if let Some(msg) = (handler)(DragPhase::End, 0.0, 0.0) { self.apply(msg); } } // Click retenido: soltar no lo dispara —el segundo tap // todavía puede llegar—, sólo lo marca. Sale al vencer. if let Some((msg, origen, vence, _)) = self.clic_retenido.take() { if std::time::Instant::now() >= vence { self.apply(msg); } else { self.clic_retenido = Some((msg, origen, vence, true)); } } } continue; } if let PointerEventKind::Press { button, .. } = e.kind { if button != BTN_LEFT && button != BTN_RIGHT { continue; } let (px, py) = (e.position.0 as f32, e.position.1 as f32); let derecho = button == BTN_RIGHT; // ── Arena de gestos: doble-tap ────────────────────────────── // Aditiva, igual que en el bucle de ventana: **no** consume el // click de abajo. Un doble-tap es dos clicks, y quien puso los // dos gestos en el mismo nodo (elegir con uno, abrir con dos) // cuenta con que el primero también haya pasado. // Con `double_tap_exclusive` el gesto además ARBITRA contra el // click del mismo nodo: el primer tap lo deja retenido y el // segundo lo descarta (ver `clic_retenido`). let mut retener = None; let mut suprimir_click = false; if !derecho { if let Some((msg, exclusivo)) = self.cache.as_ref().and_then(|c| { let i = llimphi_ui::llimphi_compositor::hit_test_double_tap( &c.mounted, &c.computed, px, py, )?; let n = c.mounted.nodes.get(i)?; Some((n.on_double_tap.clone()?, n.double_tap_exclusive.is_some())) }) { let ahora = std::time::Instant::now(); let califica = self.ultimo_tap.is_some_and(|(t, (x, y))| { llimphi_ui::doble_tap_califica( ahora.duration_since(t), (x - px) as f64, (y - py) as f64, ) }); if califica { // Consumido: un tercer tap no re-dispara. self.ultimo_tap = None; if exclusivo { self.clic_retenido = None; suprimir_click = true; } self.apply(msg); } else { self.ultimo_tap = Some((ahora, (px, py))); if exclusivo { retener = Some(ahora + llimphi_ui::DOUBLE_TAP_WINDOW); } } } } // Nodo arrastrable bajo el press izquierdo: arranca un drag. if !derecho { let handler = self.cache.as_ref().and_then(|c| { let i = hit_test_click(&c.mounted, &c.computed, px, py)?; c.mounted.nodes.get(i)?.drag.clone() }); if let Some(handler) = handler { self.drag = Some((handler, (px, py))); continue; } } let msg = self.cache.as_ref().and_then(|c| { let i = hit_test_click(&c.mounted, &c.computed, px, py)?; let n = c.mounted.nodes.get(i)?; let r = c.computed.get(n.id)?; mensaje_de_click(n, r, px, py, derecho) }); if let Some(msg) = msg.filter(|_| !suprimir_click) { match retener { Some(vence) => self.clic_retenido = Some((msg, (px, py), vence, false)), None => self.apply(msg), } } } } } } /// **Qué mensaje produce un click sobre `n`**, con `r` su rect ya calculado y /// `(px, py)` el puntero en coordenadas de la surface. /// /// Vive como función y no adentro del handler porque **acá se perdía un click**: /// la rama del botón izquierdo no consultaba `on_click_screen`, así que un nodo /// que sólo lo declara —el `⋯` de las opciones de shuma, el disparador del /// `select`, el swatch del color-picker— caía al `on_click_at` que no tiene y el /// click se iba al vacío. Andaba en winit (que sí lo despacha) y no bajo /// layer-shell, o sea: andaba en la ventana suelta y no en el cajón, que es /// donde se usa. /// /// **El orden es el mismo para los dos botones**, y ésa es la invariante que el /// test fija: primero el de coordenadas absolutas (lo necesitan los menús que se /// anclan al puntero), después el plano, después el local. Dos órdenes distintos /// para el mismo gesto es exactamente cómo se coló el agujero. pub(crate) fn mensaje_de_click( n: &llimphi_ui::llimphi_compositor::MountedNode, r: llimphi_ui::llimphi_layout::Rect, px: f32, py: f32, derecho: bool, ) -> Option { if derecho { if let Some(screen) = n.on_right_click_screen.as_ref() { if let Some(m) = screen(px, py, r.w, r.h) { return Some(m); } } if let Some(m) = n.on_right_click.clone() { return Some(m); } let at = n.on_right_click_at.as_ref()?; at(px - r.x, py - r.y, r.w, r.h) } else { if let Some(screen) = n.on_click_screen.as_ref() { if let Some(m) = screen(px, py, r.w, r.h) { return Some(m); } } if let Some(m) = n.on_click.clone() { return Some(m); } let at = n.on_click_at.as_ref()?; at(px - r.x, py - r.y, r.w, r.h) } } #[cfg(test)] mod tests_click { use super::mensaje_de_click; use llimphi_ui::llimphi_compositor::mount; use llimphi_ui::llimphi_layout::taffy::prelude::{length, Size, Style}; use llimphi_ui::llimphi_layout::LayoutTree; use llimphi_ui::View; /// Monta `v` y devuelve el mensaje que daría un click en el centro del nodo /// que tenga handlers — el mismo camino que corre el runner. fn click(v: View<&'static str>, derecho: bool) -> Option<&'static str> { let mut layout = LayoutTree::new(); let mounted = mount(&mut layout, v); let computed = layout.compute(mounted.root, (200.0, 100.0)).expect("layout"); let n = mounted .nodes .iter() .find(|n| { n.on_click.is_some() || n.on_click_at.is_some() || n.on_click_screen.is_some() || n.on_right_click.is_some() || n.on_right_click_at.is_some() || n.on_right_click_screen.is_some() }) .expect("el árbol de prueba tiene que tener un nodo con handler"); let r = computed.get(n.id).expect("rect del nodo"); mensaje_de_click(n, r, r.x + r.w / 2.0, r.y + r.h / 2.0, derecho) } fn caja() -> Style { Style { size: Size { width: length(40.0_f32), height: length(20.0_f32) }, ..Default::default() } } /// **El caso que se rompió en metal:** un nodo que declara SÓLO /// `on_click_screen` tiene que disparar con el botón izquierdo. Antes caía al /// `on_click_at` que no tiene y el click se perdía sin decir nada. #[test] fn un_nodo_con_solo_on_click_screen_dispara() { let v = View::<&'static str>::new(caja()).on_click_screen(|_x, _y, _w, _h| Some("abrí")); assert_eq!(click(v, false), Some("abrí")); } /// **Y el orden es el mismo para los dos botones.** Es la invariante que /// dejó pasar el agujero: la rama derecha consultaba el de coordenadas /// absolutas primero y la izquierda no lo consultaba nunca. #[test] fn los_dos_botones_resuelven_en_el_mismo_orden() { let izq = View::<&'static str>::new(caja()) .on_click("plano") .on_click_screen(|_x, _y, _w, _h| Some("absoluto")); assert_eq!(click(izq, false), Some("absoluto"), "el absoluto gana al plano"); let der = View::<&'static str>::new(caja()) .on_right_click("plano") .on_right_click_screen(|_x, _y, _w, _h| Some("absoluto")); assert_eq!(click(der, true), Some("absoluto")); } /// Y lo que ya andaba sigue andando: sin el absoluto, el plano; sin ninguno /// de los dos, el local — y sin handlers, nada. #[test] fn la_cadena_de_respaldo_no_cambio() { let plano = View::<&'static str>::new(caja()).on_click("plano"); assert_eq!(click(plano, false), Some("plano")); let local = View::<&'static str>::new(caja()).on_click_at(|_x, _y, _w, _h| Some("local")); assert_eq!(click(local, false), Some("local")); // Un handler que devuelve `None` no corta la cadena: cae al siguiente. let vacio = View::<&'static str>::new(caja()) .on_click_screen(|_x, _y, _w, _h| None) .on_click("plano"); assert_eq!(click(vacio, false), Some("plano")); } } impl ProvidesRegistryState for Runner { fn registry(&mut self) -> &mut RegistryState { &mut self.registry_state } registry_handlers![OutputState, SeatState]; } delegate_compositor!(@ Runner); delegate_output!(@ Runner); delegate_layer!(@ Runner); delegate_seat!(@ Runner); delegate_keyboard!(@ Runner); delegate_pointer!(@ Runner); delegate_shm!(@ Runner); delegate_registry!(@ Runner); impl ShmHandler for Runner { fn shm_state(&mut self) -> &mut Shm { &mut self.shm } }