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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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`).
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 { "" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user