tawasuyu

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definiciones

⎇ main llimphi-layer/src/lib.rs Rust · 207 líneas · 8.9 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · 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//! ## 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}