gioser

tawasuyu/brahman

Rust 532TOML 203Markdown 58JSON 197629 Definiciones

⎇ 90bffec3f169c244946d37d6a2a6d02805e52973 crates/apps/mirada-compositor/src/main.rs Rust · 977 líneas · 35.0 KB Historia Quién tocó qué crudo

feat(mirada): mover/redimensionar ventanas con el ratón 90bffec3f · 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::{
30    draw_render_elements, on_commit_buffer_handler, with_renderer_surface_state,
31};
32use smithay::backend::renderer::{Color32F, Frame, Renderer};
33use smithay::backend::winit::{self, WinitEvent};
34use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState};
35use smithay::input::pointer::PointerHandle;
36use smithay::input::{Seat, SeatHandler, SeatState};
37use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
38use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
39use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
40use smithay::reexports::wayland_server::protocol::wl_buffer;
41use smithay::reexports::wayland_server::protocol::wl_output;
42use smithay::reexports::wayland_server::protocol::wl_seat;
43use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
44use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, ListeningSocket};
45use smithay::reexports::winit::platform::pump_events::PumpStatus;
46use smithay::utils::{Rectangle, SERIAL_COUNTER};
47use smithay::utils::{Serial, Transform};
48use smithay::wayland::buffer::BufferHandler;
49use smithay::wayland::compositor::{
50    with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler,
51    CompositorState, SurfaceAttributes, TraversalAction,
52};
53use smithay::wayland::selection::data_device::{
54    ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
55};
56use smithay::wayland::selection::SelectionHandler;
57use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorationState};
58use smithay::wayland::shell::xdg::{
59    PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
60    XdgToplevelSurfaceData,
61};
62use smithay::wayland::output::OutputHandler;
63use smithay::wayland::shm::{ShmHandler, ShmState};
64use smithay::{
65    delegate_compositor, delegate_data_device, delegate_output, delegate_seat, delegate_shm,
66    delegate_xdg_decoration, delegate_xdg_shell,
67};
68
69use mirada_body::{BodyOp, BodyState};
70use mirada_brain::{
71    BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules,
72};
73use mirada_link::BodyLink;
74
75mod drm_backend;
76
77// ---------------------------------------------------------------------
78// Estado
79// ---------------------------------------------------------------------
80
81/// De dónde salen las decisiones de geometría.
82enum Brain {
83    /// El compositor lleva su propio `Desktop` — proceso único.
84    Embedded(Desktop),
85    /// Un Cerebro externo (la app `mirada`) por socket.
86    Linked(BodyLink),
87}
88
89/// Una ventana de cliente que el compositor gestiona.
90struct ManagedWindow {
91    id: u64,
92    toplevel: ToplevelSurface,
93    surface: WlSurface,
94    /// Esquina superior-izquierda de la celda asignada, según el Cerebro.
95    loc: (i32, i32),
96    /// Tamaño de la celda asignada — para centrar la ventana si el
97    /// cliente presenta una superficie más pequeña.
98    size: (i32, i32),
99    visible: bool,
100    /// `true` si flota: se compone por encima de las teseladas.
101    floating: bool,
102}
103
104/// Un arrastre de ratón en curso: mueve o redimensiona una ventana.
105struct DragGrab {
106    /// La ventana que se arrastra.
107    id: u64,
108    /// Mover (`Super`+botón izquierdo) o redimensionar (`Super`+derecho).
109    mode: DragMode,
110    /// Posición del puntero al empezar el arrastre.
111    start_pointer: (f64, f64),
112    /// Rectángulo `(x, y, w, h)` de la ventana al empezar.
113    start_rect: (i32, i32, i32, i32),
114}
115
116/// Qué le hace un arrastre a la ventana.
117#[derive(Clone, Copy)]
118enum DragMode {
119    /// Reubicar la ventana — la esquina la sigue al puntero.
120    Move,
121    /// Redimensionarla — la esquina inferior-derecha sigue al puntero.
122    Resize,
123}
124
125/// El estado global del compositor.
126struct App {
127    compositor_state: CompositorState,
128    xdg_shell_state: XdgShellState,
129    shm_state: ShmState,
130    seat_state: SeatState<Self>,
131    data_device_state: DataDeviceState,
132    seat: Seat<Self>,
133    keyboard: Option<KeyboardHandle<Self>>,
134    pointer: Option<PointerHandle<Self>>,
135    /// Posición del puntero en coordenadas globales.
136    pointer_loc: (f64, f64),
137    /// Arrastre de ventana en curso (mover o redimensionar con el ratón).
138    drag: Option<DragGrab>,
139
140    /// Ventanas gestionadas, en orden de aparición.
141    windows: Vec<ManagedWindow>,
142    /// La contabilidad del Cuerpo (mirada-body).
143    body: BodyState,
144    /// El Cerebro: embebido o enlazado.
145    brain: Brain,
146    /// Atajos globales a interceptar (los registra el Cerebro).
147    grabs: Vec<String>,
148    /// Atajo capturado en el último evento de teclado, pendiente de enviar.
149    pending_keybind: Option<String>,
150    next_id: u64,
151    running: bool,
152}
153
154impl App {
155    /// Inyecta un evento del Cuerpo en el Cerebro y aplica su respuesta.
156    fn brain_feed(&mut self, event: BodyEvent) {
157        let cmds = match &mut self.brain {
158            Brain::Embedded(desktop) => desktop.on_event(event),
159            Brain::Linked(link) => {
160                let _ = link.send(&event);
161                Vec::new()
162            }
163        };
164        self.apply_commands(cmds);
165    }
166
167    /// Drena los comandos de un Cerebro enlazado (no hace nada si es embebido).
168    fn brain_poll(&mut self) {
169        let cmds = match &self.brain {
170            Brain::Linked(link) => link.drain(),
171            Brain::Embedded(_) => Vec::new(),
172        };
173        if !cmds.is_empty() {
174            self.apply_commands(cmds);
175        }
176    }
177
178    /// Atiende una petición del API de control (`mirada-ctl`).
179    fn serve_ctl(&mut self, req: CtlRequest) -> CtlReply {
180        match req {
181            CtlRequest::Do(action) => {
182                let cmds = match &mut self.brain {
183                    Brain::Embedded(d) => Some(d.apply(action)),
184                    Brain::Linked(_) => None,
185                };
186                match cmds {
187                    Some(cmds) => {
188                        self.apply_commands(cmds);
189                        CtlReply::Ok
190                    }
191                    None => CtlReply::Error(
192                        "el Cerebro es externo; usa mirada-ctl contra la app mirada".into(),
193                    ),
194                }
195            }
196            CtlRequest::ListWindows => match &self.brain {
197                Brain::Embedded(d) => CtlReply::Windows(d.window_lines()),
198                Brain::Linked(_) => CtlReply::Error("el Cerebro es externo".into()),
199            },
200        }
201    }
202
203    /// Traduce los comandos del Cerebro a operaciones y las ejecuta.
204    fn apply_commands(&mut self, cmds: Vec<BrainCommand>) {
205        for cmd in cmds {
206            let ops = self.body.apply(cmd);
207            for op in ops {
208                self.exec_op(op);
209            }
210        }
211    }
212
213    /// Ejecuta una operación concreta sobre las superficies reales.
214    fn exec_op(&mut self, op: BodyOp) {
215        match op {
216            BodyOp::Configure { id, rect, visible, floating, fullscreen } => {
217                if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
218                    w.loc = (rect.x, rect.y);
219                    w.size = (rect.w, rect.h);
220                    w.visible = visible;
221                    w.floating = floating;
222                    w.toplevel.with_pending_state(|s| {
223                        s.size = Some((rect.w.max(1), rect.h.max(1)).into());
224                        if fullscreen {
225                            s.states.set(xdg_toplevel::State::Fullscreen);
226                        } else {
227                            s.states.unset(xdg_toplevel::State::Fullscreen);
228                        }
229                    });
230                    w.toplevel.send_pending_configure();
231                }
232            }
233            BodyOp::Focus(id) => {
234                let mut target = None;
235                for w in &self.windows {
236                    let active = w.id == id;
237                    if active {
238                        target = Some(w.surface.clone());
239                    }
240                    w.toplevel.with_pending_state(|s| {
241                        if active {
242                            s.states.set(xdg_toplevel::State::Activated);
243                        } else {
244                            s.states.unset(xdg_toplevel::State::Activated);
245                        }
246                    });
247                    w.toplevel.send_pending_configure();
248                }
249                if let Some(kb) = self.keyboard.clone() {
250                    kb.set_focus(self, target, SERIAL_COUNTER.next_serial());
251                }
252            }
253            BodyOp::Unfocus => {
254                if let Some(kb) = self.keyboard.clone() {
255                    kb.set_focus(self, Option::<WlSurface>::None, SERIAL_COUNTER.next_serial());
256                }
257            }
258            BodyOp::CloseClient(id) | BodyOp::KillClient(id) => {
259                if let Some(w) = self.windows.iter().find(|w| w.id == id) {
260                    w.toplevel.send_close();
261                }
262            }
263            BodyOp::SetGrabs(keys) => self.grabs = keys,
264            BodyOp::SetCursor(_) => {}
265            BodyOp::Shutdown => self.running = false,
266        }
267    }
268
269    /// Registra un toplevel recién creado y avisa al Cerebro.
270    fn register_toplevel(&mut self, toplevel: ToplevelSurface) {
271        let surface = toplevel.wl_surface().clone();
272        let id = self.next_id;
273        self.next_id += 1;
274
275        let (app_id, title) = with_states(&surface, |states| {
276            states
277                .data_map
278                .get::<XdgToplevelSurfaceData>()
279                .and_then(|d| d.lock().ok())
280                .map(|d| {
281                    (
282                        d.app_id.clone().unwrap_or_default(),
283                        d.title.clone().unwrap_or_default(),
284                    )
285                })
286                .unwrap_or_default()
287        });
288        let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
289        let title = if title.is_empty() { format!("ventana {id}") } else { title };
290
291        self.windows.push(ManagedWindow {
292            id,
293            toplevel,
294            surface,
295            loc: (0, 0),
296            size: (0, 0),
297            visible: false,
298            floating: false,
299        });
300        let ev = self.body.open_surface(id, app_id, title);
301        self.brain_feed(ev);
302    }
303}
304
305// ---------------------------------------------------------------------
306// Handlers de protocolo
307// ---------------------------------------------------------------------
308
309impl CompositorHandler for App {
310    fn compositor_state(&mut self) -> &mut CompositorState {
311        &mut self.compositor_state
312    }
313
314    fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
315        &client.get_data::<ClientState>().unwrap().compositor_state
316    }
317
318    fn commit(&mut self, surface: &WlSurface) {
319        on_commit_buffer_handler::<Self>(surface);
320    }
321}
322
323impl BufferHandler for App {
324    fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
325}
326
327impl ShmHandler for App {
328    fn shm_state(&self) -> &ShmState {
329        &self.shm_state
330    }
331}
332
333impl XdgShellHandler for App {
334    fn xdg_shell_state(&mut self) -> &mut XdgShellState {
335        &mut self.xdg_shell_state
336    }
337
338    fn new_toplevel(&mut self, surface: ToplevelSurface) {
339        surface.with_pending_state(|s| {
340            s.states.set(xdg_toplevel::State::Activated);
341        });
342        surface.send_configure();
343        self.register_toplevel(surface);
344    }
345
346    fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
347        let pos = self
348            .windows
349            .iter()
350            .position(|w| w.surface == *surface.wl_surface());
351        if let Some(pos) = pos {
352            let id = self.windows.remove(pos).id;
353            if let Some(ev) = self.body.close_surface(id) {
354                self.brain_feed(ev);
355            }
356        }
357    }
358
359    fn title_changed(&mut self, surface: ToplevelSurface) {
360        let id = self
361            .windows
362            .iter()
363            .find(|w| w.surface == *surface.wl_surface())
364            .map(|w| w.id);
365        let Some(id) = id else { return };
366        let title = with_states(surface.wl_surface(), |states| {
367            states
368                .data_map
369                .get::<XdgToplevelSurfaceData>()
370                .and_then(|d| d.lock().ok())
371                .and_then(|d| d.title.clone())
372                .unwrap_or_default()
373        });
374        if let Some(ev) = self.body.retitle_surface(id, title) {
375            self.brain_feed(ev);
376        }
377    }
378
379    fn fullscreen_request(
380        &mut self,
381        surface: ToplevelSurface,
382        _output: Option<wl_output::WlOutput>,
383    ) {
384        let id = self
385            .windows
386            .iter()
387            .find(|w| w.surface == *surface.wl_surface())
388            .map(|w| w.id);
389        if let Some(id) = id {
390            self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: true });
391        }
392    }
393
394    fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
395        let id = self
396            .windows
397            .iter()
398            .find(|w| w.surface == *surface.wl_surface())
399            .map(|w| w.id);
400        if let Some(id) = id {
401            self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: false });
402        }
403    }
404
405    fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
406        let _ = surface.send_configure();
407    }
408
409    fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
410
411    fn reposition_request(
412        &mut self,
413        _surface: PopupSurface,
414        _positioner: PositionerState,
415        _token: u32,
416    ) {
417    }
418}
419
420/// Decoración de ventana: carmen tesela, así que las ventanas no llevan
421/// barra de título. Le decimos a todo cliente que la decoración la pone
422/// el servidor (`ServerSide`) — y como el servidor no dibuja ninguna, la
423/// ventana queda sin marco. Sin esto, clientes como `foot` se dibujan su
424/// propia barra (CSD), que estorba en un escritorio teselante.
425impl XdgDecorationHandler for App {
426    fn new_decoration(&mut self, toplevel: ToplevelSurface) {
427        toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
428        toplevel.send_configure();
429    }
430
431    fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
432        toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
433        toplevel.send_configure();
434    }
435
436    fn unset_mode(&mut self, toplevel: ToplevelSurface) {
437        toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
438        toplevel.send_configure();
439    }
440}
441
442impl SelectionHandler for App {
443    type SelectionUserData = ();
444}
445
446impl DataDeviceHandler for App {
447    fn data_device_state(&self) -> &DataDeviceState {
448        &self.data_device_state
449    }
450}
451impl ClientDndGrabHandler for App {}
452impl ServerDndGrabHandler for App {
453    fn send(&mut self, _mime_type: String, _fd: std::os::unix::io::OwnedFd, _seat: Seat<Self>) {}
454}
455
456impl SeatHandler for App {
457    type KeyboardFocus = WlSurface;
458    type PointerFocus = WlSurface;
459    type TouchFocus = WlSurface;
460
461    fn seat_state(&mut self) -> &mut SeatState<Self> {
462        &mut self.seat_state
463    }
464
465    fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
466    fn cursor_image(
467        &mut self,
468        _seat: &Seat<Self>,
469        _image: smithay::input::pointer::CursorImageStatus,
470    ) {
471    }
472}
473
474/// El protocolo `wl_output` no necesita estado propio — basta con
475/// anunciar el global para que los clientes vean que hay un monitor.
476impl OutputHandler for App {}
477
478delegate_compositor!(App);
479delegate_xdg_shell!(App);
480delegate_xdg_decoration!(App);
481delegate_shm!(App);
482delegate_seat!(App);
483delegate_data_device!(App);
484delegate_output!(App);
485
486// ---------------------------------------------------------------------
487// Datos por cliente
488// ---------------------------------------------------------------------
489
490#[derive(Default)]
491struct ClientState {
492    compositor_state: CompositorClientState,
493}
494impl ClientData for ClientState {
495    fn initialized(&self, _id: ClientId) {}
496    fn disconnected(&self, _id: ClientId, _reason: DisconnectReason) {}
497}
498
499// ---------------------------------------------------------------------
500// Utilidades
501// ---------------------------------------------------------------------
502
503/// Construye la cadena de un atajo (`"Super+Shift+j"`) desde el estado de
504/// modificadores y el keysym, con el mismo formato que el mapa de teclas
505/// de [`mirada_brain`]. `None` si no es una tecla mapeable.
506fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option<String> {
507    let utf = xkb::keysym_to_utf8(sym);
508    let key = utf.trim_end_matches('\0');
509    let name = if key == " " {
510        "space".to_string()
511    } else {
512        // ¿Es un único carácter imprimible? Entonces la tecla es ese carácter.
513        let mut chars = key.chars();
514        match (chars.next(), chars.next()) {
515            (Some(c), None) if c.is_ascii_graphic() => c.to_ascii_lowercase().to_string(),
516            // Si no, una tecla con nombre: Return, Tab, Up, F5…
517            _ => named_key(sym)?,
518        }
519    };
520    let mut combo = String::new();
521    if mods.logo {
522        combo.push_str("Super+");
523    }
524    if mods.ctrl {
525        combo.push_str("Ctrl+");
526    }
527    if mods.shift {
528        combo.push_str("Shift+");
529    }
530    if mods.alt {
531        combo.push_str("Alt+");
532    }
533    combo.push_str(&name);
534    Some(combo)
535}
536
537/// El nombre canónico de una tecla especial — `Return`, `Tab`, `Up`,
538/// `F5`… `None` si xkb no le da un nombre razonable.
539fn named_key(sym: Keysym) -> Option<String> {
540    let name = xkb::keysym_get_name(sym);
541    if name.is_empty() || name == "NoSymbol" || name.starts_with("0x") {
542        None
543    } else {
544        Some(name)
545    }
546}
547
548/// Despacha los callbacks de frame de un árbol de superficies: avisa a
549/// cada cliente de que puede dibujar el siguiente cuadro.
550fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
551    with_surface_tree_downward(
552        surface,
553        (),
554        |_, _, &()| TraversalAction::DoChildren(()),
555        |_surf, states, &()| {
556            for callback in states
557                .cached_state
558                .get::<SurfaceAttributes>()
559                .current()
560                .frame_callbacks
561                .drain(..)
562            {
563                callback.done(time);
564            }
565        },
566        |_, _, &()| true,
567    );
568}
569
570// ---------------------------------------------------------------------
571// Bucle principal
572// ---------------------------------------------------------------------
573
574/// Dónde pintar una ventana: su celda asignada, pero si el cliente
575/// presenta una superficie más pequeña que la celda (p. ej. un terminal
576/// que redondea su tamaño a celdas de texto enteras), se centra en el
577/// hueco sobrante en vez de dejarlo todo a un lado.
578fn render_loc(w: &ManagedWindow) -> (i32, i32) {
579    match with_renderer_surface_state(&w.surface, |s| s.surface_size()) {
580        Some(Some(size)) => {
581            let dx = ((w.size.0 - size.w) / 2).max(0);
582            let dy = ((w.size.1 - size.h) / 2).max(0);
583            (w.loc.0 + dx, w.loc.1 + dy)
584        }
585        _ => w.loc,
586    }
587}
588
589/// El tamaño en píxeles de la superficie de una ventana, si el cliente
590/// ya presentó un buffer. `None` mientras no haya dibujado nada — la usa
591/// el backend DRM para acertar el rectángulo en el test de impacto del
592/// puntero.
593fn surface_px_size(w: &ManagedWindow) -> Option<(i32, i32)> {
594    with_renderer_surface_state(&w.surface, |s| s.surface_size())
595        .flatten()
596        .map(|s| (s.w, s.h))
597}
598
599/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
600fn load_user_rules() -> Rules {
601    match Rules::default_path() {
602        Some(p) => Rules::load_or_default(&p),
603        None => Rules::default(),
604    }
605}
606
607/// Crea y anuncia un `wl_output` (un monitor) en el protocolo Wayland —
608/// muchos clientes (`foot` entre ellos) se niegan a arrancar sin uno.
609/// Devuelve el [`Output`](smithay::output::Output); hay que mantenerlo
610/// vivo mientras el compositor corra.
611fn announce_output(
612    dh: &DisplayHandle,
613    name: &str,
614    width: i32,
615    height: i32,
616    refresh_mhz: i32,
617) -> smithay::output::Output {
618    use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
619    let output = Output::new(
620        name.to_string(),
621        PhysicalProperties {
622            size: (0, 0).into(),
623            subpixel: Subpixel::Unknown,
624            make: "mirada".into(),
625            model: name.to_string(),
626        },
627    );
628    output.create_global::<App>(dh);
629    let mode = Mode { size: (width, height).into(), refresh: refresh_mhz };
630    output.change_current_state(
631        Some(mode),
632        Some(Transform::Normal),
633        Some(Scale::Integer(1)),
634        Some((0, 0).into()),
635    );
636    output.set_preferred(mode);
637    output
638}
639
640/// Lo que comparten los dos backends gráficos: el `Display` de Wayland,
641/// el `App` ya armado y la maquinaria de keymap y control.
642struct Setup {
643    display: Display<App>,
644    app: App,
645    keymap_path: Option<std::path::PathBuf>,
646    keymap_watch: Option<mirada_brain::KeymapWatch>,
647    ctl: Option<CtlServer>,
648}
649
650/// Arma el estado del compositor — todo lo independiente del backend
651/// gráfico (Wayland, Cerebro, teclado, keymap, control). Cada backend
652/// (winit o DRM) registra luego su propia salida y monta su bucle.
653fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
654    let display: Display<App> = Display::new()?;
655    let dh = display.handle();
656
657    let mut seat_state = SeatState::new();
658    let seat = seat_state.new_wl_seat(&dh, "mirada");
659
660    // Anuncia el gestor de decoración: las ventanas van sin marco (ver
661    // `XdgDecorationHandler`). El `XdgDecorationState` sólo serviría para
662    // retirar el global más tarde, cosa que nunca hacemos.
663    let _ = XdgDecorationState::new::<App>(&dh);
664
665    // El keymap del usuario (`~/.config/mirada/keymap.ron`). Sólo lo usa
666    // el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
667    let keymap_path = Keymap::default_path();
668
669    // Elige el Cerebro: enlazado si `MIRADA_SOCKET` está puesto.
670    let brain = match std::env::var("MIRADA_SOCKET") {
671        Ok(path) => {
672            println!("mirada-compositor · esperando al Cerebro en {path} …");
673            let link = BodyLink::listen(&path)?;
674            println!("mirada-compositor · Cerebro conectado.");
675            Brain::Linked(link)
676        }
677        Err(_) => {
678            println!("mirada-compositor · modo autónomo (Cerebro embebido).");
679            let keymap = match &keymap_path {
680                Some(p) => Keymap::load_or_init(p),
681                None => Keymap::default(),
682            };
683            let mut desktop = Desktop::with_keymap(keymap);
684            desktop.set_rules(load_user_rules());
685            Brain::Embedded(desktop)
686        }
687    };
688
689    let mut app = App {
690        compositor_state: CompositorState::new::<App>(&dh),
691        xdg_shell_state: XdgShellState::new::<App>(&dh),
692        shm_state: ShmState::new::<App>(&dh, Vec::new()),
693        seat_state,
694        data_device_state: DataDeviceState::new::<App>(&dh),
695        seat,
696        keyboard: None,
697        pointer: None,
698        pointer_loc: (0.0, 0.0),
699        drag: None,
700        windows: Vec::new(),
701        body: BodyState::new(),
702        brain,
703        grabs: Vec::new(),
704        pending_keybind: None,
705        next_id: 1,
706        running: true,
707    };
708
709    let keyboard = app.seat.add_keyboard(Default::default(), 200, 25)?;
710    app.keyboard = Some(keyboard);
711    app.pointer = Some(app.seat.add_pointer());
712
713    // En modo embebido, el propio Desktop dicta los atajos a interceptar.
714    if let Brain::Embedded(desktop) = &app.brain {
715        let grab = desktop.grab_keys();
716        app.apply_commands(vec![grab]);
717    }
718
719    // Vigilancia del keymap para recargarlo en caliente — sólo tiene
720    // sentido con el Cerebro embebido.
721    let keymap_watch = match (&app.brain, &keymap_path) {
722        (Brain::Embedded(_), Some(p)) => Keymap::watch(p).ok(),
723        _ => None,
724    };
725    if keymap_watch.is_some() {
726        println!("mirada-compositor · vigilando el keymap (recarga en caliente).");
727    }
728
729    // API de control (mirada-ctl) — sólo con el Cerebro embebido; si es
730    // externo, el socket de control lo abre él.
731    let ctl = match &app.brain {
732        Brain::Embedded(_) => {
733            let path = mirada_brain::ctl::default_socket_path();
734            match CtlServer::bind(&path) {
735                Ok(s) => {
736                    println!("mirada-compositor · API de control en {}", path.display());
737                    Some(s)
738                }
739                Err(e) => {
740                    eprintln!("mirada-compositor · sin API de control: {e}");
741                    None
742                }
743            }
744        }
745        Brain::Linked(_) => None,
746    };
747
748    Ok(Setup { display, app, keymap_path, keymap_watch, ctl })
749}
750
751/// El backend `winit`: corre anidado dentro de una sesión gráfica.
752fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
753    let Setup {
754        mut display,
755        app: mut state,
756        keymap_path,
757        keymap_watch,
758        ctl,
759    } = build_app()?;
760    let keyboard = state.keyboard.clone().expect("teclado inicializado");
761
762    // El backend gráfico va primero. winit abre la ventana del compositor
763    // dentro de tu sesión gráfica anfitriona, y para encontrarla lee
764    // `WAYLAND_DISPLAY` / `DISPLAY` del entorno. Si publicáramos antes
765    // nuestro propio socket en `WAYLAND_DISPLAY`, winit intentaría
766    // anidarse en nosotros mismos —un socket que aún no atiende a nadie—
767    // y se quedaría colgado para siempre.
768    let (mut backend, mut winit) = match winit::init::<GlesRenderer>() {
769        Ok(pair) => pair,
770        Err(e) => {
771            eprintln!("mirada-compositor · no pude abrir la ventana: {e}");
772            eprintln!(
773                "   El backend `winit` necesita una sesión gráfica anfitriona\n   \
774                 (X11 o Wayland) donde dibujar la ventana del compositor.\n   \
775                 Aquí no hay ninguna: DISPLAY='{}', WAYLAND_DISPLAY='{}',\n   \
776                 XDG_SESSION_TYPE='{}'.\n   \
777                 Lánzalo desde un escritorio gráfico, o desde un servidor X\n   \
778                 virtual (Xvfb) al que te conectes por VNC.",
779                std::env::var("DISPLAY").unwrap_or_default(),
780                std::env::var("WAYLAND_DISPLAY").unwrap_or_default(),
781                std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "tty".into()),
782            );
783            return Err(e.into());
784        }
785    };
786
787    // Ahora sí, nuestro propio socket Wayland — y `WAYLAND_DISPLAY` se
788    // publica *después* de winit, sólo para los clientes que lancemos
789    // como procesos hijos.
790    let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
791    let socket_name = listener
792        .socket_name()
793        .and_then(|s| s.to_str())
794        .unwrap_or("wayland-?")
795        .to_string();
796    std::env::set_var("WAYLAND_DISPLAY", &socket_name);
797    println!("mirada-compositor · escuchando en WAYLAND_DISPLAY={socket_name}");
798    println!("   lanza un cliente:  WAYLAND_DISPLAY={socket_name} foot");
799
800    let start = Instant::now();
801    let mut clients = Vec::new();
802
803    // Salida inicial = el tamaño de la ventana winit.
804    let win_size = backend.window_size();
805    let _wl_output = announce_output(&display.handle(), "winit", win_size.w, win_size.h, 60_000);
806    {
807        let ev = state.body.add_output(0, win_size.w, win_size.h);
808        state.brain_feed(ev);
809    }
810
811    while state.running {
812        // 1 · Eventos del backend (teclado, redimensión, cierre).
813        let status = winit.dispatch_new_events(|event| match event {
814            WinitEvent::CloseRequested => state.running = false,
815            WinitEvent::Resized { size, .. } => {
816                let ev = state.body.remove_output(0);
817                state.brain_feed(ev);
818                let ev = state.body.add_output(0, size.w, size.h);
819                state.brain_feed(ev);
820            }
821            WinitEvent::Input(InputEvent::Keyboard { event }) => {
822                let code = event.key_code();
823                let key_state = event.state();
824                let pressed = key_state == KeyState::Pressed;
825                let time = start.elapsed().as_millis() as u32;
826                keyboard.clone().input::<(), _>(
827                    &mut state,
828                    code,
829                    key_state,
830                    SERIAL_COUNTER.next_serial(),
831                    time,
832                    |st, mods, handle| {
833                        if !pressed {
834                            return FilterResult::Forward;
835                        }
836                        if let Some(combo) = combo_string(mods, handle.modified_sym()) {
837                            if st.grabs.contains(&combo) {
838                                st.pending_keybind = Some(combo);
839                                return FilterResult::Intercept(());
840                            }
841                        }
842                        FilterResult::Forward
843                    },
844                );
845                if let Some(combo) = state.pending_keybind.take() {
846                    let ev = state.body.keybind(combo);
847                    state.brain_feed(ev);
848                }
849            }
850            _ => {}
851        });
852        if let PumpStatus::Exit(_) = status {
853            break;
854        }
855
856        // 2 · Comandos de un Cerebro enlazado.
857        state.brain_poll();
858
859        // 2 bis · Recarga del keymap si el archivo cambió en disco.
860        if keymap_watch.as_ref().is_some_and(|w| w.changed()) {
861            if let Some(path) = &keymap_path {
862                match Keymap::load(path) {
863                    Ok(km) => {
864                        let cmd = if let Brain::Embedded(d) = &mut state.brain {
865                            Some(d.set_keymap(km))
866                        } else {
867                            None
868                        };
869                        if let Some(cmd) = cmd {
870                            state.apply_commands(vec![cmd]);
871                        }
872                        println!("mirada-compositor · keymap recargado.");
873                    }
874                    Err(e) => eprintln!(
875                        "mirada-compositor · keymap inválido, conservo el anterior: {e}"
876                    ),
877                }
878            }
879        }
880
881        // 2 ter · Peticiones del API de control (mirada-ctl).
882        if let Some(ctl) = &ctl {
883            while let Some(mut conn) = ctl.poll() {
884                let reply = match conn.read_request() {
885                    Ok(Some(req)) => state.serve_ctl(req),
886                    Ok(None) => continue,
887                    Err(e) => CtlReply::Error(format!("{e}")),
888                };
889                let _ = conn.reply(&reply);
890            }
891        }
892
893        // 3 · Composición de las superficies en sus rectángulos.
894        let size = backend.window_size();
895        let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
896        {
897            let (renderer, mut framebuffer) = backend.bind().unwrap();
898            // Orden de pintado: la lista de elementos va front-to-back
899            // (índice 0 = encima), así que las flotantes —que deben
900            // quedar sobre las teseladas— se ordenan primero. `sort_by_key`
901            // es estable: dentro de cada grupo se respeta el orden de apertura.
902            let mut shown: Vec<&ManagedWindow> =
903                state.windows.iter().filter(|w| w.visible).collect();
904            shown.sort_by_key(|w| !w.floating);
905            let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = shown
906                .iter()
907                .flat_map(|w| {
908                    render_elements_from_surface_tree(
909                        renderer,
910                        &w.surface,
911                        render_loc(w),
912                        1.0,
913                        1.0,
914                        Kind::Unspecified,
915                    )
916                })
917                .collect();
918            let mut frame = renderer
919                .render(&mut framebuffer, size, Transform::Flipped180)
920                .unwrap();
921            frame
922                .clear(Color32F::new(0.05, 0.05, 0.08, 1.0), &[damage])
923                .unwrap();
924            draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap();
925            let _ = frame.finish().unwrap();
926        }
927
928        // 4 · Callbacks de frame + clientes nuevos + flush.
929        let time = start.elapsed().as_millis() as u32;
930        for w in &state.windows {
931            send_frames_surface_tree(&w.surface, time);
932        }
933        if let Some(stream) = listener.accept()? {
934            let client = display
935                .handle()
936                .insert_client(stream, Arc::new(ClientState::default()))
937                .unwrap();
938            clients.push(client);
939        }
940        display.dispatch_clients(&mut state)?;
941        display.flush_clients()?;
942
943        backend.submit(Some(&[damage])).unwrap();
944    }
945
946    println!("mirada-compositor · adiós.");
947    Ok(())
948}
949
950fn main() {
951    let arg = std::env::args().nth(1);
952    let result = match arg.as_deref() {
953        Some("--drm") => drm_backend::run(),
954        Some("--winit") => run_winit(),
955        Some(other) => {
956            eprintln!("mirada-compositor: opción desconocida «{other}» — usa --drm o --winit");
957            std::process::exit(2);
958        }
959        None => {
960            // Auto: con sesión gráfica anfitriona → winit (anidado);
961            // sin ella (una TTY pelada) → backend DRM.
962            let nested = std::env::var_os("WAYLAND_DISPLAY").is_some()
963                || std::env::var_os("DISPLAY").is_some();
964            if nested {
965                println!("mirada-compositor · sesión gráfica detectada → backend winit.");
966                run_winit()
967            } else {
968                println!("mirada-compositor · sin sesión gráfica → backend DRM.");
969                drm_backend::run()
970            }
971        }
972    };
973    if let Err(e) = result {
974        eprintln!("mirada-compositor · error: {e}");
975        std::process::exit(1);
976    }
977}