feat(mirada): nmaster, promover a maestra y smart gaps (estilo dwm)
Tanda de funciones de tiling WM, toda pura (mirada-layout/brain), sin tocar el protocolo: - nmaster: LayoutParams.master_count — cuántas ventanas van en el área maestra. MasterStack y CenteredMaster apilan N maestras; sin pila, las maestras llenan la pantalla. Acciones inc-master/dec-master (Super+, Super+.), acotadas 1..9. - Promover a maestra: Workspace::promote_focused lleva la ventana enfocada al puesto 0. Acción promote-to-master (Super+Return). - Smart gaps: una sola ventana se tesela a sangre, sin margen. combo_string del compositor canoniza ahora teclas con nombre (Return, Tab, F5, flechas…) vía xkb::keysym_get_name, no sólo caracteres imprimibles — sin eso Super+Return no sería un atajo expresable. Cableado en keymap por defecto, HUD de mirada y mirada-ctl. Verificado end-to-end con headless-ctl. mirada-layout 26->30, mirada-brain 39->41. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -62,8 +62,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`,
|
||||||
los 7 layouts en `Super+t/m/g/c/r/d/s` (o ciclar con `Super+space`), área
|
los 7 layouts en `Super+t/m/g/c/r/d/s` (o ciclar con `Super+space`), área
|
||||||
maestra `Super+h/l`, escritorios `Super+1..9`, cerrar `Super+q`. Cierra
|
maestra `Super+h/l`, `nmaster` `Super+,/.`, promover a maestra
|
||||||
la ventana del compositor para salir.
|
`Super+Return`, escritorios `Super+1..9`, cerrar `Super+q`. Cierra la
|
||||||
|
ventana del compositor para salir.
|
||||||
|
|
||||||
## Atajos de teclado
|
## Atajos de teclado
|
||||||
|
|
||||||
|
|||||||
@@ -403,12 +403,13 @@ fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option<String> {
|
|||||||
let name = if key == " " {
|
let name = if key == " " {
|
||||||
"space".to_string()
|
"space".to_string()
|
||||||
} else {
|
} else {
|
||||||
|
// ¿Es un único carácter imprimible? Entonces la tecla es ese carácter.
|
||||||
let mut chars = key.chars();
|
let mut chars = key.chars();
|
||||||
let c = chars.next()?;
|
match (chars.next(), chars.next()) {
|
||||||
if chars.next().is_some() || !c.is_ascii_graphic() {
|
(Some(c), None) if c.is_ascii_graphic() => c.to_ascii_lowercase().to_string(),
|
||||||
return None;
|
// Si no, una tecla con nombre: Return, Tab, Up, F5…
|
||||||
|
_ => named_key(sym)?,
|
||||||
}
|
}
|
||||||
c.to_ascii_lowercase().to_string()
|
|
||||||
};
|
};
|
||||||
let mut combo = String::new();
|
let mut combo = String::new();
|
||||||
if mods.logo {
|
if mods.logo {
|
||||||
@@ -427,6 +428,17 @@ fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option<String> {
|
|||||||
Some(combo)
|
Some(combo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// El nombre canónico de una tecla especial — `Return`, `Tab`, `Up`,
|
||||||
|
/// `F5`… `None` si xkb no le da un nombre razonable.
|
||||||
|
fn named_key(sym: Keysym) -> Option<String> {
|
||||||
|
let name = xkb::keysym_get_name(sym);
|
||||||
|
if name.is_empty() || name == "NoSymbol" || name.starts_with("0x") {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Despacha los callbacks de frame de un árbol de superficies: avisa a
|
/// Despacha los callbacks de frame de un árbol de superficies: avisa a
|
||||||
/// cada cliente de que puede dibujar el siguiente cuadro.
|
/// cada cliente de que puede dibujar el siguiente cuadro.
|
||||||
fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
|
fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ Acciones de mirada-ctl:
|
|||||||
grid · columns · rows · monocle
|
grid · columns · rows · monocle
|
||||||
grow-master agranda el área de la ventana maestra
|
grow-master agranda el área de la ventana maestra
|
||||||
shrink-master la encoge
|
shrink-master la encoge
|
||||||
|
inc-master / dec-master nº de ventanas en el área maestra (nmaster)
|
||||||
|
promote-to-master la ventana enfocada al puesto maestro
|
||||||
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
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
//! n abre una ventana tab / espacio cicla layout
|
//! n abre una ventana tab / espacio cicla layout
|
||||||
//! w cierra la enfocada t m g c r d s layout directo
|
//! w cierra la enfocada t m g c r d s layout directo
|
||||||
//! j / k foco siguiente/anterior h / l área maestra −/+
|
//! j / k foco siguiente/anterior h / l área maestra −/+
|
||||||
//! Shift+j / k mueve la enfocada 1..9 ir a escritorio
|
//! Shift+j / k mueve la enfocada , / . nmaster −/+
|
||||||
|
//! Enter promueve a maestra 1..9 ir a escritorio
|
||||||
//! Ctrl+1..9 enviar a escritorio
|
//! Ctrl+1..9 enviar a escritorio
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
@@ -291,6 +292,9 @@ impl Mirada {
|
|||||||
"s" => self.act(DesktopAction::SetLayout(LayoutMode::Spiral)),
|
"s" => self.act(DesktopAction::SetLayout(LayoutMode::Spiral)),
|
||||||
"h" => self.act(DesktopAction::ShrinkMaster),
|
"h" => self.act(DesktopAction::ShrinkMaster),
|
||||||
"l" => self.act(DesktopAction::GrowMaster),
|
"l" => self.act(DesktopAction::GrowMaster),
|
||||||
|
"enter" => self.act(DesktopAction::PromoteToMaster),
|
||||||
|
"," => self.act(DesktopAction::IncMaster),
|
||||||
|
"." => self.act(DesktopAction::DecMaster),
|
||||||
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 {
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ ejecuta operaciones de geometría".
|
|||||||
- **`mirada-layout`** — `Rect` + `split` (reparto exacto de píxeles),
|
- **`mirada-layout`** — `Rect` + `split` (reparto exacto de píxeles),
|
||||||
`LayoutMode` con 7 modos (`MasterStack`, `CenteredMaster`, `Spiral`
|
`LayoutMode` con 7 modos (`MasterStack`, `CenteredMaster`, `Spiral`
|
||||||
—espiral de Fibonacci—, `Grid`, `Columns`, `Rows`, `Monocle`) y
|
—espiral de Fibonacci—, `Grid`, `Columns`, `Rows`, `Monocle`) y
|
||||||
`LayoutMode::next()` para el ciclo, `Workspace` con foco cíclico y
|
`LayoutMode::next()` para el ciclo, `Workspace` con foco cíclico,
|
||||||
reordenado. Determinista.
|
reordenado y `promote_focused`. `LayoutParams` lleva `master_ratio` y
|
||||||
|
`master_count` (`nmaster`); *smart gaps* (una sola ventana va a
|
||||||
|
sangre). 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
|
||||||
@@ -111,10 +113,12 @@ 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
|
- **Layout y área maestra por el API** — los 7 modos se intercambian
|
||||||
de teselado se intercambian por el API (`mirada-ctl layout spiral`), y
|
(`SetLayout`/`CycleLayout`, `mirada-ctl layout spiral`); el área
|
||||||
el área maestra se redimensiona en caliente (`grow`/`shrink-master`,
|
maestra se redimensiona (`grow`/`shrink-master`, `Super+l`/`Super+h`);
|
||||||
atajos `Super+l`/`Super+h`).
|
`inc`/`dec-master` cambian `nmaster` (`Super+,`/`Super+.`); y
|
||||||
|
`promote-to-master` lleva la enfocada al puesto maestro (`Super+Return`
|
||||||
|
— `combo_string` ya canoniza teclas con nombre: `Return`, `Tab`, `F5`…).
|
||||||
- **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
|
||||||
@@ -136,8 +140,8 @@ gráficos para ejercitar `mirada-ctl` en modo desatendido.
|
|||||||
|
|
||||||
## Estado
|
## Estado
|
||||||
|
|
||||||
Implementado y verde: `mirada-layout` (26 tests), `mirada-protocol`
|
Implementado y verde: `mirada-layout` (30 tests), `mirada-protocol`
|
||||||
(9), `mirada-brain` (39), `mirada-link` (7), `mirada-body` (13), las
|
(9), `mirada-brain` (41), `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`).
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ pub enum DesktopAction {
|
|||||||
GrowMaster,
|
GrowMaster,
|
||||||
/// Encoge el área de la ventana maestra.
|
/// Encoge el área de la ventana maestra.
|
||||||
ShrinkMaster,
|
ShrinkMaster,
|
||||||
|
/// Mete una ventana más en el área maestra (`nmaster`).
|
||||||
|
IncMaster,
|
||||||
|
/// Saca una ventana del área maestra.
|
||||||
|
DecMaster,
|
||||||
|
/// Lleva la ventana enfocada al puesto maestro (orden de teselado).
|
||||||
|
PromoteToMaster,
|
||||||
/// 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`.
|
||||||
@@ -96,6 +102,9 @@ impl fmt::Display for DesktopAction {
|
|||||||
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::GrowMaster => f.write_str("grow-master"),
|
||||||
DesktopAction::ShrinkMaster => f.write_str("shrink-master"),
|
DesktopAction::ShrinkMaster => f.write_str("shrink-master"),
|
||||||
|
DesktopAction::IncMaster => f.write_str("inc-master"),
|
||||||
|
DesktopAction::DecMaster => f.write_str("dec-master"),
|
||||||
|
DesktopAction::PromoteToMaster => f.write_str("promote-to-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),
|
||||||
@@ -119,6 +128,9 @@ impl FromStr for DesktopAction {
|
|||||||
"cycle-layout" => Self::CycleLayout,
|
"cycle-layout" => Self::CycleLayout,
|
||||||
"grow-master" => Self::GrowMaster,
|
"grow-master" => Self::GrowMaster,
|
||||||
"shrink-master" => Self::ShrinkMaster,
|
"shrink-master" => Self::ShrinkMaster,
|
||||||
|
"inc-master" => Self::IncMaster,
|
||||||
|
"dec-master" => Self::DecMaster,
|
||||||
|
"promote-to-master" => Self::PromoteToMaster,
|
||||||
"quit" => Self::Quit,
|
"quit" => Self::Quit,
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(slug) = s.strip_prefix("layout:") {
|
if let Some(slug) = s.strip_prefix("layout:") {
|
||||||
@@ -181,6 +193,9 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> {
|
|||||||
("Super+s".into(), DesktopAction::SetLayout(LayoutMode::Spiral)),
|
("Super+s".into(), DesktopAction::SetLayout(LayoutMode::Spiral)),
|
||||||
("Super+h".into(), DesktopAction::ShrinkMaster),
|
("Super+h".into(), DesktopAction::ShrinkMaster),
|
||||||
("Super+l".into(), DesktopAction::GrowMaster),
|
("Super+l".into(), DesktopAction::GrowMaster),
|
||||||
|
("Super+Return".into(), DesktopAction::PromoteToMaster),
|
||||||
|
("Super+,".into(), DesktopAction::IncMaster),
|
||||||
|
("Super+.".into(), DesktopAction::DecMaster),
|
||||||
("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,
|
||||||
|
|||||||
@@ -190,6 +190,12 @@ impl Desktop {
|
|||||||
}
|
}
|
||||||
DesktopAction::GrowMaster => self.nudge_master(0.05),
|
DesktopAction::GrowMaster => self.nudge_master(0.05),
|
||||||
DesktopAction::ShrinkMaster => self.nudge_master(-0.05),
|
DesktopAction::ShrinkMaster => self.nudge_master(-0.05),
|
||||||
|
DesktopAction::IncMaster => self.nudge_master_count(1),
|
||||||
|
DesktopAction::DecMaster => self.nudge_master_count(-1),
|
||||||
|
DesktopAction::PromoteToMaster => {
|
||||||
|
self.workspaces[self.active].promote_focused();
|
||||||
|
self.relayout()
|
||||||
|
}
|
||||||
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;
|
||||||
@@ -224,6 +230,14 @@ impl Desktop {
|
|||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ajusta `nmaster` del escritorio activo, acotado a `1..=9`.
|
||||||
|
fn nudge_master_count(&mut self, delta: i32) -> Vec<BrainCommand> {
|
||||||
|
let ws = &mut self.workspaces[self.active];
|
||||||
|
let n = (ws.params().master_count as i32 + delta).clamp(1, 9) as usize;
|
||||||
|
ws.set_master_count(n);
|
||||||
|
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.
|
||||||
@@ -468,6 +482,32 @@ mod tests {
|
|||||||
assert!((d.active_workspace().params().master_ratio - r0).abs() < 1e-6);
|
assert!((d.active_workspace().params().master_ratio - r0).abs() < 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inc_and_dec_master_adjust_nmaster() {
|
||||||
|
let mut d = desktop_with_screen();
|
||||||
|
for id in [1, 2, 3] {
|
||||||
|
open(&mut d, id);
|
||||||
|
}
|
||||||
|
assert_eq!(d.active_workspace().params().master_count, 1);
|
||||||
|
d.apply(DesktopAction::IncMaster);
|
||||||
|
assert_eq!(d.active_workspace().params().master_count, 2);
|
||||||
|
d.apply(DesktopAction::DecMaster);
|
||||||
|
d.apply(DesktopAction::DecMaster); // no baja de 1
|
||||||
|
assert_eq!(d.active_workspace().params().master_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn promote_to_master_brings_the_focused_window_to_the_front() {
|
||||||
|
let mut d = desktop_with_screen();
|
||||||
|
for id in [1, 2, 3] {
|
||||||
|
open(&mut d, id);
|
||||||
|
}
|
||||||
|
d.apply(DesktopAction::FocusWindow(3));
|
||||||
|
d.apply(DesktopAction::PromoteToMaster);
|
||||||
|
assert_eq!(d.active_workspace().windows()[0], 3);
|
||||||
|
assert_eq!(d.focused_window(), Some(3));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn master_ratio_stays_within_bounds() {
|
fn master_ratio_stays_within_bounds() {
|
||||||
let mut d = desktop_with_screen();
|
let mut d = desktop_with_screen();
|
||||||
|
|||||||
@@ -241,6 +241,8 @@ const KEYMAP_HEADER: &str = "\
|
|||||||
// layout:<modo> master-stack | centered-master | spiral
|
// layout:<modo> master-stack | centered-master | spiral
|
||||||
// grid | columns | rows | monocle
|
// grid | columns | rows | monocle
|
||||||
// grow-master / shrink-master redimensiona el área maestra
|
// grow-master / shrink-master redimensiona el área maestra
|
||||||
|
// inc-master / dec-master nº de ventanas maestras (nmaster)
|
||||||
|
// promote-to-master la enfocada al puesto maestro
|
||||||
// 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
|
||||||
|
|||||||
@@ -52,16 +52,23 @@ impl LayoutMode {
|
|||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
pub struct LayoutParams {
|
pub struct LayoutParams {
|
||||||
pub mode: LayoutMode,
|
pub mode: LayoutMode,
|
||||||
/// Fracción del ancho para la ventana maestra en `MasterStack`
|
/// Fracción del ancho para la ventana maestra en `MasterStack` y
|
||||||
/// (se acota a `0.05..=0.95`).
|
/// `CenteredMaster` (se acota a `0.05..=0.95`).
|
||||||
pub master_ratio: f32,
|
pub master_ratio: f32,
|
||||||
|
/// Cuántas ventanas van en el área maestra (`nmaster`); al menos 1.
|
||||||
|
pub master_count: usize,
|
||||||
/// Margen en píxeles alrededor de cada ventana.
|
/// Margen en píxeles alrededor de cada ventana.
|
||||||
pub gap: i32,
|
pub gap: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LayoutParams {
|
impl Default for LayoutParams {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { mode: LayoutMode::MasterStack, master_ratio: 0.6, gap: 8 }
|
Self {
|
||||||
|
mode: LayoutMode::MasterStack,
|
||||||
|
master_ratio: 0.6,
|
||||||
|
master_count: 1,
|
||||||
|
gap: 8,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,12 +84,18 @@ pub fn tile(screen: Rect, count: usize, params: &LayoutParams) -> Vec<Rect> {
|
|||||||
LayoutMode::Columns => columns(screen, count),
|
LayoutMode::Columns => columns(screen, count),
|
||||||
LayoutMode::Rows => rows(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 => {
|
||||||
LayoutMode::CenteredMaster => centered_master(screen, count, params.master_ratio),
|
master_stack(screen, count, params.master_ratio, params.master_count)
|
||||||
|
}
|
||||||
|
LayoutMode::CenteredMaster => {
|
||||||
|
centered_master(screen, count, params.master_ratio, params.master_count)
|
||||||
|
}
|
||||||
LayoutMode::Spiral => spiral(screen, count),
|
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. *Smart
|
||||||
cells.into_iter().map(|c| c.inset(params.gap)).collect()
|
// gaps*: una sola ventana va a sangre, sin margen desperdiciado.
|
||||||
|
let gap = if count == 1 { 0 } else { params.gap };
|
||||||
|
cells.into_iter().map(|c| c.inset(gap)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Columnas verticales de igual ancho.
|
/// Columnas verticales de igual ancho.
|
||||||
@@ -139,25 +152,27 @@ fn spiral(screen: Rect, count: usize) -> Vec<Rect> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ventana maestra centrada + pila repartida en columnas a ambos lados.
|
/// `master_count` ventanas maestras centradas + el resto repartido en
|
||||||
fn centered_master(screen: Rect, count: usize, ratio: f32) -> Vec<Rect> {
|
/// columnas a ambos lados.
|
||||||
// Con una o dos ventanas no hay nada que centrar: cae a maestro+pila.
|
fn centered_master(screen: Rect, count: usize, ratio: f32, master_count: usize) -> Vec<Rect> {
|
||||||
if count <= 2 {
|
let m = master_count.clamp(1, count);
|
||||||
return master_stack(screen, count, ratio);
|
let stack = count - m;
|
||||||
|
// Centrar sólo tiene sentido con al menos una ventana por lado.
|
||||||
|
if stack < 2 {
|
||||||
|
return master_stack(screen, count, ratio, master_count);
|
||||||
}
|
}
|
||||||
let ratio = ratio.clamp(0.05, 0.95);
|
let ratio = ratio.clamp(0.05, 0.95);
|
||||||
let master_w = (screen.w as f32 * ratio).round() as i32;
|
let master_w = (screen.w as f32 * ratio).round() as i32;
|
||||||
let sides = split(screen.w - master_w, 2);
|
let sides = split(screen.w - master_w, 2);
|
||||||
let (left_w, right_w) = (sides[0].1, sides[1].1);
|
let (left_w, right_w) = (sides[0].1, sides[1].1);
|
||||||
|
|
||||||
let stack = count - 1;
|
|
||||||
let left_n = stack / 2;
|
let left_n = stack / 2;
|
||||||
let right_n = stack - left_n;
|
let right_n = stack - left_n;
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(count);
|
let mut out = Vec::with_capacity(count);
|
||||||
// 0 = la maestra, centrada.
|
// Las maestras, apiladas en la columna central — orden de teselado.
|
||||||
out.push(Rect::new(screen.x + left_w, screen.y, master_w, screen.h));
|
for (off, h) in split(screen.h, m) {
|
||||||
// Columna izquierda, luego la derecha — el orden de teselado.
|
out.push(Rect::new(screen.x + left_w, screen.y + off, master_w, h));
|
||||||
|
}
|
||||||
for (off, h) in split(screen.h, left_n) {
|
for (off, h) in split(screen.h, left_n) {
|
||||||
out.push(Rect::new(screen.x, screen.y + off, left_w, h));
|
out.push(Rect::new(screen.x, screen.y + off, left_w, h));
|
||||||
}
|
}
|
||||||
@@ -167,19 +182,27 @@ fn centered_master(screen: Rect, count: usize, ratio: f32) -> Vec<Rect> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ventana maestra a la izquierda + pila a la derecha.
|
/// `master_count` ventanas maestras a la izquierda + el resto en pila a
|
||||||
fn master_stack(screen: Rect, count: usize, ratio: f32) -> Vec<Rect> {
|
/// la derecha. Sin pila, las maestras llenan toda la pantalla.
|
||||||
if count == 1 {
|
fn master_stack(screen: Rect, count: usize, ratio: f32, master_count: usize) -> Vec<Rect> {
|
||||||
return vec![screen];
|
let m = master_count.clamp(1, count);
|
||||||
|
let stack = count - m;
|
||||||
|
if stack == 0 {
|
||||||
|
return split(screen.h, m)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(off, h)| Rect::new(screen.x, screen.y + off, screen.w, h))
|
||||||
|
.collect();
|
||||||
}
|
}
|
||||||
let ratio = ratio.clamp(0.05, 0.95);
|
let ratio = ratio.clamp(0.05, 0.95);
|
||||||
let master_w = (screen.w as f32 * ratio).round() as i32;
|
let master_w = (screen.w as f32 * ratio).round() as i32;
|
||||||
let master = Rect::new(screen.x, screen.y, master_w, screen.h);
|
|
||||||
|
|
||||||
let stack_x = screen.x + master_w;
|
let stack_x = screen.x + master_w;
|
||||||
let stack_w = screen.w - master_w;
|
let stack_w = screen.w - master_w;
|
||||||
let mut out = vec![master];
|
|
||||||
for (off, h) in split(screen.h, count - 1) {
|
let mut out = Vec::with_capacity(count);
|
||||||
|
for (off, h) in split(screen.h, m) {
|
||||||
|
out.push(Rect::new(screen.x, screen.y + off, master_w, h));
|
||||||
|
}
|
||||||
|
for (off, h) in split(screen.h, stack) {
|
||||||
out.push(Rect::new(stack_x, screen.y + off, stack_w, h));
|
out.push(Rect::new(stack_x, screen.y + off, stack_w, h));
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
@@ -192,7 +215,7 @@ mod tests {
|
|||||||
const SCREEN: Rect = Rect { x: 0, y: 0, w: 1920, h: 1080 };
|
const SCREEN: Rect = Rect { x: 0, y: 0, w: 1920, h: 1080 };
|
||||||
|
|
||||||
fn params(mode: LayoutMode) -> LayoutParams {
|
fn params(mode: LayoutMode) -> LayoutParams {
|
||||||
LayoutParams { mode, master_ratio: 0.6, gap: 0 }
|
LayoutParams { mode, gap: 0, ..LayoutParams::default() }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -296,7 +319,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gap_shrinks_every_window() {
|
fn gap_shrinks_every_window() {
|
||||||
let p = LayoutParams { mode: LayoutMode::Columns, master_ratio: 0.6, gap: 10 };
|
let p = LayoutParams { mode: LayoutMode::Columns, gap: 10, ..LayoutParams::default() };
|
||||||
for r in tile(SCREEN, 2, &p) {
|
for r in tile(SCREEN, 2, &p) {
|
||||||
// Cada celda de 960 de ancho se encoge 20 (10 por lado).
|
// Cada celda de 960 de ancho se encoge 20 (10 por lado).
|
||||||
assert_eq!(r.w, 960 - 20);
|
assert_eq!(r.w, 960 - 20);
|
||||||
@@ -304,6 +327,48 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nmaster_keeps_n_windows_in_the_master_column() {
|
||||||
|
let p = LayoutParams {
|
||||||
|
mode: LayoutMode::MasterStack,
|
||||||
|
master_count: 2,
|
||||||
|
gap: 0,
|
||||||
|
..LayoutParams::default()
|
||||||
|
};
|
||||||
|
let rects = tile(SCREEN, 4, &p);
|
||||||
|
// Dos maestras comparten el ancho maestro (60% de 1920 = 1152).
|
||||||
|
assert_eq!(rects[0].w, 1152);
|
||||||
|
assert_eq!(rects[1].w, 1152);
|
||||||
|
// Dos de pila comparten el resto.
|
||||||
|
assert_eq!(rects[2].w, 1920 - 1152);
|
||||||
|
assert_eq!(rects[3].w, 1920 - 1152);
|
||||||
|
// Las dos maestras parten la altura entre ellas.
|
||||||
|
assert_eq!(rects[0].h + rects[1].h, 1080);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nmaster_above_window_count_makes_every_window_a_master() {
|
||||||
|
let p = LayoutParams {
|
||||||
|
mode: LayoutMode::MasterStack,
|
||||||
|
master_count: 9,
|
||||||
|
gap: 0,
|
||||||
|
..LayoutParams::default()
|
||||||
|
};
|
||||||
|
let rects = tile(SCREEN, 3, &p);
|
||||||
|
// Sin pila: las tres ocupan el ancho completo.
|
||||||
|
assert!(rects.iter().all(|r| r.w == 1920));
|
||||||
|
assert_eq!(rects.iter().map(|r| r.h).sum::<i32>(), 1080);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smart_gaps_drop_the_margin_for_a_single_window() {
|
||||||
|
let p = LayoutParams { mode: LayoutMode::MasterStack, gap: 20, ..LayoutParams::default() };
|
||||||
|
// Una sola ventana: a sangre, sin margen.
|
||||||
|
assert_eq!(tile(SCREEN, 1, &p)[0], SCREEN);
|
||||||
|
// Con dos, el margen vuelve.
|
||||||
|
assert!(tile(SCREEN, 2, &p)[0].w < SCREEN.w);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn layout_is_deterministic() {
|
fn layout_is_deterministic() {
|
||||||
let p = params(LayoutMode::Grid);
|
let p = params(LayoutMode::Grid);
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ impl Workspace {
|
|||||||
self.params.master_ratio = ratio;
|
self.params.master_ratio = ratio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ajusta cuántas ventanas van en el área maestra (`nmaster`).
|
||||||
|
pub fn set_master_count(&mut self, count: usize) {
|
||||||
|
self.params.master_count = count;
|
||||||
|
}
|
||||||
|
|
||||||
/// Añade una ventana y la enfoca. Si ya estaba, sólo la enfoca.
|
/// Añade una ventana y la enfoca. Si ya estaba, sólo la enfoca.
|
||||||
pub fn add(&mut self, window: WindowId) {
|
pub fn add(&mut self, window: WindowId) {
|
||||||
if let Some(i) = self.windows.iter().position(|&w| w == window) {
|
if let Some(i) = self.windows.iter().position(|&w| w == window) {
|
||||||
@@ -125,6 +130,17 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lleva la ventana enfocada al primer puesto del orden de teselado
|
||||||
|
/// (la posición maestra); el foco la acompaña. No hace nada si ya es
|
||||||
|
/// la primera o el escritorio está vacío.
|
||||||
|
pub fn promote_focused(&mut self) {
|
||||||
|
if self.focus > 0 && self.focus < self.windows.len() {
|
||||||
|
let w = self.windows.remove(self.focus);
|
||||||
|
self.windows.insert(0, w);
|
||||||
|
self.focus = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Resuelve la geometría: el rectángulo de cada ventana dentro de
|
/// Resuelve la geometría: el rectángulo de cada ventana dentro de
|
||||||
/// `screen`, en orden de teselado.
|
/// `screen`, en orden de teselado.
|
||||||
pub fn layout(&self, screen: Rect) -> Vec<(WindowId, Rect)> {
|
pub fn layout(&self, screen: Rect) -> Vec<(WindowId, Rect)> {
|
||||||
@@ -220,6 +236,21 @@ mod tests {
|
|||||||
assert_eq!(w.windows(), &[1, 2, 3]);
|
assert_eq!(w.windows(), &[1, 2, 3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn promote_brings_the_focused_window_to_the_front() {
|
||||||
|
let mut w = ws();
|
||||||
|
for id in [1, 2, 3] {
|
||||||
|
w.add(id);
|
||||||
|
}
|
||||||
|
w.focus_window(3);
|
||||||
|
w.promote_focused();
|
||||||
|
assert_eq!(w.windows(), &[3, 1, 2]);
|
||||||
|
assert_eq!(w.focused(), Some(3));
|
||||||
|
// Promover la que ya es maestra no hace nada.
|
||||||
|
w.promote_focused();
|
||||||
|
assert_eq!(w.windows(), &[3, 1, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn layout_pairs_each_window_with_a_rect() {
|
fn layout_pairs_each_window_with_a_rect() {
|
||||||
let mut w = ws();
|
let mut w = ws();
|
||||||
|
|||||||
@@ -1037,5 +1037,15 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
nmaster + promover a maestra + smart gaps (al estilo dwm):
|
||||||
|
LayoutParams.master_count = nº de ventanas en el área maestra (MasterStack y CenteredMaster).
|
||||||
|
mirada-ctl inc-master / dec-master # Super+, / Super+. — nmaster ±1 (acotado 1..9)
|
||||||
|
mirada-ctl promote-to-master # Super+Return — la enfocada salta al puesto maestro
|
||||||
|
Smart gaps: una sola ventana va a sangre, sin margen desperdiciado.
|
||||||
|
combo_string del compositor ahora canoniza teclas con nombre (Return, Tab, F5, flechas…),
|
||||||
|
no sólo caracteres imprimibles — así Super+Return es un atajo válido del keymap.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user