tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1//! La mitad Wayland de `llimphi-layer`: el runner wlr-layer-shell de verdad.
2//!
3//! Vive en su propio archivo desde el 2026-08-29, cuando la app que hospeda este
4//! runner —`shuma`— empezó a querer compilar fuera de Linux. `wlr-layer-shell`
5//! es un protocolo de Wayland: no tiene equivalente en Windows ni en macOS, y no
6//! se trata de emularlo. Lo que se separa es **la declaración**: los tipos de
7//! configuración (`LayerConfig`, `Edge`, `Corner`…) son datos y viajan a
8//! cualquier lado; esto de acá sólo se compila donde el protocolo existe, y
9//! `crate::run` contesta un error honesto en el resto.
10//!
11//! El código es el mismo de siempre, movido tal cual.
12
13use std::error::Error;
14use std::ffi::c_void;
15use std::ptr::NonNull;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::mpsc::{channel, Receiver};
18use std::sync::{Arc, Mutex};
19
20use raw_window_handle::{
21 RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
22};
23use smithay_client_toolkit::{
24 compositor::{CompositorHandler, CompositorState},
25 delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer,
26 delegate_registry, delegate_seat, delegate_shm,
27 output::{OutputHandler, OutputState},
28 registry::{ProvidesRegistryState, RegistryState},
29 registry_handlers,
30 seat::{
31 keyboard::{KeyboardHandler, Keysym, Modifiers},
32 pointer::{
33 CursorIcon, PointerEvent, PointerEventKind, PointerHandler, ThemeSpec, ThemedPointer,
34 BTN_LEFT, BTN_RIGHT,
35 },
36 Capability, SeatHandler, SeatState,
37 },
38 shm::{Shm, ShmHandler},
39 shell::{
40 wlr_layer::{
41 Anchor as LayerAnchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler,
42 LayerSurface, LayerSurfaceConfigure,
43 },
44 WaylandSurface,
45 },
46};
47use wayland_client::{
48 globals::registry_queue_init,
49 protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
50 Connection, Proxy, QueueHandle,
51};
52
53use llimphi_ui::llimphi_compositor::{
54 consume_scroll_chain, hit_test_click, hit_test_hover, hit_test_scroll_chain, measure_text_node,
55 mount, paint, paint_range, AnimRegistry, DragFn, DragPhase, Instant as AnimInstant, Mounted,
56};
57use llimphi_ui::llimphi_hal::{wgpu, Hal, RawSurface, Surface as _};
58use llimphi_ui::llimphi_layout::{taffy, ComputedLayout, LayoutTree};
59use llimphi_ui::llimphi_raster::{peniko::color::palette, vello, Renderer};
60use llimphi_ui::llimphi_text::Typesetter;
61use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, Modifiers as LMods, NamedKey};
62use crate::{Corner, Edge, Keyboard, LayerConfig, LayerKind, Margins};
63/// Levanta el backend layer-shell y corre `A` hasta que la superficie se cierra.
64/// Devuelve error si no hay sesión Wayland o el compositor no expone
65/// `wlr-layer-shell`.
66pub fn run<A: App>(cfg: LayerConfig) -> Result<(), Box<dyn Error>> {
67 // El sentido de la rueda, igual que en el runner de ventana: una barra no
68 // puede scrollear al revés que las ventanas de la misma sesión.
69 llimphi_ui::sembrar_politica_rueda();
70 let conn = Connection::connect_to_env()?;
71 let (globals, mut event_queue) = registry_queue_init(&conn)?;
72 let qh: QueueHandle<Runner<A>> = event_queue.handle();
73
74 let compositor = CompositorState::bind(&globals, &qh)?;
75 let layer_shell = LayerShell::bind(&globals, &qh)?;
76 // `wl_shm` para cargar los pixmaps del tema de cursores (el `ThemedPointer`
77 // los necesita). El render de la app va por wgpu; esto es sólo para el cursor.
78 let shm = Shm::bind(&globals, &qh)?;
79
80 // Handle channel-backed: el `update`/efectos de la app usan su propio `Msg`;
81 // cada dispatch/spawn/spawn_periodic cae en `rx`, que `draw` drena por frame.
82 // El truco (igual que en pata): el `for_test().lift(f)` ejecuta `f` —el
83 // `send`— antes del dispatch no-op del handle de test, así el canal recibe.
84 let (tx, rx) = channel::<A::Msg>();
85 let tx = Mutex::new(tx);
86 // La bandera que atiende `Handle::quit()`: este runner no es el event loop
87 // de winit, así que el `UserEvent::Quit` que manda `quit()` no lo ve nadie.
88 // Sin esto una app que se cierra a sí misma —un runner que ejecuta y se
89 // va— anda como ventana y queda colgada como capa.
90 let salir = Arc::new(AtomicBool::new(false));
91 let handle: Handle<A::Msg> = Handle::<()>::for_test()
92 .lift(move |m: A::Msg| {
93 let _ = tx.lock().expect("tx layer").send(m);
94 })
95 .con_salida(salir.clone());
96 let model = A::init(&handle);
97
98 let resolved = resolve_anchor(&cfg);
99 let wl_surface = compositor.create_surface(&qh);
100 let layer = layer_shell.create_layer_surface(
101 &qh,
102 wl_surface,
103 cfg.layer.into(),
104 Some(cfg.namespace.clone()),
105 None,
106 );
107 layer.set_anchor(resolved.anchor);
108 layer.set_size(resolved.size.0, resolved.size.1);
109 layer.set_exclusive_zone(resolved.exclusive_zone);
110 if let Some(m) = resolved.margins {
111 layer.set_margin(m.top, m.right, m.bottom, m.left);
112 }
113 layer.set_keyboard_interactivity(cfg.keyboard.into());
114 layer.commit();
115 let size = resolved.size;
116
117 let mut runner = Runner::<A> {
118 registry_state: RegistryState::new(&globals),
119 output_state: OutputState::new(&globals, &qh),
120 seat_state: SeatState::new(&globals, &qh),
121 conn,
122 hal: None,
123 keyboard: None,
124 pointer: None,
125 themed_pointer: None,
126 shm,
127 cursor_surface: None,
128 seat: None,
129 layer,
130 compositor,
131 layer_shell,
132 cfg,
133 width: size.0.max(1),
134 height: size.1.max(1),
135 gpu: None,
136 cache: None,
137 hover_idx: None,
138 anim_registry: AnimRegistry::new(),
139 anim_viva: false,
140 dirty: true,
141 frame_pending: false,
142 drag: None,
143 ultimo_tap: None,
144 clic_retenido: None,
145 ptr: (0.0, 0.0),
146 mods: Modifiers::default(),
147 model: Some(model),
148 handle,
149 rx,
150 recreate_count: 0,
151 output_actual: None,
152 exit: false,
153 };
154
155 // Nacer en un monitor concreto necesita saber cuáles anunció el compositor,
156 // y eso recién se sabe tras un roundtrip — la superficie de arriba ya nació
157 // donde él quiso. Si el conector pedido existe, la re-creamos allá antes de
158 // pintar el primer cuadro; si no está, se queda donde nació.
159 if let Some(nombre) = runner.cfg.output.clone() {
160 event_queue.roundtrip(&mut runner)?;
161 if runner.resolver_output(&nombre).is_some() {
162 let qh2 = event_queue.handle();
163 runner.recreate_surface(&qh2);
164 }
165 }
166
167 // Diagnóstico del cierre: con `LLIMPHI_LAYER_DIAG=1` cada vuelta del bucle
168 // cuenta por dónde va. Existe porque un runner que se cierra a sí mismo
169 // quedaba vivo y no había forma de saber si el bucle no salía o si el
170 // proceso no moría después — dos causas distintas con el mismo síntoma.
171 let diag = std::env::var("LLIMPHI_LAYER_DIAG").is_ok();
172 let mut vueltas: u64 = 0;
173 while !runner.exit && !salir.load(Ordering::SeqCst) {
174 vueltas += 1;
175 if diag {
176 eprintln!("layer-diag: vuelta {vueltas} — entro a blocking_dispatch");
177 }
178 event_queue.blocking_dispatch(&mut runner)?;
179 if diag {
180 eprintln!(
181 "layer-diag: vuelta {vueltas} — volvi; exit={} salir={}",
182 runner.exit,
183 salir.load(Ordering::SeqCst)
184 );
185 }
186 }
187 if diag {
188 eprintln!("layer-diag: SALI del bucle tras {vueltas} vueltas");
189 }
190 drop(runner);
191 if diag {
192 eprintln!("layer-diag: runner dropeado");
193 }
194 drop(event_queue);
195 if diag {
196 eprintln!("layer-diag: event_queue dropeada");
197 }
198 Ok(())
199}
200
201// ── Interno ─────────────────────────────────────────────────────────────────
202
203/// Tamaño por defecto de una caja de esquina cuando `cfg.size` es `None`.
204const DEFAULT_CORNER_SIZE: (u32, u32) = (360, 280);
205
206/// El anclaje resuelto para la superficie: anclas sctk, tamaño pedido, zona
207/// exclusiva y márgenes opcionales. Unifica los dos modos (barra/caja).
208struct Resolved {
209 anchor: LayerAnchor,
210 size: (u32, u32),
211 exclusive_zone: i32,
212 margins: Option<Margins>,
213}
214
215/// Resuelve el anclaje según el modo de `cfg`: caja de esquina si `corner` es
216/// `Some`, barra de borde en caso contrario.
217fn resolve_anchor(cfg: &LayerConfig) -> Resolved {
218 // Pantalla completa gana sobre los otros dos modos: los cuatro anclajes y
219 // tamaño 0 en ambos ejes = "estirame a la salida".
220 if cfg.fullscreen {
221 return Resolved {
222 anchor: LayerAnchor::TOP
223 | LayerAnchor::BOTTOM
224 | LayerAnchor::LEFT
225 | LayerAnchor::RIGHT,
226 size: (0, 0),
227 exclusive_zone: 0,
228 margins: None,
229 };
230 }
231 match cfg.corner {
232 Some(corner) => {
233 let size = cfg.size.unwrap_or(DEFAULT_CORNER_SIZE);
234 let anchor = match corner {
235 Corner::TopLeft => LayerAnchor::TOP | LayerAnchor::LEFT,
236 Corner::TopRight => LayerAnchor::TOP | LayerAnchor::RIGHT,
237 Corner::BottomLeft => LayerAnchor::BOTTOM | LayerAnchor::LEFT,
238 Corner::BottomRight => LayerAnchor::BOTTOM | LayerAnchor::RIGHT,
239 };
240 // Una caja flota siempre: no tiene sentido reservar zona exclusiva.
241 Resolved { anchor, size, exclusive_zone: 0, margins: Some(cfg.margins) }
242 }
243 None => {
244 let (anchor, size) = anchor_and_size(cfg.edge, cfg.thickness);
245 let exclusive_zone = if cfg.exclusive { cfg.thickness as i32 } else { 0 };
246 Resolved { anchor, size, exclusive_zone, margins: None }
247 }
248 }
249}
250
251/// El anclaje sctk + el tamaño `(w, h)` pedido para un borde y grosor. El eje
252/// paralelo al borde va en 0 → el compositor lo estira a la salida completa.
253fn anchor_and_size(edge: Edge, thickness: u32) -> (LayerAnchor, (u32, u32)) {
254 match edge {
255 Edge::Top => (
256 LayerAnchor::TOP | LayerAnchor::LEFT | LayerAnchor::RIGHT,
257 (0, thickness),
258 ),
259 Edge::Bottom => (
260 LayerAnchor::BOTTOM | LayerAnchor::LEFT | LayerAnchor::RIGHT,
261 (0, thickness),
262 ),
263 Edge::Left => (
264 LayerAnchor::LEFT | LayerAnchor::TOP | LayerAnchor::BOTTOM,
265 (thickness, 0),
266 ),
267 Edge::Right => (
268 LayerAnchor::RIGHT | LayerAnchor::TOP | LayerAnchor::BOTTOM,
269 (thickness, 0),
270 ),
271 }
272}
273
274impl From<LayerKind> for Layer {
275 fn from(k: LayerKind) -> Self {
276 match k {
277 LayerKind::Background => Layer::Background,
278 LayerKind::Bottom => Layer::Bottom,
279 LayerKind::Top => Layer::Top,
280 LayerKind::Overlay => Layer::Overlay,
281 }
282 }
283}
284
285impl From<Keyboard> for KeyboardInteractivity {
286 fn from(k: Keyboard) -> Self {
287 match k {
288 Keyboard::None => KeyboardInteractivity::None,
289 Keyboard::OnDemand => KeyboardInteractivity::OnDemand,
290 Keyboard::Exclusive => KeyboardInteractivity::Exclusive,
291 }
292 }
293}
294
295/// El estado wgpu de la superficie.
296struct PanelGpu {
297 surface: RawSurface,
298 renderer: Renderer,
299 typesetter: Typesetter,
300 scene: vello::Scene,
301 layout: LayoutTree,
302}
303
304/// El árbol pintado en el último frame, para hit-test entre frames.
305struct RenderCache<Msg> {
306 mounted: Mounted<Msg>,
307 computed: ComputedLayout,
308}
309
310/// El cliente Wayland que corre un `App` como una sola layer surface.
311struct Runner<A: App> {
312 registry_state: RegistryState,
313 output_state: OutputState,
314 seat_state: SeatState,
315 conn: Connection,
316 hal: Option<Hal>,
317 keyboard: Option<wl_keyboard::WlKeyboard>,
318 pointer: Option<wl_pointer::WlPointer>,
319 /// Puntero con tema de cursores: traduce el `Cursor` del nodo bajo el
320 /// puntero a una forma de cursor del sistema (I-beam sobre texto, mano sobre
321 /// links, etc). El backend winit ya lo hacía; el layer-shell no tenía NADA.
322 themed_pointer: Option<ThemedPointer>,
323 /// `wl_shm` retenido para el tema de cursores.
324 shm: Shm,
325 /// Superficie dedicada donde el compositor pinta el cursor (la usa el
326 /// `ThemedPointer`).
327 cursor_surface: Option<wl_surface::WlSurface>,
328 seat: Option<wl_seat::WlSeat>,
329 layer: LayerSurface,
330 /// Compositor + shell + config retenidos para **re-crear** la layer-surface
331 /// si el compositor nos manda `closed` (reset/quita del output) en vez de
332 /// morir. Ver [`Runner::recreate_surface`] y el handler `closed`.
333 compositor: CompositorState,
334 layer_shell: LayerShell,
335 cfg: LayerConfig,
336 width: u32,
337 height: u32,
338 gpu: Option<PanelGpu>,
339 cache: Option<RenderCache<A::Msg>>,
340 hover_idx: Option<usize>,
341 /// Registro de **animaciones implícitas** (`View::animated`,
342 /// `animated_enter/_exit/_inout/_switch`) retenido entre cuadros.
343 ///
344 /// Hasta el 2026-09-11 este camino —el de layer-shell, por donde pintan el
345 /// cajón de shuma, las barras, los rails y todos los flyouts de pata— hacía
346 /// `mount → compute → paint` **sin registro**: las animaciones implícitas
347 /// compilaban, se declaraban y eran **inertes**. Sólo corrían en el bucle
348 /// winit (`llimphi-ui`), o sea únicamente en las apps con ventana propia.
349 /// Eso se comió, entre otras, la paginación del lienzo de shuma al cambiar
350 /// de pestaña (`b145050a9`): andaba en la shuma con ventana y no en la del
351 /// cajón, que es la que se usa.
352 anim_registry: AnimRegistry,
353 /// Alguna animación implícita sigue viva: el próximo frame-callback tiene
354 /// que **repintar** aunque el modelo no haya cambiado. Es el equivalente del
355 /// `request_redraw` que el bucle winit dispara mientras `animating`; acá el
356 /// latido de frame-callbacks ya existe, así que alcanza con volver a marcar
357 /// sucio. Se autodetiene: cuando la última animación se asienta, el
358 /// `reconcile` devuelve `false` y volvemos a dormir.
359 anim_viva: bool,
360 /// Hay algo nuevo que pintar. Arranca en `true` (nunca se pintó nada) y lo
361 /// levantan `apply`, los cambios de hover/drag, `configure` y la re-creación
362 /// de la surface; lo baja `draw` al presentar. Ver [`Runner::draw`].
363 dirty: bool,
364 /// Ya hay un frame-callback pedido y sin responder. Evita encadenar uno por
365 /// cada `draw`: `configure` también llama a `draw`, así que sin esto cada
366 /// configure suma un latido paralelo y quedan N en vuelo — el commit-storm
367 /// que le clava un core al compositor. Es la misma guarda que pata tiene
368 /// por panel (`frame_pending`); `llimphi-layer` se extrajo de pata pero se
369 /// quedó sin ella. Se limpia al llegar el callback.
370 frame_pending: bool,
371 /// Arrastre en curso: handler del nodo + última posición del puntero.
372 drag: Option<(DragFn<A::Msg>, (f32, f32))>,
373 /// Último press izquierdo sobre un nodo con doble-tap: cuándo y dónde.
374 ///
375 /// Es la mitad que le faltaba a este bucle. El de ventana tiene su arena de
376 /// gestos desde hace rato; acá no había ninguna, así que `on_double_tap`
377 /// —puesto en una fila del árbol del IDE, por ejemplo— andaba en la app
378 /// suelta y **no hacía nada en el cajón**, sin error ni aviso.
379 ultimo_tap: Option<(std::time::Instant, (f32, f32))>,
380 /// Click **retenido** por un nodo con doble-tap exclusivo
381 /// (`View::double_tap_exclusive`): `(msg, origen, vence, soltado)`.
382 ///
383 /// Acá el click sale normalmente en el PRESS. Con el gesto arbitrado no
384 /// puede: hasta que no venza la ventana del doble no se sabe si el toque fue
385 /// uno o el primero de dos. Se guarda, y lo dispara `draw` al vencer (el
386 /// latido de frame-callbacks nos despierta igual) o lo mata el segundo tap.
387 /// Es lo que hace que un doble-click en un diente fije el espacio **sin**
388 /// desplegar y replegar su panel de paso.
389 clic_retenido: Option<(A::Msg, (f32, f32), std::time::Instant, bool)>,
390 /// Dónde está el puntero, en coordenadas de la superficie.
391 ///
392 /// Hace falta porque el evento de **rueda** de Wayland no trae posición: la
393 /// posición viaja en los eventos de movimiento, y el de rueda llega con lo
394 /// que haya quedado — se lo vio llegar con `y = 0`, así que el hit-test
395 /// caía fuera de la lista y la rueda no scrolleaba nada. Medido en metal el
396 /// 2026-08-25 con `LLIMPHI_LAYER_DIAG=1`.
397 ptr: (f32, f32),
398 mods: Modifiers,
399 model: Option<A::Model>,
400 handle: Handle<A::Msg>,
401 rx: Receiver<A::Msg>,
402 /// Cierres consecutivos sin lograr pintar un frame. Resetea al presentar; si
403 /// pasa el tope, nos rendimos (`exit`) para que el supervisor reinicie con
404 /// estado limpio (output realmente ausente, no un reset transitorio).
405 recreate_count: u32,
406 /// Nombre del conector al que está atada la superficie **ahora**. `None`
407 /// mientras la eligió el compositor y nadie pidió mudarse. Es contra esto
408 /// que se compara [`App::output_deseado`] por cuadro.
409 output_actual: Option<String>,
410 exit: bool,
411}
412
413impl<A: App> Runner<A> {
414 /// Aplica un `Msg` a la app (transición pura del modelo) y marca para
415 /// repintar invalidando la caché de hit-test.
416 fn apply(&mut self, msg: A::Msg) {
417 if let Some(model) = self.model.take() {
418 self.model = Some(A::update(model, msg, &self.handle));
419 }
420 self.cache = None;
421 self.dirty = true;
422 }
423
424 /// Traduce un evento de teclado sctk al `KeyEvent` de Llimphi.
425 fn keysym_to_keyevent(
426 &self,
427 event: &smithay_client_toolkit::seat::keyboard::KeyEvent,
428 ) -> Option<KeyEvent> {
429 use smithay_client_toolkit::seat::keyboard::Keysym as K;
430 let named = match event.keysym {
431 K::Return | K::KP_Enter => Some(NamedKey::Enter),
432 K::BackSpace => Some(NamedKey::Backspace),
433 K::Tab | K::ISO_Left_Tab => Some(NamedKey::Tab),
434 K::Escape => Some(NamedKey::Escape),
435 K::Up => Some(NamedKey::ArrowUp),
436 K::Down => Some(NamedKey::ArrowDown),
437 K::Right => Some(NamedKey::ArrowRight),
438 K::Left => Some(NamedKey::ArrowLeft),
439 K::Home => Some(NamedKey::Home),
440 K::End => Some(NamedKey::End),
441 K::Page_Up => Some(NamedKey::PageUp),
442 K::Page_Down => Some(NamedKey::PageDown),
443 K::Delete => Some(NamedKey::Delete),
444 K::Insert => Some(NamedKey::Insert),
445 K::F1 => Some(NamedKey::F1),
446 K::F2 => Some(NamedKey::F2),
447 K::F3 => Some(NamedKey::F3),
448 K::F4 => Some(NamedKey::F4),
449 K::F5 => Some(NamedKey::F5),
450 K::F6 => Some(NamedKey::F6),
451 K::F7 => Some(NamedKey::F7),
452 K::F8 => Some(NamedKey::F8),
453 K::F9 => Some(NamedKey::F9),
454 K::F10 => Some(NamedKey::F10),
455 K::F11 => Some(NamedKey::F11),
456 K::F12 => Some(NamedKey::F12),
457 _ => None,
458 };
459 let modifiers = LMods {
460 shift: self.mods.shift,
461 ctrl: self.mods.ctrl,
462 alt: self.mods.alt,
463 meta: self.mods.logo,
464 };
465 let (key, text) = if let Some(n) = named {
466 (Key::Named(n), None)
467 } else {
468 let txt = match event.utf8.as_deref() {
469 Some(s) if !s.is_empty() && !s.chars().all(char::is_control) => s.to_string(),
470 _ => event.keysym.key_char()?.to_string(),
471 };
472 (Key::Character(txt.as_str().into()), Some(txt))
473 };
474 Some(KeyEvent {
475 key,
476 state: KeyState::Pressed,
477 text,
478 modifiers,
479 repeat: false,
480 })
481 }
482
483 /// Crea el estado wgpu de la superficie (idempotente).
484 fn ensure_gpu(&mut self) {
485 if self.gpu.is_some() {
486 return;
487 }
488 let display_ptr = self.conn.backend().display_ptr() as *mut c_void;
489 let surface_ptr = self.layer.wl_surface().id().as_ptr() as *mut c_void;
490 let (w, h) = (self.width, self.height);
491 let display_handle = RawDisplayHandle::Wayland(WaylandDisplayHandle::new(
492 NonNull::new(display_ptr).expect("wl_display ptr"),
493 ));
494 let window_handle = RawWindowHandle::Wayland(WaylandWindowHandle::new(
495 NonNull::new(surface_ptr).expect("wl_surface ptr"),
496 ));
497 // SAFETY: los handles apuntan a objetos Wayland que `self` mantiene vivos.
498 let make_target = || wgpu::SurfaceTargetUnsafe::RawHandle {
499 // wgpu 29 volvió opcional el display del target.
500 raw_display_handle: Some(display_handle),
501 raw_window_handle: window_handle,
502 };
503
504 let surface = if self.hal.is_none() {
505 match pollster::block_on(unsafe { Hal::new_for_raw_surface(make_target, w, h) }) {
506 Ok((hal, surface)) => {
507 self.hal = Some(hal);
508 surface
509 }
510 Err(e) => {
511 eprintln!("llimphi-layer · sin gpu: {e}");
512 return;
513 }
514 }
515 } else {
516 let hal = self.hal.as_ref().expect("hal");
517 let wgpu_surface = match unsafe { hal.instance.create_surface_unsafe(make_target()) } {
518 Ok(s) => s,
519 Err(e) => {
520 eprintln!("llimphi-layer · sin gpu: {e}");
521 return;
522 }
523 };
524 match RawSurface::from_surface(hal, wgpu_surface, display_handle, window_handle, w, h) {
525 Ok(s) => s,
526 Err(e) => {
527 eprintln!("llimphi-layer · sin gpu: {e}");
528 return;
529 }
530 }
531 };
532 let hal = self.hal.as_ref().expect("hal");
533 let renderer = Renderer::new(hal).expect("renderer");
534 self.gpu = Some(PanelGpu {
535 surface,
536 renderer,
537 typesetter: Typesetter::new(),
538 scene: vello::Scene::new(),
539 layout: LayoutTree::new(),
540 });
541 }
542
543 /// Traduce el nombre de un conector (`"DP-1"`) al `wl_output` que el
544 /// compositor anunció. `None` si ese monitor no está (desenchufado, o un
545 /// nombre que no existe) — el caller cae a «que elija el compositor».
546 fn resolver_output(&self, nombre: &str) -> Option<wl_output::WlOutput> {
547 self.output_state.outputs().find(|o| {
548 self.output_state
549 .info(o)
550 .and_then(|i| i.name)
551 .is_some_and(|n| n == nombre)
552 })
553 }
554
555 /// ¿La app pidió mudarse de monitor? Compara [`App::output_deseado`] con la
556 /// salida a la que está atada la superficie y, si difieren, la re-crea allá.
557 ///
558 /// Devuelve `true` si mudó — el caller debe **abandonar el cuadro**: la
559 /// superficie nueva todavía no tiene `configure`, y el `configure` que
560 /// llegue vuelve a llamar a `draw` con la GPU rehecha.
561 ///
562 /// Se re-crea porque no hay alternativa: en wlr-layer-shell la salida se
563 /// elige en `get_layer_surface` y después es inmutable. Lo que se paga es
564 /// la superficie y su swapchain; el proceso, el modelo (mayúsculas, capa
565 /// activa) y el dispositivo `wgpu` sobreviven intactos.
566 fn seguir_output(&mut self, qh: &QueueHandle<Self>) -> bool {
567 let Some(model) = self.model.as_ref() else {
568 return false;
569 };
570 let Some(quiere) = A::output_deseado(model).filter(|n| !n.is_empty()) else {
571 return false;
572 };
573 if self.output_actual.as_deref() == Some(quiere.as_str()) {
574 return false;
575 }
576 if self.resolver_output(&quiere).is_none() {
577 // El monitor pedido no está anunciado (todavía, o ya no). Quedarse
578 // donde está es lo correcto; reintenta solo en el próximo cuadro.
579 return false;
580 }
581 self.cfg.output = Some(quiere);
582 self.recreate_surface(qh);
583 true
584 }
585
586 /// Re-crea la layer-surface tras un `closed` del compositor (reset/quita del
587 /// output: churn DRM, DPMS, hotplug). Suelta la surface GPU vieja —atada al
588 /// `wl_surface` ya muerto; el `hal`/device **sobrevive**— y crea una nueva
589 /// con la misma config. El próximo `configure` que mande el compositor
590 /// reconstruye la GPU vía [`ensure_gpu`](Self::ensure_gpu) y `draw` re-arma
591 /// el latido. Así la barra sobrevive a un reset de output sin morir.
592 fn recreate_surface(&mut self, qh: &QueueHandle<Self>) {
593 self.gpu = None;
594 self.cache = None;
595 self.dirty = true;
596 // La surface vieja murió: su callback no va a llegar nunca. Si no
597 // liberamos el slot, el latido queda trabado para siempre.
598 self.frame_pending = false;
599 let resolved = resolve_anchor(&self.cfg);
600 // La salida pedida, si el compositor la anuncia. `None` = que elija él
601 // (el camino de siempre, y el fallback si el conector no existe: mejor
602 // una barra en el monitor equivocado que ninguna barra).
603 let salida = self
604 .cfg
605 .output
606 .as_deref()
607 .and_then(|n| self.resolver_output(n));
608 self.output_actual = salida.as_ref().and(self.cfg.output.clone());
609 let wl_surface = self.compositor.create_surface(qh);
610 let layer = self.layer_shell.create_layer_surface(
611 qh,
612 wl_surface,
613 self.cfg.layer.into(),
614 Some(self.cfg.namespace.clone()),
615 salida.as_ref(),
616 );
617 layer.set_anchor(resolved.anchor);
618 layer.set_size(resolved.size.0, resolved.size.1);
619 layer.set_exclusive_zone(resolved.exclusive_zone);
620 if let Some(m) = resolved.margins {
621 layer.set_margin(m.top, m.right, m.bottom, m.left);
622 }
623 layer.set_keyboard_interactivity(self.cfg.keyboard.into());
624 layer.commit();
625 self.layer = layer; // dropea la LayerSurface cerrada
626 }
627
628 /// Mantiene vivo el latido: pide el siguiente frame-callback. El loop de
629 /// frames se auto-sostiene así (cada `frame` re-pide el próximo), lo que
630 /// permite drenar el canal de la app por frame sin un timer aparte.
631 fn latido(&mut self, qh: &QueueHandle<Self>) {
632 if self.frame_pending {
633 return; // ya hay uno pedido; encadenar otro es el commit-storm
634 }
635 self.frame_pending = true;
636 let surface = self.layer.wl_surface();
637 surface.frame(qh, surface.clone());
638 surface.commit();
639 }
640
641 /// Avanza un frame: drena el canal, construye la vista, layout, pinta y
642 /// presenta. Cachea el árbol para el hit-test del próximo evento.
643 ///
644 /// **Sale temprano si nada cambió.** El latido de frame-callbacks se
645 /// mantiene siempre (es lo que nos despierta para drenar el canal), pero
646 /// construir vista + layout + pintar + presentar sólo se paga cuando hay
647 /// algo nuevo: un `Msg` aplicado, un cambio de hover/drag, un `configure` o
648 /// una re-creación de la surface. Antes se re-renderizaba en CADA frame
649 /// aunque la pantalla fuera idéntica — medido en metal 2026-07-21,
650 /// `pata-notify` quemaba 2.9% de un core sin un solo toast en pantalla.
651 /// Es además el criterio que ya usa el backend de ventanas (`llimphi-ui`
652 /// duerme en `ControlFlow::Wait` y pide `request_redraw` a demanda).
653 ///
654 /// Las apps que animan ya empujan `Msg` por [`Handle::spawn_periodic`]
655 /// (pata: reloj 1 s, cava y dientes 100 ms), así que siguen andando igual.
656 /// Lo que NO se sostiene solo es una `view()` que dependa del reloj sin
657 /// emitir un `Msg`: eso hay que despertarlo con un tick, como corresponde.
658 fn draw(&mut self, qh: &QueueHandle<Self>) {
659 // Click retenido que ya venció con el botón arriba: era un click simple
660 // y recién ahora se sabe (ver `clic_retenido`). El latido de
661 // frame-callbacks es el que nos trae acá a tiempo.
662 if self
663 .clic_retenido
664 .as_ref()
665 .is_some_and(|(_, _, vence, soltado)| *soltado && std::time::Instant::now() >= *vence)
666 {
667 let (msg, _, _, _) = self.clic_retenido.take().expect("retenido");
668 self.apply(msg);
669 }
670 // Drenar los Msg que la app empujó al canal (ticks/async/follow-ups).
671 // `apply` marca sucio, así que esto decide el early-return de abajo.
672 while let Ok(m) = self.rx.try_recv() {
673 self.apply(m);
674 }
675 // ¿La app quiere estar en otro monitor? Se muda ANTES de pintar: la
676 // superficie que sobrevive a esto es otra, y pintar la vieja sería tirar
677 // el cuadro. El `configure` de la nueva vuelve a entrar acá.
678 if self.seguir_output(qh) {
679 return;
680 }
681 // Nada nuevo y ya hay un frame presentado: mantené el latido y volvé.
682 // `anim_viva` cuenta como «hay algo nuevo»: una animación implícita
683 // cambia lo que se ve sin que nadie mande un `Msg`, así que es el único
684 // caso en que hay que repintar con el modelo quieto.
685 if !self.dirty && !self.anim_viva && self.cache.is_some() {
686 self.latido(qh);
687 return;
688 }
689 self.ensure_gpu();
690
691 let (w, h) = (self.width, self.height);
692 let view = A::view(self.model.as_ref().expect("model"));
693 let hover_idx = self.hover_idx;
694 let hal = match self.hal.as_ref() {
695 Some(h) => h,
696 None => {
697 self.latido(qh);
698 return;
699 }
700 };
701 let gpu = match self.gpu.as_mut() {
702 Some(g) => g,
703 None => {
704 self.latido(qh);
705 return;
706 }
707 };
708 gpu.surface.resize(w, h);
709 let frame = match gpu.surface.acquire() {
710 Ok(f) => f,
711 Err(_) => {
712 self.latido(qh);
713 return;
714 }
715 };
716 gpu.layout.clear();
717 let mounted = mount(&mut gpu.layout, view);
718 let computed = {
719 let ts = &mut gpu.typesetter;
720 let tmap = &mounted.text_measures;
721 gpu.layout
722 .compute_with_measure(mounted.root, (w as f32, h as f32), |nid, known, avail| {
723 match tmap.get(&nid) {
724 Some(tm) => measure_text_node(ts, tm, known, avail),
725 None => taffy::Size::ZERO,
726 }
727 })
728 .expect("layout")
729 };
730 // Input-region recortada al contenido interactivo (opt-in): la caja fija
731 // del overlay (un toast de 352×420, p. ej.) se traga todos los clicks de
732 // la ventana debajo aunque esté vacía o transparente. Recortando la
733 // región a la unión de los rects de nodos con handler (o VACÍA si no hay
734 // ninguno), las zonas ociosas quedan click-through. Se aplica antes del
735 // `present` (que commitea la surface) para que entre en el mismo frame.
736 if self.cfg.input_from_content {
737 use smithay_client_toolkit::compositor::Region;
738 if let Ok(region) = Region::new(&self.compositor) {
739 for n in &mounted.nodes {
740 let interactivo = n.on_click.is_some()
741 || n.on_click_at.is_some()
742 || n.on_click_screen.is_some()
743 || n.drag.is_some()
744 || n.drag_at.is_some()
745 || n.drag_velocity.is_some()
746 || n.on_right_click.is_some()
747 || n.on_right_click_at.is_some()
748 || n.on_middle_click.is_some()
749 || n.hover_fill.is_some();
750 if !interactivo {
751 continue;
752 }
753 if let Some(r) = computed.get(n.id) {
754 let (x, y, w, h) =
755 (r.x as i32, r.y as i32, r.w.ceil() as i32, r.h.ceil() as i32);
756 if w > 0 && h > 0 {
757 region.add(x, y, w, h);
758 }
759 }
760 }
761 // Región (posiblemente vacía = todo click-through) → la surface.
762 self.layer
763 .wl_surface()
764 .set_input_region(Some(region.wl_region()));
765 }
766 }
767 // **Animaciones implícitas.** El mismo orden que el bucle winit
768 // (`llimphi-ui/src/eventloop/redraw.rs`): reconciliar DESPUÉS del layout
769 // y ANTES del paint —ahí se interpolan fill/radius/alpha/transform— y
770 // hornear los underlays (el contenido viejo de un `animated_switch` con
771 // `exit_under`, que el paint apendea debajo del nuevo en su posición z).
772 let ahora = AnimInstant::now();
773 let mut mounted = mounted;
774 let anim_activa = self.anim_registry.reconcile(&mut mounted, ahora);
775 self.anim_registry.prepare_underlays(&mut mounted, ahora);
776 gpu.scene.reset();
777 paint(&mut gpu.scene, &mounted, &computed, &mut gpu.typesetter, hover_idx, None);
778 // Salidas y cross-fades. 1) Guardar la subescena de cada nodo `exit`
779 // mientras todavía está vivo (cuando desaparezca ya no hay de dónde
780 // sacarla). 2) Reproducir los fantasmas de los que se fueron, encima del
781 // contenido. Cuesta cero si ningún nodo declara salida.
782 for (idx, end, key) in self.anim_registry.live_exit_nodes(&mounted, ahora) {
783 let a = mounted.nodes[idx].anim.expect("nodo exit lleva anim");
784 let centro = computed
785 .get(mounted.nodes[idx].id)
786 .map(|r| (r.x + r.w * 0.5, r.y + r.h * 0.5))
787 .unwrap_or((0.0, 0.0));
788 // El fantasma ANTERIOR no entra a la captura del contenido vivo: si
789 // entrara, el próximo fantasma arrastraría al previo.
790 let underlay_guardado = mounted.nodes[idx].underlay.take();
791 let mut sub = vello::Scene::new();
792 paint_range(
793 &mut sub,
794 &mounted,
795 &computed,
796 &mut gpu.typesetter,
797 None,
798 None,
799 idx,
800 end,
801 vello::kurbo::Affine::IDENTITY,
802 );
803 mounted.nodes[idx].underlay = underlay_guardado;
804 self.anim_registry.store_live_exit_en(key, sub, centro, &a, ahora);
805 }
806 let fantasmas = self
807 .anim_registry
808 .replay_ghosts(&mut gpu.scene, ahora, w as f32, h as f32);
809 self.anim_viva = anim_activa || fantasmas;
810 if let Err(e) = gpu.renderer.render(hal, &gpu.scene, &frame, palette::css::TRANSPARENT) {
811 eprintln!("llimphi-layer · render: {e}");
812 }
813 gpu.surface.present(frame, hal);
814
815 // Pintamos un frame con éxito: el ciclo de re-creación (si lo hubo) se
816 // recuperó del todo. Resetea el backoff de `closed`.
817 self.recreate_count = 0;
818 self.cache = Some(RenderCache { mounted, computed });
819 // Frame presentado: hasta que algo vuelva a cambiar, los próximos
820 // callbacks sólo re-arman el latido.
821 self.dirty = false;
822 self.latido(qh);
823 }
824}
825
826// ── Handlers sctk ───────────────────────────────────────────────────────────
827
828impl<A: App> CompositorHandler for Runner<A> {
829 fn scale_factor_changed(
830 &mut self,
831 _: &Connection,
832 _: &QueueHandle<Self>,
833 _: &wl_surface::WlSurface,
834 _: i32,
835 ) {
836 }
837 fn transform_changed(
838 &mut self,
839 _: &Connection,
840 _: &QueueHandle<Self>,
841 _: &wl_surface::WlSurface,
842 _: wl_output::Transform,
843 ) {
844 }
845 fn frame(
846 &mut self,
847 _: &Connection,
848 qh: &QueueHandle<Self>,
849 _: &wl_surface::WlSurface,
850 _: u32,
851 ) {
852 // Llegó el callback que pidió `latido`: liberamos el slot para que el
853 // próximo `latido` pueda pedir el siguiente.
854 self.frame_pending = false;
855 self.draw(qh);
856 }
857 fn surface_enter(
858 &mut self,
859 _: &Connection,
860 _: &QueueHandle<Self>,
861 _: &wl_surface::WlSurface,
862 _: &wl_output::WlOutput,
863 ) {
864 }
865 fn surface_leave(
866 &mut self,
867 _: &Connection,
868 _: &QueueHandle<Self>,
869 _: &wl_surface::WlSurface,
870 _: &wl_output::WlOutput,
871 ) {
872 }
873}
874
875impl<A: App> LayerShellHandler for Runner<A> {
876 fn closed(&mut self, _: &Connection, qh: &QueueHandle<Self>, _: &LayerSurface) {
877 // El compositor cerró nuestra superficie — típicamente al resetear o
878 // quitar el output al que estábamos anclados (churn DRM, DPMS, hotplug).
879 // Antes esto mataba el proceso (`exit = true`) y la barra quedaba muerta
880 // hasta un respawn externo. Ahora re-creamos la layer-surface sobre el
881 // output vigente y seguimos vivos. Con tope: si nos cierran repetido sin
882 // lograr pintar (output realmente ausente, no un reset transitorio), nos
883 // rendimos y salimos para que el supervisor reinicie con estado limpio.
884 const MAX_RECREATES: u32 = 16;
885 self.recreate_count += 1;
886 if self.recreate_count > MAX_RECREATES {
887 eprintln!(
888 "llimphi-layer · cerrados {} veces sin recuperar; salgo (que reinicie el supervisor).",
889 self.recreate_count
890 );
891 self.exit = true;
892 return;
893 }
894 eprintln!(
895 "llimphi-layer · el compositor cerró la superficie; re-creo (intento {}).",
896 self.recreate_count
897 );
898 self.recreate_surface(qh);
899 }
900 fn configure(
901 &mut self,
902 _: &Connection,
903 qh: &QueueHandle<Self>,
904 _: &LayerSurface,
905 configure: LayerSurfaceConfigure,
906 _: u32,
907 ) {
908 let (cw, ch) = configure.new_size;
909 const MAX_DIM: u32 = 16384;
910 if (1..=MAX_DIM).contains(&cw) {
911 self.width = cw;
912 }
913 if (1..=MAX_DIM).contains(&ch) {
914 self.height = ch;
915 }
916 // Un configure siempre exige repintar: cambió el tamaño, o el compositor
917 // nos re-mapeó y el frame anterior ya no vale.
918 self.dirty = true;
919 // La app puede necesitar su tamaño (p. ej. para calcular el viewport de
920 // un scroll). El compositor estira el eje paralelo al borde, así que es
921 // aquí —no en `run`— donde se conoce el alto/ancho real.
922 let resize_msg = self
923 .model
924 .as_ref()
925 .and_then(|m| A::on_resize(m, self.width, self.height));
926 if let Some(msg) = resize_msg {
927 self.apply(msg);
928 }
929 self.draw(qh);
930 }
931}
932
933impl<A: App> OutputHandler for Runner<A> {
934 fn output_state(&mut self) -> &mut OutputState {
935 &mut self.output_state
936 }
937 fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
938 fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
939 fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
940}
941
942impl<A: App> SeatHandler for Runner<A> {
943 fn seat_state(&mut self) -> &mut SeatState {
944 &mut self.seat_state
945 }
946 fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
947 if self.seat.is_none() {
948 self.seat = Some(seat);
949 }
950 }
951 fn new_capability(
952 &mut self,
953 _: &Connection,
954 qh: &QueueHandle<Self>,
955 seat: wl_seat::WlSeat,
956 capability: Capability,
957 ) {
958 match capability {
959 Capability::Keyboard if self.keyboard.is_none() => {
960 if let Ok(kbd) = self.seat_state.get_keyboard(qh, &seat, None) {
961 self.keyboard = Some(kbd);
962 }
963 }
964 Capability::Pointer if self.pointer.is_none() => {
965 // Puntero CON tema de cursores: una superficie dedicada para el
966 // cursor + el tema del sistema. Si algo falla (raro), caemos al
967 // puntero pelado (sin cambio de forma, como estaba antes).
968 let surface = self.compositor.create_surface(qh);
969 match self.seat_state.get_pointer_with_theme(
970 qh,
971 &seat,
972 self.shm.wl_shm(),
973 surface.clone(),
974 ThemeSpec::default(),
975 ) {
976 Ok(themed) => {
977 self.pointer = Some(themed.pointer().clone());
978 self.cursor_surface = Some(surface);
979 self.themed_pointer = Some(themed);
980 }
981 Err(_) => {
982 if let Ok(ptr) = self.seat_state.get_pointer(qh, &seat) {
983 self.pointer = Some(ptr);
984 }
985 }
986 }
987 }
988 _ => {}
989 }
990 }
991 fn remove_capability(
992 &mut self,
993 _: &Connection,
994 _: &QueueHandle<Self>,
995 _: wl_seat::WlSeat,
996 capability: Capability,
997 ) {
998 match capability {
999 Capability::Keyboard => {
1000 if let Some(k) = self.keyboard.take() {
1001 k.release();
1002 }
1003 }
1004 Capability::Pointer => {
1005 self.themed_pointer = None;
1006 self.cursor_surface = None;
1007 if let Some(p) = self.pointer.take() {
1008 p.release();
1009 }
1010 }
1011 _ => {}
1012 }
1013 }
1014 fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
1015}
1016
1017impl<A: App> KeyboardHandler for Runner<A> {
1018 fn enter(
1019 &mut self,
1020 _: &Connection,
1021 _: &QueueHandle<Self>,
1022 _: &wl_keyboard::WlKeyboard,
1023 _: &wl_surface::WlSurface,
1024 _: u32,
1025 _: &[u32],
1026 _: &[Keysym],
1027 ) {
1028 }
1029 fn leave(
1030 &mut self,
1031 _: &Connection,
1032 _: &QueueHandle<Self>,
1033 _: &wl_keyboard::WlKeyboard,
1034 _: &wl_surface::WlSurface,
1035 _: u32,
1036 ) {
1037 }
1038 fn press_key(
1039 &mut self,
1040 _: &Connection,
1041 _: &QueueHandle<Self>,
1042 _: &wl_keyboard::WlKeyboard,
1043 _: u32,
1044 event: smithay_client_toolkit::seat::keyboard::KeyEvent,
1045 ) {
1046 if let Some(ke) = self.keysym_to_keyevent(&event) {
1047 let msg = A::on_key(self.model.as_ref().expect("model"), &ke);
1048 if let Some(msg) = msg {
1049 self.apply(msg);
1050 }
1051 }
1052 }
1053 fn release_key(
1054 &mut self,
1055 _: &Connection,
1056 _: &QueueHandle<Self>,
1057 _: &wl_keyboard::WlKeyboard,
1058 _: u32,
1059 _: smithay_client_toolkit::seat::keyboard::KeyEvent,
1060 ) {
1061 }
1062 fn update_modifiers(
1063 &mut self,
1064 _: &Connection,
1065 _: &QueueHandle<Self>,
1066 _: &wl_keyboard::WlKeyboard,
1067 _: u32,
1068 modifiers: Modifiers,
1069 _: u32,
1070 ) {
1071 self.mods = modifiers;
1072 }
1073}
1074
1075/// Traduce el [`Cursor`](llimphi_ui::llimphi_compositor::Cursor) del nodo bajo
1076/// el puntero a la forma de cursor del sistema. Espeja `to_winit_cursor` del
1077/// backend winit — la misma cobertura de variantes. `None` → flecha default.
1078fn cursor_to_icon(c: Option<llimphi_ui::llimphi_compositor::Cursor>) -> CursorIcon {
1079 use llimphi_ui::llimphi_compositor::Cursor as C;
1080 match c {
1081 None | Some(C::Default) => CursorIcon::Default,
1082 Some(C::Pointer) => CursorIcon::Pointer,
1083 Some(C::Text) => CursorIcon::Text,
1084 Some(C::Crosshair) => CursorIcon::Crosshair,
1085 Some(C::Move) => CursorIcon::Move,
1086 Some(C::Grab) => CursorIcon::Grab,
1087 Some(C::Grabbing) => CursorIcon::Grabbing,
1088 Some(C::NotAllowed) => CursorIcon::NotAllowed,
1089 Some(C::Copy) => CursorIcon::Copy,
1090 Some(C::NoDrop) => CursorIcon::NoDrop,
1091 Some(C::Wait) => CursorIcon::Wait,
1092 Some(C::Progress) => CursorIcon::Progress,
1093 Some(C::Help) => CursorIcon::Help,
1094 Some(C::ColResize) => CursorIcon::ColResize,
1095 Some(C::RowResize) => CursorIcon::RowResize,
1096 Some(C::EwResize) => CursorIcon::EwResize,
1097 Some(C::NsResize) => CursorIcon::NsResize,
1098 Some(C::NeswResize) => CursorIcon::NeswResize,
1099 Some(C::NwseResize) => CursorIcon::NwseResize,
1100 Some(C::ZoomIn) => CursorIcon::ZoomIn,
1101 Some(C::ZoomOut) => CursorIcon::ZoomOut,
1102 }
1103}
1104
1105impl<A: App> Runner<A> {
1106 /// Aplica al puntero la forma de cursor del nodo `hover_idx` actual. No-op si
1107 /// no hay `ThemedPointer` (el compositor no dio la capacidad temada).
1108 fn aplicar_cursor(&mut self, conn: &Connection) {
1109 let Some(themed) = self.themed_pointer.as_ref() else { return };
1110 let cur = self
1111 .hover_idx
1112 .and_then(|i| self.cache.as_ref()?.mounted.nodes.get(i))
1113 .and_then(|n| n.cursor);
1114 let _ = themed.set_cursor(conn, cursor_to_icon(cur));
1115 }
1116}
1117
1118impl<A: App> PointerHandler for Runner<A> {
1119 fn pointer_frame(
1120 &mut self,
1121 conn: &Connection,
1122 _: &QueueHandle<Self>,
1123 _: &wl_pointer::WlPointer,
1124 events: &[PointerEvent],
1125 ) {
1126 for e in events {
1127 match e.kind {
1128 PointerEventKind::Enter { .. } => {
1129 // Al entrar, fijar el cursor según el nodo bajo el puntero
1130 // (con el serial de Enter que el ThemedPointer ya rastrea).
1131 let (px, py) = (e.position.0 as f32, e.position.1 as f32);
1132 let nuevo = self
1133 .cache
1134 .as_ref()
1135 .and_then(|c| hit_test_hover(&c.mounted, &c.computed, px, py));
1136 self.ptr = (px, py);
1137 if self.hover_idx != nuevo {
1138 self.hover_idx = nuevo;
1139 self.dirty = true; // el hover se PINTA (hover_fill)
1140 }
1141 self.aplicar_cursor(conn);
1142 continue;
1143 }
1144 PointerEventKind::Motion { .. } => {
1145 let (px, py) = (e.position.0 as f32, e.position.1 as f32);
1146 self.ptr = (px, py);
1147 // Alejarse del origen mata el click retenido: fue arrastre.
1148 if let Some((_, (ox, oy), _, _)) = self.clic_retenido.as_ref() {
1149 if ((px - ox).powi(2) + (py - oy).powi(2)).sqrt() as f64
1150 > llimphi_ui::CLICK_MOVE_CANCEL
1151 {
1152 self.clic_retenido = None;
1153 }
1154 }
1155 // Drag en curso: el delta va al handler del nodo.
1156 if let Some((handler, last)) = self.drag.as_ref().map(|(h, l)| (h.clone(), *l)) {
1157 if let Some(d) = self.drag.as_mut() {
1158 d.1 = (px, py);
1159 }
1160 if let Some(msg) = (handler)(DragPhase::Move, px - last.0, py - last.1) {
1161 self.apply(msg);
1162 }
1163 continue;
1164 }
1165 let nuevo = self
1166 .cache
1167 .as_ref()
1168 .and_then(|c| hit_test_hover(&c.mounted, &c.computed, px, py));
1169 if self.hover_idx != nuevo {
1170 self.hover_idx = nuevo;
1171 self.dirty = true; // el hover se PINTA (hover_fill)
1172 // Cambió el nodo bajo el puntero → actualizar la forma.
1173 self.aplicar_cursor(conn);
1174 }
1175 continue;
1176 }
1177 PointerEventKind::Leave { .. } => {
1178 if self.hover_idx.is_some() {
1179 self.hover_idx = None;
1180 self.dirty = true; // se va el resaltado
1181 }
1182 continue;
1183 }
1184 _ => {}
1185 }
1186 if let PointerEventKind::Axis { vertical, .. } = e.kind {
1187 // Convención de Llimphi: el delta va en **líneas**. Wayland da
1188 // los clicks de rueda en `discrete` y el desplazamiento suave
1189 // (touchpad) en `absolute`, en px — la misma división por 20
1190 // que hace el runner de ventana con `PixelDelta`.
1191 //
1192 // Esa misma división dice la CLASE de dispositivo, que es lo
1193 // que decide el sentido: `discrete` sólo lo manda una rueda
1194 // física; un pad entrega nada más el continuo.
1195 let (dy, fuente) = if vertical.discrete != 0 {
1196 (vertical.discrete as f32, llimphi_ui::FuenteRueda::Raton)
1197 } else {
1198 (
1199 vertical.absolute as f32 / 20.0,
1200 llimphi_ui::FuenteRueda::Touchpad,
1201 )
1202 };
1203 let dy = llimphi_ui::politica_rueda()
1204 .aplicar(llimphi_ui::WheelDelta { x: 0.0, y: dy }, fuente)
1205 .y;
1206 if dy != 0.0 {
1207 // La posición sale de `self.ptr`, no del evento: el evento
1208 // de rueda de Wayland no trae posición (ver el campo).
1209 let (px, py) = self.ptr;
1210 // La **cadena** de nodos scrollables bajo el puntero, no el
1211 // primero: un área de scroll consume hasta su tope y deja
1212 // el sobrante al ancestro, y eso puede dar varios Msg por
1213 // evento. Es lo mismo que hace el runner de ventana.
1214 //
1215 // Esto estuvo roto desde siempre en el runner de capa:
1216 // miraba el `on_scroll` viejo, y el widget de scroll publica
1217 // la rueda por `on_scroll_take` (el reparto parcial). O sea
1218 // que NINGUNA app de capa scrolleaba con la rueda. Cazado en
1219 // metal el 2026-08-25 con `LLIMPHI_LAYER_DIAG=1`, que mostró
1220 // el hit-test funcionando (`hover=Some(36)`) y la lista de
1221 // nodos con rueda **vacía**.
1222 let msgs: Vec<A::Msg> = match self.cache.as_ref() {
1223 Some(c) => {
1224 let chain = hit_test_scroll_chain(&c.mounted, &c.computed, px, py);
1225 consume_scroll_chain(&c.mounted, &chain, 0.0, dy)
1226 }
1227 None => Vec::new(),
1228 };
1229 if std::env::var("LLIMPHI_LAYER_DIAG").is_ok() {
1230 eprintln!(
1231 "layer-diag: rueda dy={dy} en ({px},{py}) msgs={}",
1232 msgs.len()
1233 );
1234 }
1235 for msg in msgs {
1236 self.apply(msg);
1237 }
1238 }
1239 continue;
1240 }
1241 if let PointerEventKind::Release { button, .. } = e.kind {
1242 if button == BTN_LEFT {
1243 if let Some((handler, _)) = self.drag.take() {
1244 if let Some(msg) = (handler)(DragPhase::End, 0.0, 0.0) {
1245 self.apply(msg);
1246 }
1247 }
1248 // Click retenido: soltar no lo dispara —el segundo tap
1249 // todavía puede llegar—, sólo lo marca. Sale al vencer.
1250 if let Some((msg, origen, vence, _)) = self.clic_retenido.take() {
1251 if std::time::Instant::now() >= vence {
1252 self.apply(msg);
1253 } else {
1254 self.clic_retenido = Some((msg, origen, vence, true));
1255 }
1256 }
1257 }
1258 continue;
1259 }
1260 if let PointerEventKind::Press { button, .. } = e.kind {
1261 if button != BTN_LEFT && button != BTN_RIGHT {
1262 continue;
1263 }
1264 let (px, py) = (e.position.0 as f32, e.position.1 as f32);
1265 let derecho = button == BTN_RIGHT;
1266 // ── Arena de gestos: doble-tap ──────────────────────────────
1267 // Aditiva, igual que en el bucle de ventana: **no** consume el
1268 // click de abajo. Un doble-tap es dos clicks, y quien puso los
1269 // dos gestos en el mismo nodo (elegir con uno, abrir con dos)
1270 // cuenta con que el primero también haya pasado.
1271 // Con `double_tap_exclusive` el gesto además ARBITRA contra el
1272 // click del mismo nodo: el primer tap lo deja retenido y el
1273 // segundo lo descarta (ver `clic_retenido`).
1274 let mut retener = None;
1275 let mut suprimir_click = false;
1276 if !derecho {
1277 if let Some((msg, exclusivo)) = self.cache.as_ref().and_then(|c| {
1278 let i = llimphi_ui::llimphi_compositor::hit_test_double_tap(
1279 &c.mounted,
1280 &c.computed,
1281 px,
1282 py,
1283 )?;
1284 let n = c.mounted.nodes.get(i)?;
1285 Some((n.on_double_tap.clone()?, n.double_tap_exclusive.is_some()))
1286 }) {
1287 let ahora = std::time::Instant::now();
1288 let califica = self.ultimo_tap.is_some_and(|(t, (x, y))| {
1289 llimphi_ui::doble_tap_califica(
1290 ahora.duration_since(t),
1291 (x - px) as f64,
1292 (y - py) as f64,
1293 )
1294 });
1295 if califica {
1296 // Consumido: un tercer tap no re-dispara.
1297 self.ultimo_tap = None;
1298 if exclusivo {
1299 self.clic_retenido = None;
1300 suprimir_click = true;
1301 }
1302 self.apply(msg);
1303 } else {
1304 self.ultimo_tap = Some((ahora, (px, py)));
1305 if exclusivo {
1306 retener = Some(ahora + llimphi_ui::DOUBLE_TAP_WINDOW);
1307 }
1308 }
1309 }
1310 }
1311 // Nodo arrastrable bajo el press izquierdo: arranca un drag.
1312 if !derecho {
1313 let handler = self.cache.as_ref().and_then(|c| {
1314 let i = hit_test_click(&c.mounted, &c.computed, px, py)?;
1315 c.mounted.nodes.get(i)?.drag.clone()
1316 });
1317 if let Some(handler) = handler {
1318 self.drag = Some((handler, (px, py)));
1319 continue;
1320 }
1321 }
1322 let msg = self.cache.as_ref().and_then(|c| {
1323 let i = hit_test_click(&c.mounted, &c.computed, px, py)?;
1324 let n = c.mounted.nodes.get(i)?;
1325 let r = c.computed.get(n.id)?;
1326 mensaje_de_click(n, r, px, py, derecho)
1327 });
1328 if let Some(msg) = msg.filter(|_| !suprimir_click) {
1329 match retener {
1330 Some(vence) => self.clic_retenido = Some((msg, (px, py), vence, false)),
1331 None => self.apply(msg),
1332 }
1333 }
1334 }
1335 }
1336 }
1337}
1338
1339/// **Qué mensaje produce un click sobre `n`**, con `r` su rect ya calculado y
1340/// `(px, py)` el puntero en coordenadas de la surface.
1341///
1342/// Vive como función y no adentro del handler porque **acá se perdía un click**:
1343/// la rama del botón izquierdo no consultaba `on_click_screen`, así que un nodo
1344/// que sólo lo declara —el `⋯` de las opciones de shuma, el disparador del
1345/// `select`, el swatch del color-picker— caía al `on_click_at` que no tiene y el
1346/// click se iba al vacío. Andaba en winit (que sí lo despacha) y no bajo
1347/// layer-shell, o sea: andaba en la ventana suelta y no en el cajón, que es
1348/// donde se usa.
1349///
1350/// **El orden es el mismo para los dos botones**, y ésa es la invariante que el
1351/// test fija: primero el de coordenadas absolutas (lo necesitan los menús que se
1352/// anclan al puntero), después el plano, después el local. Dos órdenes distintos
1353/// para el mismo gesto es exactamente cómo se coló el agujero.
1354pub(crate) fn mensaje_de_click<Msg: Clone>(
1355 n: &llimphi_ui::llimphi_compositor::MountedNode<Msg>,
1356 r: llimphi_ui::llimphi_layout::Rect,
1357 px: f32,
1358 py: f32,
1359 derecho: bool,
1360) -> Option<Msg> {
1361 if derecho {
1362 if let Some(screen) = n.on_right_click_screen.as_ref() {
1363 if let Some(m) = screen(px, py, r.w, r.h) {
1364 return Some(m);
1365 }
1366 }
1367 if let Some(m) = n.on_right_click.clone() {
1368 return Some(m);
1369 }
1370 let at = n.on_right_click_at.as_ref()?;
1371 at(px - r.x, py - r.y, r.w, r.h)
1372 } else {
1373 if let Some(screen) = n.on_click_screen.as_ref() {
1374 if let Some(m) = screen(px, py, r.w, r.h) {
1375 return Some(m);
1376 }
1377 }
1378 if let Some(m) = n.on_click.clone() {
1379 return Some(m);
1380 }
1381 let at = n.on_click_at.as_ref()?;
1382 at(px - r.x, py - r.y, r.w, r.h)
1383 }
1384}
1385
1386#[cfg(test)]
1387mod tests_click {
1388 use super::mensaje_de_click;
1389 use llimphi_ui::llimphi_compositor::mount;
1390 use llimphi_ui::llimphi_layout::taffy::prelude::{length, Size, Style};
1391 use llimphi_ui::llimphi_layout::LayoutTree;
1392 use llimphi_ui::View;
1393
1394 /// Monta `v` y devuelve el mensaje que daría un click en el centro del nodo
1395 /// que tenga handlers — el mismo camino que corre el runner.
1396 fn click(v: View<&'static str>, derecho: bool) -> Option<&'static str> {
1397 let mut layout = LayoutTree::new();
1398 let mounted = mount(&mut layout, v);
1399 let computed = layout.compute(mounted.root, (200.0, 100.0)).expect("layout");
1400 let n = mounted
1401 .nodes
1402 .iter()
1403 .find(|n| {
1404 n.on_click.is_some()
1405 || n.on_click_at.is_some()
1406 || n.on_click_screen.is_some()
1407 || n.on_right_click.is_some()
1408 || n.on_right_click_at.is_some()
1409 || n.on_right_click_screen.is_some()
1410 })
1411 .expect("el árbol de prueba tiene que tener un nodo con handler");
1412 let r = computed.get(n.id).expect("rect del nodo");
1413 mensaje_de_click(n, r, r.x + r.w / 2.0, r.y + r.h / 2.0, derecho)
1414 }
1415
1416 fn caja() -> Style {
1417 Style {
1418 size: Size { width: length(40.0_f32), height: length(20.0_f32) },
1419 ..Default::default()
1420 }
1421 }
1422
1423 /// **El caso que se rompió en metal:** un nodo que declara SÓLO
1424 /// `on_click_screen` tiene que disparar con el botón izquierdo. Antes caía al
1425 /// `on_click_at` que no tiene y el click se perdía sin decir nada.
1426 #[test]
1427 fn un_nodo_con_solo_on_click_screen_dispara() {
1428 let v = View::<&'static str>::new(caja()).on_click_screen(|_x, _y, _w, _h| Some("abrí"));
1429 assert_eq!(click(v, false), Some("abrí"));
1430 }
1431
1432 /// **Y el orden es el mismo para los dos botones.** Es la invariante que
1433 /// dejó pasar el agujero: la rama derecha consultaba el de coordenadas
1434 /// absolutas primero y la izquierda no lo consultaba nunca.
1435 #[test]
1436 fn los_dos_botones_resuelven_en_el_mismo_orden() {
1437 let izq = View::<&'static str>::new(caja())
1438 .on_click("plano")
1439 .on_click_screen(|_x, _y, _w, _h| Some("absoluto"));
1440 assert_eq!(click(izq, false), Some("absoluto"), "el absoluto gana al plano");
1441
1442 let der = View::<&'static str>::new(caja())
1443 .on_right_click("plano")
1444 .on_right_click_screen(|_x, _y, _w, _h| Some("absoluto"));
1445 assert_eq!(click(der, true), Some("absoluto"));
1446 }
1447
1448 /// Y lo que ya andaba sigue andando: sin el absoluto, el plano; sin ninguno
1449 /// de los dos, el local — y sin handlers, nada.
1450 #[test]
1451 fn la_cadena_de_respaldo_no_cambio() {
1452 let plano = View::<&'static str>::new(caja()).on_click("plano");
1453 assert_eq!(click(plano, false), Some("plano"));
1454
1455 let local = View::<&'static str>::new(caja()).on_click_at(|_x, _y, _w, _h| Some("local"));
1456 assert_eq!(click(local, false), Some("local"));
1457
1458 // Un handler que devuelve `None` no corta la cadena: cae al siguiente.
1459 let vacio = View::<&'static str>::new(caja())
1460 .on_click_screen(|_x, _y, _w, _h| None)
1461 .on_click("plano");
1462 assert_eq!(click(vacio, false), Some("plano"));
1463 }
1464}
1465
1466impl<A: App> ProvidesRegistryState for Runner<A> {
1467 fn registry(&mut self) -> &mut RegistryState {
1468 &mut self.registry_state
1469 }
1470 registry_handlers![OutputState, SeatState];
1471}
1472
1473delegate_compositor!(@<A: App> Runner<A>);
1474delegate_output!(@<A: App> Runner<A>);
1475delegate_layer!(@<A: App> Runner<A>);
1476delegate_seat!(@<A: App> Runner<A>);
1477delegate_keyboard!(@<A: App> Runner<A>);
1478delegate_pointer!(@<A: App> Runner<A>);
1479delegate_shm!(@<A: App> Runner<A>);
1480delegate_registry!(@<A: App> Runner<A>);
1481
1482impl<A: App> ShmHandler for Runner<A> {
1483 fn shm_state(&mut self) -> &mut Shm {
1484 &mut self.shm
1485 }
1486}
1487