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//! `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//! ## Dónde corre
25//!
26//! El runner de verdad vive en `wayland.rs` y **sólo se compila en Linux**, que
27//! es donde existe `wlr-layer-shell`. Los tipos de configuración de acá abajo
28//! son datos y compilan en todas partes: una app que ofrece «modo dock» puede
29//! nombrarlos sin cfguearse entera, y [`run`] le contesta con un error donde el
30//! protocolo no está. Emular una barra anclada sobre otro sistema de ventanas no
31//! es portar esto: es escribir otro runner (una appbar de Win32, por ejemplo), y
32//! ése sería otro crate.
33
34use std::error::Error;
35
36use llimphi_ui::App;
37
38#[cfg(any(target_os = "linux", target_os = "freebsd"))]
39mod wayland;
40
41// ── API pública ─────────────────────────────────────────────────────────────
42
43/// El borde al que se ancla la barra.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Edge {
46 Top,
47 Bottom,
48 Left,
49 Right,
50}
51
52/// La esquina a la que se ancla una **caja** de tamaño fijo (no una barra de
53/// borde completo). La usa, p. ej., un daemon de notificaciones: una caja en
54/// la esquina, no una franja que cruza la pantalla.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Corner {
57 TopLeft,
58 TopRight,
59 BottomLeft,
60 BottomRight,
61}
62
63/// Márgenes (px) respecto de los bordes a los que la superficie se ancla.
64/// El compositor los respeta como separación entre la caja y el borde.
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
66pub struct Margins {
67 pub top: i32,
68 pub right: i32,
69 pub bottom: i32,
70 pub left: i32,
71}
72
73impl Margins {
74 /// El mismo margen en los cuatro lados.
75 pub fn all(px: i32) -> Self {
76 Self { top: px, right: px, bottom: px, left: px }
77 }
78}
79
80/// La capa de composición (orden en Z respecto de las ventanas).
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum LayerKind {
83 /// Detrás de las ventanas (fondo de escritorio).
84 Background,
85 /// Bajo las ventanas normales.
86 Bottom,
87 /// Sobre las ventanas normales (lo típico para una barra).
88 Top,
89 /// Por encima de todo (popups, lock screens).
90 Overlay,
91}
92
93/// Cuánto teclado acepta la superficie.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum Keyboard {
96 /// No recibe teclado.
97 None,
98 /// Lo recibe al ser clickeada/enfocada (lo razonable para una barra que a
99 /// veces toma texto, p. ej. un input).
100 OnDemand,
101 /// Acapara el teclado mientras exista (drawers/popups modales).
102 Exclusive,
103}
104
105/// Configuración de la superficie de capa.
106///
107/// Dos modos de anclaje, según `corner`:
108/// - **Barra de borde** (`corner: None`, el default): se ancla a `edge` y el
109/// compositor estira el eje paralelo a toda la salida; `thickness` fija el
110/// perpendicular. Es el modo waybar/eww.
111/// - **Caja de esquina** (`corner: Some(_)`): se ancla a una esquina con
112/// `size` explícito (ancho, alto) y `margins`. Ignora `edge`/`thickness` y
113/// nunca reserva zona exclusiva (flota). Es el modo de un toast/OSD/popover.
114#[derive(Debug, Clone)]
115pub struct LayerConfig {
116 pub edge: Edge,
117 /// Grosor (px) en el eje perpendicular al borde (alto para Top/Bottom, ancho
118 /// para Left/Right). El otro eje lo estira el compositor al tamaño de salida.
119 /// Solo aplica en modo barra de borde (`corner: None`).
120 pub thickness: u32,
121 pub layer: LayerKind,
122 /// Si `true`, reserva la franja (las ventanas no la tapan); si `false`, flota.
123 /// En modo caja de esquina siempre flota, independientemente de este valor.
124 pub exclusive: bool,
125 pub keyboard: Keyboard,
126 /// El namespace que ve el compositor (para reglas por-superficie).
127 pub namespace: String,
128 /// Si es `Some`, ancla una **caja** a esa esquina en vez de una barra de
129 /// borde. Requiere `size`; usa `margins`.
130 pub corner: Option<Corner>,
131 /// Tamaño explícito `(ancho, alto)` en px para el modo caja de esquina.
132 /// Ignorado en modo barra de borde. Si falta en modo esquina, se usa un
133 /// default razonable.
134 pub size: Option<(u32, u32)>,
135 /// Separación (px) respecto de los bordes anclados, en modo caja de esquina.
136 pub margins: Margins,
137 /// Si `true`, la **input-region** de la surface se recorta cada frame a la
138 /// unión de los rects de los nodos interactivos (clicks/drag/hover). Las
139 /// zonas transparentes u ociosas quedan **click-through** hacia la ventana de
140 /// atrás. Pensado para overlays flotantes tipo toast/OSD/tooltip que tienen
141 /// una caja fija grande pero sólo pintan contenido en parte de ella: sin esto,
142 /// la caja entera (aun vacía) se traga los clicks de la ventana debajo. Los
143 /// modales que SÍ quieren tragar todo su rect (scrim de logout/lock/polkit)
144 /// lo dejan en `false` (default = comportamiento histórico: acepta todo).
145 pub input_from_content: bool,
146 /// Si `true`, la superficie **cubre la salida entera**: se ancla a los
147 /// cuatro bordes con tamaño `(0, 0)` —el compositor la estira— y nunca
148 /// reserva zona exclusiva. Ignora `edge`/`thickness`/`corner`.
149 ///
150 /// Es el tercer modo del layer-shell, y el que necesita un overlay que
151 /// oscurece la pantalla y flota una tarjeta encima (un runner tipo KRunner,
152 /// un lock, un scrim de logout). Antes sólo había barra-de-borde y
153 /// caja-de-esquina, así que ese overlay terminaba siendo una **ventana**, y
154 /// entonces el compositor la teselea: entra al mosaico como una pestaña
155 /// más, esconde la que había y su fondo semitransparente queda sobre nada.
156 /// Verificado en metal con el launcher el 2026-08-25 (medido: las tres
157 /// zonas de fondo daban el mismo valor, señal de que atrás no había nada).
158 pub fullscreen: bool,
159 /// En qué **monitor** nacer: nombre del conector (`"DP-1"`, `"eDP-1"`).
160 /// `None` (default) = lo elige el compositor, que es lo que pasaba siempre.
161 ///
162 /// Es sólo el arranque. Para **mudarse** de monitor en caliente, la app
163 /// implementa [`App::output_deseado`]: este runner la consulta por cuadro y
164 /// re-crea la superficie cuando el nombre cambia (wlr-layer-shell fija la
165 /// salida al crear la superficie; no hay forma de cambiarla después).
166 pub output: Option<String>,
167}
168
169impl Default for LayerConfig {
170 fn default() -> Self {
171 Self {
172 edge: Edge::Bottom,
173 thickness: 40,
174 layer: LayerKind::Top,
175 exclusive: true,
176 keyboard: Keyboard::OnDemand,
177 namespace: "llimphi-layer".to_string(),
178 corner: None,
179 size: None,
180 margins: Margins::default(),
181 input_from_content: false,
182 fullscreen: false,
183 output: None,
184 }
185 }
186}
187/// Levanta el backend layer-shell y corre `A` hasta que la superficie se cierra.
188/// Devuelve error si no hay sesión Wayland o el compositor no expone
189/// `wlr-layer-shell`.
190#[cfg(any(target_os = "linux", target_os = "freebsd"))]
191pub fn run<A: App>(cfg: LayerConfig) -> Result<(), Box<dyn Error>> {
192 wayland::run::<A>(cfg)
193}
194
195/// Donde no hay Wayland no hay `wlr-layer-shell`, así que esto **falla y lo dice**.
196///
197/// No es un no-op: una app que pide dockearse y recibe `Ok(())` se quedaría sin
198/// ventana y sin explicación. El error es la respuesta correcta — la misma que
199/// da el camino de Linux cuando el compositor no expone el protocolo, así que
200/// quien llama ya lo contempla.
201#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
202pub fn run<A: App>(_cfg: LayerConfig) -> Result<(), Box<dyn Error>> {
203 Err("llimphi-layer: wlr-layer-shell es un protocolo de Wayland y este sistema \
204 no lo tiene; una app que quiera anclarse a un borde acá necesita un \
205 runner propio de la plataforma"
206 .into())
207}