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:
sergio
2026-05-21 00:37:16 +00:00
parent b31f988833
commit 8821d34bd5
10 changed files with 293 additions and 68 deletions
@@ -2,7 +2,7 @@
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 crate::action::{DesktopAction, WORKSPACE_COUNT};
@@ -180,7 +180,7 @@ impl Desktop {
}
}
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.relayout()
}
@@ -188,6 +188,8 @@ impl Desktop {
self.workspaces[self.active].set_mode(mode);
self.relayout()
}
DesktopAction::GrowMaster => self.nudge_master(0.05),
DesktopAction::ShrinkMaster => self.nudge_master(-0.05),
DesktopAction::SwitchWorkspace(n) => {
if n < self.workspaces.len() && n != self.active {
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
/// [`BrainCommand::Place`]. Sin salida conectada, no hay nada que
/// 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)]
mod tests {
use super::*;
use mirada_layout::LayoutMode;
/// Un escritorio con una salida 1920×1080 ya conectada.
fn desktop_with_screen() -> Desktop {
@@ -442,19 +444,42 @@ mod tests {
}
#[test]
fn cycle_layout_walks_the_four_modes() {
fn cycle_layout_walks_every_mode_and_returns() {
let mut d = desktop_with_screen();
open(&mut d, 1);
assert_eq!(d.active_workspace().params().mode, LayoutMode::MasterStack);
for expected in [
LayoutMode::Monocle,
LayoutMode::Grid,
LayoutMode::Columns,
LayoutMode::MasterStack,
] {
let start = d.active_workspace().params().mode;
for _ in 0..LayoutMode::ALL.len() {
let before = d.active_workspace().params().mode;
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]