1c6aafbc24
mirada-layout (el motor de teselado del compositor) pasa a `no_std +
alloc` para poder compilarse también en bare-metal — es el primer
crate-núcleo que brahman y renaser compartirán.
- `#![cfg_attr(not(test), no_std)]` + `extern crate alloc`: usa
`alloc::{vec, collections::BTreeMap}` en vez de `std`.
- Matemática de punto flotante vía `libm` (`sqrt`/`ceil`/`round` viven
en `std`, no en `core`).
- `serde` pasa a feature opcional: los consumidores Linux
(mirada-protocol/brain) la activan; un consumidor bare-metal no
necesita (de)serializar el layout.
- Deps declaradas directas (no `workspace = true`): un núcleo que
cruzará fronteras de workspace se mantiene autocontenido.
Verificado: `cargo build --target x86_64-unknown-none` compila;
32 tests verdes; mirada-protocol/brain sin regresión.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
385 lines
13 KiB
Rust
385 lines
13 KiB
Rust
//! 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<Rect> {
|
||
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<Rect> {
|
||
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<Rect> {
|
||
// `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<Rect> {
|
||
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<Rect> {
|
||
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<Rect> {
|
||
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<Rect> {
|
||
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::<i32>(), 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<LayoutMode> = 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::<i32>(), 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::<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]
|
||
fn layout_is_deterministic() {
|
||
let p = params(LayoutMode::Grid);
|
||
assert_eq!(tile(SCREEN, 7, &p), tile(SCREEN, 7, &p));
|
||
}
|
||
}
|