//! `llimphi-layer` — corre un [`App`] de Llimphi como una **superficie //! wlr-layer-shell** (una barra anclada a un borde), no como ventana. Hace que //! una app se siente al nivel de eww/waybar en cualquier compositor wlroots //! (mirada, Hyprland, Sway, river…): se ancla a un borde, declara una *exclusive //! zone* opcional —el compositor le reserva la franja y tesela el resto— y se //! pinta reusando el pipeline de Llimphi (`mount → compute → paint → render`). //! //! Es la plumbing `smithay-client-toolkit` + `wgpu` que `pata` probó en //! producción, extraída a un runner **genérico** sobre el trait `App`. A //! diferencia de pata (N superficies + tray/dock/sidebar/…), este corre **una** //! superficie y nada de dominio: la app provee `view`/`update`/`on_key` y este //! crate le presta el borde. Útil, p. ej., para que `shuma` se dockee como barra. //! //! ```ignore //! struct MiBarra; //! impl llimphi_ui::App for MiBarra { /* … */ } //! llimphi_layer::run::(llimphi_layer::LayerConfig { //! edge: llimphi_layer::Edge::Bottom, //! thickness: 40, //! keyboard: llimphi_layer::Keyboard::OnDemand, //! ..Default::default() //! })?; //! ``` //! ## Dónde corre //! //! El runner de verdad vive en `wayland.rs` y **sólo se compila en Linux**, que //! es donde existe `wlr-layer-shell`. Los tipos de configuración de acá abajo //! son datos y compilan en todas partes: una app que ofrece «modo dock» puede //! nombrarlos sin cfguearse entera, y [`run`] le contesta con un error donde el //! protocolo no está. Emular una barra anclada sobre otro sistema de ventanas no //! es portar esto: es escribir otro runner (una appbar de Win32, por ejemplo), y //! ése sería otro crate. use std::error::Error; use llimphi_ui::App; #[cfg(any(target_os = "linux", target_os = "freebsd"))] mod wayland; // ── API pública ───────────────────────────────────────────────────────────── /// El borde al que se ancla la barra. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Edge { Top, Bottom, Left, Right, } /// La esquina a la que se ancla una **caja** de tamaño fijo (no una barra de /// borde completo). La usa, p. ej., un daemon de notificaciones: una caja en /// la esquina, no una franja que cruza la pantalla. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Corner { TopLeft, TopRight, BottomLeft, BottomRight, } /// Márgenes (px) respecto de los bordes a los que la superficie se ancla. /// El compositor los respeta como separación entre la caja y el borde. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct Margins { pub top: i32, pub right: i32, pub bottom: i32, pub left: i32, } impl Margins { /// El mismo margen en los cuatro lados. pub fn all(px: i32) -> Self { Self { top: px, right: px, bottom: px, left: px } } } /// La capa de composición (orden en Z respecto de las ventanas). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LayerKind { /// Detrás de las ventanas (fondo de escritorio). Background, /// Bajo las ventanas normales. Bottom, /// Sobre las ventanas normales (lo típico para una barra). Top, /// Por encima de todo (popups, lock screens). Overlay, } /// Cuánto teclado acepta la superficie. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Keyboard { /// No recibe teclado. None, /// Lo recibe al ser clickeada/enfocada (lo razonable para una barra que a /// veces toma texto, p. ej. un input). OnDemand, /// Acapara el teclado mientras exista (drawers/popups modales). Exclusive, } /// Configuración de la superficie de capa. /// /// Dos modos de anclaje, según `corner`: /// - **Barra de borde** (`corner: None`, el default): se ancla a `edge` y el /// compositor estira el eje paralelo a toda la salida; `thickness` fija el /// perpendicular. Es el modo waybar/eww. /// - **Caja de esquina** (`corner: Some(_)`): se ancla a una esquina con /// `size` explícito (ancho, alto) y `margins`. Ignora `edge`/`thickness` y /// nunca reserva zona exclusiva (flota). Es el modo de un toast/OSD/popover. #[derive(Debug, Clone)] pub struct LayerConfig { pub edge: Edge, /// Grosor (px) en el eje perpendicular al borde (alto para Top/Bottom, ancho /// para Left/Right). El otro eje lo estira el compositor al tamaño de salida. /// Solo aplica en modo barra de borde (`corner: None`). pub thickness: u32, pub layer: LayerKind, /// Si `true`, reserva la franja (las ventanas no la tapan); si `false`, flota. /// En modo caja de esquina siempre flota, independientemente de este valor. pub exclusive: bool, pub keyboard: Keyboard, /// El namespace que ve el compositor (para reglas por-superficie). pub namespace: String, /// Si es `Some`, ancla una **caja** a esa esquina en vez de una barra de /// borde. Requiere `size`; usa `margins`. pub corner: Option, /// Tamaño explícito `(ancho, alto)` en px para el modo caja de esquina. /// Ignorado en modo barra de borde. Si falta en modo esquina, se usa un /// default razonable. pub size: Option<(u32, u32)>, /// Separación (px) respecto de los bordes anclados, en modo caja de esquina. pub margins: Margins, /// Si `true`, la **input-region** de la surface se recorta cada frame a la /// unión de los rects de los nodos interactivos (clicks/drag/hover). Las /// zonas transparentes u ociosas quedan **click-through** hacia la ventana de /// atrás. Pensado para overlays flotantes tipo toast/OSD/tooltip que tienen /// una caja fija grande pero sólo pintan contenido en parte de ella: sin esto, /// la caja entera (aun vacía) se traga los clicks de la ventana debajo. Los /// modales que SÍ quieren tragar todo su rect (scrim de logout/lock/polkit) /// lo dejan en `false` (default = comportamiento histórico: acepta todo). pub input_from_content: bool, /// Si `true`, la superficie **cubre la salida entera**: se ancla a los /// cuatro bordes con tamaño `(0, 0)` —el compositor la estira— y nunca /// reserva zona exclusiva. Ignora `edge`/`thickness`/`corner`. /// /// Es el tercer modo del layer-shell, y el que necesita un overlay que /// oscurece la pantalla y flota una tarjeta encima (un runner tipo KRunner, /// un lock, un scrim de logout). Antes sólo había barra-de-borde y /// caja-de-esquina, así que ese overlay terminaba siendo una **ventana**, y /// entonces el compositor la teselea: entra al mosaico como una pestaña /// más, esconde la que había y su fondo semitransparente queda sobre nada. /// Verificado en metal con el launcher el 2026-08-25 (medido: las tres /// zonas de fondo daban el mismo valor, señal de que atrás no había nada). pub fullscreen: bool, /// En qué **monitor** nacer: nombre del conector (`"DP-1"`, `"eDP-1"`). /// `None` (default) = lo elige el compositor, que es lo que pasaba siempre. /// /// Es sólo el arranque. Para **mudarse** de monitor en caliente, la app /// implementa [`App::output_deseado`]: este runner la consulta por cuadro y /// re-crea la superficie cuando el nombre cambia (wlr-layer-shell fija la /// salida al crear la superficie; no hay forma de cambiarla después). pub output: Option, } impl Default for LayerConfig { fn default() -> Self { Self { edge: Edge::Bottom, thickness: 40, layer: LayerKind::Top, exclusive: true, keyboard: Keyboard::OnDemand, namespace: "llimphi-layer".to_string(), corner: None, size: None, margins: Margins::default(), input_from_content: false, fullscreen: false, output: None, } } } /// Levanta el backend layer-shell y corre `A` hasta que la superficie se cierra. /// Devuelve error si no hay sesión Wayland o el compositor no expone /// `wlr-layer-shell`. #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub fn run(cfg: LayerConfig) -> Result<(), Box> { wayland::run::(cfg) } /// Donde no hay Wayland no hay `wlr-layer-shell`, así que esto **falla y lo dice**. /// /// No es un no-op: una app que pide dockearse y recibe `Ok(())` se quedaría sin /// ventana y sin explicación. El error es la respuesta correcta — la misma que /// da el camino de Linux cuando el compositor no expone el protocolo, así que /// quien llama ya lo contempla. #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] pub fn run(_cfg: LayerConfig) -> Result<(), Box> { Err("llimphi-layer: wlr-layer-shell es un protocolo de Wayland y este sistema \ no lo tiene; una app que quiera anclarse a un borde acá necesita un \ runner propio de la plataforma" .into()) }