Sin decoración, las ventanas se confundían entre sí. Ahora el backend DRM dibuja un marco fino alrededor de cada ventana: azul la que tiene el foco del teclado, gris las demás. - `ManagedWindow` gana `focused: bool` (lo fija `exec_op` al atender `BodyOp::Focus`/`Unfocus`) y `borders: [SolidColorBuffer; 4]` — un búfer por lado, cada uno con su `Id` estable para el seguimiento de daño; `SolidColorBuffer` sube su contador sólo si tamaño o color cambian, así un marco quieto no fuerza recomposición. - El enum `Frame` pasa de `Cursor` a `Solid`: una variante de color sólido que sirve para el cursor y para los marcos (dos variantes con el mismo tipo chocarían en el `From` que genera `render_elements!`). - `render()` en dos pasos: refresca los búferes (tamaño = contenido, color = foco) y luego arma los elementos. El marco va metido hacia adentro, sobre el borde de la superficie, así no pisa al vecino. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
13 KiB
modules/mirada/ — Compositor Wayland (carmen)
Propósito. Un compositor Wayland teselante. La decisión de diseño central es partirlo en dos procesos, el Cerebro y el Cuerpo:
- El Cuerpo (
mirada-compositor, sobresmithay) habla Wayland con los clientes, posee el hardware (DRM/GPU/libinput) y compone las superficies reales. Los píxeles nunca salen de él — composición zero-copy. - El Cerebro (la app
mirada, GPUI) decide dónde va cada ventana —pura aritmética de rectángulos— y orquesta el escritorio: layouts, atajos, focos, escritorios virtuales.
Los dos hablan por un socket Unix con un contrato mínimo de enums. Así, toda la lógica espacial es agnóstica de Wayland y se prueba sin un servidor gráfico; el Cuerpo queda reducido a "habla el protocolo y ejecuta operaciones de geometría".
Crates
| crate | tipo | rol |
|---|---|---|
mirada-layout |
lib | Motor de teselado: Rect, modos de layout, Workspace (ventanas, foco) |
mirada-protocol |
lib | Contrato Cerebro↔Cuerpo: BrainCommand/BodyEvent + marco de cable |
mirada-brain |
lib | Orquestador del escritorio: Desktop, eventos→comandos, atajos |
mirada-link |
lib | Transporte: el socket Unix con hilo lector + canal |
mirada-body |
lib | Contabilidad del Cuerpo: BodyState, traduce comandos a BodyOp |
mirada (app) |
bin/GPUI | El Cerebro: ventana que tesela el escritorio y manda geometría |
mirada-compositor |
bin/smithay | El Cuerpo: compositor Wayland real (backend winit, anidado) |
mirada-ctl (app) |
bin/CLI | Control externo del Cerebro (estilo swaymsg): acciones y consultas |
Flujo
mirada-layout ─► mirada-protocol ─► mirada-brain ─► [app mirada · Cerebro]
│ │
│ mirada-link
│ │
└────► mirada-body ─► [mirada-compositor · Cuerpo]
- El Cuerpo reporta hardware/clientes con
BodyEvent(salida conectada, ventana abierta, atajo pulsado…). - El Cerebro (
Desktop::on_event) recalcula y emiteBrainCommand(Placecon la geometría completa,Close,GrabKeys…). - El Cuerpo (
BodyState::apply) traduce cada comando aBodyOpconcretas y sólo emite lo que de verdad cambia.
Detalle por crate
mirada-layout—Rect+split(reparto exacto de píxeles),LayoutModecon 7 modos (MasterStack,CenteredMaster,Spiral—espiral de Fibonacci—,Grid,Columns,Rows,Monocle) yLayoutMode::next()para el ciclo,Workspacecon foco cíclico, reordenado ypromote_focused.LayoutParamsllevamaster_ratioymaster_count(nmaster); smart gaps (una sola ventana va a sangre). Determinista.mirada-protocol—WindowPlacement, los enumsBrainCommandyBodyEvent, el marcopostcardcon prefijou32LE (write_frame/read_frame, guardMAX_FRAME) y el puenteplacements(&Workspace, Rect).mirada-brain—Desktop: salidas, 9 escritorios virtuales, registro de ventanas.on_event(BodyEvent) -> Vec<BrainCommand>;DesktopAction+Keymapconfigurable (set_keymapen caliente) +Rulesde ventana. Multi-monitor: cadaOutputmuestra un escritorio;relayout()tesela todas las salidas en un soloPlace.mirada-link—Link<Out,In>sobre socket Unix; hilo lector de fondo + canalmpscpara sondeo no bloqueante.BrainLink/BodyLink,connected_pair(socketpair),connect/listenpor ruta.mirada-body—BodyState: salidas + superficies;applytraduceBrainCommand→BodyOp(idempotente), los mutadores del backend devuelven losBodyEventa mandar. Ejemploheadless: un Cuerpo sin gráficos guiado por stdin para ejercitar el bucle entero.mirada(app) — envuelveDesktopy lo pinta (barra de escritorios + modo + foco, lienzo teselado). El lienzo dibuja todas las salidas a escala, cada una con su marco. ConMIRADA_SOCKETconecta a un Cuerpo; sin él corre en simulación (ventanas sintéticas, teclado de la propia ventana). Pips de escritorio y ventanas clicables.mirada-ctl(app) — CLI de control: parsea la acción de los argumentos (DesktopAction: FromStr) y la manda al Cerebro por el socket de control;windowsyactionspara consultar.
Atajos de teclado configurables
El keymap vive sólo en el Cerebro (mirada-brain::Keymap). El Cuerpo
nunca lo ve: recibe únicamente la lista de cadenas a interceptar en un
GrabKeys, hace un Vec::contains ciego y devuelve la combinación
pulsada como Keybind; es Desktop quien la traduce a DesktopAction.
Esa separación —qué interceptar (lista barata, Cuerpo) vs. qué
significa (el mapa, Cerebro)— hace innecesario cualquier candado o
Arc: el mapa es monohilo y la lista se reemplaza de golpe.
- Disco — RON de texto en
~/.config/mirada/keymap.ron, editable a mano y versionable. La app lo crea documentado en el primer arranque; si está corrupto, avisa y usa el de por defecto sin pisar el archivo. - Cable — sólo viaja la lista de cadenas (
GrabKeys), vía el marcopostcardque ya existe. No hay formato binario de configuración. - Vocabulario — la acción es una cadena estable (
"focus-next","layout:grid","workspace:3"):DesktopAction: Display + FromStr. - Recarga en caliente —
Keymap::watch(sobrenotify) vigila el archivo; al cambiar, el dueño delDesktoprecarga, llama aset_keymapy reenvía elGrabKeys. Sin reiniciar. - Configurador — no hay ejecutable aparte: el editor de texto del
usuario, y la app
mirada(que a futuro puede dibujar un editor visual sobre el mismo APIKeymap).cargo run -p mirada-brain --example keymap-defaultimprime el archivo por defecto.
API de acciones
Toda acción de escritorio converge en un único embudo:
Desktop::apply(DesktopAction) -> Vec<BrainCommand>. El keymap no es más
que un front-end (Keybind → lookup → apply); hay otros tres:
DesktopAction::FocusWindow(WindowId)— direccionamiento directo de una ventana (no sólo ciclar conFocusNext/Prev); si está en otro escritorio, salta a él. Lo usan la taskbar ymirada-ctl.- Ventanas flotantes —
ToggleFloat(Super+f) saca la enfocada del teselado a un rectángulo libre (centrado, 60 %);Workspaceguarda las flotantes aparte,layout()las pone al final yWindowPlacement/BodyOp::Configurellevanfloating: boolpara que el Cuerpo las componga por encima. - Pantalla completa —
ToggleFullscreen(Super+Shift+f): la ventana cubre toda la salida (sin gap), oculta al resto y se lleva el foco;Workspace.fullscreen: Option<WindowId>, y el Cuerpo le fija el estadoxdg_toplevel Fullscreen. También atiende la petición del propio cliente (xdg set_fullscreen→BodyEvent::FullscreenRequest), así que un reproductor o un juego entran a pantalla completa solos. - Multi-monitor — cada
Outputmuestra un escritorio distinto;SwitchWorkspaceactúa sobre la salida enfocada (y la intercambia si el escritorio pedido ya lo muestra otra salida);FocusOutputNext(Super+o) mueve el foco entre monitores. El foco del teclado es único — sólo la ventana enfocada de la salida enfocada. - Scratchpad —
SendToScratchpadguarda la ventana enfocada (sale del teselado, en ningún escritorio);ToggleScratchpad(Super+`) la invoca flotando y centrada en el escritorio actual, o la oculta — estilo terminal desplegable.Desktop.scratchpad: Vec<WindowId>;mirada-ctl windowsla lista comoesc scratch. - Layout y área maestra por el API — los 7 modos se intercambian
(
SetLayout/CycleLayout,mirada-ctl layout spiral); el área maestra se redimensiona (grow/shrink-master,Super+l/Super+h);inc/dec-mastercambiannmaster(Super+,/Super+.); ypromote-to-masterlleva la enfocada al puesto maestro (Super+Return—combo_stringya canoniza teclas con nombre:Return,Tab,F5…). - Lanzar programas —
DesktopAction::Spawn(String)(forma textualspawn:<comando>,Super+Shift+Return→spawn:footpor defecto) produce unBrainCommand::Spawn; el Cuerpo lo ejecuta consh -c, y el hijo heredaWAYLAND_DISPLAY.DesktopActiondeja de serCopypor llevar el comando. - HUD interactivo (app
mirada) — los pips de escritorio y las ventanas del lienzo son clicables: clic =applyde la acción. mirada-ctl— control externo por línea de comandos (mirada-ctl focus-next,workspace 3,windows). Habla con el Cerebro por un socket Unix aparte; el módulomirada-brain::ctldefineCtlRequest/CtlReply(marcopostcard),CtlServer/CtlConnysend_request. El Cerebro (la appmiradasiempre;mirada-compositorsólo embebido) abre el socket y atiende en su bucle.DesktopActionviaja como enum serializado: contrato tipado de punta a punta.
cargo run -p mirada-brain --example headless-ctl levanta un Cerebro sin
gráficos para ejercitar mirada-ctl en modo desatendido.
Reglas de ventana
mirada-brain::rules decide qué hacer con una ventana al abrirse:
config declarativa en RON (~/.config/mirada/rules.ron), mismo patrón
que el keymap. Cada Rule casa por subcadena de app_id y/o title
(sin distinguir mayúsculas; vacío = cualquiera) y aplica un destino:
workspace (1..9) y/o floating. Gana la primera regla que case.
El Desktop consulta Rules::resolve en cada WindowOpened — el evento
ya trae app_id/title — y manda la ventana a su escritorio, flotando
si toca. Se carga al arrancar (la primera vez se escribe una plantilla
con ejemplos comentados); las reglas afectan a las ventanas futuras, no
a las ya abiertas.
Dependencias
- Todos los
libcon#![forbid(unsafe_code)]. Cero Wayland, cerosmithayen los seis crates de arriba. - El acoplamiento a Wayland/hardware vive sólo en
mirada-compositor.
Estado
Implementado y verde: mirada-layout (32 tests), mirada-protocol
(11), mirada-brain (65), mirada-link (7), mirada-body (14), las
apps mirada y mirada-compositor (compilan; verificación visual
manual) y mirada-ctl (CLI, probado vía el ejemplo headless-ctl).
El Cuerpo ya existe: mirada-compositor es un compositor Wayland
teselante real sobre smithay. Habla wl_compositor/xdg_shell/
wl_shm/wl_seat/wl_output/wl_data_device/xdg-decoration, compone
las superficies de los clientes con GlesRenderer y aplica la geometría
del Cerebro. Fuerza decoración ServerSide y no dibuja ninguna: las
ventanas teseladas van sin marco (nada de barras de título de cliente).
Reusa mirada-body (contabilidad) y mirada-link (cable). Dos modos de
Cerebro: autónomo (Desktop embebido) o enlazado (MIRADA_SOCKET
→ la app mirada).
Dos backends gráficos (main() elige; --winit/--drm lo fuerzan):
winit— corre anidado, una ventana en la sesión gráfica actual.drm(drm_backend.rs) — corre nativo sobre una TTY, sin sesión anfitriona:libseat(sesión),udev(GPU),DrmDevice+ GBM + EGL +DrmCompositor,libinput(teclado y ratón), buclecalloop. Verificado en hardware: sesión, render, teclado, atajos, clientes, salida limpia. El ratón pinta un cursor de software (unSolidColorRenderElementmarcadoKind::Cursor, encima de todo en un enumFramede elementos de render); el foco sigue al puntero (BodyEvent::PointerEntered) y clics y rueda van a la ventana debajo.Super+arrastre mueve/redimensiona: el Cuerpo calcula el rectángulo y emiteBodyEvent::WindowFloatTo { id, rect }; el Cerebro hace flotar la ventana ahí (Workspace::set_floating). Cada ventana lleva un marco de 2 px (4SolidColorBufferpor ventana,Idestable): azul si tiene el foco, gris si no.
Pendiente — refinamientos del Cuerpo:
| capa pendiente | rol |
|---|---|
puntero en winit |
ratón en el backend anidado (hoy sólo el backend DRM) |
mirada-input |
repetición de teclas, gestos; conmutación de VT, hotplug |
mirada-sandbox |
aislamiento de clientes sobre arje-incarnate |
CRIU (congelar/restaurar ventanas) queda anotado como futuro.