Files
brahman/crates/modules/mirada/mirada-layout/src/layout.rs
T
sergio 1c6aafbc24 feat(mirada): mirada-layout no_std — primer núcleo compartible con renaser
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>
2026-05-22 14:37:03 +00:00

385 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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, &params(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, &params(mode)).len(), n, "modo {mode:?}");
}
}
}
#[test]
fn rows_partition_the_height_exactly() {
let rects = tile(SCREEN, 3, &params(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, &params(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, &params(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, &params(LayoutMode::Monocle)) {
assert_eq!(r, SCREEN);
}
}
#[test]
fn columns_partition_the_width_exactly() {
let rects = tile(SCREEN, 3, &params(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, &params(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, &params(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, &params(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));
}
}