gioser

tawasuyu/brahman

Rust 521TOML 202Markdown 57JSON 197505 Definiciones

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>

4719f7c9f950c56872881be910c853d72a8867b0 · por sergio · (hace 5 meses) · padre 2dd8ff139 · ver el árbol en este commit

Delta semántico · 35

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

  • cambió por dentro 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

12 archivos · +230 −22

diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs
index de69296..f49cd6a 100644
--- a/crates/apps/mirada-compositor/src/main.rs
+++ b/crates/apps/mirada-compositor/src/main.rs
@@ -82,6 +82,8 @@ struct ManagedWindow {
     /// Esquina superior-izquierda en píxeles, según el Cerebro.
     loc: (i32, i32),
     visible: bool,
+    /// `true` si flota: se compone por encima de las teseladas.
+    floating: bool,
 }
 
 /// El estado global del compositor.
@@ -170,10 +172,11 @@ 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 } => {
+            BodyOp::Configure { id, rect, visible, floating } => {
                 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());
                     });
@@ -244,6 +247,7 @@ impl App {
             surface,
             loc: (0, 0),
             visible: false,
+            floating: false,
         });
         let ev = self.body.open_surface(id, app_id, title);
         self.brain_feed(ev);
@@ -684,10 +688,15 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
         let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
         {
             let (renderer, mut framebuffer) = backend.bind().unwrap();
-            let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = state
-                .windows
+            // Orden de pintado: la lista de elementos va front-to-back
+            // (índice 0 = encima), así que las flotantes —que deben
+            // quedar sobre las teseladas— se ordenan primero. `sort_by_key`
+            // es estable: dentro de cada grupo se respeta el orden de apertura.
+            let mut shown: Vec<&ManagedWindow> =
+                state.windows.iter().filter(|w| w.visible).collect();
+            shown.sort_by_key(|w| !w.floating);
+            let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = shown
                 .iter()
-                .filter(|w| w.visible)
                 .flat_map(|w| {
                     render_elements_from_surface_tree(
                         renderer,
diff --git a/crates/apps/mirada-ctl/src/main.rs b/crates/apps/mirada-ctl/src/main.rs
index 83c68b0..0f596ea 100644
--- a/crates/apps/mirada-ctl/src/main.rs
+++ b/crates/apps/mirada-ctl/src/main.rs
@@ -120,6 +120,7 @@ Acciones de mirada-ctl:
   move-forward               adelanta la ventana enfocada en el teselado
   move-backward              la atrasa
   close-focused              cierra la ventana enfocada
+  toggle-float               alterna flotante / teselada 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 7ac08c0..3e117fb 100644
--- a/crates/apps/mirada/src/main.rs
+++ b/crates/apps/mirada/src/main.rs
@@ -21,10 +21,10 @@
 //! ```text
 //!   n            abre una ventana          tab / espacio  cicla layout
 //!   w            cierra la enfocada        t m g c r d s  layout directo
-//!   j / k        foco siguiente/anterior   h / l          área maestra −/+
-//!   Shift+j / k  mueve la enfocada         , / .          nmaster −/+
-//!   Enter        promueve a maestra        1..9           ir a escritorio
-//!                                          Ctrl+1..9      enviar a escritorio
+//!   f            flota / tesela            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
 //! ```
 //!
 //! Los pips de escritorio y las ventanas del lienzo son **clicables**, y
@@ -278,6 +278,7 @@ impl Mirada {
         match ks.key.as_str() {
             "n" if !connected => self.open_window(),
             "w" => self.act(DesktopAction::CloseFocused),
+            "f" => self.act(DesktopAction::ToggleFloat),
             "j" if shift => self.act(DesktopAction::MoveForward),
             "k" if shift => self.act(DesktopAction::MoveBackward),
             "j" => self.act(DesktopAction::FocusNext),
@@ -440,6 +441,11 @@ 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 {
+                "· ventana flotante ·"
+            } else {
+                "· superficie del Cuerpo ·"
+            };
 
             canvas = canvas.child(
                 div()
@@ -486,7 +492,7 @@ impl Render for Mirada {
                             .gap(px(4.))
                             .text_color(theme.fg_disabled)
                             .child(SharedString::from(app_id))
-                            .child("· superficie del Cuerpo ·"),
+                            .child(kind_label),
                     ),
             );
         }
diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md
index ccec167..129948f 100644
--- a/crates/modules/mirada/SDD.md
+++ b/crates/modules/mirada/SDD.md
@@ -113,6 +113,11 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres:
 - **`DesktopAction::FocusWindow(WindowId)`** — direccionamiento directo de
   una ventana (no sólo ciclar con `FocusNext`/`Prev`); si está en otro
   escritorio, salta a él. Lo usan la taskbar y `mirada-ctl`.
+- **Ventanas flotantes** — `ToggleFloat` (`Super+f`) saca la enfocada del
+  teselado a un rectángulo libre (centrado, 60 %); `Workspace` guarda las
+  flotantes aparte, `layout()` las pone al final y `WindowPlacement`
+  /`BodyOp::Configure` llevan `floating: bool` para que el Cuerpo las
+  componga por encima.
 - **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`);
@@ -140,8 +145,8 @@ gráficos para ejercitar `mirada-ctl` en modo desatendido.
 
 ## Estado
 
-Implementado y verde: `mirada-layout` (30 tests), `mirada-protocol`
-(9), `mirada-brain` (41), `mirada-link` (7), `mirada-body` (13), las
+Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol`
+(10), `mirada-brain` (42), `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 385806d..f5060c4 100644
--- a/crates/modules/mirada/mirada-body/src/lib.rs
+++ b/crates/modules/mirada/mirada-body/src/lib.rs
@@ -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();
diff --git a/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs b/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs
index 77240d9..19702d6 100644
--- a/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs
+++ b/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs
@@ -54,12 +54,13 @@ fn main() {
                                 BrainCommand::Place(places) => {
                                     for p in places {
                                         eprintln!(
-                                            "    win {} → {:>5}×{:<4} @ ({:>5},{:>4}){}",
+                                            "    win {} → {:>5}×{:<4} @ ({:>5},{:>4}){}{}",
                                             p.id,
                                             p.rect.w,
                                             p.rect.h,
                                             p.rect.x,
                                             p.rect.y,
+                                            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 39df510..438cc54 100644
--- a/crates/modules/mirada/mirada-brain/src/action.rs
+++ b/crates/modules/mirada/mirada-brain/src/action.rs
@@ -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)),
diff --git a/crates/modules/mirada/mirada-brain/src/desktop.rs b/crates/modules/mirada/mirada-brain/src/desktop.rs
index 36629a0..2500803 100644
--- a/crates/modules/mirada/mirada-brain/src/desktop.rs
+++ b/crates/modules/mirada/mirada-brain/src/desktop.rs
@@ -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();
diff --git a/crates/modules/mirada/mirada-brain/src/keymap.rs b/crates/modules/mirada/mirada-brain/src/keymap.rs
index 2de0ea4..918e7ae 100644
--- a/crates/modules/mirada/mirada-brain/src/keymap.rs
+++ b/crates/modules/mirada/mirada-brain/src/keymap.rs
@@ -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
diff --git a/crates/modules/mirada/mirada-layout/src/workspace.rs b/crates/modules/mirada/mirada-layout/src/workspace.rs
index 4d68d84..938a362 100644
--- a/crates/modules/mirada/mirada-layout/src/workspace.rs
+++ b/crates/modules/mirada/mirada-layout/src/workspace.rs
@@ -1,5 +1,7 @@
 //! `Workspace` — un conjunto de ventanas, su foco y su modo de teselado.
 
+use std::collections::BTreeMap;
+
 use serde::{Deserialize, Serialize};
 
 use crate::geometry::Rect;
@@ -16,12 +18,20 @@ pub struct Workspace {
     /// Índice de la ventana enfocada en `windows`.
     focus: usize,
     params: LayoutParams,
+    /// 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>,
 }
 
 impl Workspace {
     /// Escritorio vacío con los parámetros dados.
     pub fn new(params: LayoutParams) -> Self {
-        Self { windows: Vec::new(), focus: 0, params }
+        Self {
+            windows: Vec::new(),
+            focus: 0,
+            params,
+            floating: BTreeMap::new(),
+        }
     }
 
     pub fn len(&self) -> usize {
@@ -73,6 +83,7 @@ impl Workspace {
             return false;
         };
         self.windows.remove(i);
+        self.floating.remove(&window);
         if i < self.focus {
             self.focus -= 1;
         }
@@ -82,6 +93,24 @@ impl Workspace {
         true
     }
 
+    /// Marca una ventana como flotante en `rect`, o la devuelve al
+    /// teselado con `None`. La ventana sigue en el orden de foco.
+    pub fn set_floating(&mut self, window: WindowId, rect: Option<Rect>) {
+        match rect {
+            Some(r) => {
+                self.floating.insert(window, r);
+            }
+            None => {
+                self.floating.remove(&window);
+            }
+        }
+    }
+
+    /// `true` si la ventana está flotando.
+    pub fn is_floating(&self, window: WindowId) -> bool {
+        self.floating.contains_key(&window)
+    }
+
     /// Ventana enfocada, o `None` si el escritorio está vacío.
     pub fn focused(&self) -> Option<WindowId> {
         self.windows.get(self.focus).copied()
@@ -142,10 +171,24 @@ impl Workspace {
     }
 
     /// Resuelve la geometría: el rectángulo de cada ventana dentro de
-    /// `screen`, en orden de teselado.
+    /// `screen`. Primero las teseladas en orden de teselado, luego las
+    /// flotantes con su propio rectángulo — éstas van al final para que
+    /// el Cuerpo las pinte encima.
     pub fn layout(&self, screen: Rect) -> Vec<(WindowId, Rect)> {
-        let rects = tile(screen, self.windows.len(), &self.params);
-        self.windows.iter().copied().zip(rects).collect()
+        let tiled: Vec<WindowId> = self
+            .windows
+            .iter()
+            .copied()
+            .filter(|id| !self.floating.contains_key(id))
+            .collect();
+        let rects = tile(screen, tiled.len(), &self.params);
+        let mut out: Vec<(WindowId, Rect)> = tiled.into_iter().zip(rects).collect();
+        for &id in &self.windows {
+            if let Some(&rect) = self.floating.get(&id) {
+                out.push((id, rect));
+            }
+        }
+        out
     }
 }
 
@@ -268,4 +311,35 @@ mod tests {
     fn empty_workspace_lays_out_nothing() {
         assert!(ws().layout(Rect::new(0, 0, 800, 600)).is_empty());
     }
+
+    #[test]
+    fn a_floating_window_keeps_its_rect_and_goes_last() {
+        let mut w = ws();
+        for id in [1, 2, 3] {
+            w.add(id);
+        }
+        let float_rect = Rect::new(50, 50, 400, 300);
+        w.set_floating(2, Some(float_rect));
+        assert!(w.is_floating(2));
+        let placed = w.layout(Rect::new(0, 0, 1920, 1080));
+        assert_eq!(placed.len(), 3);
+        // La flotante va al final, con su rectángulo intacto.
+        assert_eq!(placed[2], (2, float_rect));
+        let ids: Vec<_> = placed.iter().map(|(id, _)| *id).collect();
+        assert_eq!(ids, vec![1, 3, 2]);
+        // Devolverla al teselado.
+        w.set_floating(2, None);
+        assert!(!w.is_floating(2));
+        assert_eq!(w.layout(Rect::new(0, 0, 1920, 1080)).len(), 3);
+    }
+
+    #[test]
+    fn removing_a_window_clears_its_floating_state() {
+        let mut w = ws();
+        w.add(1);
+        w.set_floating(1, Some(Rect::new(0, 0, 100, 100)));
+        w.remove(1);
+        w.add(1); // mismo id, ventana nueva: ya no flota
+        assert!(!w.is_floating(1));
+    }
 }
diff --git a/crates/modules/mirada/mirada-protocol/src/lib.rs b/crates/modules/mirada/mirada-protocol/src/lib.rs
index 7e0a9c2..58701e4 100644
--- a/crates/modules/mirada/mirada-protocol/src/lib.rs
+++ b/crates/modules/mirada/mirada-protocol/src/lib.rs
@@ -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));
diff --git a/vamos.txt b/vamos.txt
index 531d115..9ffe4d6 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -1047,5 +1047,14 @@
 
 
 
+  Ventanas flotantes — toggle-float (Super+f):
+  La ventana enfocada sale del teselado a un rectángulo libre (centrado, 60% de pantalla);
+  las demás reteselan sin ella. Workspace guarda las flotantes aparte; layout() las pone al final.
+  WindowPlacement y BodyOp::Configure llevan floating: bool — el Cuerpo las compone por encima
+  (mirada-compositor ordena las flotantes al frente de la lista front-to-back).
+  mirada-ctl toggle-float    # alterna flotante / teselada
+
+
+