gioser

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definiciones

⎇ be61ddb6eb441193c4374a243fbb00aed47b42de crates/modules/mirada/mirada-layout/src/layout.rs Rust · 377 líneas · 12.8 KB Historia Quién tocó qué crudo

feat(mirada): nmaster, promover a maestra y smart gaps (estilo dwm) 2dd8ff139 · sergio ·

1//! Modos de teselado — cómo se reparte la pantalla entre ventanas.
2
3use serde::{Deserialize, Serialize};
4
5use crate::geometry::{split, Rect};
6
7/// Estrategia de teselado.
8///
9/// Las variantes nuevas se añaden **al final** para no mover los índices
10/// con que `postcard` las serializa en el API de control.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum LayoutMode {
14    /// Una ventana maestra a la izquierda; el resto apiladas a la derecha.
15    MasterStack,
16    /// Todas a pantalla completa, superpuestas — sólo se ve la enfocada.
17    Monocle,
18    /// Rejilla uniforme.
19    Grid,
20    /// Columnas verticales de igual ancho.
21    Columns,
22    /// Filas horizontales de igual alto.
23    Rows,
24    /// Ventana maestra centrada; el resto en columnas a ambos lados.
25    /// Pensado para monitores anchos.
26    CenteredMaster,
27    /// Espiral de Fibonacci: cada ventana parte por la mitad el espacio
28    /// que queda, alternando el sentido del corte.
29    Spiral,
30}
31
32impl LayoutMode {
33    /// Todos los modos, en el orden del ciclo de `CycleLayout`.
34    pub const ALL: [LayoutMode; 7] = [
35        LayoutMode::MasterStack,
36        LayoutMode::CenteredMaster,
37        LayoutMode::Spiral,
38        LayoutMode::Grid,
39        LayoutMode::Columns,
40        LayoutMode::Rows,
41        LayoutMode::Monocle,
42    ];
43
44    /// El siguiente modo en el ciclo (envuelve al llegar al final).
45    pub fn next(self) -> LayoutMode {
46        let i = Self::ALL.iter().position(|&m| m == self).unwrap_or(0);
47        Self::ALL[(i + 1) % Self::ALL.len()]
48    }
49}
50
51/// Parámetros del teselado.
52#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
53pub struct LayoutParams {
54    pub mode: LayoutMode,
55    /// Fracción del ancho para la ventana maestra en `MasterStack` y
56    /// `CenteredMaster` (se acota a `0.05..=0.95`).
57    pub master_ratio: f32,
58    /// Cuántas ventanas van en el área maestra (`nmaster`); al menos 1.
59    pub master_count: usize,
60    /// Margen en píxeles alrededor de cada ventana.
61    pub gap: i32,
62}
63
64impl Default for LayoutParams {
65    fn default() -> Self {
66        Self {
67            mode: LayoutMode::MasterStack,
68            master_ratio: 0.6,
69            master_count: 1,
70            gap: 8,
71        }
72    }
73}
74
75/// Calcula el rectángulo de cada una de las `count` ventanas dentro de
76/// `screen`. El vector resultante tiene exactamente `count` elementos,
77/// en el mismo orden que las ventanas.
78pub fn tile(screen: Rect, count: usize, params: &LayoutParams) -> Vec<Rect> {
79    if count == 0 {
80        return Vec::new();
81    }
82    let cells = match params.mode {
83        LayoutMode::Monocle => vec![screen; count],
84        LayoutMode::Columns => columns(screen, count),
85        LayoutMode::Rows => rows(screen, count),
86        LayoutMode::Grid => grid(screen, count),
87        LayoutMode::MasterStack => {
88            master_stack(screen, count, params.master_ratio, params.master_count)
89        }
90        LayoutMode::CenteredMaster => {
91            centered_master(screen, count, params.master_ratio, params.master_count)
92        }
93        LayoutMode::Spiral => spiral(screen, count),
94    };
95    // El margen se aplica al final, uniforme para todos los modos. *Smart
96    // gaps*: una sola ventana va a sangre, sin margen desperdiciado.
97    let gap = if count == 1 { 0 } else { params.gap };
98    cells.into_iter().map(|c| c.inset(gap)).collect()
99}
100
101/// Columnas verticales de igual ancho.
102fn columns(screen: Rect, count: usize) -> Vec<Rect> {
103    split(screen.w, count)
104        .into_iter()
105        .map(|(off, w)| Rect::new(screen.x + off, screen.y, w, screen.h))
106        .collect()
107}
108
109/// Rejilla `cols × rows` lo más cuadrada posible.
110fn grid(screen: Rect, count: usize) -> Vec<Rect> {
111    let cols = (count as f64).sqrt().ceil() as usize;
112    let rows = count.div_ceil(cols);
113    let col_parts = split(screen.w, cols);
114    let row_parts = split(screen.h, rows);
115    (0..count)
116        .map(|i| {
117            let (cx, cw) = col_parts[i % cols];
118            let (ry, rh) = row_parts[i / cols];
119            Rect::new(screen.x + cx, screen.y + ry, cw, rh)
120        })
121        .collect()
122}
123
124/// Filas horizontales de igual alto.
125fn rows(screen: Rect, count: usize) -> Vec<Rect> {
126    split(screen.h, count)
127        .into_iter()
128        .map(|(off, h)| Rect::new(screen.x, screen.y + off, screen.w, h))
129        .collect()
130}
131
132/// Espiral de Fibonacci: cada ventana se queda con la mitad del espacio
133/// libre y la siguiente recurre en la otra mitad, alternando el corte.
134/// La última ventana llena todo lo que sobra.
135fn spiral(screen: Rect, count: usize) -> Vec<Rect> {
136    let mut out = Vec::with_capacity(count);
137    let mut area = screen;
138    let mut horizontal = true;
139    for _ in 1..count {
140        if horizontal {
141            let p = split(area.w, 2);
142            out.push(Rect::new(area.x, area.y, p[0].1, area.h));
143            area = Rect::new(area.x + p[1].0, area.y, p[1].1, area.h);
144        } else {
145            let p = split(area.h, 2);
146            out.push(Rect::new(area.x, area.y, area.w, p[0].1));
147            area = Rect::new(area.x, area.y + p[1].0, area.w, p[1].1);
148        }
149        horizontal = !horizontal;
150    }
151    out.push(area);
152    out
153}
154
155/// `master_count` ventanas maestras centradas + el resto repartido en
156/// columnas a ambos lados.
157fn centered_master(screen: Rect, count: usize, ratio: f32, master_count: usize) -> Vec<Rect> {
158    let m = master_count.clamp(1, count);
159    let stack = count - m;
160    // Centrar sólo tiene sentido con al menos una ventana por lado.
161    if stack < 2 {
162        return master_stack(screen, count, ratio, master_count);
163    }
164    let ratio = ratio.clamp(0.05, 0.95);
165    let master_w = (screen.w as f32 * ratio).round() as i32;
166    let sides = split(screen.w - master_w, 2);
167    let (left_w, right_w) = (sides[0].1, sides[1].1);
168    let left_n = stack / 2;
169    let right_n = stack - left_n;
170
171    let mut out = Vec::with_capacity(count);
172    // Las maestras, apiladas en la columna central — orden de teselado.
173    for (off, h) in split(screen.h, m) {
174        out.push(Rect::new(screen.x + left_w, screen.y + off, master_w, h));
175    }
176    for (off, h) in split(screen.h, left_n) {
177        out.push(Rect::new(screen.x, screen.y + off, left_w, h));
178    }
179    for (off, h) in split(screen.h, right_n) {
180        out.push(Rect::new(screen.x + left_w + master_w, screen.y + off, right_w, h));
181    }
182    out
183}
184
185/// `master_count` ventanas maestras a la izquierda + el resto en pila a
186/// la derecha. Sin pila, las maestras llenan toda la pantalla.
187fn master_stack(screen: Rect, count: usize, ratio: f32, master_count: usize) -> Vec<Rect> {
188    let m = master_count.clamp(1, count);
189    let stack = count - m;
190    if stack == 0 {
191        return split(screen.h, m)
192            .into_iter()
193            .map(|(off, h)| Rect::new(screen.x, screen.y + off, screen.w, h))
194            .collect();
195    }
196    let ratio = ratio.clamp(0.05, 0.95);
197    let master_w = (screen.w as f32 * ratio).round() as i32;
198    let stack_x = screen.x + master_w;
199    let stack_w = screen.w - master_w;
200
201    let mut out = Vec::with_capacity(count);
202    for (off, h) in split(screen.h, m) {
203        out.push(Rect::new(screen.x, screen.y + off, master_w, h));
204    }
205    for (off, h) in split(screen.h, stack) {
206        out.push(Rect::new(stack_x, screen.y + off, stack_w, h));
207    }
208    out
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    const SCREEN: Rect = Rect { x: 0, y: 0, w: 1920, h: 1080 };
216
217    fn params(mode: LayoutMode) -> LayoutParams {
218        LayoutParams { mode, gap: 0, ..LayoutParams::default() }
219    }
220
221    #[test]
222    fn empty_count_yields_no_rects() {
223        assert!(tile(SCREEN, 0, &params(LayoutMode::Grid)).is_empty());
224    }
225
226    #[test]
227    fn tile_count_matches_window_count() {
228        for mode in LayoutMode::ALL {
229            for n in 1..=9 {
230                assert_eq!(tile(SCREEN, n, &params(mode)).len(), n, "modo {mode:?}");
231            }
232        }
233    }
234
235    #[test]
236    fn rows_partition_the_height_exactly() {
237        let rects = tile(SCREEN, 3, &params(LayoutMode::Rows));
238        assert_eq!(rects.iter().map(|r| r.h).sum::<i32>(), 1080);
239        assert!(rects.iter().all(|r| r.w == 1920));
240    }
241
242    #[test]
243    fn spiral_tiles_cover_the_screen_without_overlap() {
244        for n in 1..=9 {
245            let total: i64 = tile(SCREEN, n, &params(LayoutMode::Spiral))
246                .iter()
247                .map(|r| r.area())
248                .sum();
249            assert_eq!(total, SCREEN.area(), "espiral con {n} ventanas");
250        }
251    }
252
253    #[test]
254    fn centered_master_centers_the_master_and_covers_the_screen() {
255        let rects = tile(SCREEN, 5, &params(LayoutMode::CenteredMaster));
256        let master = rects[0];
257        // Hueco a la izquierda y a la derecha de la maestra: iguales ±1px.
258        let left = master.x - SCREEN.x;
259        let right = (SCREEN.x + SCREEN.w) - (master.x + master.w);
260        assert!((left - right).abs() <= 1, "maestra no centrada: {left} vs {right}");
261        let total: i64 = rects.iter().map(|r| r.area()).sum();
262        assert_eq!(total, SCREEN.area());
263    }
264
265    #[test]
266    fn layout_mode_next_cycles_through_every_mode() {
267        let mut visited: Vec<LayoutMode> = Vec::new();
268        let mut m = LayoutMode::MasterStack;
269        for _ in 0..LayoutMode::ALL.len() {
270            assert!(!visited.contains(&m), "modo repetido en el ciclo: {m:?}");
271            visited.push(m);
272            m = m.next();
273        }
274        // Tras una vuelta completa, de vuelta al inicio.
275        assert_eq!(m, LayoutMode::MasterStack);
276        for mode in LayoutMode::ALL {
277            assert!(visited.contains(&mode), "el ciclo no pasa por {mode:?}");
278        }
279    }
280
281    #[test]
282    fn monocle_gives_every_window_the_full_screen() {
283        for r in tile(SCREEN, 4, &params(LayoutMode::Monocle)) {
284            assert_eq!(r, SCREEN);
285        }
286    }
287
288    #[test]
289    fn columns_partition_the_width_exactly() {
290        let rects = tile(SCREEN, 3, &params(LayoutMode::Columns));
291        assert_eq!(rects.iter().map(|r| r.w).sum::<i32>(), 1920);
292        // Todas ocupan el alto completo.
293        assert!(rects.iter().all(|r| r.h == 1080));
294    }
295
296    #[test]
297    fn master_stack_master_takes_its_ratio() {
298        let rects = tile(SCREEN, 3, &params(LayoutMode::MasterStack));
299        // 60% de 1920 = 1152.
300        assert_eq!(rects[0].w, 1152);
301        // Las dos de la pila comparten el resto del ancho y el alto.
302        assert_eq!(rects[1].w, 1920 - 1152);
303        assert_eq!(rects[1].h + rects[2].h, 1080);
304    }
305
306    #[test]
307    fn master_stack_single_window_fills_screen() {
308        let rects = tile(SCREEN, 1, &params(LayoutMode::MasterStack));
309        assert_eq!(rects[0], SCREEN);
310    }
311
312    #[test]
313    fn grid_tiles_cover_the_screen_without_overlap() {
314        // 4 ventanas → rejilla 2×2, cada una un cuarto.
315        let rects = tile(SCREEN, 4, &params(LayoutMode::Grid));
316        let total: i64 = rects.iter().map(|r| r.area()).sum();
317        assert_eq!(total, SCREEN.area());
318    }
319
320    #[test]
321    fn gap_shrinks_every_window() {
322        let p = LayoutParams { mode: LayoutMode::Columns, gap: 10, ..LayoutParams::default() };
323        for r in tile(SCREEN, 2, &p) {
324            // Cada celda de 960 de ancho se encoge 20 (10 por lado).
325            assert_eq!(r.w, 960 - 20);
326            assert_eq!(r.h, 1080 - 20);
327        }
328    }
329
330    #[test]
331    fn nmaster_keeps_n_windows_in_the_master_column() {
332        let p = LayoutParams {
333            mode: LayoutMode::MasterStack,
334            master_count: 2,
335            gap: 0,
336            ..LayoutParams::default()
337        };
338        let rects = tile(SCREEN, 4, &p);
339        // Dos maestras comparten el ancho maestro (60% de 1920 = 1152).
340        assert_eq!(rects[0].w, 1152);
341        assert_eq!(rects[1].w, 1152);
342        // Dos de pila comparten el resto.
343        assert_eq!(rects[2].w, 1920 - 1152);
344        assert_eq!(rects[3].w, 1920 - 1152);
345        // Las dos maestras parten la altura entre ellas.
346        assert_eq!(rects[0].h + rects[1].h, 1080);
347    }
348
349    #[test]
350    fn nmaster_above_window_count_makes_every_window_a_master() {
351        let p = LayoutParams {
352            mode: LayoutMode::MasterStack,
353            master_count: 9,
354            gap: 0,
355            ..LayoutParams::default()
356        };
357        let rects = tile(SCREEN, 3, &p);
358        // Sin pila: las tres ocupan el ancho completo.
359        assert!(rects.iter().all(|r| r.w == 1920));
360        assert_eq!(rects.iter().map(|r| r.h).sum::<i32>(), 1080);
361    }
362
363    #[test]
364    fn smart_gaps_drop_the_margin_for_a_single_window() {
365        let p = LayoutParams { mode: LayoutMode::MasterStack, gap: 20, ..LayoutParams::default() };
366        // Una sola ventana: a sangre, sin margen.
367        assert_eq!(tile(SCREEN, 1, &p)[0], SCREEN);
368        // Con dos, el margen vuelve.
369        assert!(tile(SCREEN, 2, &p)[0].w < SCREEN.w);
370    }
371
372    #[test]
373    fn layout_is_deterministic() {
374        let p = params(LayoutMode::Grid);
375        assert_eq!(tile(SCREEN, 7, &p), tile(SCREEN, 7, &p));
376    }
377}