From f9c4bf594e361ce3a59435f2b263840ae81ac87a Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 21 May 2026 01:32:08 +0000 Subject: [PATCH] feat(mirada): fullscreen iniciado por el cliente + HUD multi-salida MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/apps/mirada-compositor/src/main.rs | 27 ++++++++ crates/apps/mirada/src/main.rs | 63 +++++++++++++++++-- crates/modules/mirada/SDD.md | 9 ++- .../mirada/mirada-brain/src/desktop.rs | 42 +++++++++++++ .../modules/mirada/mirada-protocol/src/lib.rs | 3 + vamos.txt | 8 +++ 6 files changed, 143 insertions(+), 9 deletions(-) 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, + ) { + 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`, 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. + + +