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>
369 lines
11 KiB
Rust
369 lines
11 KiB
Rust
//! `Workspace` — un conjunto de ventanas, su foco y su modo de teselado.
|
|
|
|
use alloc::collections::BTreeMap;
|
|
use alloc::vec::Vec;
|
|
// El macro `vec!` sólo lo usan los tests de este módulo.
|
|
#[cfg(test)]
|
|
use alloc::vec;
|
|
|
|
#[cfg(feature = "serde")]
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::geometry::Rect;
|
|
use crate::layout::{tile, LayoutMode, LayoutParams};
|
|
|
|
/// Identificador de una ventana (una superficie Wayland).
|
|
pub type WindowId = u64;
|
|
|
|
/// Un escritorio: ventanas en orden de teselado + la enfocada + el modo.
|
|
#[derive(Debug, Clone)]
|
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
pub struct Workspace {
|
|
/// Ventanas en orden de teselado (la 0 es la maestra en `MasterStack`).
|
|
windows: Vec<WindowId>,
|
|
/// Índice de la ventana enfocada en `windows`.
|
|
focus: usize,
|
|
params: LayoutParams,
|
|
/// Ventanas flotantes y su rectángulo: salen del teselado y se pintan
|
|
/// encima. Las que no están aquí se teselan normalmente.
|
|
floating: BTreeMap<WindowId, Rect>,
|
|
/// La ventana en pantalla completa, si hay alguna: cubre toda la
|
|
/// salida y oculta al resto.
|
|
fullscreen: Option<WindowId>,
|
|
}
|
|
|
|
impl Workspace {
|
|
/// Escritorio vacío con los parámetros dados.
|
|
pub fn new(params: LayoutParams) -> Self {
|
|
Self {
|
|
windows: Vec::new(),
|
|
focus: 0,
|
|
params,
|
|
floating: BTreeMap::new(),
|
|
fullscreen: None,
|
|
}
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.windows.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.windows.is_empty()
|
|
}
|
|
|
|
/// Ventanas en orden de teselado.
|
|
pub fn windows(&self) -> &[WindowId] {
|
|
&self.windows
|
|
}
|
|
|
|
pub fn params(&self) -> &LayoutParams {
|
|
&self.params
|
|
}
|
|
|
|
/// Cambia el modo de teselado.
|
|
pub fn set_mode(&mut self, mode: LayoutMode) {
|
|
self.params.mode = mode;
|
|
}
|
|
|
|
/// Ajusta la fracción de la ventana maestra.
|
|
pub fn set_master_ratio(&mut self, ratio: f32) {
|
|
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.
|
|
pub fn add(&mut self, window: WindowId) {
|
|
if let Some(i) = self.windows.iter().position(|&w| w == window) {
|
|
self.focus = i;
|
|
} else {
|
|
self.windows.push(window);
|
|
self.focus = self.windows.len() - 1;
|
|
}
|
|
}
|
|
|
|
/// Quita una ventana. `false` si no estaba. El foco se reajusta para
|
|
/// seguir apuntando a una ventana válida.
|
|
pub fn remove(&mut self, window: WindowId) -> bool {
|
|
let Some(i) = self.windows.iter().position(|&w| w == window) else {
|
|
return false;
|
|
};
|
|
self.windows.remove(i);
|
|
self.floating.remove(&window);
|
|
if self.fullscreen == Some(window) {
|
|
self.fullscreen = None;
|
|
}
|
|
if i < self.focus {
|
|
self.focus -= 1;
|
|
}
|
|
if self.focus >= self.windows.len() {
|
|
self.focus = self.windows.len().saturating_sub(1);
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Marca una ventana como flotante en `rect`, o la devuelve al
|
|
/// teselado con `None`. La ventana sigue en el orden de foco.
|
|
pub fn set_floating(&mut self, window: WindowId, rect: Option<Rect>) {
|
|
match rect {
|
|
Some(r) => {
|
|
self.floating.insert(window, r);
|
|
}
|
|
None => {
|
|
self.floating.remove(&window);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `true` si la ventana está flotando.
|
|
pub fn is_floating(&self, window: WindowId) -> bool {
|
|
self.floating.contains_key(&window)
|
|
}
|
|
|
|
/// La ventana en pantalla completa de este escritorio, si hay alguna.
|
|
pub fn fullscreen(&self) -> Option<WindowId> {
|
|
self.fullscreen
|
|
}
|
|
|
|
/// Pone (o quita, con `None`) la ventana en pantalla completa.
|
|
pub fn set_fullscreen(&mut self, window: Option<WindowId>) {
|
|
self.fullscreen = window;
|
|
}
|
|
|
|
/// Ventana enfocada, o `None` si el escritorio está vacío.
|
|
pub fn focused(&self) -> Option<WindowId> {
|
|
self.windows.get(self.focus).copied()
|
|
}
|
|
|
|
/// Mueve el foco a la ventana siguiente (cíclico).
|
|
pub fn focus_next(&mut self) {
|
|
if !self.windows.is_empty() {
|
|
self.focus = (self.focus + 1) % self.windows.len();
|
|
}
|
|
}
|
|
|
|
/// Mueve el foco a la ventana anterior (cíclico).
|
|
pub fn focus_prev(&mut self) {
|
|
if !self.windows.is_empty() {
|
|
self.focus = (self.focus + self.windows.len() - 1) % self.windows.len();
|
|
}
|
|
}
|
|
|
|
/// Enfoca una ventana por id. `false` si no está en el escritorio.
|
|
pub fn focus_window(&mut self, window: WindowId) -> bool {
|
|
match self.windows.iter().position(|&w| w == window) {
|
|
Some(i) => {
|
|
self.focus = i;
|
|
true
|
|
}
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
/// Intercambia la ventana enfocada con la siguiente en el orden de
|
|
/// teselado; el foco la acompaña. No hace nada si ya es la última.
|
|
pub fn move_focused_forward(&mut self) {
|
|
if self.focus + 1 < self.windows.len() {
|
|
self.windows.swap(self.focus, self.focus + 1);
|
|
self.focus += 1;
|
|
}
|
|
}
|
|
|
|
/// Intercambia la ventana enfocada con la anterior. No hace nada si
|
|
/// ya es la primera.
|
|
pub fn move_focused_backward(&mut self) {
|
|
if self.focus > 0 && !self.windows.is_empty() {
|
|
self.windows.swap(self.focus, self.focus - 1);
|
|
self.focus -= 1;
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
/// `screen`. Primero las teseladas en orden de teselado, luego las
|
|
/// flotantes con su propio rectángulo — éstas van al final para que
|
|
/// el Cuerpo las pinte encima.
|
|
pub fn layout(&self, screen: Rect) -> Vec<(WindowId, Rect)> {
|
|
let tiled: Vec<WindowId> = self
|
|
.windows
|
|
.iter()
|
|
.copied()
|
|
.filter(|id| !self.floating.contains_key(id))
|
|
.collect();
|
|
let rects = tile(screen, tiled.len(), &self.params);
|
|
let mut out: Vec<(WindowId, Rect)> = tiled.into_iter().zip(rects).collect();
|
|
for &id in &self.windows {
|
|
if let Some(&rect) = self.floating.get(&id) {
|
|
out.push((id, rect));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn ws() -> Workspace {
|
|
Workspace::new(LayoutParams::default())
|
|
}
|
|
|
|
#[test]
|
|
fn add_focuses_the_new_window() {
|
|
let mut w = ws();
|
|
w.add(10);
|
|
w.add(20);
|
|
assert_eq!(w.focused(), Some(20));
|
|
assert_eq!(w.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn adding_an_existing_window_just_focuses_it() {
|
|
let mut w = ws();
|
|
w.add(10);
|
|
w.add(20);
|
|
w.add(10);
|
|
assert_eq!(w.focused(), Some(10));
|
|
assert_eq!(w.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn focus_cycles_both_ways() {
|
|
let mut w = ws();
|
|
for id in [1, 2, 3] {
|
|
w.add(id);
|
|
}
|
|
assert_eq!(w.focused(), Some(3));
|
|
w.focus_next();
|
|
assert_eq!(w.focused(), Some(1)); // dio la vuelta
|
|
w.focus_prev();
|
|
assert_eq!(w.focused(), Some(3));
|
|
}
|
|
|
|
#[test]
|
|
fn remove_keeps_focus_valid() {
|
|
let mut w = ws();
|
|
for id in [1, 2, 3] {
|
|
w.add(id);
|
|
}
|
|
w.focus_window(2);
|
|
w.remove(2);
|
|
// El foco se mantiene dentro de rango.
|
|
assert!(w.focused().is_some());
|
|
assert_eq!(w.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_before_focus_shifts_it() {
|
|
let mut w = ws();
|
|
for id in [1, 2, 3] {
|
|
w.add(id);
|
|
}
|
|
w.focus_window(3); // focus = 2
|
|
w.remove(1); // quita una anterior
|
|
assert_eq!(w.focused(), Some(3)); // sigue enfocada la 3
|
|
}
|
|
|
|
#[test]
|
|
fn remove_last_window_empties_workspace() {
|
|
let mut w = ws();
|
|
w.add(7);
|
|
assert!(w.remove(7));
|
|
assert!(w.is_empty());
|
|
assert_eq!(w.focused(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn move_focused_reorders_tiling() {
|
|
let mut w = ws();
|
|
for id in [1, 2, 3] {
|
|
w.add(id);
|
|
}
|
|
w.focus_window(1); // primera
|
|
w.move_focused_forward();
|
|
assert_eq!(w.windows(), &[2, 1, 3]);
|
|
assert_eq!(w.focused(), Some(1)); // el foco la acompañó
|
|
w.move_focused_backward();
|
|
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]
|
|
fn layout_pairs_each_window_with_a_rect() {
|
|
let mut w = ws();
|
|
for id in [100, 200, 300] {
|
|
w.add(id);
|
|
}
|
|
let screen = Rect::new(0, 0, 1920, 1080);
|
|
let placed = w.layout(screen);
|
|
assert_eq!(placed.len(), 3);
|
|
let ids: Vec<_> = placed.iter().map(|(id, _)| *id).collect();
|
|
assert_eq!(ids, vec![100, 200, 300]);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_workspace_lays_out_nothing() {
|
|
assert!(ws().layout(Rect::new(0, 0, 800, 600)).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_floating_window_keeps_its_rect_and_goes_last() {
|
|
let mut w = ws();
|
|
for id in [1, 2, 3] {
|
|
w.add(id);
|
|
}
|
|
let float_rect = Rect::new(50, 50, 400, 300);
|
|
w.set_floating(2, Some(float_rect));
|
|
assert!(w.is_floating(2));
|
|
let placed = w.layout(Rect::new(0, 0, 1920, 1080));
|
|
assert_eq!(placed.len(), 3);
|
|
// La flotante va al final, con su rectángulo intacto.
|
|
assert_eq!(placed[2], (2, float_rect));
|
|
let ids: Vec<_> = placed.iter().map(|(id, _)| *id).collect();
|
|
assert_eq!(ids, vec![1, 3, 2]);
|
|
// Devolverla al teselado.
|
|
w.set_floating(2, None);
|
|
assert!(!w.is_floating(2));
|
|
assert_eq!(w.layout(Rect::new(0, 0, 1920, 1080)).len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn removing_a_window_clears_its_floating_state() {
|
|
let mut w = ws();
|
|
w.add(1);
|
|
w.set_floating(1, Some(Rect::new(0, 0, 100, 100)));
|
|
w.remove(1);
|
|
w.add(1); // mismo id, ventana nueva: ya no flota
|
|
assert!(!w.is_floating(1));
|
|
}
|
|
}
|