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:
@@ -42,6 +42,10 @@ pub enum DesktopAction {
|
||||
CycleLayout,
|
||||
/// Fija un modo de teselado concreto.
|
||||
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).
|
||||
SwitchWorkspace(usize),
|
||||
/// Manda la ventana enfocada al escritorio virtual `n`.
|
||||
@@ -58,6 +62,9 @@ fn layout_slug(mode: LayoutMode) -> &'static str {
|
||||
LayoutMode::Monocle => "monocle",
|
||||
LayoutMode::Grid => "grid",
|
||||
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,
|
||||
"grid" => LayoutMode::Grid,
|
||||
"columns" => LayoutMode::Columns,
|
||||
"rows" => LayoutMode::Rows,
|
||||
"centered-master" => LayoutMode::CenteredMaster,
|
||||
"spiral" => LayoutMode::Spiral,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -84,6 +94,8 @@ impl fmt::Display for DesktopAction {
|
||||
DesktopAction::CloseFocused => f.write_str("close-focused"),
|
||||
DesktopAction::CycleLayout => f.write_str("cycle-layout"),
|
||||
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.
|
||||
DesktopAction::SwitchWorkspace(n) => write!(f, "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,
|
||||
"close-focused" => Self::CloseFocused,
|
||||
"cycle-layout" => Self::CycleLayout,
|
||||
"grow-master" => Self::GrowMaster,
|
||||
"shrink-master" => Self::ShrinkMaster,
|
||||
"quit" => Self::Quit,
|
||||
_ => {
|
||||
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+g".into(), DesktopAction::SetLayout(LayoutMode::Grid)),
|
||||
("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),
|
||||
];
|
||||
// Un escritorio por dígito: `Super+1`..`Super+9` lo activan,
|
||||
@@ -214,12 +233,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_layout_mode_round_trips() {
|
||||
for mode in [
|
||||
LayoutMode::MasterStack,
|
||||
LayoutMode::Monocle,
|
||||
LayoutMode::Grid,
|
||||
LayoutMode::Columns,
|
||||
] {
|
||||
for mode in LayoutMode::ALL {
|
||||
let a = DesktopAction::SetLayout(mode);
|
||||
assert_eq!(a, a.to_string().parse().unwrap());
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -238,7 +238,9 @@ const KEYMAP_HEADER: &str = "\
|
||||
// move-forward / move-backward reordena la ventana enfocada
|
||||
// close-focused cierra la enfocada
|
||||
// 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)
|
||||
// send-to-workspace:N manda la enfocada al escritorio N
|
||||
// quit apaga el compositor
|
||||
|
||||
Reference in New Issue
Block a user