From be61ddb6eb441193c4374a243fbb00aed47b42de Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 21 May 2026 01:07:01 +0000 Subject: [PATCH] =?UTF-8?q?feat(mirada):=20pantalla=20completa=20real=20?= =?UTF-8?q?=E2=80=94=20toggle-fullscreen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ToggleFullscreen (Super+Shift+f) lleva la ventana enfocada a pantalla completa: cubre toda la salida sin gap, oculta al resto y se lleva el foco. Distinto del modo Monocle (un modo de teselado): es un estado por ventana que ignora el layout. - Workspace.fullscreen: Option; set_fullscreen / fullscreen(); remove() lo limpia si se cierra esa ventana. - placements() da a la fullscreen el rect completo y marca al resto visible: false. WindowPlacement y BodyOp::Configure llevan fullscreen: bool. - mirada-compositor fija el estado xdg_toplevel::Fullscreen en la superficie, para que el cliente lo sepa. - Cableado en keymap, HUD de mirada y mirada-ctl. Verificado end-to-end con headless-ctl. mirada-protocol 10->11, mirada-brain 51->52. Co-Authored-By: Claude Opus 4.7 --- crates/apps/mirada-compositor/src/main.rs | 7 +++- crates/apps/mirada-ctl/src/main.rs | 1 + crates/apps/mirada/src/main.rs | 7 ++-- crates/modules/mirada/SDD.md | 6 +++- crates/modules/mirada/mirada-body/src/lib.rs | 22 +++++++++++-- .../mirada-brain/examples/headless-ctl.rs | 8 ++++- .../modules/mirada/mirada-brain/src/action.rs | 5 +++ .../mirada/mirada-brain/src/desktop.rs | 29 ++++++++++++++++ .../modules/mirada/mirada-brain/src/keymap.rs | 1 + .../mirada/mirada-layout/src/workspace.rs | 17 ++++++++++ .../modules/mirada/mirada-protocol/src/lib.rs | 33 +++++++++++++++++-- vamos.txt | 8 +++++ 12 files changed, 133 insertions(+), 11 deletions(-) diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs index 435b928..090f05a 100644 --- a/crates/apps/mirada-compositor/src/main.rs +++ b/crates/apps/mirada-compositor/src/main.rs @@ -174,13 +174,18 @@ impl App { /// Ejecuta una operación concreta sobre las superficies reales. fn exec_op(&mut self, op: BodyOp) { match op { - BodyOp::Configure { id, rect, visible, floating } => { + BodyOp::Configure { id, rect, visible, floating, fullscreen } => { if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) { w.loc = (rect.x, rect.y); w.visible = visible; w.floating = floating; w.toplevel.with_pending_state(|s| { s.size = Some((rect.w.max(1), rect.h.max(1)).into()); + if fullscreen { + s.states.set(xdg_toplevel::State::Fullscreen); + } else { + s.states.unset(xdg_toplevel::State::Fullscreen); + } }); w.toplevel.send_pending_configure(); } diff --git a/crates/apps/mirada-ctl/src/main.rs b/crates/apps/mirada-ctl/src/main.rs index 0f596ea..3185a1a 100644 --- a/crates/apps/mirada-ctl/src/main.rs +++ b/crates/apps/mirada-ctl/src/main.rs @@ -121,6 +121,7 @@ Acciones de mirada-ctl: move-backward la atrasa close-focused cierra la ventana enfocada toggle-float alterna flotante / teselada la enfocada + toggle-fullscreen alterna pantalla completa en la enfocada cycle-layout pasa al siguiente modo de teselado layout master-stack · centered-master · spiral grid · columns · rows · monocle diff --git a/crates/apps/mirada/src/main.rs b/crates/apps/mirada/src/main.rs index 6f3cfe8..71c0476 100644 --- a/crates/apps/mirada/src/main.rs +++ b/crates/apps/mirada/src/main.rs @@ -21,7 +21,7 @@ //! ```text //! n abre una ventana tab / espacio cicla layout //! w cierra la enfocada t m g c r d s layout directo -//! f flota / tesela h / l área maestra −/+ +//! f / Shift+f flota / pantalla completa h / l área maestra −/+ //! j / k foco siguiente/anterior , / . nmaster −/+ //! Shift+j / k mueve la enfocada 1..9 ir a escritorio //! Enter promueve a maestra Ctrl+1..9 enviar a escritorio @@ -283,6 +283,7 @@ impl Mirada { match ks.key.as_str() { "n" if !connected => self.open_window(), "w" => self.act(DesktopAction::CloseFocused), + "f" if shift => self.act(DesktopAction::ToggleFullscreen), "f" => self.act(DesktopAction::ToggleFloat), "j" if shift => self.act(DesktopAction::MoveForward), "k" if shift => self.act(DesktopAction::MoveBackward), @@ -454,7 +455,9 @@ impl Render for Mirada { let tb_bg = if p.focused { theme.accent } else { theme.bg_row_hover }; let tb_fg = if p.focused { on_accent } else { theme.fg_muted }; let pid = p.id; - let kind_label = if p.floating { + let kind_label = if p.fullscreen { + "· pantalla completa ·" + } else if p.floating { "· ventana flotante ·" } else { "· superficie del Cuerpo ·" diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md index 3056470..657de06 100644 --- a/crates/modules/mirada/SDD.md +++ b/crates/modules/mirada/SDD.md @@ -118,6 +118,10 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres: flotantes aparte, `layout()` las pone al final y `WindowPlacement` /`BodyOp::Configure` llevan `floating: bool` para que el Cuerpo las componga por encima. +- **Pantalla completa** — `ToggleFullscreen` (`Super+Shift+f`): la ventana + cubre toda la salida (sin gap), oculta al resto y se lleva el foco; + `Workspace.fullscreen: Option`, y el Cuerpo le fija el estado + `xdg_toplevel Fullscreen`. - **Layout y área maestra por el API** — los 7 modos se intercambian (`SetLayout`/`CycleLayout`, `mirada-ctl layout spiral`); el área maestra se redimensiona (`grow`/`shrink-master`, `Super+l`/`Super+h`); @@ -160,7 +164,7 @@ a las ya abiertas. ## Estado Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol` -(10), `mirada-brain` (51), `mirada-link` (7), `mirada-body` (14), las +(11), `mirada-brain` (52), `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-body/src/lib.rs b/crates/modules/mirada/mirada-body/src/lib.rs index f5060c4..8d06577 100644 --- a/crates/modules/mirada/mirada-body/src/lib.rs +++ b/crates/modules/mirada/mirada-body/src/lib.rs @@ -31,6 +31,8 @@ pub struct Surface { pub focused: bool, /// `true` si flota: el backend la pinta por encima de las teseladas. pub floating: bool, + /// `true` si está en pantalla completa. + pub fullscreen: bool, } impl Surface { @@ -42,6 +44,7 @@ impl Surface { visible: false, focused: false, floating: false, + fullscreen: false, } } } @@ -49,9 +52,16 @@ impl Surface { /// Una orden concreta para el backend (smithay, headless, …). #[derive(Debug, Clone, PartialEq, Eq)] pub enum BodyOp { - /// Recoloca una superficie, la muestra u oculta y dice si flota - /// (las flotantes se componen por encima de las teseladas). - Configure { id: WindowId, rect: Rect, visible: bool, floating: bool }, + /// Recoloca una superficie, la muestra u oculta y dice si flota o + /// está en pantalla completa (el backend ajusta el orden de pintado + /// y el estado `xdg_toplevel` en consecuencia). + Configure { + id: WindowId, + rect: Rect, + visible: bool, + floating: bool, + fullscreen: bool, + }, /// Da el foco del teclado a una superficie. Focus(WindowId), /// Quita el foco a todas las superficies. @@ -104,15 +114,18 @@ impl BodyState { if s.geometry != Some(p.rect) || s.visible != p.visible || s.floating != p.floating + || s.fullscreen != p.fullscreen { s.geometry = Some(p.rect); s.visible = p.visible; s.floating = p.floating; + s.fullscreen = p.fullscreen; ops.push(BodyOp::Configure { id: p.id, rect: p.rect, visible: p.visible, floating: p.floating, + fullscreen: p.fullscreen, }); } } @@ -128,6 +141,7 @@ impl BodyState { rect, visible: false, floating: s.floating, + fullscreen: s.fullscreen, }); } } @@ -248,6 +262,7 @@ mod tests { visible, focused, floating: false, + fullscreen: false, } } @@ -310,6 +325,7 @@ mod tests { rect: Rect::new(0, 0, 800, 600), visible: false, floating: false, + fullscreen: false, })); assert!(!b.surface(2).unwrap().visible); } diff --git a/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs b/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs index 19702d6..3129bf0 100644 --- a/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs +++ b/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs @@ -60,7 +60,13 @@ fn main() { p.rect.h, p.rect.x, p.rect.y, - if p.floating { " ~flotante" } else { "" }, + if p.fullscreen { + " ~pantalla" + } else if p.floating { + " ~flotante" + } else { + "" + }, if p.focused { " *" } else { "" }, ); } diff --git a/crates/modules/mirada/mirada-brain/src/action.rs b/crates/modules/mirada/mirada-brain/src/action.rs index 438cc54..819b36b 100644 --- a/crates/modules/mirada/mirada-brain/src/action.rs +++ b/crates/modules/mirada/mirada-brain/src/action.rs @@ -40,6 +40,8 @@ pub enum DesktopAction { CloseFocused, /// Alterna entre flotante y teselada la ventana enfocada. ToggleFloat, + /// Alterna pantalla completa en la ventana enfocada. + ToggleFullscreen, /// Pasa al siguiente modo de teselado. CycleLayout, /// Fija un modo de teselado concreto. @@ -101,6 +103,7 @@ impl fmt::Display for DesktopAction { DesktopAction::MoveBackward => f.write_str("move-backward"), DesktopAction::CloseFocused => f.write_str("close-focused"), DesktopAction::ToggleFloat => f.write_str("toggle-float"), + DesktopAction::ToggleFullscreen => f.write_str("toggle-fullscreen"), DesktopAction::CycleLayout => f.write_str("cycle-layout"), DesktopAction::SetLayout(m) => write!(f, "layout:{}", layout_slug(*m)), DesktopAction::GrowMaster => f.write_str("grow-master"), @@ -129,6 +132,7 @@ impl FromStr for DesktopAction { "move-backward" => Self::MoveBackward, "close-focused" => Self::CloseFocused, "toggle-float" => Self::ToggleFloat, + "toggle-fullscreen" => Self::ToggleFullscreen, "cycle-layout" => Self::CycleLayout, "grow-master" => Self::GrowMaster, "shrink-master" => Self::ShrinkMaster, @@ -188,6 +192,7 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> { ("Super+Shift+k".into(), DesktopAction::MoveBackward), ("Super+q".into(), DesktopAction::CloseFocused), ("Super+f".into(), DesktopAction::ToggleFloat), + ("Super+Shift+f".into(), DesktopAction::ToggleFullscreen), ("Super+space".into(), DesktopAction::CycleLayout), ("Super+t".into(), DesktopAction::SetLayout(LayoutMode::MasterStack)), ("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)), diff --git a/crates/modules/mirada/mirada-brain/src/desktop.rs b/crates/modules/mirada/mirada-brain/src/desktop.rs index f8e4a63..b7d5a6d 100644 --- a/crates/modules/mirada/mirada-brain/src/desktop.rs +++ b/crates/modules/mirada/mirada-brain/src/desktop.rs @@ -218,6 +218,18 @@ impl Desktop { } self.relayout() } + DesktopAction::ToggleFullscreen => { + let Some(id) = self.workspaces[self.active].focused() else { + return Vec::new(); + }; + let ws = &mut self.workspaces[self.active]; + if ws.fullscreen() == Some(id) { + ws.set_fullscreen(None); + } else { + ws.set_fullscreen(Some(id)); + } + self.relayout() + } DesktopAction::CycleLayout => { let next = self.workspaces[self.active].params().mode.next(); self.workspaces[self.active].set_mode(next); @@ -475,6 +487,23 @@ mod tests { assert!(!places(&cmds).iter().find(|x| x.id == 2).unwrap().floating); } + #[test] + fn toggle_fullscreen_covers_the_screen_and_hides_the_rest() { + let mut d = desktop_with_screen(); + for id in [1, 2, 3] { + open(&mut d, id); + } + let cmds = d.apply(DesktopAction::ToggleFullscreen); // sobre la 3 + let p = places(&cmds); + let fs = p.iter().find(|x| x.id == 3).unwrap(); + assert!(fs.fullscreen && fs.visible); + assert_eq!(fs.rect, d.screen().unwrap()); + assert!(p.iter().filter(|x| x.id != 3).all(|x| !x.visible)); + // Alternar de nuevo restaura el teselado: las tres visibles. + let cmds = d.apply(DesktopAction::ToggleFullscreen); + assert_eq!(places(&cmds).iter().filter(|x| x.visible).count(), 3); + } + #[test] fn a_rule_sends_a_new_window_to_its_workspace() { let mut d = desktop_with_screen(); diff --git a/crates/modules/mirada/mirada-brain/src/keymap.rs b/crates/modules/mirada/mirada-brain/src/keymap.rs index 918e7ae..f59985e 100644 --- a/crates/modules/mirada/mirada-brain/src/keymap.rs +++ b/crates/modules/mirada/mirada-brain/src/keymap.rs @@ -238,6 +238,7 @@ const KEYMAP_HEADER: &str = "\ // move-forward / move-backward reordena la ventana enfocada // close-focused cierra la enfocada // toggle-float alterna flotante / teselada +// toggle-fullscreen alterna pantalla completa // cycle-layout siguiente modo de teselado // layout: master-stack | centered-master | spiral // grid | columns | rows | monocle diff --git a/crates/modules/mirada/mirada-layout/src/workspace.rs b/crates/modules/mirada/mirada-layout/src/workspace.rs index 938a362..dbead27 100644 --- a/crates/modules/mirada/mirada-layout/src/workspace.rs +++ b/crates/modules/mirada/mirada-layout/src/workspace.rs @@ -21,6 +21,9 @@ pub struct Workspace { /// Ventanas flotantes y su rectángulo: salen del teselado y se pintan /// encima. Las que no están aquí se teselan normalmente. floating: BTreeMap, + /// La ventana en pantalla completa, si hay alguna: cubre toda la + /// salida y oculta al resto. + fullscreen: Option, } impl Workspace { @@ -31,6 +34,7 @@ impl Workspace { focus: 0, params, floating: BTreeMap::new(), + fullscreen: None, } } @@ -84,6 +88,9 @@ impl Workspace { }; self.windows.remove(i); self.floating.remove(&window); + if self.fullscreen == Some(window) { + self.fullscreen = None; + } if i < self.focus { self.focus -= 1; } @@ -111,6 +118,16 @@ impl Workspace { self.floating.contains_key(&window) } + /// La ventana en pantalla completa de este escritorio, si hay alguna. + pub fn fullscreen(&self) -> Option { + self.fullscreen + } + + /// Pone (o quita, con `None`) la ventana en pantalla completa. + pub fn set_fullscreen(&mut self, window: Option) { + self.fullscreen = window; + } + /// Ventana enfocada, o `None` si el escritorio está vacío. pub fn focused(&self) -> Option { self.windows.get(self.focus).copied() diff --git a/crates/modules/mirada/mirada-protocol/src/lib.rs b/crates/modules/mirada/mirada-protocol/src/lib.rs index 58701e4..a8e9169 100644 --- a/crates/modules/mirada/mirada-protocol/src/lib.rs +++ b/crates/modules/mirada/mirada-protocol/src/lib.rs @@ -48,6 +48,8 @@ pub struct WindowPlacement { pub focused: bool, /// `true` si flota (fuera del teselado): el Cuerpo la pinta encima. pub floating: bool, + /// `true` si está en pantalla completa: cubre toda la salida. + pub fullscreen: bool, } /// Una orden del Cerebro al Cuerpo. @@ -140,20 +142,31 @@ pub fn read_frame(r: &mut R) -> io::Result Vec { + let fullscreen = ws.fullscreen(); let monocle = ws.params().mode == LayoutMode::Monocle; let focused = ws.focused(); ws.layout(screen) .into_iter() .map(|(id, rect)| { - let is_focused = focused == Some(id); let floating = ws.is_floating(id); + let is_fs = fullscreen == Some(id); + // Con una ventana en pantalla completa manda ella: ocupa toda + // la salida, es la única visible y se lleva el foco. + let (rect, visible, is_focused) = match fullscreen { + Some(_) => (if is_fs { screen } else { rect }, is_fs, is_fs), + None => { + let f = focused == Some(id); + // Una flotante siempre se ve; en `Monocle`, sólo la enfocada. + (rect, floating || !monocle || f, f) + } + }; WindowPlacement { id, rect, - // Una flotante siempre se ve; en `Monocle`, sólo la enfocada. - visible: floating || !monocle || is_focused, + visible, focused: is_focused, floating, + fullscreen: is_fs, } }) .collect() @@ -183,6 +196,7 @@ mod tests { visible: true, focused: true, floating: false, + fullscreen: false, }]); let mut buf = Vec::new(); write_frame(&mut buf, &cmd).unwrap(); @@ -277,6 +291,19 @@ mod tests { assert_eq!(f.rect, Rect::new(0, 0, 200, 200)); } + #[test] + fn a_fullscreen_window_covers_the_screen_and_hides_the_rest() { + let mut w = ws(LayoutMode::Columns); + w.set_fullscreen(Some(20)); + let p = placements(&w, SCREEN); + let fs = p.iter().find(|x| x.id == 20).unwrap(); + assert!(fs.fullscreen); + assert!(fs.focused, "la ventana en pantalla completa se lleva el foco"); + assert_eq!(fs.rect, SCREEN); + // El resto queda oculto. + assert!(p.iter().filter(|x| x.id != 20).all(|x| !x.visible)); + } + #[test] fn placements_fill_a_place_command_round_trip() { let cmd = BrainCommand::Place(placements(&ws(LayoutMode::Grid), SCREEN)); diff --git a/vamos.txt b/vamos.txt index 3ec5838..5f02e12 100644 --- a/vamos.txt +++ b/vamos.txt @@ -1065,5 +1065,13 @@ + Pantalla completa — toggle-fullscreen (Super+Shift+f): + La ventana enfocada cubre toda la salida (sin gap), oculta al resto y se lleva el foco. + Workspace.fullscreen: Option; WindowPlacement/BodyOp::Configure llevan fullscreen: bool; + el Cuerpo le fija el estado xdg_toplevel Fullscreen. Alternar de nuevo restaura el teselado. + mirada-ctl toggle-fullscreen + + +