gioser

tawasuyu/brahman

Rust 332TOML 127JSON 17Markdown 154710 Definiciones

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

Dos remates de la tanda WM.

Fullscreen del cliente:
- BodyEvent::FullscreenRequest { id, fullscreen }. mirada-compositor
  implementa XdgShellHandler::fullscreen_request / unfullscreen_request
  y avisa al Cerebro; Desktop::on_event fija el fullscreen en el
  escritorio que tiene la ventana. Así un reproductor o un juego que
  llama a xdg set_fullscreen entra a pantalla completa solo.

HUD multi-salida (app mirada):
- El lienzo dibuja todas las salidas a escala (encaja su caja
  envolvente en el lienzo fijo; con una salida, 1:1), cada una con su
  marco y su número/escritorio. En simulación, Shift+n añade un monitor.

mirada-brain 63->65 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

f9c4bf594e361ce3a59435f2b263840ae81ac87a · por sergio · (hace 5 meses) · padre 13d2ae71f · ver el árbol en este commit

Delta semántico · 8

crates/apps/mirada-compositor/src/main.rs

crates/apps/mirada/src/main.rs

crates/modules/mirada/mirada-brain/src/desktop.rs

crates/modules/mirada/mirada-protocol/src/lib.rs

6 archivos · +143 −9

diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs
index 090f05a..27d8120 100644
--- a/crates/apps/mirada-compositor/src/main.rs
+++ b/crates/apps/mirada-compositor/src/main.rs
@@ -34,6 +34,7 @@ use smithay::input::{Seat, SeatHandler, SeatState};
 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
 use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
 use smithay::reexports::wayland_server::protocol::wl_buffer;
+use smithay::reexports::wayland_server::protocol::wl_output;
 use smithay::reexports::wayland_server::protocol::wl_seat;
 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
 use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
@@ -335,6 +336,32 @@ impl XdgShellHandler for App {
         }
     }
 
+    fn fullscreen_request(
+        &mut self,
+        surface: ToplevelSurface,
+        _output: Option<wl_output::WlOutput>,
+    ) {
+        let id = self
+            .windows
+            .iter()
+            .find(|w| w.surface == *surface.wl_surface())
+            .map(|w| w.id);
+        if let Some(id) = id {
+            self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: true });
+        }
+    }
+
+    fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
+        let id = self
+            .windows
+            .iter()
+            .find(|w| w.surface == *surface.wl_surface())
+            .map(|w| w.id);
+        if let Some(id) = id {
+            self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: false });
+        }
+    }
+
     fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
         let _ = surface.send_configure();
     }
diff --git a/crates/apps/mirada/src/main.rs b/crates/apps/mirada/src/main.rs
index 90def87..7501ef6 100644
--- a/crates/apps/mirada/src/main.rs
+++ b/crates/apps/mirada/src/main.rs
@@ -19,7 +19,7 @@
 //! Teclas (simulación):
 //!
 //! ```text
-//!   n            abre una ventana          tab / espacio  cicla layout
+//!   n / Shift+n  abre ventana / monitor    tab / espacio  cicla layout
 //!   w            cierra la enfocada        t m g c r d s  layout directo
 //!   f / Shift+f  flota / pantalla completa h / l          área maestra −/+
 //!   j / k        foco siguiente/anterior   , / .          nmaster −/+
@@ -282,6 +282,11 @@ impl Mirada {
         let connected = self.link.is_some();
 
         match ks.key.as_str() {
+            "n" if shift && !connected => {
+                // Simulación: añade un monitor más, en fila a la derecha.
+                let id = self.desktop.outputs().len() as u32;
+                self.feed(BodyEvent::OutputAdded { id, width: SCREEN_W, height: SCREEN_H });
+            }
             "n" if !connected => self.open_window(),
             "w" => self.act(DesktopAction::CloseFocused),
             "f" if shift => self.act(DesktopAction::ToggleFullscreen),
@@ -428,7 +433,22 @@ impl Render for Mirada {
                     .child(SharedString::from(format!("foco: {focus_label}"))),
             );
 
-        // --- Lienzo: el escritorio teselado --------------------------
+        // --- Lienzo: el escritorio teselado, a escala ----------------
+        // El lienzo es de tamaño fijo; el contenido vive en el espacio
+        // global de las salidas. `scale` encaja ese espacio en el lienzo
+        // — con una sola salida, escala 1:1.
+        let outs = self.desktop.outputs();
+        let (bb_w, bb_h) = if outs.is_empty() {
+            (SCREEN_W as f32, SCREEN_H as f32)
+        } else {
+            let w = outs.iter().map(|o| o.rect.x + o.rect.w).max().unwrap_or(SCREEN_W);
+            let h = outs.iter().map(|o| o.rect.y + o.rect.h).max().unwrap_or(SCREEN_H);
+            (w as f32, h as f32)
+        };
+        let scale = (SCREEN_W as f32 / bb_w)
+            .min(SCREEN_H as f32 / bb_h)
+            .min(1.0);
+
         let mut canvas = div()
             .relative()
             .w(px(SCREEN_W as f32))
@@ -436,6 +456,37 @@ impl Render for Mirada {
             .bg(canvas_bg)
             .overflow_hidden();
 
+        // Un marco por salida, con su número y el escritorio que muestra.
+        for (i, o) in outs.iter().enumerate() {
+            let is_focused_out = i == self.desktop.focused_output();
+            canvas = canvas.child(
+                div()
+                    .absolute()
+                    .left(px(o.rect.x as f32 * scale))
+                    .top(px(o.rect.y as f32 * scale))
+                    .w(px(o.rect.w as f32 * scale))
+                    .h(px(o.rect.h as f32 * scale))
+                    .border_1()
+                    .border_color(if is_focused_out {
+                        theme.accent
+                    } else {
+                        theme.border
+                    })
+                    .child(
+                        div()
+                            .absolute()
+                            .top(px(2.))
+                            .left(px(4.))
+                            .text_color(theme.fg_disabled)
+                            .child(SharedString::from(format!(
+                                "salida {} · escritorio {}",
+                                i + 1,
+                                o.workspace + 1
+                            ))),
+                    ),
+            );
+        }
+
         let visible = self.placements.iter().filter(|p| p.visible).count();
         if visible == 0 {
             canvas = canvas.child(
@@ -470,10 +521,10 @@ impl Render for Mirada {
             canvas = canvas.child(
                 div()
                     .absolute()
-                    .left(px(p.rect.x as f32))
-                    .top(px(p.rect.y as f32))
-                    .w(px(p.rect.w as f32))
-                    .h(px(p.rect.h as f32))
+                    .left(px(p.rect.x as f32 * scale))
+                    .top(px(p.rect.y as f32 * scale))
+                    .w(px(p.rect.w as f32 * scale))
+                    .h(px(p.rect.h as f32 * scale))
                     .border_2()
                     .border_color(border)
                     .bg(win_bg)
diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md
index 9f1b3bc..a25604a 100644
--- a/crates/modules/mirada/SDD.md
+++ b/crates/modules/mirada/SDD.md
@@ -72,7 +72,8 @@ ejecuta operaciones de geometría".
   devuelven los `BodyEvent` a mandar. Ejemplo `headless`: un Cuerpo sin
   gráficos guiado por stdin para ejercitar el bucle entero.
 - **`mirada` (app)** — envuelve `Desktop` y lo pinta (barra de
-  escritorios + modo + foco, lienzo teselado). Con `MIRADA_SOCKET`
+  escritorios + modo + foco, lienzo teselado). El lienzo dibuja **todas
+  las salidas a escala**, cada una con su marco. Con `MIRADA_SOCKET`
   conecta a un Cuerpo; sin él corre en **simulación** (ventanas
   sintéticas, teclado de la propia ventana). Pips de escritorio y
   ventanas clicables.
@@ -122,7 +123,9 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres:
 - **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 estado
-  `xdg_toplevel Fullscreen`.
+  `xdg_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 `Output` muestra un escritorio distinto;
   `SwitchWorkspace` actúa sobre la salida enfocada (y la intercambia si
   el escritorio pedido ya lo muestra otra salida); `FocusOutputNext`
@@ -175,7 +178,7 @@ a las ya abiertas.
 ## Estado
 
 Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol`
-(11), `mirada-brain` (63), `mirada-link` (7), `mirada-body` (14), las
+(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`).
 
diff --git a/crates/modules/mirada/mirada-brain/src/desktop.rs b/crates/modules/mirada/mirada-brain/src/desktop.rs
index 7c31699..1a284bc 100644
--- a/crates/modules/mirada/mirada-brain/src/desktop.rs
+++ b/crates/modules/mirada/mirada-brain/src/desktop.rs
@@ -188,6 +188,27 @@ impl Desktop {
                 Some(action) => self.apply(action),
                 None => Vec::new(),
             },
+            BodyEvent::FullscreenRequest { id, fullscreen } => {
+                // El cliente (un reproductor, un juego) pidió pantalla
+                // completa: la fijamos en el escritorio que tiene la ventana.
+                let mut changed = false;
+                for ws in &mut self.workspaces {
+                    if ws.windows().contains(&id) {
+                        if fullscreen {
+                            ws.set_fullscreen(Some(id));
+                        } else if ws.fullscreen() == Some(id) {
+                            ws.set_fullscreen(None);
+                        }
+                        changed = true;
+                        break;
+                    }
+                }
+                if changed {
+                    self.relayout()
+                } else {
+                    Vec::new()
+                }
+            }
         }
     }
 
@@ -985,6 +1006,27 @@ mod tests {
         assert!(d.apply(DesktopAction::ToggleScratchpad).is_empty());
     }
 
+    #[test]
+    fn a_client_fullscreen_request_is_honoured() {
+        let mut d = desktop_with_screen();
+        open(&mut d, 1);
+        open(&mut d, 2);
+        let cmds = d.on_event(BodyEvent::FullscreenRequest { id: 1, fullscreen: true });
+        assert!(places(&cmds).iter().find(|x| x.id == 1).unwrap().fullscreen);
+        // El cliente la suelta.
+        let cmds = d.on_event(BodyEvent::FullscreenRequest { id: 1, fullscreen: false });
+        assert!(!places(&cmds).iter().find(|x| x.id == 1).unwrap().fullscreen);
+    }
+
+    #[test]
+    fn a_fullscreen_request_for_an_unknown_window_does_nothing() {
+        let mut d = desktop_with_screen();
+        open(&mut d, 1);
+        assert!(d
+            .on_event(BodyEvent::FullscreenRequest { id: 99, fullscreen: true })
+            .is_empty());
+    }
+
     #[test]
     fn window_lines_show_a_stashed_window_as_workspace_zero() {
         let mut d = desktop_with_screen();
diff --git a/crates/modules/mirada/mirada-protocol/src/lib.rs b/crates/modules/mirada/mirada-protocol/src/lib.rs
index a8e9169..b1a48bc 100644
--- a/crates/modules/mirada/mirada-protocol/src/lib.rs
+++ b/crates/modules/mirada/mirada-protocol/src/lib.rs
@@ -88,6 +88,9 @@ pub enum BodyEvent {
     Keybind(String),
     /// El puntero entró en una ventana — el Cerebro puede enfocar al pasar.
     PointerEntered { id: WindowId },
+    /// Un cliente pidió pantalla completa para su ventana (`true`), o la
+    /// soltó (`false`) — `xdg_toplevel.set_fullscreen`.
+    FullscreenRequest { id: WindowId, fullscreen: bool },
 }
 
 /// Tamaño máximo de un marco, en bytes. Acota el búfer de [`read_frame`]
diff --git a/vamos.txt b/vamos.txt
index 41dff69..abe0851 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -1090,5 +1090,13 @@
 
 
 
+  Fullscreen iniciado por el cliente + HUD multi-salida:
+  BodyEvent::FullscreenRequest — cuando un cliente pide xdg set_fullscreen, el Cuerpo
+  (XdgShellHandler::fullscreen_request) avisa al Cerebro y la ventana entra a pantalla completa sola.
+  El lienzo de la app mirada dibuja ahora todas las salidas a escala, cada una con su marco;
+  en simulación, Shift+n añade un monitor más.
+
+
+