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
@@ -38,6 +38,8 @@ pub enum DesktopAction {
MoveBackward,
/// Cierra la ventana enfocada (cierre ordenado).
CloseFocused,
/// Alterna entre flotante y teselada la ventana enfocada.
ToggleFloat,
/// Pasa al siguiente modo de teselado.
CycleLayout,
/// Fija un modo de teselado concreto.
@@ -98,6 +100,7 @@ impl fmt::Display for DesktopAction {
DesktopAction::MoveForward => f.write_str("move-forward"),
DesktopAction::MoveBackward => f.write_str("move-backward"),
DesktopAction::CloseFocused => f.write_str("close-focused"),
DesktopAction::ToggleFloat => f.write_str("toggle-float"),
DesktopAction::CycleLayout => f.write_str("cycle-layout"),
DesktopAction::SetLayout(m) => write!(f, "layout:{}", layout_slug(*m)),
DesktopAction::GrowMaster => f.write_str("grow-master"),
@@ -125,6 +128,7 @@ impl FromStr for DesktopAction {
"move-forward" => Self::MoveForward,
"move-backward" => Self::MoveBackward,
"close-focused" => Self::CloseFocused,
"toggle-float" => Self::ToggleFloat,
"cycle-layout" => Self::CycleLayout,
"grow-master" => Self::GrowMaster,
"shrink-master" => Self::ShrinkMaster,
@@ -183,6 +187,7 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> {
("Super+Shift+j".into(), DesktopAction::MoveForward),
("Super+Shift+k".into(), DesktopAction::MoveBackward),
("Super+q".into(), DesktopAction::CloseFocused),
("Super+f".into(), DesktopAction::ToggleFloat),
("Super+space".into(), DesktopAction::CycleLayout),
("Super+t".into(), DesktopAction::SetLayout(LayoutMode::MasterStack)),
("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)),
@@ -179,6 +179,22 @@ impl Desktop {
None => Vec::new(),
}
}
DesktopAction::ToggleFloat => {
let Some(id) = self.workspaces[self.active].focused() else {
return Vec::new();
};
let screen = self.screen();
let ws = &mut self.workspaces[self.active];
if ws.is_floating(id) {
ws.set_floating(id, None);
} else {
let rect = screen
.map(centered_float_rect)
.unwrap_or_else(|| Rect::new(100, 100, 800, 600));
ws.set_floating(id, Some(rect));
}
self.relayout()
}
DesktopAction::CycleLayout => {
let next = self.workspaces[self.active].params().mode.next();
self.workspaces[self.active].set_mode(next);
@@ -306,6 +322,18 @@ impl Desktop {
}
}
/// El rectángulo flotante por defecto: 60 % de la pantalla, centrado.
fn centered_float_rect(screen: Rect) -> Rect {
let w = screen.w * 3 / 5;
let h = screen.h * 3 / 5;
Rect::new(
screen.x + (screen.w - w) / 2,
screen.y + (screen.h - h) / 2,
w,
h,
)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -409,6 +437,21 @@ mod tests {
assert!(!w2.focused);
}
#[test]
fn toggle_float_marks_the_focused_window_and_floats_it_last() {
let mut d = desktop_with_screen();
open(&mut d, 1);
open(&mut d, 2); // enfocada
let cmds = d.apply(DesktopAction::ToggleFloat);
let p = places(&cmds);
assert!(p.iter().find(|x| x.id == 2).unwrap().floating);
// La flotante va al final de la lista — orden de pintado.
assert_eq!(p.last().unwrap().id, 2);
// Alternar de nuevo la devuelve al teselado.
let cmds = d.apply(DesktopAction::ToggleFloat);
assert!(!places(&cmds).iter().find(|x| x.id == 2).unwrap().floating);
}
#[test]
fn without_a_screen_nothing_is_placed() {
let mut d = Desktop::new();
@@ -237,6 +237,7 @@ const KEYMAP_HEADER: &str = "\
// focus-next / focus-prev mueve el foco
// move-forward / move-backward reordena la ventana enfocada
// close-focused cierra la enfocada
// toggle-float alterna flotante / teselada
// cycle-layout siguiente modo de teselado
// layout:<modo> master-stack | centered-master | spiral
// grid | columns | rows | monocle