tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! `llimphi-layer` — corre un [`App`] de Llimphi como una **superficie
2//! wlr-layer-shell** (una barra anclada a un borde), no como ventana. Hace que
3//! una app se siente al nivel de eww/waybar en cualquier compositor wlroots
4//! (mirada, Hyprland, Sway, river…): se ancla a un borde, declara una *exclusive
5//! zone* opcional —el compositor le reserva la franja y tesela el resto— y se
6//! pinta reusando el pipeline de Llimphi (`mount → compute → paint → render`).
7//!
8//! Es la plumbing `smithay-client-toolkit` + `wgpu` que `pata` probó en
9//! producción, extraída a un runner **genérico** sobre el trait `App`. A
10//! diferencia de pata (N superficies + tray/dock/sidebar/…), este corre **una**
11//! superficie y nada de dominio: la app provee `view`/`update`/`on_key` y este
12//! crate le presta el borde. Útil, p. ej., para que `shuma` se dockee como barra.
13//!
14//! ```ignore
15//! struct MiBarra;
16//! impl llimphi_ui::App for MiBarra { /* … */ }
17//! llimphi_layer::run::<MiBarra>(llimphi_layer::LayerConfig {
18//! edge: llimphi_layer::Edge::Bottom,
19//! thickness: 40,
20//! keyboard: llimphi_layer::Keyboard::OnDemand,
21//! ..Default::default()
22//! })?;
23//! ```
24
25use std::error::Error;
26use std::ffi::c_void;
27use std::ptr::NonNull;
28use std::sync::mpsc::{channel, Receiver};
29use std::sync::Mutex;
30
31use raw_window_handle::{
32 RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
33};
34use smithay_client_toolkit::{
35 compositor::{CompositorHandler, CompositorState},
36 delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer,
37 delegate_registry, delegate_seat, delegate_shm,
38 output::{OutputHandler, OutputState},
39 registry::{ProvidesRegistryState, RegistryState},
40 registry_handlers,
41 seat::{
42 keyboard::{KeyboardHandler, Keysym, Modifiers},
43 pointer::{
44 CursorIcon, PointerEvent, PointerEventKind, PointerHandler, ThemeSpec, ThemedPointer,
45 BTN_LEFT, BTN_RIGHT,
46 },
47 Capability, SeatHandler, SeatState,
48 },
49 shm::{Shm, ShmHandler},
50 shell::{
51 wlr_layer::{
52 Anchor as LayerAnchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler,
53 LayerSurface, LayerSurfaceConfigure,
54 },
55 WaylandSurface,
56 },
57};
58use wayland_client::{
59 globals::registry_queue_init,
60 protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
61 Connection, Proxy, QueueHandle,
62};
63
64use llimphi_ui::llimphi_compositor::{
65 hit_test_click, hit_test_hover, hit_test_scroll, measure_text_node, mount, paint, DragFn,
66 DragPhase, Mounted,
67};
68use llimphi_ui::llimphi_hal::{wgpu, Hal, RawSurface, Surface as _};
69use llimphi_ui::llimphi_layout::{taffy, ComputedLayout, LayoutTree};
70use llimphi_ui::llimphi_raster::{peniko::color::palette, vello, Renderer};
71use llimphi_ui::llimphi_text::Typesetter;
72use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, Modifiers as LMods, NamedKey};
73
74// ── API pública ─────────────────────────────────────────────────────────────
75
76/// El borde al que se ancla la barra.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum Edge {
79 Top,
80 Bottom,
81 Left,
82 Right,
83}
84
85/// La esquina a la que se ancla una **caja** de tamaño fijo (no una barra de
86/// borde completo). La usa, p. ej., un daemon de notificaciones: una caja en
87/// la esquina, no una franja que cruza la pantalla.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum Corner {
90 TopLeft,
91 TopRight,
92 BottomLeft,
93 BottomRight,
94}
95
96/// Márgenes (px) respecto de los bordes a los que la superficie se ancla.
97/// El compositor los respeta como separación entre la caja y el borde.
98#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
99pub struct Margins {
100 pub top: i32,
101 pub right: i32,
102 pub bottom: i32,
103 pub left: i32,
104}
105
106impl Margins {
107 /// El mismo margen en los cuatro lados.
108 pub fn all(px: i32) -> Self {
109 Self { top: px, right: px, bottom: px, left: px }
110 }
111}
112
113/// La capa de composición (orden en Z respecto de las ventanas).
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum LayerKind {
116 /// Detrás de las ventanas (fondo de escritorio).
117 Background,
118 /// Bajo las ventanas normales.
119 Bottom,
120 /// Sobre las ventanas normales (lo típico para una barra).
121 Top,
122 /// Por encima de todo (popups, lock screens).
123 Overlay,
124}
125
126/// Cuánto teclado acepta la superficie.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum Keyboard {
129 /// No recibe teclado.
130 None,
131 /// Lo recibe al ser clickeada/enfocada (lo razonable para una barra que a
132 /// veces toma texto, p. ej. un input).
133 OnDemand,
134 /// Acapara el teclado mientras exista (drawers/popups modales).
135 Exclusive,
136}
137
138/// Configuración de la superficie de capa.
139///
140/// Dos modos de anclaje, según `corner`:
141/// - **Barra de borde** (`corner: None`, el default): se ancla a `edge` y el
142/// compositor estira el eje paralelo a toda la salida; `thickness` fija el
143/// perpendicular. Es el modo waybar/eww.
144/// - **Caja de esquina** (`corner: Some(_)`): se ancla a una esquina con
145/// `size` explícito (ancho, alto) y `margins`. Ignora `edge`/`thickness` y
146/// nunca reserva zona exclusiva (flota). Es el modo de un toast/OSD/popover.
147#[derive(Debug, Clone)]
148pub struct LayerConfig {
149 pub edge: Edge,
150 /// Grosor (px) en el eje perpendicular al borde (alto para Top/Bottom, ancho
151 /// para Left/Right). El otro eje lo estira el compositor al tamaño de salida.
152 /// Solo aplica en modo barra de borde (`corner: None`).
153 pub thickness: u32,
154 pub layer: LayerKind,
155 /// Si `true`, reserva la franja (las ventanas no la tapan); si `false`, flota.
156 /// En modo caja de esquina siempre flota, independientemente de este valor.
157 pub exclusive: bool,
158 pub keyboard: Keyboard,
159 /// El namespace que ve el compositor (para reglas por-superficie).
160 pub namespace: String,
161 /// Si es `Some`, ancla una **caja** a esa esquina en vez de una barra de
162 /// borde. Requiere `size`; usa `margins`.
163 pub corner: Option<Corner>,
164 /// Tamaño explícito `(ancho, alto)` en px para el modo caja de esquina.
165 /// Ignorado en modo barra de borde. Si falta en modo esquina, se usa un
166 /// default razonable.
167 pub size: Option<(u32, u32)>,
168 /// Separación (px) respecto de los bordes anclados, en modo caja de esquina.
169 pub margins: Margins,
170 /// Si `true`, la **input-region** de la surface se recorta cada frame a la
171 /// unión de los rects de los nodos interactivos (clicks/drag/hover). Las
172 /// zonas transparentes u ociosas quedan **click-through** hacia la ventana de
173 /// atrás. Pensado para overlays flotantes tipo toast/OSD/tooltip que tienen
174 /// una caja fija grande pero sólo pintan contenido en parte de ella: sin esto,
175 /// la caja entera (aun vacía) se traga los clicks de la ventana debajo. Los
176 /// modales que SÍ quieren tragar todo su rect (scrim de logout/lock/polkit)
177 /// lo dejan en `false` (default = comportamiento histórico: acepta todo).
178 pub input_from_content: bool,
179}
180
181impl Default for LayerConfig {
182 fn default() -> Self {
183 Self {
184 edge: Edge::Bottom,
185 thickness: 40,
186 layer: LayerKind::Top,
187 exclusive: true,
188 keyboard: Keyboard::OnDemand,
189 namespace: "llimphi-layer".to_string(),
190 corner: None,
191 size: None,
192 margins: Margins::default(),
193 input_from_content: false,
194 }
195 }
196}
197
198/// Levanta el backend layer-shell y corre `A` hasta que la superficie se cierra.
199/// Devuelve error si no hay sesión Wayland o el compositor no expone
200/// `wlr-layer-shell`.
201pub fn run<A: App>(cfg: LayerConfig) -> Result<(), Box<dyn Error>> {
202 let conn = Connection::connect_to_env()?;
203 let (globals, mut event_queue) = registry_queue_init(&conn)?;
204 let qh: QueueHandle<Runner<A>> = event_queue.handle();
205
206 let compositor = CompositorState::bind(&globals, &qh)?;
207 let layer_shell = LayerShell::bind(&globals, &qh)?;
208 // `wl_shm` para cargar los pixmaps del tema de cursores (el `ThemedPointer`
209 // los necesita). El render de la app va por wgpu; esto es sólo para el cursor.
210 let shm = Shm::bind(&globals, &qh)?;
211
212 // Handle channel-backed: el `update`/efectos de la app usan su propio `Msg`;
213 // cada dispatch/spawn/spawn_periodic cae en `rx`, que `draw` drena por frame.
214 // El truco (igual que en pata): el `for_test().lift(f)` ejecuta `f` —el
215 // `send`— antes del dispatch no-op del handle de test, así el canal recibe.
216 let (tx, rx) = channel::<A::Msg>();
217 let tx = Mutex::new(tx);
218 let handle: Handle<A::Msg> = Handle::<()>::for_test().lift(move |m: A::Msg| {
219 let _ = tx.lock().expect("tx layer").send(m);
220 });
221 let model = A::init(&handle);
222
223 let resolved = resolve_anchor(&cfg);
224 let wl_surface = compositor.create_surface(&qh);
225 let layer = layer_shell.create_layer_surface(
226 &qh,
227 wl_surface,
228 cfg.layer.into(),
229 Some(cfg.namespace.clone()),
230 None,
231 );
232 layer.set_anchor(resolved.anchor);
233 layer.set_size(resolved.size.0, resolved.size.1);
234 layer.set_exclusive_zone(resolved.exclusive_zone);
235 if let Some(m) = resolved.margins {
236 layer.set_margin(m.top, m.right, m.bottom, m.left);
237 }
238 layer.set_keyboard_interactivity(cfg.keyboard.into());
239 layer.commit();
240 let size = resolved.size;
241
242 let mut runner = Runner::<A> {
243 registry_state: RegistryState::new(&globals),
244 output_state: OutputState::new(&globals, &qh),
245 seat_state: SeatState::new(&globals, &qh),
246 conn,
247 hal: None,
248 keyboard: None,
249 pointer: None,
250 themed_pointer: None,
251 shm,
252 cursor_surface: None,
253 seat: None,
254 layer,
255 compositor,
256 layer_shell,
257 cfg,
258 width: size.0.max(1),
259 height: size.1.max(1),
260 gpu: None,
261 cache: None,
262 hover_idx: None,
263 dirty: true,
264 frame_pending: false,
265 drag: None,
266 mods: Modifiers::default(),
267 model: Some(model),
268 handle,
269 rx,
270 recreate_count: 0,
271 exit: false,
272 };
273
274 while !runner.exit {
275 event_queue.blocking_dispatch(&mut runner)?;
276 }
277 Ok(())
278}
279
280// ── Interno ─────────────────────────────────────────────────────────────────
281
282/// Tamaño por defecto de una caja de esquina cuando `cfg.size` es `None`.
283const DEFAULT_CORNER_SIZE: (u32, u32) = (360, 280);
284
285/// El anclaje resuelto para la superficie: anclas sctk, tamaño pedido, zona
286/// exclusiva y márgenes opcionales. Unifica los dos modos (barra/caja).
287struct Resolved {
288 anchor: LayerAnchor,
289 size: (u32, u32),
290 exclusive_zone: i32,
291 margins: Option<Margins>,
292}
293
294/// Resuelve el anclaje según el modo de `cfg`: caja de esquina si `corner` es
295/// `Some`, barra de borde en caso contrario.
296fn resolve_anchor(cfg: &LayerConfig) -> Resolved {
297 match cfg.corner {
298 Some(corner) => {
299 let size = cfg.size.unwrap_or(DEFAULT_CORNER_SIZE);
300 let anchor = match corner {
301 Corner::TopLeft => LayerAnchor::TOP | LayerAnchor::LEFT,
302 Corner::TopRight => LayerAnchor::TOP | LayerAnchor::RIGHT,
303 Corner::BottomLeft => LayerAnchor::BOTTOM | LayerAnchor::LEFT,
304 Corner::BottomRight => LayerAnchor::BOTTOM | LayerAnchor::RIGHT,
305 };
306 // Una caja flota siempre: no tiene sentido reservar zona exclusiva.
307 Resolved { anchor, size, exclusive_zone: 0, margins: Some(cfg.margins) }
308 }
309 None => {
310 let (anchor, size) = anchor_and_size(cfg.edge, cfg.thickness);
311 let exclusive_zone = if cfg.exclusive { cfg.thickness as i32 } else { 0 };
312 Resolved { anchor, size, exclusive_zone, margins: None }
313 }
314 }
315}
316
317/// El anclaje sctk + el tamaño `(w, h)` pedido para un borde y grosor. El eje
318/// paralelo al borde va en 0 → el compositor lo estira a la salida completa.
319fn anchor_and_size(edge: Edge, thickness: u32) -> (LayerAnchor, (u32, u32)) {
320 match edge {
321 Edge::Top => (
322 LayerAnchor::TOP | LayerAnchor::LEFT | LayerAnchor::RIGHT,
323 (0, thickness),
324 ),
325 Edge::Bottom => (
326 LayerAnchor::BOTTOM | LayerAnchor::LEFT | LayerAnchor::RIGHT,
327 (0, thickness),
328 ),
329 Edge::Left => (
330 LayerAnchor::LEFT | LayerAnchor::TOP | LayerAnchor::BOTTOM,
331 (thickness, 0),
332 ),
333 Edge::Right => (
334 LayerAnchor::RIGHT | LayerAnchor::TOP | LayerAnchor::BOTTOM,
335 (thickness, 0),
336 ),
337 }
338}
339
340impl From<LayerKind> for Layer {
341 fn from(k: LayerKind) -> Self {
342 match k {
343 LayerKind::Background => Layer::Background,
344 LayerKind::Bottom => Layer::Bottom,
345 LayerKind::Top => Layer::Top,
346 LayerKind::Overlay => Layer::Overlay,
347 }
348 }
349}
350
351impl From<Keyboard> for KeyboardInteractivity {
352 fn from(k: Keyboard) -> Self {
353 match k {
354 Keyboard::None => KeyboardInteractivity::None,
355 Keyboard::OnDemand => KeyboardInteractivity::OnDemand,
356 Keyboard::Exclusive => KeyboardInteractivity::Exclusive,
357 }
358 }
359}
360
361/// El estado wgpu de la superficie.
362struct PanelGpu {
363 surface: RawSurface,
364 renderer: Renderer,
365 typesetter: Typesetter,
366 scene: vello::Scene,
367 layout: LayoutTree,
368}
369
370/// El árbol pintado en el último frame, para hit-test entre frames.
371struct RenderCache<Msg> {
372 mounted: Mounted<Msg>,
373 computed: ComputedLayout,
374}
375
376/// El cliente Wayland que corre un `App` como una sola layer surface.
377struct Runner<A: App> {
378 registry_state: RegistryState,
379 output_state: OutputState,
380 seat_state: SeatState,
381 conn: Connection,
382 hal: Option<Hal>,
383 keyboard: Option<wl_keyboard::WlKeyboard>,
384 pointer: Option<wl_pointer::WlPointer>,
385 /// Puntero con tema de cursores: traduce el `Cursor` del nodo bajo el
386 /// puntero a una forma de cursor del sistema (I-beam sobre texto, mano sobre
387 /// links, etc). El backend winit ya lo hacía; el layer-shell no tenía NADA.
388 themed_pointer: Option<ThemedPointer>,
389 /// `wl_shm` retenido para el tema de cursores.
390 shm: Shm,
391 /// Superficie dedicada donde el compositor pinta el cursor (la usa el
392 /// `ThemedPointer`).
393 cursor_surface: Option<wl_surface::WlSurface>,
394 seat: Option<wl_seat::WlSeat>,
395 layer: LayerSurface,
396 /// Compositor + shell + config retenidos para **re-crear** la layer-surface
397 /// si el compositor nos manda `closed` (reset/quita del output) en vez de
398 /// morir. Ver [`Runner::recreate_surface`] y el handler `closed`.
399 compositor: CompositorState,
400 layer_shell: LayerShell,
401 cfg: LayerConfig,
402 width: u32,
403 height: u32,
404 gpu: Option<PanelGpu>,
405 cache: Option<RenderCache<A::Msg>>,
406 hover_idx: Option<usize>,
407 /// Hay algo nuevo que pintar. Arranca en `true` (nunca se pintó nada) y lo
408 /// levantan `apply`, los cambios de hover/drag, `configure` y la re-creación
409 /// de la surface; lo baja `draw` al presentar. Ver [`Runner::draw`].
410 dirty: bool,
411 /// Ya hay un frame-callback pedido y sin responder. Evita encadenar uno por
412 /// cada `draw`: `configure` también llama a `draw`, así que sin esto cada
413 /// configure suma un latido paralelo y quedan N en vuelo — el commit-storm
414 /// que le clava un core al compositor. Es la misma guarda que pata tiene
415 /// por panel (`frame_pending`); `llimphi-layer` se extrajo de pata pero se
416 /// quedó sin ella. Se limpia al llegar el callback.
417 frame_pending: bool,
418 /// Arrastre en curso: handler del nodo + última posición del puntero.
419 drag: Option<(DragFn<A::Msg>, (f32, f32))>,
420 mods: Modifiers,
421 model: Option<A::Model>,
422 handle: Handle<A::Msg>,
423 rx: Receiver<A::Msg>,
424 /// Cierres consecutivos sin lograr pintar un frame. Resetea al presentar; si
425 /// pasa el tope, nos rendimos (`exit`) para que el supervisor reinicie con
426 /// estado limpio (output realmente ausente, no un reset transitorio).
427 recreate_count: u32,
428 exit: bool,
429}
430
431impl<A: App> Runner<A> {
432 /// Aplica un `Msg` a la app (transición pura del modelo) y marca para
433 /// repintar invalidando la caché de hit-test.
434 fn apply(&mut self, msg: A::Msg) {
435 if let Some(model) = self.model.take() {
436 self.model = Some(A::update(model, msg, &self.handle));
437 }
438 self.cache = None;
439 self.dirty = true;
440 }
441
442 /// Traduce un evento de teclado sctk al `KeyEvent` de Llimphi.
443 fn keysym_to_keyevent(
444 &self,
445 event: &smithay_client_toolkit::seat::keyboard::KeyEvent,
446 ) -> Option<KeyEvent> {
447 use smithay_client_toolkit::seat::keyboard::Keysym as K;
448 let named = match event.keysym {
449 K::Return | K::KP_Enter => Some(NamedKey::Enter),
450 K::BackSpace => Some(NamedKey::Backspace),
451 K::Tab | K::ISO_Left_Tab => Some(NamedKey::Tab),
452 K::Escape => Some(NamedKey::Escape),
453 K::Up => Some(NamedKey::ArrowUp),
454 K::Down => Some(NamedKey::ArrowDown),
455 K::Right => Some(NamedKey::ArrowRight),
456 K::Left => Some(NamedKey::ArrowLeft),
457 K::Home => Some(NamedKey::Home),
458 K::End => Some(NamedKey::End),
459 K::Page_Up => Some(NamedKey::PageUp),
460 K::Page_Down => Some(NamedKey::PageDown),
461 K::Delete => Some(NamedKey::Delete),
462 K::Insert => Some(NamedKey::Insert),
463 K::F1 => Some(NamedKey::F1),
464 K::F2 => Some(NamedKey::F2),
465 K::F3 => Some(NamedKey::F3),
466 K::F4 => Some(NamedKey::F4),
467 K::F5 => Some(NamedKey::F5),
468 K::F6 => Some(NamedKey::F6),
469 K::F7 => Some(NamedKey::F7),
470 K::F8 => Some(NamedKey::F8),
471 K::F9 => Some(NamedKey::F9),
472 K::F10 => Some(NamedKey::F10),
473 K::F11 => Some(NamedKey::F11),
474 K::F12 => Some(NamedKey::F12),
475 _ => None,
476 };
477 let modifiers = LMods {
478 shift: self.mods.shift,
479 ctrl: self.mods.ctrl,
480 alt: self.mods.alt,
481 meta: self.mods.logo,
482 };
483 let (key, text) = if let Some(n) = named {
484 (Key::Named(n), None)
485 } else {
486 let txt = match event.utf8.as_deref() {
487 Some(s) if !s.is_empty() && !s.chars().all(char::is_control) => s.to_string(),
488 _ => event.keysym.key_char()?.to_string(),
489 };
490 (Key::Character(txt.as_str().into()), Some(txt))
491 };
492 Some(KeyEvent {
493 key,
494 state: KeyState::Pressed,
495 text,
496 modifiers,
497 repeat: false,
498 })
499 }
500
501 /// Crea el estado wgpu de la superficie (idempotente).
502 fn ensure_gpu(&mut self) {
503 if self.gpu.is_some() {
504 return;
505 }
506 let display_ptr = self.conn.backend().display_ptr() as *mut c_void;
507 let surface_ptr = self.layer.wl_surface().id().as_ptr() as *mut c_void;
508 let (w, h) = (self.width, self.height);
509 let display_handle = RawDisplayHandle::Wayland(WaylandDisplayHandle::new(
510 NonNull::new(display_ptr).expect("wl_display ptr"),
511 ));
512 let window_handle = RawWindowHandle::Wayland(WaylandWindowHandle::new(
513 NonNull::new(surface_ptr).expect("wl_surface ptr"),
514 ));
515 // SAFETY: los handles apuntan a objetos Wayland que `self` mantiene vivos.
516 let make_target = || wgpu::SurfaceTargetUnsafe::RawHandle {
517 raw_display_handle: display_handle,
518 raw_window_handle: window_handle,
519 };
520
521 let surface = if self.hal.is_none() {
522 match pollster::block_on(unsafe { Hal::new_for_raw_surface(make_target, w, h) }) {
523 Ok((hal, surface)) => {
524 self.hal = Some(hal);
525 surface
526 }
527 Err(e) => {
528 eprintln!("llimphi-layer · sin gpu: {e}");
529 return;
530 }
531 }
532 } else {
533 let hal = self.hal.as_ref().expect("hal");
534 let wgpu_surface = match unsafe { hal.instance.create_surface_unsafe(make_target()) } {
535 Ok(s) => s,
536 Err(e) => {
537 eprintln!("llimphi-layer · sin gpu: {e}");
538 return;
539 }
540 };
541 match RawSurface::from_surface(hal, wgpu_surface, display_handle, window_handle, w, h) {
542 Ok(s) => s,
543 Err(e) => {
544 eprintln!("llimphi-layer · sin gpu: {e}");
545 return;
546 }
547 }
548 };
549 let hal = self.hal.as_ref().expect("hal");
550 let renderer = Renderer::new(hal).expect("renderer");
551 self.gpu = Some(PanelGpu {
552 surface,
553 renderer,
554 typesetter: Typesetter::new(),
555 scene: vello::Scene::new(),
556 layout: LayoutTree::new(),
557 });
558 }
559
560 /// Re-crea la layer-surface tras un `closed` del compositor (reset/quita del
561 /// output: churn DRM, DPMS, hotplug). Suelta la surface GPU vieja —atada al
562 /// `wl_surface` ya muerto; el `hal`/device **sobrevive**— y crea una nueva
563 /// con la misma config. El próximo `configure` que mande el compositor
564 /// reconstruye la GPU vía [`ensure_gpu`](Self::ensure_gpu) y `draw` re-arma
565 /// el latido. Así la barra sobrevive a un reset de output sin morir.
566 fn recreate_surface(&mut self, qh: &QueueHandle<Self>) {
567 self.gpu = None;
568 self.cache = None;
569 self.dirty = true;
570 // La surface vieja murió: su callback no va a llegar nunca. Si no
571 // liberamos el slot, el latido queda trabado para siempre.
572 self.frame_pending = false;
573 let resolved = resolve_anchor(&self.cfg);
574 let wl_surface = self.compositor.create_surface(qh);
575 let layer = self.layer_shell.create_layer_surface(
576 qh,
577 wl_surface,
578 self.cfg.layer.into(),
579 Some(self.cfg.namespace.clone()),
580 None,
581 );
582 layer.set_anchor(resolved.anchor);
583 layer.set_size(resolved.size.0, resolved.size.1);
584 layer.set_exclusive_zone(resolved.exclusive_zone);
585 if let Some(m) = resolved.margins {
586 layer.set_margin(m.top, m.right, m.bottom, m.left);
587 }
588 layer.set_keyboard_interactivity(self.cfg.keyboard.into());
589 layer.commit();
590 self.layer = layer; // dropea la LayerSurface cerrada
591 }
592
593 /// Mantiene vivo el latido: pide el siguiente frame-callback. El loop de
594 /// frames se auto-sostiene así (cada `frame` re-pide el próximo), lo que
595 /// permite drenar el canal de la app por frame sin un timer aparte.
596 fn latido(&mut self, qh: &QueueHandle<Self>) {
597 if self.frame_pending {
598 return; // ya hay uno pedido; encadenar otro es el commit-storm
599 }
600 self.frame_pending = true;
601 let surface = self.layer.wl_surface();
602 surface.frame(qh, surface.clone());
603 surface.commit();
604 }
605
606 /// Avanza un frame: drena el canal, construye la vista, layout, pinta y
607 /// presenta. Cachea el árbol para el hit-test del próximo evento.
608 ///
609 /// **Sale temprano si nada cambió.** El latido de frame-callbacks se
610 /// mantiene siempre (es lo que nos despierta para drenar el canal), pero
611 /// construir vista + layout + pintar + presentar sólo se paga cuando hay
612 /// algo nuevo: un `Msg` aplicado, un cambio de hover/drag, un `configure` o
613 /// una re-creación de la surface. Antes se re-renderizaba en CADA frame
614 /// aunque la pantalla fuera idéntica — medido en metal 2026-07-21,
615 /// `pata-notify` quemaba 2.9% de un core sin un solo toast en pantalla.
616 /// Es además el criterio que ya usa el backend de ventanas (`llimphi-ui`
617 /// duerme en `ControlFlow::Wait` y pide `request_redraw` a demanda).
618 ///
619 /// Las apps que animan ya empujan `Msg` por [`Handle::spawn_periodic`]
620 /// (pata: reloj 1 s, cava y dientes 100 ms), así que siguen andando igual.
621 /// Lo que NO se sostiene solo es una `view()` que dependa del reloj sin
622 /// emitir un `Msg`: eso hay que despertarlo con un tick, como corresponde.
623 fn draw(&mut self, qh: &QueueHandle<Self>) {
624 // Drenar los Msg que la app empujó al canal (ticks/async/follow-ups).
625 // `apply` marca sucio, así que esto decide el early-return de abajo.
626 while let Ok(m) = self.rx.try_recv() {
627 self.apply(m);
628 }
629 // Nada nuevo y ya hay un frame presentado: mantené el latido y volvé.
630 if !self.dirty && self.cache.is_some() {
631 self.latido(qh);
632 return;
633 }
634 self.ensure_gpu();
635
636 let (w, h) = (self.width, self.height);
637 let view = A::view(self.model.as_ref().expect("model"));
638 let hover_idx = self.hover_idx;
639 let hal = match self.hal.as_ref() {
640 Some(h) => h,
641 None => {
642 self.latido(qh);
643 return;
644 }
645 };
646 let gpu = match self.gpu.as_mut() {
647 Some(g) => g,
648 None => {
649 self.latido(qh);
650 return;
651 }
652 };
653 gpu.surface.resize(w, h);
654 let frame = match gpu.surface.acquire() {
655 Ok(f) => f,
656 Err(_) => {
657 self.latido(qh);
658 return;
659 }
660 };
661 gpu.layout.clear();
662 let mounted = mount(&mut gpu.layout, view);
663 let computed = {
664 let ts = &mut gpu.typesetter;
665 let tmap = &mounted.text_measures;
666 gpu.layout
667 .compute_with_measure(mounted.root, (w as f32, h as f32), |nid, known, avail| {
668 match tmap.get(&nid) {
669 Some(tm) => measure_text_node(ts, tm, known, avail),
670 None => taffy::Size::ZERO,
671 }
672 })
673 .expect("layout")
674 };
675 // Input-region recortada al contenido interactivo (opt-in): la caja fija
676 // del overlay (un toast de 352×420, p. ej.) se traga todos los clicks de
677 // la ventana debajo aunque esté vacía o transparente. Recortando la
678 // región a la unión de los rects de nodos con handler (o VACÍA si no hay
679 // ninguno), las zonas ociosas quedan click-through. Se aplica antes del
680 // `present` (que commitea la surface) para que entre en el mismo frame.
681 if self.cfg.input_from_content {
682 use smithay_client_toolkit::compositor::Region;
683 if let Ok(region) = Region::new(&self.compositor) {
684 for n in &mounted.nodes {
685 let interactivo = n.on_click.is_some()
686 || n.on_click_at.is_some()
687 || n.on_click_screen.is_some()
688 || n.drag.is_some()
689 || n.drag_at.is_some()
690 || n.drag_velocity.is_some()
691 || n.on_right_click.is_some()
692 || n.on_right_click_at.is_some()
693 || n.on_middle_click.is_some()
694 || n.hover_fill.is_some();
695 if !interactivo {
696 continue;
697 }
698 if let Some(r) = computed.get(n.id) {
699 let (x, y, w, h) =
700 (r.x as i32, r.y as i32, r.w.ceil() as i32, r.h.ceil() as i32);
701 if w > 0 && h > 0 {
702 region.add(x, y, w, h);
703 }
704 }
705 }
706 // Región (posiblemente vacía = todo click-through) → la surface.
707 self.layer
708 .wl_surface()
709 .set_input_region(Some(region.wl_region()));
710 }
711 }
712 gpu.scene.reset();
713 paint(&mut gpu.scene, &mounted, &computed, &mut gpu.typesetter, hover_idx, None);
714 if let Err(e) = gpu.renderer.render(hal, &gpu.scene, &frame, palette::css::TRANSPARENT) {
715 eprintln!("llimphi-layer · render: {e}");
716 }
717 gpu.surface.present(frame, hal);
718
719 // Pintamos un frame con éxito: el ciclo de re-creación (si lo hubo) se
720 // recuperó del todo. Resetea el backoff de `closed`.
721 self.recreate_count = 0;
722 self.cache = Some(RenderCache { mounted, computed });
723 // Frame presentado: hasta que algo vuelva a cambiar, los próximos
724 // callbacks sólo re-arman el latido.
725 self.dirty = false;
726 self.latido(qh);
727 }
728}
729
730// ── Handlers sctk ───────────────────────────────────────────────────────────
731
732impl<A: App> CompositorHandler for Runner<A> {
733 fn scale_factor_changed(
734 &mut self,
735 _: &Connection,
736 _: &QueueHandle<Self>,
737 _: &wl_surface::WlSurface,
738 _: i32,
739 ) {
740 }
741 fn transform_changed(
742 &mut self,
743 _: &Connection,
744 _: &QueueHandle<Self>,
745 _: &wl_surface::WlSurface,
746 _: wl_output::Transform,
747 ) {
748 }
749 fn frame(
750 &mut self,
751 _: &Connection,
752 qh: &QueueHandle<Self>,
753 _: &wl_surface::WlSurface,
754 _: u32,
755 ) {
756 // Llegó el callback que pidió `latido`: liberamos el slot para que el
757 // próximo `latido` pueda pedir el siguiente.
758 self.frame_pending = false;
759 self.draw(qh);
760 }
761 fn surface_enter(
762 &mut self,
763 _: &Connection,
764 _: &QueueHandle<Self>,
765 _: &wl_surface::WlSurface,
766 _: &wl_output::WlOutput,
767 ) {
768 }
769 fn surface_leave(
770 &mut self,
771 _: &Connection,
772 _: &QueueHandle<Self>,
773 _: &wl_surface::WlSurface,
774 _: &wl_output::WlOutput,
775 ) {
776 }
777}
778
779impl<A: App> LayerShellHandler for Runner<A> {
780 fn closed(&mut self, _: &Connection, qh: &QueueHandle<Self>, _: &LayerSurface) {
781 // El compositor cerró nuestra superficie — típicamente al resetear o
782 // quitar el output al que estábamos anclados (churn DRM, DPMS, hotplug).
783 // Antes esto mataba el proceso (`exit = true`) y la barra quedaba muerta
784 // hasta un respawn externo. Ahora re-creamos la layer-surface sobre el
785 // output vigente y seguimos vivos. Con tope: si nos cierran repetido sin
786 // lograr pintar (output realmente ausente, no un reset transitorio), nos
787 // rendimos y salimos para que el supervisor reinicie con estado limpio.
788 const MAX_RECREATES: u32 = 16;
789 self.recreate_count += 1;
790 if self.recreate_count > MAX_RECREATES {
791 eprintln!(
792 "llimphi-layer · cerrados {} veces sin recuperar; salgo (que reinicie el supervisor).",
793 self.recreate_count
794 );
795 self.exit = true;
796 return;
797 }
798 eprintln!(
799 "llimphi-layer · el compositor cerró la superficie; re-creo (intento {}).",
800 self.recreate_count
801 );
802 self.recreate_surface(qh);
803 }
804 fn configure(
805 &mut self,
806 _: &Connection,
807 qh: &QueueHandle<Self>,
808 _: &LayerSurface,
809 configure: LayerSurfaceConfigure,
810 _: u32,
811 ) {
812 let (cw, ch) = configure.new_size;
813 const MAX_DIM: u32 = 16384;
814 if (1..=MAX_DIM).contains(&cw) {
815 self.width = cw;
816 }
817 if (1..=MAX_DIM).contains(&ch) {
818 self.height = ch;
819 }
820 // Un configure siempre exige repintar: cambió el tamaño, o el compositor
821 // nos re-mapeó y el frame anterior ya no vale.
822 self.dirty = true;
823 // La app puede necesitar su tamaño (p. ej. para calcular el viewport de
824 // un scroll). El compositor estira el eje paralelo al borde, así que es
825 // aquí —no en `run`— donde se conoce el alto/ancho real.
826 let resize_msg = self
827 .model
828 .as_ref()
829 .and_then(|m| A::on_resize(m, self.width, self.height));
830 if let Some(msg) = resize_msg {
831 self.apply(msg);
832 }
833 self.draw(qh);
834 }
835}
836
837impl<A: App> OutputHandler for Runner<A> {
838 fn output_state(&mut self) -> &mut OutputState {
839 &mut self.output_state
840 }
841 fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
842 fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
843 fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
844}
845
846impl<A: App> SeatHandler for Runner<A> {
847 fn seat_state(&mut self) -> &mut SeatState {
848 &mut self.seat_state
849 }
850 fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
851 if self.seat.is_none() {
852 self.seat = Some(seat);
853 }
854 }
855 fn new_capability(
856 &mut self,
857 _: &Connection,
858 qh: &QueueHandle<Self>,
859 seat: wl_seat::WlSeat,
860 capability: Capability,
861 ) {
862 match capability {
863 Capability::Keyboard if self.keyboard.is_none() => {
864 if let Ok(kbd) = self.seat_state.get_keyboard(qh, &seat, None) {
865 self.keyboard = Some(kbd);
866 }
867 }
868 Capability::Pointer if self.pointer.is_none() => {
869 // Puntero CON tema de cursores: una superficie dedicada para el
870 // cursor + el tema del sistema. Si algo falla (raro), caemos al
871 // puntero pelado (sin cambio de forma, como estaba antes).
872 let surface = self.compositor.create_surface(qh);
873 match self.seat_state.get_pointer_with_theme(
874 qh,
875 &seat,
876 self.shm.wl_shm(),
877 surface.clone(),
878 ThemeSpec::default(),
879 ) {
880 Ok(themed) => {
881 self.pointer = Some(themed.pointer().clone());
882 self.cursor_surface = Some(surface);
883 self.themed_pointer = Some(themed);
884 }
885 Err(_) => {
886 if let Ok(ptr) = self.seat_state.get_pointer(qh, &seat) {
887 self.pointer = Some(ptr);
888 }
889 }
890 }
891 }
892 _ => {}
893 }
894 }
895 fn remove_capability(
896 &mut self,
897 _: &Connection,
898 _: &QueueHandle<Self>,
899 _: wl_seat::WlSeat,
900 capability: Capability,
901 ) {
902 match capability {
903 Capability::Keyboard => {
904 if let Some(k) = self.keyboard.take() {
905 k.release();
906 }
907 }
908 Capability::Pointer => {
909 self.themed_pointer = None;
910 self.cursor_surface = None;
911 if let Some(p) = self.pointer.take() {
912 p.release();
913 }
914 }
915 _ => {}
916 }
917 }
918 fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
919}
920
921impl<A: App> KeyboardHandler for Runner<A> {
922 fn enter(
923 &mut self,
924 _: &Connection,
925 _: &QueueHandle<Self>,
926 _: &wl_keyboard::WlKeyboard,
927 _: &wl_surface::WlSurface,
928 _: u32,
929 _: &[u32],
930 _: &[Keysym],
931 ) {
932 }
933 fn leave(
934 &mut self,
935 _: &Connection,
936 _: &QueueHandle<Self>,
937 _: &wl_keyboard::WlKeyboard,
938 _: &wl_surface::WlSurface,
939 _: u32,
940 ) {
941 }
942 fn press_key(
943 &mut self,
944 _: &Connection,
945 _: &QueueHandle<Self>,
946 _: &wl_keyboard::WlKeyboard,
947 _: u32,
948 event: smithay_client_toolkit::seat::keyboard::KeyEvent,
949 ) {
950 if let Some(ke) = self.keysym_to_keyevent(&event) {
951 let msg = A::on_key(self.model.as_ref().expect("model"), &ke);
952 if let Some(msg) = msg {
953 self.apply(msg);
954 }
955 }
956 }
957 fn release_key(
958 &mut self,
959 _: &Connection,
960 _: &QueueHandle<Self>,
961 _: &wl_keyboard::WlKeyboard,
962 _: u32,
963 _: smithay_client_toolkit::seat::keyboard::KeyEvent,
964 ) {
965 }
966 fn update_modifiers(
967 &mut self,
968 _: &Connection,
969 _: &QueueHandle<Self>,
970 _: &wl_keyboard::WlKeyboard,
971 _: u32,
972 modifiers: Modifiers,
973 _: u32,
974 ) {
975 self.mods = modifiers;
976 }
977}
978
979/// Traduce el [`Cursor`](llimphi_ui::llimphi_compositor::Cursor) del nodo bajo
980/// el puntero a la forma de cursor del sistema. Espeja `to_winit_cursor` del
981/// backend winit — la misma cobertura de variantes. `None` → flecha default.
982fn cursor_to_icon(c: Option<llimphi_ui::llimphi_compositor::Cursor>) -> CursorIcon {
983 use llimphi_ui::llimphi_compositor::Cursor as C;
984 match c {
985 None | Some(C::Default) => CursorIcon::Default,
986 Some(C::Pointer) => CursorIcon::Pointer,
987 Some(C::Text) => CursorIcon::Text,
988 Some(C::Crosshair) => CursorIcon::Crosshair,
989 Some(C::Move) => CursorIcon::Move,
990 Some(C::Grab) => CursorIcon::Grab,
991 Some(C::Grabbing) => CursorIcon::Grabbing,
992 Some(C::NotAllowed) => CursorIcon::NotAllowed,
993 Some(C::Wait) => CursorIcon::Wait,
994 Some(C::Progress) => CursorIcon::Progress,
995 Some(C::Help) => CursorIcon::Help,
996 Some(C::ColResize) => CursorIcon::ColResize,
997 Some(C::RowResize) => CursorIcon::RowResize,
998 Some(C::EwResize) => CursorIcon::EwResize,
999 Some(C::NsResize) => CursorIcon::NsResize,
1000 Some(C::NeswResize) => CursorIcon::NeswResize,
1001 Some(C::NwseResize) => CursorIcon::NwseResize,
1002 Some(C::ZoomIn) => CursorIcon::ZoomIn,
1003 Some(C::ZoomOut) => CursorIcon::ZoomOut,
1004 }
1005}
1006
1007impl<A: App> Runner<A> {
1008 /// Aplica al puntero la forma de cursor del nodo `hover_idx` actual. No-op si
1009 /// no hay `ThemedPointer` (el compositor no dio la capacidad temada).
1010 fn aplicar_cursor(&mut self, conn: &Connection) {
1011 let Some(themed) = self.themed_pointer.as_ref() else { return };
1012 let cur = self
1013 .hover_idx
1014 .and_then(|i| self.cache.as_ref()?.mounted.nodes.get(i))
1015 .and_then(|n| n.cursor);
1016 let _ = themed.set_cursor(conn, cursor_to_icon(cur));
1017 }
1018}
1019
1020impl<A: App> PointerHandler for Runner<A> {
1021 fn pointer_frame(
1022 &mut self,
1023 conn: &Connection,
1024 _: &QueueHandle<Self>,
1025 _: &wl_pointer::WlPointer,
1026 events: &[PointerEvent],
1027 ) {
1028 for e in events {
1029 match e.kind {
1030 PointerEventKind::Enter { .. } => {
1031 // Al entrar, fijar el cursor según el nodo bajo el puntero
1032 // (con el serial de Enter que el ThemedPointer ya rastrea).
1033 let (px, py) = (e.position.0 as f32, e.position.1 as f32);
1034 let nuevo = self
1035 .cache
1036 .as_ref()
1037 .and_then(|c| hit_test_hover(&c.mounted, &c.computed, px, py));
1038 if self.hover_idx != nuevo {
1039 self.hover_idx = nuevo;
1040 self.dirty = true; // el hover se PINTA (hover_fill)
1041 }
1042 self.aplicar_cursor(conn);
1043 continue;
1044 }
1045 PointerEventKind::Motion { .. } => {
1046 let (px, py) = (e.position.0 as f32, e.position.1 as f32);
1047 // Drag en curso: el delta va al handler del nodo.
1048 if let Some((handler, last)) = self.drag.as_ref().map(|(h, l)| (h.clone(), *l)) {
1049 if let Some(d) = self.drag.as_mut() {
1050 d.1 = (px, py);
1051 }
1052 if let Some(msg) = (handler)(DragPhase::Move, px - last.0, py - last.1) {
1053 self.apply(msg);
1054 }
1055 continue;
1056 }
1057 let nuevo = self
1058 .cache
1059 .as_ref()
1060 .and_then(|c| hit_test_hover(&c.mounted, &c.computed, px, py));
1061 if self.hover_idx != nuevo {
1062 self.hover_idx = nuevo;
1063 self.dirty = true; // el hover se PINTA (hover_fill)
1064 // Cambió el nodo bajo el puntero → actualizar la forma.
1065 self.aplicar_cursor(conn);
1066 }
1067 continue;
1068 }
1069 PointerEventKind::Leave { .. } => {
1070 if self.hover_idx.is_some() {
1071 self.hover_idx = None;
1072 self.dirty = true; // se va el resaltado
1073 }
1074 continue;
1075 }
1076 _ => {}
1077 }
1078 if let PointerEventKind::Axis { vertical, .. } = e.kind {
1079 let dy = if vertical.discrete != 0 {
1080 vertical.discrete as f32
1081 } else {
1082 vertical.absolute as f32 / 20.0
1083 };
1084 if dy != 0.0 {
1085 let (px, py) = (e.position.0 as f32, e.position.1 as f32);
1086 let msg = self.cache.as_ref().and_then(|c| {
1087 hit_test_scroll(&c.mounted, &c.computed, px, py)
1088 .and_then(|i| c.mounted.nodes.get(i))
1089 .and_then(|n| n.on_scroll.as_ref().and_then(|h| h(0.0, dy)))
1090 });
1091 if let Some(msg) = msg {
1092 self.apply(msg);
1093 }
1094 }
1095 continue;
1096 }
1097 if let PointerEventKind::Release { button, .. } = e.kind {
1098 if button == BTN_LEFT {
1099 if let Some((handler, _)) = self.drag.take() {
1100 if let Some(msg) = (handler)(DragPhase::End, 0.0, 0.0) {
1101 self.apply(msg);
1102 }
1103 }
1104 }
1105 continue;
1106 }
1107 if let PointerEventKind::Press { button, .. } = e.kind {
1108 if button != BTN_LEFT && button != BTN_RIGHT {
1109 continue;
1110 }
1111 let (px, py) = (e.position.0 as f32, e.position.1 as f32);
1112 let derecho = button == BTN_RIGHT;
1113 // Nodo arrastrable bajo el press izquierdo: arranca un drag.
1114 if !derecho {
1115 let handler = self.cache.as_ref().and_then(|c| {
1116 let i = hit_test_click(&c.mounted, &c.computed, px, py)?;
1117 c.mounted.nodes.get(i)?.drag.clone()
1118 });
1119 if let Some(handler) = handler {
1120 self.drag = Some((handler, (px, py)));
1121 continue;
1122 }
1123 }
1124 let msg = self.cache.as_ref().and_then(|c| {
1125 let i = hit_test_click(&c.mounted, &c.computed, px, py)?;
1126 let n = c.mounted.nodes.get(i)?;
1127 if derecho {
1128 // `on_right_click_screen` (coords absolutas, para menús al
1129 // puntero) gana; luego `on_right_click_at` (local, grillas/
1130 // canvas); luego el `on_right_click` plano.
1131 if let Some(screen) = n.on_right_click_screen.as_ref() {
1132 let r = c.computed.get(n.id)?;
1133 if let Some(m) = screen(px, py, r.w, r.h) {
1134 return Some(m);
1135 }
1136 }
1137 if let Some(m) = n.on_right_click.clone() {
1138 return Some(m);
1139 }
1140 let at = n.on_right_click_at.as_ref()?;
1141 let r = c.computed.get(n.id)?;
1142 at(px - r.x, py - r.y, r.w, r.h)
1143 } else {
1144 if let Some(m) = n.on_click.clone() {
1145 return Some(m);
1146 }
1147 let at = n.on_click_at.as_ref()?;
1148 let r = c.computed.get(n.id)?;
1149 at(px - r.x, py - r.y, r.w, r.h)
1150 }
1151 });
1152 if let Some(msg) = msg {
1153 self.apply(msg);
1154 }
1155 }
1156 }
1157 }
1158}
1159
1160impl<A: App> ProvidesRegistryState for Runner<A> {
1161 fn registry(&mut self) -> &mut RegistryState {
1162 &mut self.registry_state
1163 }
1164 registry_handlers![OutputState, SeatState];
1165}
1166
1167delegate_compositor!(@<A: App> Runner<A>);
1168delegate_output!(@<A: App> Runner<A>);
1169delegate_layer!(@<A: App> Runner<A>);
1170delegate_seat!(@<A: App> Runner<A>);
1171delegate_keyboard!(@<A: App> Runner<A>);
1172delegate_pointer!(@<A: App> Runner<A>);
1173delegate_shm!(@<A: App> Runner<A>);
1174delegate_registry!(@<A: App> Runner<A>);
1175
1176impl<A: App> ShmHandler for Runner<A> {
1177 fn shm_state(&mut self) -> &mut Shm {
1178 &mut self.shm
1179 }
1180}