feat(mirada): ventanas flotantes — toggle-float

Una ventana puede salir del teselado y flotar: conserva su propio
rectángulo y se compone por encima de las teseladas.

- Workspace guarda las flotantes en un mapa aparte; layout() tesela
  sólo las no-flotantes y añade las flotantes al final (orden de
  pintado). set_floating / is_floating.
- WindowPlacement y BodyOp::Configure llevan floating: bool. BodyState
  detecta el cambio de floating como cualquier otro reconfigure.
- DesktopAction::ToggleFloat (Super+f): saca la enfocada a un
  rectángulo centrado al 60 % de la pantalla, o la devuelve al teselado.
  En Monocle, una flotante sigue visible.
- mirada-compositor ordena las flotantes al frente de la lista
  front-to-back de elementos → se pintan encima.
- HUD de mirada marca las flotantes; mirada-ctl toggle-float.

Verificado end-to-end con headless-ctl. mirada-layout 30->32,
mirada-protocol 9->10, mirada-body 13->14, mirada-brain 41->42.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 00:55:33 +00:00
parent 2dd8ff139e
commit 4719f7c9f9
12 changed files with 230 additions and 22 deletions
@@ -46,6 +46,8 @@ pub struct WindowPlacement {
pub visible: bool,
/// `true` si esta ventana tiene el foco del teclado.
pub focused: bool,
/// `true` si flota (fuera del teselado): el Cuerpo la pinta encima.
pub floating: bool,
}
/// Una orden del Cerebro al Cuerpo.
@@ -144,11 +146,14 @@ pub fn placements(ws: &Workspace, screen: Rect) -> Vec<WindowPlacement> {
.into_iter()
.map(|(id, rect)| {
let is_focused = focused == Some(id);
let floating = ws.is_floating(id);
WindowPlacement {
id,
rect,
visible: !monocle || is_focused,
// Una flotante siempre se ve; en `Monocle`, sólo la enfocada.
visible: floating || !monocle || is_focused,
focused: is_focused,
floating,
}
})
.collect()
@@ -177,6 +182,7 @@ mod tests {
rect: Rect::new(0, 0, 800, 600),
visible: true,
focused: true,
floating: false,
}]);
let mut buf = Vec::new();
write_frame(&mut buf, &cmd).unwrap();
@@ -259,6 +265,18 @@ mod tests {
assert!(placements(&empty, SCREEN).is_empty());
}
#[test]
fn a_floating_window_is_marked_and_stays_visible_in_monocle() {
let mut w = ws(LayoutMode::Monocle); // Monocle oculta las no enfocadas
w.set_floating(10, Some(Rect::new(0, 0, 200, 200)));
let p = placements(&w, SCREEN);
let f = p.iter().find(|x| x.id == 10).unwrap();
assert!(f.floating);
assert!(f.visible, "una flotante se ve aunque el modo sea Monocle");
// Y conserva su rectángulo flotante.
assert_eq!(f.rect, Rect::new(0, 0, 200, 200));
}
#[test]
fn placements_fill_a_place_command_round_trip() {
let cmd = BrainCommand::Place(placements(&ws(LayoutMode::Grid), SCREEN));