feat(mirada): mirada-layout — motor de teselado del compositor Wayland
Rect + split (reparto exacto de píxeles), 4 modos de layout (MasterStack, Monocle, Grid, Columns) con tile(), y Workspace: ventanas en orden de teselado, foco cíclico, reordenado y resolución de geometría. Determinista, agnóstico de Wayland/smithay. 22 tests. #![forbid(unsafe_code)]. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+7
@@ -7346,6 +7346,13 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mirada-layout"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.15"
|
||||
|
||||
@@ -153,6 +153,11 @@ members = [
|
||||
# ============================================================
|
||||
"crates/modules/charka/charka-bcd",
|
||||
|
||||
# ============================================================
|
||||
# modules/mirada/ — Compositor Wayland
|
||||
# ============================================================
|
||||
"crates/modules/mirada/mirada-layout",
|
||||
|
||||
# ============================================================
|
||||
# modules/nakui/ — ERP matemático (categórico)
|
||||
# ============================================================
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# modules/mirada/ — Compositor Wayland
|
||||
|
||||
**Propósito.** Un compositor Wayland teselante con aislamiento de
|
||||
clientes (sobre `arje-incarnate`) y delegación de regiones. El cerebro
|
||||
espacial —cómo se reparte la pantalla— se mantiene puro y aislado de
|
||||
Wayland para poder probarlo sin un servidor gráfico.
|
||||
|
||||
## Crates
|
||||
|
||||
| crate | tipo | rol |
|
||||
| --------------- | ---- | ------------------------------------------------------------ |
|
||||
| `mirada-layout` | lib | Motor de teselado: `Rect`, modos de layout, `Workspace` (ventanas, foco) |
|
||||
|
||||
## mirada-layout
|
||||
|
||||
- `Rect` + `split` — reparto exacto de píxeles (sin pérdidas).
|
||||
- `LayoutMode` — `MasterStack`, `Monocle`, `Grid`, `Columns`; `tile`
|
||||
calcula el rectángulo de cada ventana.
|
||||
- `Workspace` — ventanas en orden de teselado, foco cíclico, reordenado
|
||||
(`move_focused_forward/backward`) y `layout` que resuelve la geometría.
|
||||
- Determinista: misma pantalla + mismas ventanas → misma distribución.
|
||||
|
||||
## Dependencias
|
||||
|
||||
- `mirada-layout` ← sólo `serde`. `#![forbid(unsafe_code)]`.
|
||||
- Cero Wayland, cero `smithay` — ese acoplamiento vive en los crates de
|
||||
integración pendientes.
|
||||
|
||||
## Estado
|
||||
|
||||
`mirada-layout` implementado y verde (22 tests). **Pendiente** (la capa
|
||||
que toca hardware/protocolo, no verificable en modo desatendido):
|
||||
|
||||
| crate pendiente | rol |
|
||||
| ------------------ | ---------------------------------------------------- |
|
||||
| `mirada-compositor`| integración `smithay`: superficies, buffers, salidas |
|
||||
| `mirada-input` | teclado/ratón, atajos, asignación de foco |
|
||||
| `mirada-sandbox` | aislamiento de clientes sobre `arje-incarnate` |
|
||||
|
||||
CRIU (congelar/restaurar ventanas) queda anotado como futuro.
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "mirada-layout"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "mirada — motor de teselado del compositor Wayland: reparte la pantalla entre ventanas según el modo de layout. Agnóstico de Wayland y de smithay."
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Geometría — el rectángulo en coordenadas de pantalla.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Un rectángulo en píxeles de pantalla. El origen `(0,0)` es la
|
||||
/// esquina superior-izquierda; `x` crece a la derecha, `y` hacia abajo.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Rect {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub w: i32,
|
||||
pub h: i32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
|
||||
Self { x, y, w, h }
|
||||
}
|
||||
|
||||
/// Área en píxeles cuadrados.
|
||||
pub fn area(&self) -> i64 {
|
||||
self.w.max(0) as i64 * self.h.max(0) as i64
|
||||
}
|
||||
|
||||
/// `true` si el rectángulo tiene ancho y alto positivos.
|
||||
pub fn is_visible(&self) -> bool {
|
||||
self.w > 0 && self.h > 0
|
||||
}
|
||||
|
||||
/// Encoge el rectángulo `g` píxeles por cada lado. Si el margen se
|
||||
/// come toda la dimensión, ésta queda en `0` (no negativa).
|
||||
pub fn inset(&self, g: i32) -> Rect {
|
||||
Rect {
|
||||
x: self.x + g,
|
||||
y: self.y + g,
|
||||
w: (self.w - 2 * g).max(0),
|
||||
h: (self.h - 2 * g).max(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` si `(px, py)` cae dentro del rectángulo.
|
||||
pub fn contains(&self, px: i32, py: i32) -> bool {
|
||||
px >= self.x && px < self.x + self.w && py >= self.y && py < self.y + self.h
|
||||
}
|
||||
}
|
||||
|
||||
/// Reparte `total` píxeles en `n` tramos contiguos sin perder ni un
|
||||
/// píxel: las fronteras caen en `total · k / n`, así que la suma de los
|
||||
/// tamaños es exactamente `total`. Devuelve `(offset, tamaño)` por tramo.
|
||||
pub fn split(total: i32, n: usize) -> Vec<(i32, i32)> {
|
||||
if n == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let total = total.max(0) as i64;
|
||||
let n64 = n as i64;
|
||||
(0..n)
|
||||
.map(|k| {
|
||||
let start = total * k as i64 / n64;
|
||||
let end = total * (k as i64 + 1) / n64;
|
||||
(start as i32, (end - start) as i32)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn inset_shrinks_by_gap_on_every_side() {
|
||||
let r = Rect::new(0, 0, 100, 80).inset(5);
|
||||
assert_eq!(r, Rect::new(5, 5, 90, 70));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inset_clamps_to_zero() {
|
||||
let r = Rect::new(0, 0, 8, 8).inset(10);
|
||||
assert_eq!((r.w, r.h), (0, 0));
|
||||
assert!(!r.is_visible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_loses_no_pixels() {
|
||||
for n in 1..=13 {
|
||||
let parts = split(1000, n);
|
||||
assert_eq!(parts.len(), n);
|
||||
assert_eq!(parts.iter().map(|(_, s)| *s).sum::<i32>(), 1000);
|
||||
// Los tramos son contiguos.
|
||||
for w in parts.windows(2) {
|
||||
assert_eq!(w[0].0 + w[0].1, w[1].0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_checks_bounds() {
|
||||
let r = Rect::new(10, 10, 20, 20);
|
||||
assert!(r.contains(10, 10));
|
||||
assert!(r.contains(29, 29));
|
||||
assert!(!r.contains(30, 30));
|
||||
assert!(!r.contains(9, 15));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
//! Modos de teselado — cómo se reparte la pantalla entre ventanas.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::geometry::{split, Rect};
|
||||
|
||||
/// Estrategia de teselado.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[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,
|
||||
}
|
||||
|
||||
/// Parámetros del teselado.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct LayoutParams {
|
||||
pub mode: LayoutMode,
|
||||
/// Fracción del ancho para la ventana maestra en `MasterStack`
|
||||
/// (se acota a `0.05..=0.95`).
|
||||
pub master_ratio: f32,
|
||||
/// 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, 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::Grid => grid(screen, count),
|
||||
LayoutMode::MasterStack => master_stack(screen, count, params.master_ratio),
|
||||
};
|
||||
// El margen se aplica al final, uniforme para todos los modos.
|
||||
cells.into_iter().map(|c| c.inset(params.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> {
|
||||
let cols = (count as f64).sqrt().ceil() 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()
|
||||
}
|
||||
|
||||
/// Ventana maestra a la izquierda + pila a la derecha.
|
||||
fn master_stack(screen: Rect, count: usize, ratio: f32) -> Vec<Rect> {
|
||||
if count == 1 {
|
||||
return vec![screen];
|
||||
}
|
||||
let ratio = ratio.clamp(0.05, 0.95);
|
||||
let master_w = (screen.w as f32 * ratio).round() as i32;
|
||||
let master = Rect::new(screen.x, screen.y, master_w, screen.h);
|
||||
|
||||
let stack_x = screen.x + master_w;
|
||||
let stack_w = screen.w - master_w;
|
||||
let mut out = vec![master];
|
||||
for (off, h) in split(screen.h, count - 1) {
|
||||
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, master_ratio: 0.6, gap: 0 }
|
||||
}
|
||||
|
||||
#[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::MasterStack,
|
||||
LayoutMode::Monocle,
|
||||
LayoutMode::Grid,
|
||||
LayoutMode::Columns,
|
||||
] {
|
||||
for n in 1..=9 {
|
||||
assert_eq!(tile(SCREEN, n, ¶ms(mode)).len(), n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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, master_ratio: 0.6, gap: 10 };
|
||||
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 layout_is_deterministic() {
|
||||
let p = params(LayoutMode::Grid);
|
||||
assert_eq!(tile(SCREEN, 7, &p), tile(SCREEN, 7, &p));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//! `mirada-layout` — el motor de teselado del compositor Wayland.
|
||||
//!
|
||||
//! mirada es un compositor Wayland; este crate es su cerebro espacial,
|
||||
//! aislado de Wayland y de `smithay`. Decide *dónde* va cada ventana —
|
||||
//! un cálculo puro sobre rectángulos— para que el compositor sólo tenga
|
||||
//! que aplicar la geometría a las superficies reales.
|
||||
//!
|
||||
//! - [`geometry`] — el [`Rect`] y el reparto exacto de píxeles.
|
||||
//! - [`layout`] — los modos de teselado y la función [`tile`].
|
||||
//! - [`workspace`] — el [`Workspace`]: ventanas, foco y modo.
|
||||
//!
|
||||
//! Todo es determinista y testeable sin un servidor gráfico: la misma
|
||||
//! pantalla y las mismas ventanas dan siempre la misma distribución.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod geometry;
|
||||
pub mod layout;
|
||||
pub mod workspace;
|
||||
|
||||
pub use geometry::Rect;
|
||||
pub use layout::{tile, LayoutMode, LayoutParams};
|
||||
pub use workspace::{Workspace, WindowId};
|
||||
@@ -0,0 +1,240 @@
|
||||
//! `Workspace` — un conjunto de ventanas, su foco y su modo de teselado.
|
||||
|
||||
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, 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,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
/// Escritorio vacío con los parámetros dados.
|
||||
pub fn new(params: LayoutParams) -> Self {
|
||||
Self { windows: Vec::new(), focus: 0, params }
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
if i < self.focus {
|
||||
self.focus -= 1;
|
||||
}
|
||||
if self.focus >= self.windows.len() {
|
||||
self.focus = self.windows.len().saturating_sub(1);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resuelve la geometría: el rectángulo de cada ventana dentro de
|
||||
/// `screen`, en orden de teselado.
|
||||
pub fn layout(&self, screen: Rect) -> Vec<(WindowId, Rect)> {
|
||||
let rects = tile(screen, self.windows.len(), &self.params);
|
||||
self.windows.iter().copied().zip(rects).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[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 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());
|
||||
}
|
||||
}
|
||||
@@ -189,3 +189,18 @@ Gdk-Message: 05:42:34.451: Error reading events from display: Broken pipe
|
||||
[Child 27081, MediaDecoderStateMachine #2] WARNING: 72495d46ca60 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279
|
||||
[Child 27081, MediaDecoderStateMachine #2] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630
|
||||
[Child 27081, MediaDecoderStateMachine #2] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168
|
||||
[Child 27081, MediaDecoderStateMachine #9] WARNING: 72496b642700 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279
|
||||
[Child 27081, MediaDecoderStateMachine #9] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630
|
||||
[Child 27081, MediaDecoderStateMachine #9] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168
|
||||
[Child 27081, MediaDecoderStateMachine #9] WARNING: 72495d49e4c0 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279
|
||||
[Child 27081, MediaDecoderStateMachine #9] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630
|
||||
[Child 27081, MediaDecoderStateMachine #9] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168
|
||||
[Child 27081, MediaDecoderStateMachine #12] WARNING: 72495eb5d3a0 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279
|
||||
[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630
|
||||
[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168
|
||||
[Child 27081, MediaDecoderStateMachine #12] WARNING: 72496b6425e0 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279
|
||||
[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630
|
||||
[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168
|
||||
[Child 27081, MediaDecoderStateMachine #12] WARNING: 72495d46cdc0 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279
|
||||
[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630
|
||||
[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168
|
||||
|
||||
Reference in New Issue
Block a user