gioser

tawasuyu/brahman

Rust 521TOML 202Markdown 57JSON 197505 Definitions

⎇ f9c4bf594e361ce3a59435f2b263840ae81ac87a crates/apps/mirada-compositor/src/main.rs Rust · 792 lines · 28.0 KB History Who touched what raw

feat(mirada): fullscreen iniciado por el cliente + HUD multi-salida f9c4bf594 · sergio ·

1//! `mirada-compositor` — el Cuerpo del compositor carmen.
2//!
3//! Un compositor Wayland teselante real, sobre `smithay`, con backend
4//! `winit`: corre **anidado** como una ventana dentro de tu sesión
5//! gráfica actual (X11 o Wayland). Habla el protocolo Wayland con los
6//! clientes, compone sus superficies y aplica la geometría que decide el
7//! Cerebro.
8//!
9//! Dos modos:
10//!
11//! - **Autónomo** (por defecto): lleva un [`Desktop`] embebido — es un
12//!   compositor teselante completo en un solo proceso. Lánzalo y abre
13//!   clientes; el teclado (`Super+…`) maneja el escritorio.
14//! - **Enlazado** (`MIRADA_SOCKET=/ruta`): el Cuerpo escucha ahí y la
15//!   app `mirada` (el Cerebro GPUI) se conecta; la geometría viaja por
16//!   [`mirada_link`].
17//!
18//! Cómo probarlo en un Linux real: ver `crates/apps/mirada-compositor/README.md`.
19
20use std::sync::Arc;
21use std::time::Instant;
22
23use smithay::backend::input::{InputEvent, KeyState, KeyboardKeyEvent};
24use smithay::backend::renderer::element::surface::{
25    render_elements_from_surface_tree, WaylandSurfaceRenderElement,
26};
27use smithay::backend::renderer::element::Kind;
28use smithay::backend::renderer::gles::GlesRenderer;
29use smithay::backend::renderer::utils::{draw_render_elements, on_commit_buffer_handler};
30use smithay::backend::renderer::{Color32F, Frame, Renderer};
31use smithay::backend::winit::{self, WinitEvent};
32use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState};
33use smithay::input::{Seat, SeatHandler, SeatState};
34use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
35use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
36use smithay::reexports::wayland_server::protocol::wl_buffer;
37use smithay::reexports::wayland_server::protocol::wl_output;
38use smithay::reexports::wayland_server::protocol::wl_seat;
39use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
40use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
41use smithay::reexports::winit::platform::pump_events::PumpStatus;
42use smithay::utils::{Rectangle, SERIAL_COUNTER};
43use smithay::utils::{Serial, Transform};
44use smithay::wayland::buffer::BufferHandler;
45use smithay::wayland::compositor::{
46    with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler,
47    CompositorState, SurfaceAttributes, TraversalAction,
48};
49use smithay::wayland::selection::data_device::{
50    ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
51};
52use smithay::wayland::selection::SelectionHandler;
53use smithay::wayland::shell::xdg::{
54    PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
55    XdgToplevelSurfaceData,
56};
57use smithay::wayland::shm::{ShmHandler, ShmState};
58use smithay::{
59    delegate_compositor, delegate_data_device, delegate_seat, delegate_shm, delegate_xdg_shell,
60};
61
62use mirada_body::{BodyOp, BodyState};
63use mirada_brain::{
64    BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules,
65};
66use mirada_link::BodyLink;
67
68// ---------------------------------------------------------------------
69// Estado
70// ---------------------------------------------------------------------
71
72/// De dónde salen las decisiones de geometría.
73enum Brain {
74    /// El compositor lleva su propio `Desktop` — proceso único.
75    Embedded(Desktop),
76    /// Un Cerebro externo (la app `mirada`) por socket.
77    Linked(BodyLink),
78}
79
80/// Una ventana de cliente que el compositor gestiona.
81struct ManagedWindow {
82    id: u64,
83    toplevel: ToplevelSurface,
84    surface: WlSurface,
85    /// Esquina superior-izquierda en píxeles, según el Cerebro.
86    loc: (i32, i32),
87    visible: bool,
88    /// `true` si flota: se compone por encima de las teseladas.
89    floating: bool,
90}
91
92/// El estado global del compositor.
93struct App {
94    compositor_state: CompositorState,
95    xdg_shell_state: XdgShellState,
96    shm_state: ShmState,
97    seat_state: SeatState<Self>,
98    data_device_state: DataDeviceState,
99    seat: Seat<Self>,
100    keyboard: Option<KeyboardHandle<Self>>,
101
102    /// Ventanas gestionadas, en orden de aparición.
103    windows: Vec<ManagedWindow>,
104    /// La contabilidad del Cuerpo (mirada-body).
105    body: BodyState,
106    /// El Cerebro: embebido o enlazado.
107    brain: Brain,
108    /// Atajos globales a interceptar (los registra el Cerebro).
109    grabs: Vec<String>,
110    /// Atajo capturado en el último evento de teclado, pendiente de enviar.
111    pending_keybind: Option<String>,
112    next_id: u64,
113    running: bool,
114}
115
116impl App {
117    /// Inyecta un evento del Cuerpo en el Cerebro y aplica su respuesta.
118    fn brain_feed(&mut self, event: BodyEvent) {
119        let cmds = match &mut self.brain {
120            Brain::Embedded(desktop) => desktop.on_event(event),
121            Brain::Linked(link) => {
122                let _ = link.send(&event);
123                Vec::new()
124            }
125        };
126        self.apply_commands(cmds);
127    }
128
129    /// Drena los comandos de un Cerebro enlazado (no hace nada si es embebido).
130    fn brain_poll(&mut self) {
131        let cmds = match &self.brain {
132            Brain::Linked(link) => link.drain(),
133            Brain::Embedded(_) => Vec::new(),
134        };
135        if !cmds.is_empty() {
136            self.apply_commands(cmds);
137        }
138    }
139
140    /// Atiende una petición del API de control (`mirada-ctl`).
141    fn serve_ctl(&mut self, req: CtlRequest) -> CtlReply {
142        match req {
143            CtlRequest::Do(action) => {
144                let cmds = match &mut self.brain {
145                    Brain::Embedded(d) => Some(d.apply(action)),
146                    Brain::Linked(_) => None,
147                };
148                match cmds {
149                    Some(cmds) => {
150                        self.apply_commands(cmds);
151                        CtlReply::Ok
152                    }
153                    None => CtlReply::Error(
154                        "el Cerebro es externo; usa mirada-ctl contra la app mirada".into(),
155                    ),
156                }
157            }
158            CtlRequest::ListWindows => match &self.brain {
159                Brain::Embedded(d) => CtlReply::Windows(d.window_lines()),
160                Brain::Linked(_) => CtlReply::Error("el Cerebro es externo".into()),
161            },
162        }
163    }
164
165    /// Traduce los comandos del Cerebro a operaciones y las ejecuta.
166    fn apply_commands(&mut self, cmds: Vec<BrainCommand>) {
167        for cmd in cmds {
168            let ops = self.body.apply(cmd);
169            for op in ops {
170                self.exec_op(op);
171            }
172        }
173    }
174
175    /// Ejecuta una operación concreta sobre las superficies reales.
176    fn exec_op(&mut self, op: BodyOp) {
177        match op {
178            BodyOp::Configure { id, rect, visible, floating, fullscreen } => {
179                if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
180                    w.loc = (rect.x, rect.y);
181                    w.visible = visible;
182                    w.floating = floating;
183                    w.toplevel.with_pending_state(|s| {
184                        s.size = Some((rect.w.max(1), rect.h.max(1)).into());
185                        if fullscreen {
186                            s.states.set(xdg_toplevel::State::Fullscreen);
187                        } else {
188                            s.states.unset(xdg_toplevel::State::Fullscreen);
189                        }
190                    });
191                    w.toplevel.send_pending_configure();
192                }
193            }
194            BodyOp::Focus(id) => {
195                let mut target = None;
196                for w in &self.windows {
197                    let active = w.id == id;
198                    if active {
199                        target = Some(w.surface.clone());
200                    }
201                    w.toplevel.with_pending_state(|s| {
202                        if active {
203                            s.states.set(xdg_toplevel::State::Activated);
204                        } else {
205                            s.states.unset(xdg_toplevel::State::Activated);
206                        }
207                    });
208                    w.toplevel.send_pending_configure();
209                }
210                if let Some(kb) = self.keyboard.clone() {
211                    kb.set_focus(self, target, SERIAL_COUNTER.next_serial());
212                }
213            }
214            BodyOp::Unfocus => {
215                if let Some(kb) = self.keyboard.clone() {
216                    kb.set_focus(self, Option::<WlSurface>::None, SERIAL_COUNTER.next_serial());
217                }
218            }
219            BodyOp::CloseClient(id) | BodyOp::KillClient(id) => {
220                if let Some(w) = self.windows.iter().find(|w| w.id == id) {
221                    w.toplevel.send_close();
222                }
223            }
224            BodyOp::SetGrabs(keys) => self.grabs = keys,
225            BodyOp::SetCursor(_) => {}
226            BodyOp::Shutdown => self.running = false,
227        }
228    }
229
230    /// Registra un toplevel recién creado y avisa al Cerebro.
231    fn register_toplevel(&mut self, toplevel: ToplevelSurface) {
232        let surface = toplevel.wl_surface().clone();
233        let id = self.next_id;
234        self.next_id += 1;
235
236        let (app_id, title) = with_states(&surface, |states| {
237            states
238                .data_map
239                .get::<XdgToplevelSurfaceData>()
240                .and_then(|d| d.lock().ok())
241                .map(|d| {
242                    (
243                        d.app_id.clone().unwrap_or_default(),
244                        d.title.clone().unwrap_or_default(),
245                    )
246                })
247                .unwrap_or_default()
248        });
249        let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
250        let title = if title.is_empty() { format!("ventana {id}") } else { title };
251
252        self.windows.push(ManagedWindow {
253            id,
254            toplevel,
255            surface,
256            loc: (0, 0),
257            visible: false,
258            floating: false,
259        });
260        let ev = self.body.open_surface(id, app_id, title);
261        self.brain_feed(ev);
262    }
263}
264
265// ---------------------------------------------------------------------
266// Handlers de protocolo
267// ---------------------------------------------------------------------
268
269impl CompositorHandler for App {
270    fn compositor_state(&mut self) -> &mut CompositorState {
271        &mut self.compositor_state
272    }
273
274    fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
275        &client.get_data::<ClientState>().unwrap().compositor_state
276    }
277
278    fn commit(&mut self, surface: &WlSurface) {
279        on_commit_buffer_handler::<Self>(surface);
280    }
281}
282
283impl BufferHandler for App {
284    fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
285}
286
287impl ShmHandler for App {
288    fn shm_state(&self) -> &ShmState {
289        &self.shm_state
290    }
291}
292
293impl XdgShellHandler for App {
294    fn xdg_shell_state(&mut self) -> &mut XdgShellState {
295        &mut self.xdg_shell_state
296    }
297
298    fn new_toplevel(&mut self, surface: ToplevelSurface) {
299        surface.with_pending_state(|s| {
300            s.states.set(xdg_toplevel::State::Activated);
301        });
302        surface.send_configure();
303        self.register_toplevel(surface);
304    }
305
306    fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
307        let pos = self
308            .windows
309            .iter()
310            .position(|w| w.surface == *surface.wl_surface());
311        if let Some(pos) = pos {
312            let id = self.windows.remove(pos).id;
313            if let Some(ev) = self.body.close_surface(id) {
314                self.brain_feed(ev);
315            }
316        }
317    }
318
319    fn title_changed(&mut self, surface: ToplevelSurface) {
320        let id = self
321            .windows
322            .iter()
323            .find(|w| w.surface == *surface.wl_surface())
324            .map(|w| w.id);
325        let Some(id) = id else { return };
326        let title = with_states(surface.wl_surface(), |states| {
327            states
328                .data_map
329                .get::<XdgToplevelSurfaceData>()
330                .and_then(|d| d.lock().ok())
331                .and_then(|d| d.title.clone())
332                .unwrap_or_default()
333        });
334        if let Some(ev) = self.body.retitle_surface(id, title) {
335            self.brain_feed(ev);
336        }
337    }
338
339    fn fullscreen_request(
340        &mut self,
341        surface: ToplevelSurface,
342        _output: Option<wl_output::WlOutput>,
343    ) {
344        let id = self
345            .windows
346            .iter()
347            .find(|w| w.surface == *surface.wl_surface())
348            .map(|w| w.id);
349        if let Some(id) = id {
350            self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: true });
351        }
352    }
353
354    fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
355        let id = self
356            .windows
357            .iter()
358            .find(|w| w.surface == *surface.wl_surface())
359            .map(|w| w.id);
360        if let Some(id) = id {
361            self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: false });
362        }
363    }
364
365    fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
366        let _ = surface.send_configure();
367    }
368
369    fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
370
371    fn reposition_request(
372        &mut self,
373        _surface: PopupSurface,
374        _positioner: PositionerState,
375        _token: u32,
376    ) {
377    }
378}
379
380impl SelectionHandler for App {
381    type SelectionUserData = ();
382}
383
384impl DataDeviceHandler for App {
385    fn data_device_state(&self) -> &DataDeviceState {
386        &self.data_device_state
387    }
388}
389impl ClientDndGrabHandler for App {}
390impl ServerDndGrabHandler for App {
391    fn send(&mut self, _mime_type: String, _fd: std::os::unix::io::OwnedFd, _seat: Seat<Self>) {}
392}
393
394impl SeatHandler for App {
395    type KeyboardFocus = WlSurface;
396    type PointerFocus = WlSurface;
397    type TouchFocus = WlSurface;
398
399    fn seat_state(&mut self) -> &mut SeatState<Self> {
400        &mut self.seat_state
401    }
402
403    fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
404    fn cursor_image(
405        &mut self,
406        _seat: &Seat<Self>,
407        _image: smithay::input::pointer::CursorImageStatus,
408    ) {
409    }
410}
411
412delegate_compositor!(App);
413delegate_xdg_shell!(App);
414delegate_shm!(App);
415delegate_seat!(App);
416delegate_data_device!(App);
417
418// ---------------------------------------------------------------------
419// Datos por cliente
420// ---------------------------------------------------------------------
421
422#[derive(Default)]
423struct ClientState {
424    compositor_state: CompositorClientState,
425}
426impl ClientData for ClientState {
427    fn initialized(&self, _id: ClientId) {}
428    fn disconnected(&self, _id: ClientId, _reason: DisconnectReason) {}
429}
430
431// ---------------------------------------------------------------------
432// Utilidades
433// ---------------------------------------------------------------------
434
435/// Construye la cadena de un atajo (`"Super+Shift+j"`) desde el estado de
436/// modificadores y el keysym, con el mismo formato que el mapa de teclas
437/// de [`mirada_brain`]. `None` si no es una tecla mapeable.
438fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option<String> {
439    let utf = xkb::keysym_to_utf8(sym);
440    let key = utf.trim_end_matches('\0');
441    let name = if key == " " {
442        "space".to_string()
443    } else {
444        // ¿Es un único carácter imprimible? Entonces la tecla es ese carácter.
445        let mut chars = key.chars();
446        match (chars.next(), chars.next()) {
447            (Some(c), None) if c.is_ascii_graphic() => c.to_ascii_lowercase().to_string(),
448            // Si no, una tecla con nombre: Return, Tab, Up, F5…
449            _ => named_key(sym)?,
450        }
451    };
452    let mut combo = String::new();
453    if mods.logo {
454        combo.push_str("Super+");
455    }
456    if mods.ctrl {
457        combo.push_str("Ctrl+");
458    }
459    if mods.shift {
460        combo.push_str("Shift+");
461    }
462    if mods.alt {
463        combo.push_str("Alt+");
464    }
465    combo.push_str(&name);
466    Some(combo)
467}
468
469/// El nombre canónico de una tecla especial — `Return`, `Tab`, `Up`,
470/// `F5`… `None` si xkb no le da un nombre razonable.
471fn named_key(sym: Keysym) -> Option<String> {
472    let name = xkb::keysym_get_name(sym);
473    if name.is_empty() || name == "NoSymbol" || name.starts_with("0x") {
474        None
475    } else {
476        Some(name)
477    }
478}
479
480/// Despacha los callbacks de frame de un árbol de superficies: avisa a
481/// cada cliente de que puede dibujar el siguiente cuadro.
482fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
483    with_surface_tree_downward(
484        surface,
485        (),
486        |_, _, &()| TraversalAction::DoChildren(()),
487        |_surf, states, &()| {
488            for callback in states
489                .cached_state
490                .get::<SurfaceAttributes>()
491                .current()
492                .frame_callbacks
493                .drain(..)
494            {
495                callback.done(time);
496            }
497        },
498        |_, _, &()| true,
499    );
500}
501
502// ---------------------------------------------------------------------
503// Bucle principal
504// ---------------------------------------------------------------------
505
506/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
507fn load_user_rules() -> Rules {
508    match Rules::default_path() {
509        Some(p) => Rules::load_or_default(&p),
510        None => Rules::default(),
511    }
512}
513
514fn run() -> Result<(), Box<dyn std::error::Error>> {
515    let mut display: Display<App> = Display::new()?;
516    let dh = display.handle();
517
518    let mut seat_state = SeatState::new();
519    let seat = seat_state.new_wl_seat(&dh, "mirada");
520
521    // El keymap del usuario (`~/.config/mirada/keymap.ron`). Sólo lo usa
522    // el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
523    let keymap_path = Keymap::default_path();
524
525    // Elige el Cerebro: enlazado si `MIRADA_SOCKET` está puesto.
526    let brain = match std::env::var("MIRADA_SOCKET") {
527        Ok(path) => {
528            println!("mirada-compositor · esperando al Cerebro en {path} …");
529            let link = BodyLink::listen(&path)?;
530            println!("mirada-compositor · Cerebro conectado.");
531            Brain::Linked(link)
532        }
533        Err(_) => {
534            println!("mirada-compositor · modo autónomo (Cerebro embebido).");
535            let keymap = match &keymap_path {
536                Some(p) => Keymap::load_or_init(p),
537                None => Keymap::default(),
538            };
539            let mut desktop = Desktop::with_keymap(keymap);
540            desktop.set_rules(load_user_rules());
541            Brain::Embedded(desktop)
542        }
543    };
544
545    let mut state = App {
546        compositor_state: CompositorState::new::<App>(&dh),
547        xdg_shell_state: XdgShellState::new::<App>(&dh),
548        shm_state: ShmState::new::<App>(&dh, Vec::new()),
549        seat_state,
550        data_device_state: DataDeviceState::new::<App>(&dh),
551        seat,
552        keyboard: None,
553        windows: Vec::new(),
554        body: BodyState::new(),
555        brain,
556        grabs: Vec::new(),
557        pending_keybind: None,
558        next_id: 1,
559        running: true,
560    };
561
562    let keyboard = state.seat.add_keyboard(Default::default(), 200, 25)?;
563    state.keyboard = Some(keyboard.clone());
564
565    // En modo embebido, el propio Desktop dicta los atajos a interceptar.
566    if let Brain::Embedded(desktop) = &state.brain {
567        let grab = desktop.grab_keys();
568        state.apply_commands(vec![grab]);
569    }
570
571    // Vigilancia del keymap para recargarlo en caliente — sólo tiene
572    // sentido con el Cerebro embebido.
573    let keymap_watch = match (&state.brain, &keymap_path) {
574        (Brain::Embedded(_), Some(p)) => Keymap::watch(p).ok(),
575        _ => None,
576    };
577    if keymap_watch.is_some() {
578        println!("mirada-compositor · vigilando el keymap (recarga en caliente).");
579    }
580
581    // API de control (mirada-ctl) — sólo con el Cerebro embebido; si es
582    // externo, el socket de control lo abre él.
583    let ctl = match &state.brain {
584        Brain::Embedded(_) => {
585            let path = mirada_brain::ctl::default_socket_path();
586            match CtlServer::bind(&path) {
587                Ok(s) => {
588                    println!("mirada-compositor · API de control en {}", path.display());
589                    Some(s)
590                }
591                Err(e) => {
592                    eprintln!("mirada-compositor · sin API de control: {e}");
593                    None
594                }
595            }
596        }
597        Brain::Linked(_) => None,
598    };
599
600    // El backend gráfico va primero. winit abre la ventana del compositor
601    // dentro de tu sesión gráfica anfitriona, y para encontrarla lee
602    // `WAYLAND_DISPLAY` / `DISPLAY` del entorno. Si publicáramos antes
603    // nuestro propio socket en `WAYLAND_DISPLAY`, winit intentaría
604    // anidarse en nosotros mismos —un socket que aún no atiende a nadie—
605    // y se quedaría colgado para siempre.
606    let (mut backend, mut winit) = match winit::init::<GlesRenderer>() {
607        Ok(pair) => pair,
608        Err(e) => {
609            eprintln!("mirada-compositor · no pude abrir la ventana: {e}");
610            eprintln!(
611                "   El backend `winit` necesita una sesión gráfica anfitriona\n   \
612                 (X11 o Wayland) donde dibujar la ventana del compositor.\n   \
613                 Aquí no hay ninguna: DISPLAY='{}', WAYLAND_DISPLAY='{}',\n   \
614                 XDG_SESSION_TYPE='{}'.\n   \
615                 Lánzalo desde un escritorio gráfico, o desde un servidor X\n   \
616                 virtual (Xvfb) al que te conectes por VNC.",
617                std::env::var("DISPLAY").unwrap_or_default(),
618                std::env::var("WAYLAND_DISPLAY").unwrap_or_default(),
619                std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "tty".into()),
620            );
621            return Err(e.into());
622        }
623    };
624
625    // Ahora sí, nuestro propio socket Wayland — y `WAYLAND_DISPLAY` se
626    // publica *después* de winit, sólo para los clientes que lancemos
627    // como procesos hijos.
628    let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
629    let socket_name = listener
630        .socket_name()
631        .and_then(|s| s.to_str())
632        .unwrap_or("wayland-?")
633        .to_string();
634    std::env::set_var("WAYLAND_DISPLAY", &socket_name);
635    println!("mirada-compositor · escuchando en WAYLAND_DISPLAY={socket_name}");
636    println!("   lanza un cliente:  WAYLAND_DISPLAY={socket_name} foot");
637
638    let start = Instant::now();
639    let mut clients = Vec::new();
640
641    // Salida inicial = el tamaño de la ventana winit.
642    {
643        let size = backend.window_size();
644        let ev = state.body.add_output(0, size.w, size.h);
645        state.brain_feed(ev);
646    }
647
648    while state.running {
649        // 1 · Eventos del backend (teclado, redimensión, cierre).
650        let status = winit.dispatch_new_events(|event| match event {
651            WinitEvent::CloseRequested => state.running = false,
652            WinitEvent::Resized { size, .. } => {
653                let ev = state.body.remove_output(0);
654                state.brain_feed(ev);
655                let ev = state.body.add_output(0, size.w, size.h);
656                state.brain_feed(ev);
657            }
658            WinitEvent::Input(InputEvent::Keyboard { event }) => {
659                let code = event.key_code();
660                let key_state = event.state();
661                let pressed = key_state == KeyState::Pressed;
662                let time = start.elapsed().as_millis() as u32;
663                keyboard.clone().input::<(), _>(
664                    &mut state,
665                    code,
666                    key_state,
667                    SERIAL_COUNTER.next_serial(),
668                    time,
669                    |st, mods, handle| {
670                        if !pressed {
671                            return FilterResult::Forward;
672                        }
673                        if let Some(combo) = combo_string(mods, handle.modified_sym()) {
674                            if st.grabs.contains(&combo) {
675                                st.pending_keybind = Some(combo);
676                                return FilterResult::Intercept(());
677                            }
678                        }
679                        FilterResult::Forward
680                    },
681                );
682                if let Some(combo) = state.pending_keybind.take() {
683                    let ev = state.body.keybind(combo);
684                    state.brain_feed(ev);
685                }
686            }
687            _ => {}
688        });
689        if let PumpStatus::Exit(_) = status {
690            break;
691        }
692
693        // 2 · Comandos de un Cerebro enlazado.
694        state.brain_poll();
695
696        // 2 bis · Recarga del keymap si el archivo cambió en disco.
697        if keymap_watch.as_ref().is_some_and(|w| w.changed()) {
698            if let Some(path) = &keymap_path {
699                match Keymap::load(path) {
700                    Ok(km) => {
701                        let cmd = if let Brain::Embedded(d) = &mut state.brain {
702                            Some(d.set_keymap(km))
703                        } else {
704                            None
705                        };
706                        if let Some(cmd) = cmd {
707                            state.apply_commands(vec![cmd]);
708                        }
709                        println!("mirada-compositor · keymap recargado.");
710                    }
711                    Err(e) => eprintln!(
712                        "mirada-compositor · keymap inválido, conservo el anterior: {e}"
713                    ),
714                }
715            }
716        }
717
718        // 2 ter · Peticiones del API de control (mirada-ctl).
719        if let Some(ctl) = &ctl {
720            while let Some(mut conn) = ctl.poll() {
721                let reply = match conn.read_request() {
722                    Ok(Some(req)) => state.serve_ctl(req),
723                    Ok(None) => continue,
724                    Err(e) => CtlReply::Error(format!("{e}")),
725                };
726                let _ = conn.reply(&reply);
727            }
728        }
729
730        // 3 · Composición de las superficies en sus rectángulos.
731        let size = backend.window_size();
732        let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
733        {
734            let (renderer, mut framebuffer) = backend.bind().unwrap();
735            // Orden de pintado: la lista de elementos va front-to-back
736            // (índice 0 = encima), así que las flotantes —que deben
737            // quedar sobre las teseladas— se ordenan primero. `sort_by_key`
738            // es estable: dentro de cada grupo se respeta el orden de apertura.
739            let mut shown: Vec<&ManagedWindow> =
740                state.windows.iter().filter(|w| w.visible).collect();
741            shown.sort_by_key(|w| !w.floating);
742            let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = shown
743                .iter()
744                .flat_map(|w| {
745                    render_elements_from_surface_tree(
746                        renderer,
747                        &w.surface,
748                        w.loc,
749                        1.0,
750                        1.0,
751                        Kind::Unspecified,
752                    )
753                })
754                .collect();
755            let mut frame = renderer
756                .render(&mut framebuffer, size, Transform::Flipped180)
757                .unwrap();
758            frame
759                .clear(Color32F::new(0.05, 0.05, 0.08, 1.0), &[damage])
760                .unwrap();
761            draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap();
762            let _ = frame.finish().unwrap();
763        }
764
765        // 4 · Callbacks de frame + clientes nuevos + flush.
766        let time = start.elapsed().as_millis() as u32;
767        for w in &state.windows {
768            send_frames_surface_tree(&w.surface, time);
769        }
770        if let Some(stream) = listener.accept()? {
771            let client = display
772                .handle()
773                .insert_client(stream, Arc::new(ClientState::default()))
774                .unwrap();
775            clients.push(client);
776        }
777        display.dispatch_clients(&mut state)?;
778        display.flush_clients()?;
779
780        backend.submit(Some(&[damage])).unwrap();
781    }
782
783    println!("mirada-compositor · adiós.");
784    Ok(())
785}
786
787fn main() {
788    if let Err(e) = run() {
789        eprintln!("mirada-compositor · error: {e}");
790        std::process::exit(1);
791    }
792}