//! Modos de teselado — cómo se reparte la pantalla entre ventanas. use alloc::{vec, vec::Vec}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::geometry::{split, Rect}; /// 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)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] pub enum LayoutMode { /// Una ventana maestra a la izquierda; el resto apiladas a la derecha. MasterStack, /// Todas a pantalla completa, superpuestas — sólo se ve la enfocada. Monocle, /// Rejilla uniforme. Grid, /// Columnas verticales de igual ancho. 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. #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct LayoutParams { pub mode: LayoutMode, /// Fracción del ancho para la ventana maestra en `MasterStack` y /// `CenteredMaster` (se acota a `0.05..=0.95`). 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. pub gap: i32, } impl Default for LayoutParams { fn default() -> Self { Self { mode: LayoutMode::MasterStack, master_ratio: 0.6, master_count: 1, gap: 8, } } } /// Calcula el rectángulo de cada una de las `count` ventanas dentro de /// `screen`. El vector resultante tiene exactamente `count` elementos, /// en el mismo orden que las ventanas. pub fn tile(screen: Rect, count: usize, params: &LayoutParams) -> Vec { if count == 0 { return Vec::new(); } let cells = match params.mode { LayoutMode::Monocle => vec![screen; count], LayoutMode::Columns => columns(screen, count), LayoutMode::Rows => rows(screen, count), LayoutMode::Grid => grid(screen, count), LayoutMode::MasterStack => { 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), }; // El margen se aplica al final, uniforme para todos los modos. *Smart // 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. fn columns(screen: Rect, count: usize) -> Vec { split(screen.w, count) .into_iter() .map(|(off, w)| Rect::new(screen.x + off, screen.y, w, screen.h)) .collect() } /// Rejilla `cols × rows` lo más cuadrada posible. fn grid(screen: Rect, count: usize) -> Vec { // `libm` en vez de los métodos de `f64`: `sqrt`/`ceil` viven en // `std`, no en `core` — y este crate es `no_std`. let cols = libm::ceil(libm::sqrt(count as f64)) as usize; let rows = count.div_ceil(cols); let col_parts = split(screen.w, cols); let row_parts = split(screen.h, rows); (0..count) .map(|i| { let (cx, cw) = col_parts[i % cols]; let (ry, rh) = row_parts[i / cols]; Rect::new(screen.x + cx, screen.y + ry, cw, rh) }) .collect() } /// Filas horizontales de igual alto. fn rows(screen: Rect, count: usize) -> Vec { 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 { 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 } /// `master_count` ventanas maestras centradas + el resto repartido en /// columnas a ambos lados. fn centered_master(screen: Rect, count: usize, ratio: f32, master_count: usize) -> Vec { let m = master_count.clamp(1, count); 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 master_w = libm::roundf(screen.w as f32 * ratio) as i32; let sides = split(screen.w - master_w, 2); let (left_w, right_w) = (sides[0].1, sides[1].1); let left_n = stack / 2; let right_n = stack - left_n; let mut out = Vec::with_capacity(count); // Las maestras, apiladas en la columna central — orden de teselado. for (off, h) in split(screen.h, m) { out.push(Rect::new(screen.x + left_w, screen.y + off, master_w, h)); } 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 } /// `master_count` ventanas maestras a la izquierda + el resto en pila a /// la derecha. Sin pila, las maestras llenan toda la pantalla. fn master_stack(screen: Rect, count: usize, ratio: f32, master_count: usize) -> Vec { 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 master_w = libm::roundf(screen.w as f32 * ratio) as i32; let stack_x = screen.x + master_w; let stack_w = screen.w - master_w; 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 } #[cfg(test)] mod tests { use super::*; const SCREEN: Rect = Rect { x: 0, y: 0, w: 1920, h: 1080 }; fn params(mode: LayoutMode) -> LayoutParams { LayoutParams { mode, gap: 0, ..LayoutParams::default() } } #[test] fn empty_count_yields_no_rects() { assert!(tile(SCREEN, 0, ¶ms(LayoutMode::Grid)).is_empty()); } #[test] fn tile_count_matches_window_count() { for mode in LayoutMode::ALL { for n in 1..=9 { 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::(), 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 = 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] fn monocle_gives_every_window_the_full_screen() { for r in tile(SCREEN, 4, ¶ms(LayoutMode::Monocle)) { assert_eq!(r, SCREEN); } } #[test] fn columns_partition_the_width_exactly() { let rects = tile(SCREEN, 3, ¶ms(LayoutMode::Columns)); assert_eq!(rects.iter().map(|r| r.w).sum::(), 1920); // Todas ocupan el alto completo. assert!(rects.iter().all(|r| r.h == 1080)); } #[test] fn master_stack_master_takes_its_ratio() { let rects = tile(SCREEN, 3, ¶ms(LayoutMode::MasterStack)); // 60% de 1920 = 1152. assert_eq!(rects[0].w, 1152); // Las dos de la pila comparten el resto del ancho y el alto. assert_eq!(rects[1].w, 1920 - 1152); assert_eq!(rects[1].h + rects[2].h, 1080); } #[test] fn master_stack_single_window_fills_screen() { let rects = tile(SCREEN, 1, ¶ms(LayoutMode::MasterStack)); assert_eq!(rects[0], SCREEN); } #[test] fn grid_tiles_cover_the_screen_without_overlap() { // 4 ventanas → rejilla 2×2, cada una un cuarto. let rects = tile(SCREEN, 4, ¶ms(LayoutMode::Grid)); let total: i64 = rects.iter().map(|r| r.area()).sum(); assert_eq!(total, SCREEN.area()); } #[test] fn gap_shrinks_every_window() { let p = LayoutParams { mode: LayoutMode::Columns, gap: 10, ..LayoutParams::default() }; for r in tile(SCREEN, 2, &p) { // Cada celda de 960 de ancho se encoge 20 (10 por lado). assert_eq!(r.w, 960 - 20); assert_eq!(r.h, 1080 - 20); } } #[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::(), 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] fn layout_is_deterministic() { let p = params(LayoutMode::Grid); assert_eq!(tile(SCREEN, 7, &p), tile(SCREEN, 7, &p)); } }