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
+41 -5
View File
@@ -29,19 +29,29 @@ pub struct Surface {
pub geometry: Option<Rect>,
pub visible: bool,
pub focused: bool,
/// `true` si flota: el backend la pinta por encima de las teseladas.
pub floating: bool,
}
impl Surface {
fn new(app_id: String, title: String) -> Self {
Self { app_id, title, geometry: None, visible: false, focused: false }
Self {
app_id,
title,
geometry: None,
visible: false,
focused: false,
floating: false,
}
}
}
/// Una orden concreta para el backend (smithay, headless, …).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BodyOp {
/// Recoloca una superficie y la muestra u oculta.
Configure { id: WindowId, rect: Rect, visible: bool },
/// 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 },
/// Da el foco del teclado a una superficie.
Focus(WindowId),
/// Quita el foco a todas las superficies.
@@ -91,13 +101,18 @@ impl BodyState {
new_focus = Some(p.id);
}
if let Some(s) = self.surfaces.get_mut(&p.id) {
if s.geometry != Some(p.rect) || s.visible != p.visible {
if s.geometry != Some(p.rect)
|| s.visible != p.visible
|| s.floating != p.floating
{
s.geometry = Some(p.rect);
s.visible = p.visible;
s.floating = p.floating;
ops.push(BodyOp::Configure {
id: p.id,
rect: p.rect,
visible: p.visible,
floating: p.floating,
});
}
}
@@ -108,7 +123,12 @@ impl BodyState {
if !listed.contains(id) && s.visible {
s.visible = false;
let rect = s.geometry.unwrap_or(Rect::new(0, 0, 0, 0));
ops.push(BodyOp::Configure { id: *id, rect, visible: false });
ops.push(BodyOp::Configure {
id: *id,
rect,
visible: false,
floating: s.floating,
});
}
}
@@ -227,6 +247,7 @@ mod tests {
rect: Rect::new(0, 0, 800, 600),
visible,
focused,
floating: false,
}
}
@@ -288,6 +309,7 @@ mod tests {
id: 2,
rect: Rect::new(0, 0, 800, 600),
visible: false,
floating: false,
}));
assert!(!b.surface(2).unwrap().visible);
}
@@ -369,6 +391,20 @@ mod tests {
assert_eq!(ops, vec![BodyOp::Focus(2)]);
}
#[test]
fn a_floating_change_alone_triggers_a_configure() {
let mut b = body_with_two();
let mut p1 = placement(1, true, true);
b.apply(BrainCommand::Place(vec![p1, placement(2, true, false)]));
// Sólo cambia `floating` — misma geometría y visibilidad.
p1.floating = true;
let ops = b.apply(BrainCommand::Place(vec![p1, placement(2, true, false)]));
assert!(ops
.iter()
.any(|o| matches!(o, BodyOp::Configure { id: 1, floating: true, .. })));
assert!(b.surface(1).unwrap().floating);
}
#[test]
fn visible_iterates_only_shown_surfaces() {
let mut b = body_with_two();