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>
This commit is contained in:
sergio
2026-05-21 01:07:01 +00:00
parent 6dfd9e62ac
commit be61ddb6eb
12 changed files with 133 additions and 11 deletions
@@ -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));