gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 238TOML 102Markdown 83Shell 24178 Definiciones

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 llimphi-layer/src/lib.rs Rust · 1180 líneas · 45.1 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

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}