feat(mirada): pantalla completa real — toggle-fullscreen
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<WindowId>; 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 <noreply@anthropic.com>
be61ddb6eb441193c4374a243fbb00aed47b42de · ruwaq sergio · 2026-05-21T01:07:01Z (5 killa ñawpaq) · tayta 6dfd9e62a · kay commitpi sach'ata qhaway
Semántico sayk'uy · 27
crates/apps/mirada-compositor/src/main.rs
crates/apps/mirada-ctl/src/main.rs
crates/apps/mirada/src/main.rs
crates/modules/mirada/mirada-body/src/lib.rs
crates/modules/mirada/mirada-brain/examples/headless-ctl.rs
ukhunpi t'ikrakurqan fn main
crates/modules/mirada/mirada-brain/src/action.rs
crates/modules/mirada/mirada-brain/src/desktop.rs
crates/modules/mirada/mirada-brain/src/keymap.rs
crates/modules/mirada/mirada-layout/src/workspace.rs
crates/modules/mirada/mirada-protocol/src/lib.rs
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 <modo> 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<WindowId>`, 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:<modo> 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<WindowId, Rect>,
+ /// La ventana en pantalla completa, si hay alguna: cubre toda la
+ /// salida y oculta al resto.
+ fullscreen: Option<WindowId>,
}
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<WindowId> {
+ self.fullscreen
+ }
+
+ /// Pone (o quita, con `None`) la ventana en pantalla completa.
+ pub fn set_fullscreen(&mut self, window: Option<WindowId>) {
+ self.fullscreen = window;
+ }
+
/// Ventana enfocada, o `None` si el escritorio está vacío.
pub fn focused(&self) -> Option<WindowId> {
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: Read, T: DeserializeOwned>(r: &mut R) -> io::Result<Option<
/// En modo [`LayoutMode::Monocle`] sólo la ventana enfocada queda
/// `visible`; en el resto de modos todas lo están.
pub fn placements(ws: &Workspace, screen: Rect) -> Vec<WindowPlacement> {
+ 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<WindowId>; 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
+
+
+