feat(mirada): 3 layouts nuevos + redimensionar el área maestra
mirada-layout pasa de 4 a 7 modos de teselado, todos intercambiables por el API (SetLayout / CycleLayout / mirada-ctl layout <modo>): - Rows: filas horizontales de igual alto (complemento de Columns). - Spiral: espiral de Fibonacci — cada ventana parte por la mitad el espacio restante, alternando el sentido del corte. - CenteredMaster: maestra centrada + pila a ambos lados (monitores anchos). LayoutMode::ALL + next() definen el ciclo. Añade dos acciones, GrowMaster/ShrinkMaster (Super+l / Super+h), que ajustan master_ratio en caliente — ese parámetro existía pero no había forma de tocarlo. Cableado completo: tile(), cycle, slugs Display/FromStr, keymap por defecto (Super+r/d/s), HUD de mirada, mirada-ctl actions. El ejemplo headless-ctl ahora imprime la geometría para verificar los layouts. mirada-layout 22->26 tests, mirada-brain 37->39. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -61,8 +61,9 @@ WAYLAND_DISPLAY=wayland-1 foot # o weston-terminal, alacritty, …
|
|||||||
|
|
||||||
Las ventanas se teselan solas. El teclado, con la ventana del compositor
|
Las ventanas se teselan solas. El teclado, con la ventana del compositor
|
||||||
enfocada, maneja el escritorio con atajos `Super+…`: foco `Super+j/k`,
|
enfocada, maneja el escritorio con atajos `Super+…`: foco `Super+j/k`,
|
||||||
ciclar layout `Super+space`, escritorios `Super+1..9`, cerrar `Super+q`.
|
los 7 layouts en `Super+t/m/g/c/r/d/s` (o ciclar con `Super+space`), área
|
||||||
Cierra la ventana del compositor para salir.
|
maestra `Super+h/l`, escritorios `Super+1..9`, cerrar `Super+q`. Cierra
|
||||||
|
la ventana del compositor para salir.
|
||||||
|
|
||||||
## Atajos de teclado
|
## Atajos de teclado
|
||||||
|
|
||||||
|
|||||||
@@ -109,18 +109,25 @@ fn print_help() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn print_actions() {
|
fn print_actions() {
|
||||||
println!(
|
// Cadena multilínea literal: la indentación de cada línea es la que
|
||||||
"Acciones de mirada-ctl:\n \
|
// se imprime (el `\` tras la comilla se come sólo el primer salto).
|
||||||
focus-next mueve el foco a la siguiente ventana\n \
|
print!(
|
||||||
focus-prev mueve el foco a la anterior\n \
|
"\
|
||||||
focus-window <id> enfoca la ventana <id> (ver: mirada-ctl windows)\n \
|
Acciones de mirada-ctl:
|
||||||
move-forward adelanta la ventana enfocada en el teselado\n \
|
focus-next mueve el foco a la siguiente ventana
|
||||||
move-backward la atrasa\n \
|
focus-prev mueve el foco a la anterior
|
||||||
close-focused cierra la ventana enfocada\n \
|
focus-window <id> enfoca la ventana <id> (ver: mirada-ctl windows)
|
||||||
cycle-layout pasa al siguiente modo de teselado\n \
|
move-forward adelanta la ventana enfocada en el teselado
|
||||||
layout <modo> master-stack | monocle | grid | columns\n \
|
move-backward la atrasa
|
||||||
workspace <n> activa el escritorio n (1..9)\n \
|
close-focused cierra la ventana enfocada
|
||||||
send-to-workspace <n> manda la enfocada al escritorio n\n \
|
cycle-layout pasa al siguiente modo de teselado
|
||||||
quit apaga el compositor"
|
layout <modo> master-stack · centered-master · spiral
|
||||||
|
grid · columns · rows · monocle
|
||||||
|
grow-master agranda el área de la ventana maestra
|
||||||
|
shrink-master la encoge
|
||||||
|
workspace <n> activa el escritorio n (1..9)
|
||||||
|
send-to-workspace <n> manda la enfocada al escritorio n
|
||||||
|
quit apaga el compositor
|
||||||
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,10 @@
|
|||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! n abre una ventana tab / espacio cicla layout
|
//! n abre una ventana tab / espacio cicla layout
|
||||||
//! w cierra la enfocada t g c m layout directo
|
//! w cierra la enfocada t m g c r d s layout directo
|
||||||
//! j / k foco siguiente/anterior 1..9 ir a escritorio
|
//! j / k foco siguiente/anterior h / l área maestra −/+
|
||||||
//! Shift+j / k mueve la enfocada Ctrl+1..9 enviar a escritorio
|
//! Shift+j / k mueve la enfocada 1..9 ir a escritorio
|
||||||
|
//! Ctrl+1..9 enviar a escritorio
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Los pips de escritorio y las ventanas del lienzo son **clicables**, y
|
//! Los pips de escritorio y las ventanas del lienzo son **clicables**, y
|
||||||
@@ -285,6 +286,11 @@ impl Mirada {
|
|||||||
"m" => self.act(DesktopAction::SetLayout(LayoutMode::Monocle)),
|
"m" => self.act(DesktopAction::SetLayout(LayoutMode::Monocle)),
|
||||||
"g" => self.act(DesktopAction::SetLayout(LayoutMode::Grid)),
|
"g" => self.act(DesktopAction::SetLayout(LayoutMode::Grid)),
|
||||||
"c" => self.act(DesktopAction::SetLayout(LayoutMode::Columns)),
|
"c" => self.act(DesktopAction::SetLayout(LayoutMode::Columns)),
|
||||||
|
"r" => self.act(DesktopAction::SetLayout(LayoutMode::Rows)),
|
||||||
|
"d" => self.act(DesktopAction::SetLayout(LayoutMode::CenteredMaster)),
|
||||||
|
"s" => self.act(DesktopAction::SetLayout(LayoutMode::Spiral)),
|
||||||
|
"h" => self.act(DesktopAction::ShrinkMaster),
|
||||||
|
"l" => self.act(DesktopAction::GrowMaster),
|
||||||
d if d.len() == 1 && d.as_bytes()[0].is_ascii_digit() && d != "0" => {
|
d if d.len() == 1 && d.as_bytes()[0].is_ascii_digit() && d != "0" => {
|
||||||
let n = (d.as_bytes()[0] - b'1') as usize;
|
let n = (d.as_bytes()[0] - b'1') as usize;
|
||||||
if ctrl {
|
if ctrl {
|
||||||
@@ -312,6 +318,9 @@ fn mode_name(m: LayoutMode) -> &'static str {
|
|||||||
LayoutMode::Monocle => "monóculo",
|
LayoutMode::Monocle => "monóculo",
|
||||||
LayoutMode::Grid => "rejilla",
|
LayoutMode::Grid => "rejilla",
|
||||||
LayoutMode::Columns => "columnas",
|
LayoutMode::Columns => "columnas",
|
||||||
|
LayoutMode::Rows => "filas",
|
||||||
|
LayoutMode::CenteredMaster => "maestro centrado",
|
||||||
|
LayoutMode::Spiral => "espiral",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ ejecuta operaciones de geometría".
|
|||||||
## Detalle por crate
|
## Detalle por crate
|
||||||
|
|
||||||
- **`mirada-layout`** — `Rect` + `split` (reparto exacto de píxeles),
|
- **`mirada-layout`** — `Rect` + `split` (reparto exacto de píxeles),
|
||||||
`LayoutMode` (`MasterStack`/`Monocle`/`Grid`/`Columns`), `Workspace`
|
`LayoutMode` con 7 modos (`MasterStack`, `CenteredMaster`, `Spiral`
|
||||||
con foco cíclico y reordenado. Determinista.
|
—espiral de Fibonacci—, `Grid`, `Columns`, `Rows`, `Monocle`) y
|
||||||
|
`LayoutMode::next()` para el ciclo, `Workspace` con foco cíclico y
|
||||||
|
reordenado. Determinista.
|
||||||
- **`mirada-protocol`** — `WindowPlacement`, los enums `BrainCommand` y
|
- **`mirada-protocol`** — `WindowPlacement`, los enums `BrainCommand` y
|
||||||
`BodyEvent`, el marco `postcard` con prefijo `u32` LE
|
`BodyEvent`, el marco `postcard` con prefijo `u32` LE
|
||||||
(`write_frame`/`read_frame`, guard `MAX_FRAME`) y el puente
|
(`write_frame`/`read_frame`, guard `MAX_FRAME`) y el puente
|
||||||
@@ -109,6 +111,10 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres:
|
|||||||
- **`DesktopAction::FocusWindow(WindowId)`** — direccionamiento directo de
|
- **`DesktopAction::FocusWindow(WindowId)`** — direccionamiento directo de
|
||||||
una ventana (no sólo ciclar con `FocusNext`/`Prev`); si está en otro
|
una ventana (no sólo ciclar con `FocusNext`/`Prev`); si está en otro
|
||||||
escritorio, salta a él. Lo usan la taskbar y `mirada-ctl`.
|
escritorio, salta a él. Lo usan la taskbar y `mirada-ctl`.
|
||||||
|
- **`SetLayout`/`CycleLayout`/`GrowMaster`/`ShrinkMaster`** — los 7 modos
|
||||||
|
de teselado se intercambian por el API (`mirada-ctl layout spiral`), y
|
||||||
|
el área maestra se redimensiona en caliente (`grow`/`shrink-master`,
|
||||||
|
atajos `Super+l`/`Super+h`).
|
||||||
- **HUD interactivo** (app `mirada`) — los pips de escritorio y las
|
- **HUD interactivo** (app `mirada`) — los pips de escritorio y las
|
||||||
ventanas del lienzo son clicables: clic = `apply` de la acción.
|
ventanas del lienzo son clicables: clic = `apply` de la acción.
|
||||||
- **`mirada-ctl`** — control externo por línea de comandos
|
- **`mirada-ctl`** — control externo por línea de comandos
|
||||||
@@ -130,8 +136,8 @@ gráficos para ejercitar `mirada-ctl` en modo desatendido.
|
|||||||
|
|
||||||
## Estado
|
## Estado
|
||||||
|
|
||||||
Implementado y verde: `mirada-layout` (22 tests), `mirada-protocol`
|
Implementado y verde: `mirada-layout` (26 tests), `mirada-protocol`
|
||||||
(9), `mirada-brain` (37), `mirada-link` (7), `mirada-body` (13), las
|
(9), `mirada-brain` (39), `mirada-link` (7), `mirada-body` (13), las
|
||||||
apps `mirada` y `mirada-compositor` (compilan; verificación visual
|
apps `mirada` y `mirada-compositor` (compilan; verificación visual
|
||||||
manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`).
|
manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`).
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
println!("Cerebro headless · control en {}", path.display());
|
eprintln!("Cerebro headless · control en {}", path.display());
|
||||||
|
|
||||||
// Una pantalla y tres ventanas de muestra.
|
// Una pantalla y tres ventanas de muestra.
|
||||||
let mut desktop = Desktop::new();
|
let mut desktop = Desktop::new();
|
||||||
@@ -40,21 +40,37 @@ fn main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
print_state(&desktop);
|
print_state(&desktop);
|
||||||
println!(" esperando a mirada-ctl …");
|
eprintln!(" esperando a mirada-ctl …");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(mut conn) = server.poll() {
|
if let Some(mut conn) = server.poll() {
|
||||||
if let Ok(Some(req)) = conn.read_request() {
|
if let Ok(Some(req)) = conn.read_request() {
|
||||||
let reply = match req {
|
let reply = match req {
|
||||||
CtlRequest::Do(action) => {
|
CtlRequest::Do(action) => {
|
||||||
let cmds = desktop.apply(action);
|
eprintln!("· {action}");
|
||||||
|
for cmd in desktop.apply(action) {
|
||||||
|
match cmd {
|
||||||
|
// La geometría que el Cerebro mandaría al Cuerpo.
|
||||||
|
BrainCommand::Place(places) => {
|
||||||
|
for p in places {
|
||||||
|
eprintln!(
|
||||||
|
" win {} → {:>5}×{:<4} @ ({:>5},{:>4}){}",
|
||||||
|
p.id,
|
||||||
|
p.rect.w,
|
||||||
|
p.rect.h,
|
||||||
|
p.rect.x,
|
||||||
|
p.rect.y,
|
||||||
|
if p.focused { " *" } else { "" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Sin Cuerpo: simulamos nosotros el cierre.
|
// Sin Cuerpo: simulamos nosotros el cierre.
|
||||||
for cmd in cmds {
|
BrainCommand::Close(id) | BrainCommand::Kill(id) => {
|
||||||
if let BrainCommand::Close(id) | BrainCommand::Kill(id) = cmd {
|
|
||||||
desktop.on_event(BodyEvent::WindowClosed { id });
|
desktop.on_event(BodyEvent::WindowClosed { id });
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
println!("· {action}");
|
|
||||||
print_state(&desktop);
|
print_state(&desktop);
|
||||||
CtlReply::Ok
|
CtlReply::Ok
|
||||||
}
|
}
|
||||||
@@ -68,10 +84,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn print_state(d: &Desktop) {
|
fn print_state(d: &Desktop) {
|
||||||
println!(
|
let ws = d.active_workspace();
|
||||||
" escritorio {} · foco {:?} · ventanas/escritorio {:?}",
|
eprintln!(
|
||||||
|
" escritorio {} · {:?} (maestra {:.0}%) · foco {:?}",
|
||||||
d.active_index() + 1,
|
d.active_index() + 1,
|
||||||
|
ws.params().mode,
|
||||||
|
ws.params().master_ratio * 100.0,
|
||||||
d.focused_window(),
|
d.focused_window(),
|
||||||
d.workspace_loads(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ pub enum DesktopAction {
|
|||||||
CycleLayout,
|
CycleLayout,
|
||||||
/// Fija un modo de teselado concreto.
|
/// Fija un modo de teselado concreto.
|
||||||
SetLayout(LayoutMode),
|
SetLayout(LayoutMode),
|
||||||
|
/// Agranda el área de la ventana maestra (`MasterStack`/`CenteredMaster`).
|
||||||
|
GrowMaster,
|
||||||
|
/// Encoge el área de la ventana maestra.
|
||||||
|
ShrinkMaster,
|
||||||
/// Activa el escritorio virtual `n` (índice 0-based).
|
/// Activa el escritorio virtual `n` (índice 0-based).
|
||||||
SwitchWorkspace(usize),
|
SwitchWorkspace(usize),
|
||||||
/// Manda la ventana enfocada al escritorio virtual `n`.
|
/// Manda la ventana enfocada al escritorio virtual `n`.
|
||||||
@@ -58,6 +62,9 @@ fn layout_slug(mode: LayoutMode) -> &'static str {
|
|||||||
LayoutMode::Monocle => "monocle",
|
LayoutMode::Monocle => "monocle",
|
||||||
LayoutMode::Grid => "grid",
|
LayoutMode::Grid => "grid",
|
||||||
LayoutMode::Columns => "columns",
|
LayoutMode::Columns => "columns",
|
||||||
|
LayoutMode::Rows => "rows",
|
||||||
|
LayoutMode::CenteredMaster => "centered-master",
|
||||||
|
LayoutMode::Spiral => "spiral",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +75,9 @@ fn layout_from_slug(slug: &str) -> Option<LayoutMode> {
|
|||||||
"monocle" => LayoutMode::Monocle,
|
"monocle" => LayoutMode::Monocle,
|
||||||
"grid" => LayoutMode::Grid,
|
"grid" => LayoutMode::Grid,
|
||||||
"columns" => LayoutMode::Columns,
|
"columns" => LayoutMode::Columns,
|
||||||
|
"rows" => LayoutMode::Rows,
|
||||||
|
"centered-master" => LayoutMode::CenteredMaster,
|
||||||
|
"spiral" => LayoutMode::Spiral,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -84,6 +94,8 @@ impl fmt::Display for DesktopAction {
|
|||||||
DesktopAction::CloseFocused => f.write_str("close-focused"),
|
DesktopAction::CloseFocused => f.write_str("close-focused"),
|
||||||
DesktopAction::CycleLayout => f.write_str("cycle-layout"),
|
DesktopAction::CycleLayout => f.write_str("cycle-layout"),
|
||||||
DesktopAction::SetLayout(m) => write!(f, "layout:{}", layout_slug(*m)),
|
DesktopAction::SetLayout(m) => write!(f, "layout:{}", layout_slug(*m)),
|
||||||
|
DesktopAction::GrowMaster => f.write_str("grow-master"),
|
||||||
|
DesktopAction::ShrinkMaster => f.write_str("shrink-master"),
|
||||||
// Los escritorios se numeran 1-based de cara al usuario.
|
// Los escritorios se numeran 1-based de cara al usuario.
|
||||||
DesktopAction::SwitchWorkspace(n) => write!(f, "workspace:{}", n + 1),
|
DesktopAction::SwitchWorkspace(n) => write!(f, "workspace:{}", n + 1),
|
||||||
DesktopAction::SendToWorkspace(n) => write!(f, "send-to-workspace:{}", n + 1),
|
DesktopAction::SendToWorkspace(n) => write!(f, "send-to-workspace:{}", n + 1),
|
||||||
@@ -105,6 +117,8 @@ impl FromStr for DesktopAction {
|
|||||||
"move-backward" => Self::MoveBackward,
|
"move-backward" => Self::MoveBackward,
|
||||||
"close-focused" => Self::CloseFocused,
|
"close-focused" => Self::CloseFocused,
|
||||||
"cycle-layout" => Self::CycleLayout,
|
"cycle-layout" => Self::CycleLayout,
|
||||||
|
"grow-master" => Self::GrowMaster,
|
||||||
|
"shrink-master" => Self::ShrinkMaster,
|
||||||
"quit" => Self::Quit,
|
"quit" => Self::Quit,
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(slug) = s.strip_prefix("layout:") {
|
if let Some(slug) = s.strip_prefix("layout:") {
|
||||||
@@ -162,6 +176,11 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> {
|
|||||||
("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)),
|
("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)),
|
||||||
("Super+g".into(), DesktopAction::SetLayout(LayoutMode::Grid)),
|
("Super+g".into(), DesktopAction::SetLayout(LayoutMode::Grid)),
|
||||||
("Super+c".into(), DesktopAction::SetLayout(LayoutMode::Columns)),
|
("Super+c".into(), DesktopAction::SetLayout(LayoutMode::Columns)),
|
||||||
|
("Super+r".into(), DesktopAction::SetLayout(LayoutMode::Rows)),
|
||||||
|
("Super+d".into(), DesktopAction::SetLayout(LayoutMode::CenteredMaster)),
|
||||||
|
("Super+s".into(), DesktopAction::SetLayout(LayoutMode::Spiral)),
|
||||||
|
("Super+h".into(), DesktopAction::ShrinkMaster),
|
||||||
|
("Super+l".into(), DesktopAction::GrowMaster),
|
||||||
("Super+Shift+e".into(), DesktopAction::Quit),
|
("Super+Shift+e".into(), DesktopAction::Quit),
|
||||||
];
|
];
|
||||||
// Un escritorio por dígito: `Super+1`..`Super+9` lo activan,
|
// Un escritorio por dígito: `Super+1`..`Super+9` lo activan,
|
||||||
@@ -214,12 +233,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_layout_mode_round_trips() {
|
fn every_layout_mode_round_trips() {
|
||||||
for mode in [
|
for mode in LayoutMode::ALL {
|
||||||
LayoutMode::MasterStack,
|
|
||||||
LayoutMode::Monocle,
|
|
||||||
LayoutMode::Grid,
|
|
||||||
LayoutMode::Columns,
|
|
||||||
] {
|
|
||||||
let a = DesktopAction::SetLayout(mode);
|
let a = DesktopAction::SetLayout(mode);
|
||||||
assert_eq!(a, a.to_string().parse().unwrap());
|
assert_eq!(a, a.to_string().parse().unwrap());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use mirada_layout::{LayoutMode, LayoutParams, Rect, WindowId, Workspace};
|
use mirada_layout::{LayoutParams, Rect, WindowId, Workspace};
|
||||||
use mirada_protocol::{placements, BodyEvent, BrainCommand, OutputId};
|
use mirada_protocol::{placements, BodyEvent, BrainCommand, OutputId};
|
||||||
|
|
||||||
use crate::action::{DesktopAction, WORKSPACE_COUNT};
|
use crate::action::{DesktopAction, WORKSPACE_COUNT};
|
||||||
@@ -180,7 +180,7 @@ impl Desktop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DesktopAction::CycleLayout => {
|
DesktopAction::CycleLayout => {
|
||||||
let next = cycle_mode(self.workspaces[self.active].params().mode);
|
let next = self.workspaces[self.active].params().mode.next();
|
||||||
self.workspaces[self.active].set_mode(next);
|
self.workspaces[self.active].set_mode(next);
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
@@ -188,6 +188,8 @@ impl Desktop {
|
|||||||
self.workspaces[self.active].set_mode(mode);
|
self.workspaces[self.active].set_mode(mode);
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
|
DesktopAction::GrowMaster => self.nudge_master(0.05),
|
||||||
|
DesktopAction::ShrinkMaster => self.nudge_master(-0.05),
|
||||||
DesktopAction::SwitchWorkspace(n) => {
|
DesktopAction::SwitchWorkspace(n) => {
|
||||||
if n < self.workspaces.len() && n != self.active {
|
if n < self.workspaces.len() && n != self.active {
|
||||||
self.active = n;
|
self.active = n;
|
||||||
@@ -213,6 +215,15 @@ impl Desktop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ajusta la fracción del área maestra del escritorio activo (la usan
|
||||||
|
/// `MasterStack` y `CenteredMaster`), acotada a `0.05..=0.95`.
|
||||||
|
fn nudge_master(&mut self, delta: f32) -> Vec<BrainCommand> {
|
||||||
|
let ws = &mut self.workspaces[self.active];
|
||||||
|
let ratio = (ws.params().master_ratio + delta).clamp(0.05, 0.95);
|
||||||
|
ws.set_master_ratio(ratio);
|
||||||
|
self.relayout()
|
||||||
|
}
|
||||||
|
|
||||||
/// Recalcula la geometría del escritorio activo y la empaqueta en un
|
/// Recalcula la geometría del escritorio activo y la empaqueta en un
|
||||||
/// [`BrainCommand::Place`]. Sin salida conectada, no hay nada que
|
/// [`BrainCommand::Place`]. Sin salida conectada, no hay nada que
|
||||||
/// colocar.
|
/// colocar.
|
||||||
@@ -281,19 +292,10 @@ impl Desktop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// El siguiente modo en el ciclo de [`DesktopAction::CycleLayout`].
|
|
||||||
fn cycle_mode(mode: LayoutMode) -> LayoutMode {
|
|
||||||
match mode {
|
|
||||||
LayoutMode::MasterStack => LayoutMode::Monocle,
|
|
||||||
LayoutMode::Monocle => LayoutMode::Grid,
|
|
||||||
LayoutMode::Grid => LayoutMode::Columns,
|
|
||||||
LayoutMode::Columns => LayoutMode::MasterStack,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use mirada_layout::LayoutMode;
|
||||||
|
|
||||||
/// Un escritorio con una salida 1920×1080 ya conectada.
|
/// Un escritorio con una salida 1920×1080 ya conectada.
|
||||||
fn desktop_with_screen() -> Desktop {
|
fn desktop_with_screen() -> Desktop {
|
||||||
@@ -442,19 +444,42 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_layout_walks_the_four_modes() {
|
fn cycle_layout_walks_every_mode_and_returns() {
|
||||||
let mut d = desktop_with_screen();
|
let mut d = desktop_with_screen();
|
||||||
open(&mut d, 1);
|
open(&mut d, 1);
|
||||||
assert_eq!(d.active_workspace().params().mode, LayoutMode::MasterStack);
|
let start = d.active_workspace().params().mode;
|
||||||
for expected in [
|
for _ in 0..LayoutMode::ALL.len() {
|
||||||
LayoutMode::Monocle,
|
let before = d.active_workspace().params().mode;
|
||||||
LayoutMode::Grid,
|
|
||||||
LayoutMode::Columns,
|
|
||||||
LayoutMode::MasterStack,
|
|
||||||
] {
|
|
||||||
d.on_event(BodyEvent::Keybind("Super+space".into()));
|
d.on_event(BodyEvent::Keybind("Super+space".into()));
|
||||||
assert_eq!(d.active_workspace().params().mode, expected);
|
assert_eq!(d.active_workspace().params().mode, before.next());
|
||||||
}
|
}
|
||||||
|
// Una vuelta completa devuelve al modo inicial.
|
||||||
|
assert_eq!(d.active_workspace().params().mode, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grow_and_shrink_master_adjust_the_ratio() {
|
||||||
|
let mut d = desktop_with_screen();
|
||||||
|
open(&mut d, 1);
|
||||||
|
let r0 = d.active_workspace().params().master_ratio;
|
||||||
|
d.apply(DesktopAction::GrowMaster);
|
||||||
|
assert!(d.active_workspace().params().master_ratio > r0);
|
||||||
|
d.apply(DesktopAction::ShrinkMaster);
|
||||||
|
assert!((d.active_workspace().params().master_ratio - r0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn master_ratio_stays_within_bounds() {
|
||||||
|
let mut d = desktop_with_screen();
|
||||||
|
open(&mut d, 1);
|
||||||
|
for _ in 0..50 {
|
||||||
|
d.apply(DesktopAction::GrowMaster);
|
||||||
|
}
|
||||||
|
assert!(d.active_workspace().params().master_ratio <= 0.95);
|
||||||
|
for _ in 0..50 {
|
||||||
|
d.apply(DesktopAction::ShrinkMaster);
|
||||||
|
}
|
||||||
|
assert!(d.active_workspace().params().master_ratio >= 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -238,7 +238,9 @@ const KEYMAP_HEADER: &str = "\
|
|||||||
// move-forward / move-backward reordena la ventana enfocada
|
// move-forward / move-backward reordena la ventana enfocada
|
||||||
// close-focused cierra la enfocada
|
// close-focused cierra la enfocada
|
||||||
// cycle-layout siguiente modo de teselado
|
// cycle-layout siguiente modo de teselado
|
||||||
// layout:master-stack | layout:monocle | layout:grid | layout:columns
|
// layout:<modo> master-stack | centered-master | spiral
|
||||||
|
// grid | columns | rows | monocle
|
||||||
|
// grow-master / shrink-master redimensiona el área maestra
|
||||||
// workspace:N activa el escritorio N (1..9)
|
// workspace:N activa el escritorio N (1..9)
|
||||||
// send-to-workspace:N manda la enfocada al escritorio N
|
// send-to-workspace:N manda la enfocada al escritorio N
|
||||||
// quit apaga el compositor
|
// quit apaga el compositor
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use crate::geometry::{split, Rect};
|
use crate::geometry::{split, Rect};
|
||||||
|
|
||||||
/// Estrategia de teselado.
|
/// Estrategia de teselado.
|
||||||
|
///
|
||||||
|
/// Las variantes nuevas se añaden **al final** para no mover los índices
|
||||||
|
/// con que `postcard` las serializa en el API de control.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum LayoutMode {
|
pub enum LayoutMode {
|
||||||
@@ -16,6 +19,33 @@ pub enum LayoutMode {
|
|||||||
Grid,
|
Grid,
|
||||||
/// Columnas verticales de igual ancho.
|
/// Columnas verticales de igual ancho.
|
||||||
Columns,
|
Columns,
|
||||||
|
/// Filas horizontales de igual alto.
|
||||||
|
Rows,
|
||||||
|
/// Ventana maestra centrada; el resto en columnas a ambos lados.
|
||||||
|
/// Pensado para monitores anchos.
|
||||||
|
CenteredMaster,
|
||||||
|
/// Espiral de Fibonacci: cada ventana parte por la mitad el espacio
|
||||||
|
/// que queda, alternando el sentido del corte.
|
||||||
|
Spiral,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutMode {
|
||||||
|
/// Todos los modos, en el orden del ciclo de `CycleLayout`.
|
||||||
|
pub const ALL: [LayoutMode; 7] = [
|
||||||
|
LayoutMode::MasterStack,
|
||||||
|
LayoutMode::CenteredMaster,
|
||||||
|
LayoutMode::Spiral,
|
||||||
|
LayoutMode::Grid,
|
||||||
|
LayoutMode::Columns,
|
||||||
|
LayoutMode::Rows,
|
||||||
|
LayoutMode::Monocle,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// El siguiente modo en el ciclo (envuelve al llegar al final).
|
||||||
|
pub fn next(self) -> LayoutMode {
|
||||||
|
let i = Self::ALL.iter().position(|&m| m == self).unwrap_or(0);
|
||||||
|
Self::ALL[(i + 1) % Self::ALL.len()]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parámetros del teselado.
|
/// Parámetros del teselado.
|
||||||
@@ -45,8 +75,11 @@ pub fn tile(screen: Rect, count: usize, params: &LayoutParams) -> Vec<Rect> {
|
|||||||
let cells = match params.mode {
|
let cells = match params.mode {
|
||||||
LayoutMode::Monocle => vec![screen; count],
|
LayoutMode::Monocle => vec![screen; count],
|
||||||
LayoutMode::Columns => columns(screen, count),
|
LayoutMode::Columns => columns(screen, count),
|
||||||
|
LayoutMode::Rows => rows(screen, count),
|
||||||
LayoutMode::Grid => grid(screen, count),
|
LayoutMode::Grid => grid(screen, count),
|
||||||
LayoutMode::MasterStack => master_stack(screen, count, params.master_ratio),
|
LayoutMode::MasterStack => master_stack(screen, count, params.master_ratio),
|
||||||
|
LayoutMode::CenteredMaster => centered_master(screen, count, params.master_ratio),
|
||||||
|
LayoutMode::Spiral => spiral(screen, count),
|
||||||
};
|
};
|
||||||
// El margen se aplica al final, uniforme para todos los modos.
|
// El margen se aplica al final, uniforme para todos los modos.
|
||||||
cells.into_iter().map(|c| c.inset(params.gap)).collect()
|
cells.into_iter().map(|c| c.inset(params.gap)).collect()
|
||||||
@@ -75,6 +108,65 @@ fn grid(screen: Rect, count: usize) -> Vec<Rect> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filas horizontales de igual alto.
|
||||||
|
fn rows(screen: Rect, count: usize) -> Vec<Rect> {
|
||||||
|
split(screen.h, count)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(off, h)| Rect::new(screen.x, screen.y + off, screen.w, h))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Espiral de Fibonacci: cada ventana se queda con la mitad del espacio
|
||||||
|
/// libre y la siguiente recurre en la otra mitad, alternando el corte.
|
||||||
|
/// La última ventana llena todo lo que sobra.
|
||||||
|
fn spiral(screen: Rect, count: usize) -> Vec<Rect> {
|
||||||
|
let mut out = Vec::with_capacity(count);
|
||||||
|
let mut area = screen;
|
||||||
|
let mut horizontal = true;
|
||||||
|
for _ in 1..count {
|
||||||
|
if horizontal {
|
||||||
|
let p = split(area.w, 2);
|
||||||
|
out.push(Rect::new(area.x, area.y, p[0].1, area.h));
|
||||||
|
area = Rect::new(area.x + p[1].0, area.y, p[1].1, area.h);
|
||||||
|
} else {
|
||||||
|
let p = split(area.h, 2);
|
||||||
|
out.push(Rect::new(area.x, area.y, area.w, p[0].1));
|
||||||
|
area = Rect::new(area.x, area.y + p[1].0, area.w, p[1].1);
|
||||||
|
}
|
||||||
|
horizontal = !horizontal;
|
||||||
|
}
|
||||||
|
out.push(area);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ventana maestra centrada + pila repartida en columnas a ambos lados.
|
||||||
|
fn centered_master(screen: Rect, count: usize, ratio: f32) -> Vec<Rect> {
|
||||||
|
// Con una o dos ventanas no hay nada que centrar: cae a maestro+pila.
|
||||||
|
if count <= 2 {
|
||||||
|
return master_stack(screen, count, ratio);
|
||||||
|
}
|
||||||
|
let ratio = ratio.clamp(0.05, 0.95);
|
||||||
|
let master_w = (screen.w as f32 * ratio).round() as i32;
|
||||||
|
let sides = split(screen.w - master_w, 2);
|
||||||
|
let (left_w, right_w) = (sides[0].1, sides[1].1);
|
||||||
|
|
||||||
|
let stack = count - 1;
|
||||||
|
let left_n = stack / 2;
|
||||||
|
let right_n = stack - left_n;
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(count);
|
||||||
|
// 0 = la maestra, centrada.
|
||||||
|
out.push(Rect::new(screen.x + left_w, screen.y, master_w, screen.h));
|
||||||
|
// Columna izquierda, luego la derecha — el orden de teselado.
|
||||||
|
for (off, h) in split(screen.h, left_n) {
|
||||||
|
out.push(Rect::new(screen.x, screen.y + off, left_w, h));
|
||||||
|
}
|
||||||
|
for (off, h) in split(screen.h, right_n) {
|
||||||
|
out.push(Rect::new(screen.x + left_w + master_w, screen.y + off, right_w, h));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Ventana maestra a la izquierda + pila a la derecha.
|
/// Ventana maestra a la izquierda + pila a la derecha.
|
||||||
fn master_stack(screen: Rect, count: usize, ratio: f32) -> Vec<Rect> {
|
fn master_stack(screen: Rect, count: usize, ratio: f32) -> Vec<Rect> {
|
||||||
if count == 1 {
|
if count == 1 {
|
||||||
@@ -110,18 +202,59 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tile_count_matches_window_count() {
|
fn tile_count_matches_window_count() {
|
||||||
for mode in [
|
for mode in LayoutMode::ALL {
|
||||||
LayoutMode::MasterStack,
|
|
||||||
LayoutMode::Monocle,
|
|
||||||
LayoutMode::Grid,
|
|
||||||
LayoutMode::Columns,
|
|
||||||
] {
|
|
||||||
for n in 1..=9 {
|
for n in 1..=9 {
|
||||||
assert_eq!(tile(SCREEN, n, ¶ms(mode)).len(), n);
|
assert_eq!(tile(SCREEN, n, ¶ms(mode)).len(), n, "modo {mode:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rows_partition_the_height_exactly() {
|
||||||
|
let rects = tile(SCREEN, 3, ¶ms(LayoutMode::Rows));
|
||||||
|
assert_eq!(rects.iter().map(|r| r.h).sum::<i32>(), 1080);
|
||||||
|
assert!(rects.iter().all(|r| r.w == 1920));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spiral_tiles_cover_the_screen_without_overlap() {
|
||||||
|
for n in 1..=9 {
|
||||||
|
let total: i64 = tile(SCREEN, n, ¶ms(LayoutMode::Spiral))
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.area())
|
||||||
|
.sum();
|
||||||
|
assert_eq!(total, SCREEN.area(), "espiral con {n} ventanas");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn centered_master_centers_the_master_and_covers_the_screen() {
|
||||||
|
let rects = tile(SCREEN, 5, ¶ms(LayoutMode::CenteredMaster));
|
||||||
|
let master = rects[0];
|
||||||
|
// Hueco a la izquierda y a la derecha de la maestra: iguales ±1px.
|
||||||
|
let left = master.x - SCREEN.x;
|
||||||
|
let right = (SCREEN.x + SCREEN.w) - (master.x + master.w);
|
||||||
|
assert!((left - right).abs() <= 1, "maestra no centrada: {left} vs {right}");
|
||||||
|
let total: i64 = rects.iter().map(|r| r.area()).sum();
|
||||||
|
assert_eq!(total, SCREEN.area());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_mode_next_cycles_through_every_mode() {
|
||||||
|
let mut visited: Vec<LayoutMode> = Vec::new();
|
||||||
|
let mut m = LayoutMode::MasterStack;
|
||||||
|
for _ in 0..LayoutMode::ALL.len() {
|
||||||
|
assert!(!visited.contains(&m), "modo repetido en el ciclo: {m:?}");
|
||||||
|
visited.push(m);
|
||||||
|
m = m.next();
|
||||||
|
}
|
||||||
|
// Tras una vuelta completa, de vuelta al inicio.
|
||||||
|
assert_eq!(m, LayoutMode::MasterStack);
|
||||||
|
for mode in LayoutMode::ALL {
|
||||||
|
assert!(visited.contains(&mode), "el ciclo no pasa por {mode:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn monocle_gives_every_window_the_full_screen() {
|
fn monocle_gives_every_window_the_full_screen() {
|
||||||
for r in tile(SCREEN, 4, ¶ms(LayoutMode::Monocle)) {
|
for r in tile(SCREEN, 4, ¶ms(LayoutMode::Monocle)) {
|
||||||
|
|||||||
@@ -1027,5 +1027,15 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Layouts — 7 modos de teselado, intercambiables por el API:
|
||||||
|
master-stack · centered-master · spiral (espiral de Fibonacci) · grid · columns · rows · monocle
|
||||||
|
mirada-ctl layout spiral # fija un modo
|
||||||
|
mirada-ctl cycle-layout # siguiente modo (LayoutMode::next)
|
||||||
|
mirada-ctl grow-master / shrink-master # redimensiona el área maestra (Super+l / Super+h)
|
||||||
|
Atajos directos: Super+t/m/g/c/r/d/s. CenteredMaster y Spiral pensados para monitores anchos.
|
||||||
|
El motor (mirada-layout::tile) es puro y determinista; split() reparte sin perder un píxel.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user