ee27108f6c
Fase 2 del plan «shell»: carmen reconoce la ventana del shell y le
reserva su sitio, en vez de teselarla como una más.
Una ventana cuyo `app_id` es `carmen.shell` no entra en el teselado:
carmen le reserva una franja de 40 px al pie de la salida, la dimensiona
y la fija ahí, y la compone sobre todas las demás. El Cerebro tesela el
resto de ventanas en el área que queda.
- `mirada-protocol`: nuevo `BodyEvent::OutputResized { id, w, h }` — el
Cerebro cambia el área útil de una salida **sin** perder el escritorio
que muestra (a diferencia de quitar y volver a añadir la salida — que,
de paso, era un bug latente al redimensionar la ventana winit).
- `mirada-brain`: `Desktop` atiende `OutputResized` (test nuevo).
- `mirada-body`: `BodyState::resize_output`.
- `mirada-compositor`: `ManagedWindow.is_shell`, `App.output_size`,
`dock_shell`/`output_changed`; `register_toplevel` no registra el
shell en el Cerebro; al cerrarse libera la franja. El shell se compone
y se enfoca con el ratón aunque no viva en el Cerebro; no lleva marco.
El backend winit usa ahora `resize_output` al redimensionar.
GPUI no habla `wlr-layer-shell`, así que el acople es por `app_id`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1119 lines
42 KiB
Rust
1119 lines
42 KiB
Rust
//! El [`Desktop`] — el estado del escritorio y el bucle `evento → comandos`.
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use mirada_layout::{LayoutParams, Rect, WindowId, Workspace};
|
||
use mirada_protocol::{placements, BodyEvent, BrainCommand, OutputId};
|
||
|
||
use crate::action::{DesktopAction, WORKSPACE_COUNT};
|
||
use crate::keymap::Keymap;
|
||
use crate::rules::Rules;
|
||
|
||
/// Lo que el Cerebro sabe de una ventana: su identidad de aplicación.
|
||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||
pub struct WindowInfo {
|
||
pub app_id: String,
|
||
pub title: String,
|
||
}
|
||
|
||
/// Una salida física y el escritorio virtual que muestra ahora mismo.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub struct Output {
|
||
pub id: OutputId,
|
||
/// Rectángulo en el espacio global — las salidas van en fila horizontal.
|
||
pub rect: Rect,
|
||
/// Índice del escritorio que esta salida muestra.
|
||
pub workspace: usize,
|
||
}
|
||
|
||
/// El estado completo del escritorio.
|
||
///
|
||
/// Mantiene las salidas físicas, [`WORKSPACE_COUNT`] escritorios
|
||
/// virtuales, el registro de ventanas, el keymap y las reglas. El único
|
||
/// punto de entrada es [`Desktop::on_event`]: traga un [`BodyEvent`],
|
||
/// muta el estado y devuelve los [`BrainCommand`]s a enviar al Cuerpo.
|
||
///
|
||
/// **Multi-monitor**: cada salida muestra un escritorio distinto; el
|
||
/// teselado se calcula para todas y el `Place` resultante las cubre. Un
|
||
/// escritorio se ve en una salida como mucho — pedir uno que ya muestra
|
||
/// otra salida las intercambia.
|
||
pub struct Desktop {
|
||
/// Salidas físicas, en fila horizontal y en orden de aparición.
|
||
outputs: Vec<Output>,
|
||
/// Escritorios virtuales — `WORKSPACE_COUNT` fijos.
|
||
workspaces: Vec<Workspace>,
|
||
/// Índice (en `outputs`) de la salida con el foco.
|
||
focused_output: usize,
|
||
/// Identidad de cada ventana conocida.
|
||
windows: HashMap<WindowId, WindowInfo>,
|
||
/// Atajos globales → acción. Configurable, recargable en caliente.
|
||
keymap: Keymap,
|
||
/// Reglas de ventana — escritorio/flotante por `app_id`/título.
|
||
rules: Rules,
|
||
/// Ventanas del scratchpad: se invocan flotando y se ocultan a
|
||
/// voluntad; mientras están guardadas no viven en ningún escritorio.
|
||
scratchpad: Vec<WindowId>,
|
||
}
|
||
|
||
impl Default for Desktop {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
impl Desktop {
|
||
/// Escritorio recién arrancado: sin salidas ni ventanas, con los
|
||
/// escritorios virtuales vacíos y el mapa de teclas por defecto.
|
||
pub fn new() -> Self {
|
||
Self::with_keymap(Keymap::default())
|
||
}
|
||
|
||
/// Como [`Desktop::new`], pero con un keymap dado — el que la app
|
||
/// cargó del archivo de configuración del usuario.
|
||
pub fn with_keymap(keymap: Keymap) -> Self {
|
||
let workspaces = (0..WORKSPACE_COUNT)
|
||
.map(|_| Workspace::new(LayoutParams::default()))
|
||
.collect();
|
||
Self {
|
||
outputs: Vec::new(),
|
||
workspaces,
|
||
focused_output: 0,
|
||
windows: HashMap::new(),
|
||
keymap,
|
||
rules: Rules::default(),
|
||
scratchpad: Vec::new(),
|
||
}
|
||
}
|
||
|
||
/// Reemplaza las reglas de ventana. Se aplican a las ventanas que se
|
||
/// abran a partir de ahora; las ya abiertas no se tocan.
|
||
pub fn set_rules(&mut self, rules: Rules) {
|
||
self.rules = rules;
|
||
}
|
||
|
||
/// El comando que registra los atajos globales en el Cuerpo. La app
|
||
/// lo envía al conectar, y de nuevo tras cada recarga del keymap.
|
||
pub fn grab_keys(&self) -> BrainCommand {
|
||
BrainCommand::GrabKeys(self.keymap.grab_list())
|
||
}
|
||
|
||
/// Reemplaza el keymap en caliente. Devuelve el [`BrainCommand`] que
|
||
/// el dueño debe enviar al Cuerpo para reajustar qué teclas intercepta.
|
||
pub fn set_keymap(&mut self, keymap: Keymap) -> BrainCommand {
|
||
self.keymap = keymap;
|
||
self.grab_keys()
|
||
}
|
||
|
||
/// El keymap vigente — para un HUD o un editor visual de atajos.
|
||
pub fn keymap(&self) -> &Keymap {
|
||
&self.keymap
|
||
}
|
||
|
||
/// Geometría de la salida enfocada, si hay alguna conectada.
|
||
pub fn screen(&self) -> Option<Rect> {
|
||
self.outputs.get(self.focused_output).map(|o| o.rect)
|
||
}
|
||
|
||
/// Procesa un evento del Cuerpo: muta el estado y devuelve los
|
||
/// comandos a enviar de vuelta.
|
||
pub fn on_event(&mut self, event: BodyEvent) -> Vec<BrainCommand> {
|
||
match event {
|
||
BodyEvent::OutputAdded { id, width, height } => {
|
||
// La salida nueva muestra el primer escritorio que no
|
||
// muestre ya otra salida.
|
||
let taken: Vec<usize> = self.outputs.iter().map(|o| o.workspace).collect();
|
||
let workspace = (0..self.workspaces.len())
|
||
.find(|n| !taken.contains(n))
|
||
.unwrap_or(0);
|
||
self.outputs.push(Output {
|
||
id,
|
||
rect: Rect::new(0, 0, width, height),
|
||
workspace,
|
||
});
|
||
self.reflow_outputs();
|
||
self.relayout()
|
||
}
|
||
BodyEvent::OutputRemoved { id } => {
|
||
self.outputs.retain(|o| o.id != id);
|
||
if self.focused_output >= self.outputs.len() {
|
||
self.focused_output = self.outputs.len().saturating_sub(1);
|
||
}
|
||
self.reflow_outputs();
|
||
self.relayout()
|
||
}
|
||
BodyEvent::OutputResized { id, width, height } => {
|
||
// Sólo cambia el área útil; el escritorio que muestra la
|
||
// salida se conserva.
|
||
if let Some(o) = self.outputs.iter_mut().find(|o| o.id == id) {
|
||
o.rect.w = width;
|
||
o.rect.h = height;
|
||
self.reflow_outputs();
|
||
self.relayout()
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
BodyEvent::WindowOpened { id, app_id, title } => {
|
||
// Las reglas pueden mandarla a otro escritorio o hacerla flotar.
|
||
let outcome = self.rules.resolve(&app_id, &title);
|
||
self.windows.insert(id, WindowInfo { app_id, title });
|
||
let ws = outcome
|
||
.workspace
|
||
.filter(|&n| n < self.workspaces.len())
|
||
.unwrap_or(self.active_index());
|
||
self.workspaces[ws].add(id);
|
||
if outcome.floating {
|
||
let rect = self
|
||
.screen()
|
||
.map(centered_float_rect)
|
||
.unwrap_or_else(|| Rect::new(100, 100, 800, 600));
|
||
self.workspaces[ws].set_floating(id, Some(rect));
|
||
}
|
||
self.relayout()
|
||
}
|
||
BodyEvent::WindowClosed { id } => {
|
||
self.windows.remove(&id);
|
||
self.scratchpad.retain(|&w| w != id);
|
||
for ws in &mut self.workspaces {
|
||
ws.remove(id);
|
||
}
|
||
self.relayout()
|
||
}
|
||
BodyEvent::WindowRetitled { id, title } => {
|
||
if let Some(info) = self.windows.get_mut(&id) {
|
||
info.title = title;
|
||
}
|
||
// Un cambio de título no altera la geometría.
|
||
Vec::new()
|
||
}
|
||
BodyEvent::PointerEntered { id } => {
|
||
// Foco al pasar el puntero, sólo si la ventana está en el
|
||
// escritorio activo.
|
||
let active = self.active_index();
|
||
if self.workspaces[active].focus_window(id) {
|
||
self.relayout()
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
BodyEvent::Keybind(key) => match self.keymap.lookup(&key) {
|
||
Some(action) => self.apply(action),
|
||
None => Vec::new(),
|
||
},
|
||
BodyEvent::FullscreenRequest { id, fullscreen } => {
|
||
// El cliente (un reproductor, un juego) pidió pantalla
|
||
// completa: la fijamos en el escritorio que tiene la ventana.
|
||
let mut changed = false;
|
||
for ws in &mut self.workspaces {
|
||
if ws.windows().contains(&id) {
|
||
if fullscreen {
|
||
ws.set_fullscreen(Some(id));
|
||
} else if ws.fullscreen() == Some(id) {
|
||
ws.set_fullscreen(None);
|
||
}
|
||
changed = true;
|
||
break;
|
||
}
|
||
}
|
||
if changed {
|
||
self.relayout()
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
BodyEvent::WindowFloatTo { id, rect } => {
|
||
// Arrastre interactivo: la ventana pasa a flotar en el
|
||
// rectángulo dado, en el escritorio donde viva.
|
||
let mut changed = false;
|
||
for ws in &mut self.workspaces {
|
||
if ws.windows().contains(&id) {
|
||
ws.set_floating(id, Some(rect));
|
||
changed = true;
|
||
break;
|
||
}
|
||
}
|
||
if changed {
|
||
self.relayout()
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Aplica una acción de escritorio directamente (sin pasar por una
|
||
/// tecla). Útil para disparar acciones desde un HUD.
|
||
pub fn apply(&mut self, action: DesktopAction) -> Vec<BrainCommand> {
|
||
let active = self.active_index();
|
||
match action {
|
||
DesktopAction::FocusNext => {
|
||
self.workspaces[active].focus_next();
|
||
self.relayout()
|
||
}
|
||
DesktopAction::FocusPrev => {
|
||
self.workspaces[active].focus_prev();
|
||
self.relayout()
|
||
}
|
||
DesktopAction::FocusWindow(id) => {
|
||
// En el escritorio activo basta enfocar; si la ventana
|
||
// está en otro, lo traemos a la salida enfocada.
|
||
if self.workspaces[active].focus_window(id) {
|
||
return self.relayout();
|
||
}
|
||
for n in 0..self.workspaces.len() {
|
||
if n != active && self.workspaces[n].focus_window(id) {
|
||
self.show_workspace(n);
|
||
return self.relayout();
|
||
}
|
||
}
|
||
Vec::new()
|
||
}
|
||
DesktopAction::MoveForward => {
|
||
self.workspaces[active].move_focused_forward();
|
||
self.relayout()
|
||
}
|
||
DesktopAction::MoveBackward => {
|
||
self.workspaces[active].move_focused_backward();
|
||
self.relayout()
|
||
}
|
||
DesktopAction::CloseFocused => {
|
||
// Pedimos el cierre; el estado se actualiza al recibir el
|
||
// `WindowClosed` de vuelta, no antes.
|
||
match self.workspaces[active].focused() {
|
||
Some(id) => vec![BrainCommand::Close(id)],
|
||
None => Vec::new(),
|
||
}
|
||
}
|
||
DesktopAction::ToggleFloat => {
|
||
let Some(id) = self.workspaces[active].focused() else {
|
||
return Vec::new();
|
||
};
|
||
let screen = self.screen();
|
||
let ws = &mut self.workspaces[active];
|
||
if ws.is_floating(id) {
|
||
ws.set_floating(id, None);
|
||
} else {
|
||
let rect = screen
|
||
.map(centered_float_rect)
|
||
.unwrap_or_else(|| Rect::new(100, 100, 800, 600));
|
||
ws.set_floating(id, Some(rect));
|
||
}
|
||
self.relayout()
|
||
}
|
||
DesktopAction::ToggleFullscreen => {
|
||
let Some(id) = self.workspaces[active].focused() else {
|
||
return Vec::new();
|
||
};
|
||
let ws = &mut self.workspaces[active];
|
||
if ws.fullscreen() == Some(id) {
|
||
ws.set_fullscreen(None);
|
||
} else {
|
||
ws.set_fullscreen(Some(id));
|
||
}
|
||
self.relayout()
|
||
}
|
||
DesktopAction::SendToScratchpad => {
|
||
let Some(id) = self.workspaces[active].focused() else {
|
||
return Vec::new();
|
||
};
|
||
for ws in &mut self.workspaces {
|
||
ws.remove(id);
|
||
}
|
||
if !self.scratchpad.contains(&id) {
|
||
self.scratchpad.push(id);
|
||
}
|
||
self.relayout()
|
||
}
|
||
DesktopAction::ToggleScratchpad => {
|
||
// ¿Hay alguna ventana del scratchpad en el escritorio activo?
|
||
let shown: Vec<WindowId> = self.workspaces[active]
|
||
.windows()
|
||
.iter()
|
||
.copied()
|
||
.filter(|id| self.scratchpad.contains(id))
|
||
.collect();
|
||
if !shown.is_empty() {
|
||
for id in shown {
|
||
self.workspaces[active].remove(id);
|
||
}
|
||
self.relayout()
|
||
} else if let Some(&id) = self.scratchpad.first() {
|
||
// La traemos de donde esté y la mostramos flotando.
|
||
for ws in &mut self.workspaces {
|
||
ws.remove(id);
|
||
}
|
||
let rect = self
|
||
.screen()
|
||
.map(centered_float_rect)
|
||
.unwrap_or_else(|| Rect::new(100, 100, 800, 600));
|
||
self.workspaces[active].add(id);
|
||
self.workspaces[active].set_floating(id, Some(rect));
|
||
self.relayout()
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
DesktopAction::CycleLayout => {
|
||
let next = self.workspaces[active].params().mode.next();
|
||
self.workspaces[active].set_mode(next);
|
||
self.relayout()
|
||
}
|
||
DesktopAction::SetLayout(mode) => {
|
||
self.workspaces[active].set_mode(mode);
|
||
self.relayout()
|
||
}
|
||
DesktopAction::GrowMaster => self.nudge_master(0.05),
|
||
DesktopAction::ShrinkMaster => self.nudge_master(-0.05),
|
||
DesktopAction::IncMaster => self.nudge_master_count(1),
|
||
DesktopAction::DecMaster => self.nudge_master_count(-1),
|
||
DesktopAction::PromoteToMaster => {
|
||
self.workspaces[active].promote_focused();
|
||
self.relayout()
|
||
}
|
||
DesktopAction::SwitchWorkspace(n) => {
|
||
if n < self.workspaces.len() && n != active {
|
||
self.show_workspace(n);
|
||
self.relayout()
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
DesktopAction::SendToWorkspace(n) => {
|
||
if n >= self.workspaces.len() || n == active {
|
||
return Vec::new();
|
||
}
|
||
match self.workspaces[active].focused() {
|
||
Some(id) => {
|
||
self.workspaces[active].remove(id);
|
||
self.workspaces[n].add(id);
|
||
self.relayout()
|
||
}
|
||
None => Vec::new(),
|
||
}
|
||
}
|
||
DesktopAction::FocusOutputNext => {
|
||
if self.outputs.len() > 1 {
|
||
self.focused_output = (self.focused_output + 1) % self.outputs.len();
|
||
self.relayout()
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
DesktopAction::Spawn(cmd) => vec![BrainCommand::Spawn(cmd)],
|
||
DesktopAction::Quit => vec![BrainCommand::Shutdown],
|
||
}
|
||
}
|
||
|
||
/// El índice del escritorio activo — el que muestra la salida
|
||
/// enfocada. `0` si todavía no hay ninguna salida.
|
||
pub fn active_index(&self) -> usize {
|
||
self.outputs
|
||
.get(self.focused_output)
|
||
.map(|o| o.workspace)
|
||
.unwrap_or(0)
|
||
}
|
||
|
||
/// Hace que la salida enfocada muestre el escritorio `n`. Si otra
|
||
/// salida ya lo mostraba, intercambian — así ningún escritorio se
|
||
/// ve en dos sitios a la vez.
|
||
fn show_workspace(&mut self, n: usize) {
|
||
if n >= self.workspaces.len() || self.focused_output >= self.outputs.len() {
|
||
return;
|
||
}
|
||
let current = self.outputs[self.focused_output].workspace;
|
||
if current == n {
|
||
return;
|
||
}
|
||
if let Some(other) = self.outputs.iter().position(|o| o.workspace == n) {
|
||
self.outputs[other].workspace = current;
|
||
}
|
||
self.outputs[self.focused_output].workspace = n;
|
||
}
|
||
|
||
/// Recoloca las salidas en fila horizontal, en su orden de aparición.
|
||
fn reflow_outputs(&mut self) {
|
||
let mut x = 0;
|
||
for o in &mut self.outputs {
|
||
o.rect.x = x;
|
||
o.rect.y = 0;
|
||
x += o.rect.w;
|
||
}
|
||
}
|
||
|
||
/// Ajusta la fracción del área maestra del escritorio activo (la usan
|
||
/// `MasterStack` y `CenteredMaster`), acotada a `0.05..=0.95`.
|
||
fn nudge_master(&mut self, delta: f32) -> Vec<BrainCommand> {
|
||
let active = self.active_index();
|
||
let ws = &mut self.workspaces[active];
|
||
let ratio = (ws.params().master_ratio + delta).clamp(0.05, 0.95);
|
||
ws.set_master_ratio(ratio);
|
||
self.relayout()
|
||
}
|
||
|
||
/// Ajusta `nmaster` del escritorio activo, acotado a `1..=9`.
|
||
fn nudge_master_count(&mut self, delta: i32) -> Vec<BrainCommand> {
|
||
let active = self.active_index();
|
||
let ws = &mut self.workspaces[active];
|
||
let n = (ws.params().master_count as i32 + delta).clamp(1, 9) as usize;
|
||
ws.set_master_count(n);
|
||
self.relayout()
|
||
}
|
||
|
||
/// Recalcula la geometría de **todas** las salidas y la empaqueta en
|
||
/// un único [`BrainCommand::Place`]. Sin salidas, no hay nada que
|
||
/// colocar.
|
||
fn relayout(&self) -> Vec<BrainCommand> {
|
||
if self.outputs.is_empty() {
|
||
return Vec::new();
|
||
}
|
||
let mut all = Vec::new();
|
||
for o in &self.outputs {
|
||
all.extend(placements(&self.workspaces[o.workspace], o.rect));
|
||
}
|
||
// El foco del teclado es único: sólo la ventana enfocada de la
|
||
// salida enfocada. `placements` marca el foco por escritorio (lo
|
||
// necesita para la visibilidad en `Monocle`); aquí lo unificamos.
|
||
let global_focus = self.focused_window();
|
||
for p in &mut all {
|
||
p.focused = Some(p.id) == global_focus;
|
||
}
|
||
vec![BrainCommand::Place(all)]
|
||
}
|
||
|
||
// --- Accesores de sólo lectura, para el HUD de la app GPUI ---------
|
||
|
||
/// El escritorio activo — el de la salida enfocada.
|
||
pub fn active_workspace(&self) -> &Workspace {
|
||
&self.workspaces[self.active_index()]
|
||
}
|
||
|
||
/// Las salidas conectadas, en orden, con el escritorio que muestran.
|
||
pub fn outputs(&self) -> &[Output] {
|
||
&self.outputs
|
||
}
|
||
|
||
/// Índice (en [`outputs`](Desktop::outputs)) de la salida enfocada.
|
||
pub fn focused_output(&self) -> usize {
|
||
self.focused_output
|
||
}
|
||
|
||
/// Identidad de una ventana conocida.
|
||
pub fn window_info(&self, id: WindowId) -> Option<&WindowInfo> {
|
||
self.windows.get(&id)
|
||
}
|
||
|
||
/// La ventana con el foco del teclado: la enfocada del escritorio
|
||
/// activo — o su ventana en pantalla completa, si la hay.
|
||
pub fn focused_window(&self) -> Option<WindowId> {
|
||
let ws = &self.workspaces[self.active_index()];
|
||
ws.fullscreen().or_else(|| ws.focused())
|
||
}
|
||
|
||
/// Cuántas ventanas hay en cada escritorio virtual.
|
||
pub fn workspace_loads(&self) -> Vec<usize> {
|
||
self.workspaces.iter().map(Workspace::len).collect()
|
||
}
|
||
|
||
/// Una vista de todas las ventanas conocidas, en todos los
|
||
/// escritorios — la base de `mirada-ctl windows` y de una taskbar.
|
||
pub fn window_lines(&self) -> Vec<crate::ctl::WindowLine> {
|
||
let active = self.active_index();
|
||
let mut lines = Vec::new();
|
||
for (n, ws) in self.workspaces.iter().enumerate() {
|
||
let ws_focus = ws.focused();
|
||
for &id in ws.windows() {
|
||
let info = self.windows.get(&id);
|
||
lines.push(crate::ctl::WindowLine {
|
||
id,
|
||
app_id: info.map(|i| i.app_id.clone()).unwrap_or_default(),
|
||
title: info.map(|i| i.title.clone()).unwrap_or_default(),
|
||
workspace: n + 1,
|
||
focused: n == active && ws_focus == Some(id),
|
||
});
|
||
}
|
||
}
|
||
// Ventanas guardadas en el scratchpad — en ningún escritorio.
|
||
for &id in &self.scratchpad {
|
||
let stashed = !self.workspaces.iter().any(|ws| ws.windows().contains(&id));
|
||
if stashed {
|
||
let info = self.windows.get(&id);
|
||
lines.push(crate::ctl::WindowLine {
|
||
id,
|
||
app_id: info.map(|i| i.app_id.clone()).unwrap_or_default(),
|
||
title: info.map(|i| i.title.clone()).unwrap_or_default(),
|
||
workspace: 0, // 0 = guardada en el scratchpad
|
||
focused: false,
|
||
});
|
||
}
|
||
}
|
||
lines
|
||
}
|
||
}
|
||
|
||
/// El rectángulo flotante por defecto: 60 % de la pantalla, centrado.
|
||
fn centered_float_rect(screen: Rect) -> Rect {
|
||
let w = screen.w * 3 / 5;
|
||
let h = screen.h * 3 / 5;
|
||
Rect::new(
|
||
screen.x + (screen.w - w) / 2,
|
||
screen.y + (screen.h - h) / 2,
|
||
w,
|
||
h,
|
||
)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use mirada_layout::LayoutMode;
|
||
|
||
/// Un escritorio con una salida 1920×1080 ya conectada.
|
||
fn desktop_with_screen() -> Desktop {
|
||
let mut d = Desktop::new();
|
||
d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
|
||
d
|
||
}
|
||
|
||
fn open(d: &mut Desktop, id: WindowId) -> Vec<BrainCommand> {
|
||
d.on_event(BodyEvent::WindowOpened {
|
||
id,
|
||
app_id: format!("app{id}"),
|
||
title: format!("win {id}"),
|
||
})
|
||
}
|
||
|
||
/// Extrae las colocaciones de un único `Place`.
|
||
fn places(cmds: &[BrainCommand]) -> &[mirada_protocol::WindowPlacement] {
|
||
match cmds {
|
||
[BrainCommand::Place(p)] => p,
|
||
other => panic!("se esperaba un solo Place, no {other:?}"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn grab_keys_lists_the_whole_keymap() {
|
||
let d = Desktop::new();
|
||
match d.grab_keys() {
|
||
BrainCommand::GrabKeys(keys) => {
|
||
assert!(keys.contains(&"Super+j".to_string()));
|
||
assert!(keys.contains(&"Super+Shift+e".to_string()));
|
||
}
|
||
other => panic!("se esperaba GrabKeys, no {other:?}"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn set_keymap_swaps_the_bindings_and_regrabs() {
|
||
let mut d = desktop_with_screen();
|
||
for id in [1, 2, 3] {
|
||
open(&mut d, id);
|
||
}
|
||
// El keymap por defecto no usa Alt.
|
||
assert!(d.on_event(BodyEvent::Keybind("Alt+x".into())).is_empty());
|
||
// Cargamos un keymap a medida; el comando devuelto re-registra grabs.
|
||
let custom = crate::Keymap::from_ron(r#"( bindings: { "Alt+x": "focus-prev" } )"#).unwrap();
|
||
match d.set_keymap(custom) {
|
||
BrainCommand::GrabKeys(keys) => assert_eq!(keys, vec!["Alt+x".to_string()]),
|
||
other => panic!("se esperaba GrabKeys, no {other:?}"),
|
||
}
|
||
// Ahora «Alt+x» sí mueve el foco, y «Super+j» ya no.
|
||
assert_eq!(d.focused_window(), Some(3));
|
||
d.on_event(BodyEvent::Keybind("Alt+x".into()));
|
||
assert_eq!(d.focused_window(), Some(2));
|
||
assert!(d.on_event(BodyEvent::Keybind("Super+j".into())).is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn focus_window_addresses_a_specific_window() {
|
||
let mut d = desktop_with_screen();
|
||
for id in [1, 2, 3] {
|
||
open(&mut d, id);
|
||
}
|
||
assert_eq!(d.focused_window(), Some(3));
|
||
d.apply(DesktopAction::FocusWindow(1));
|
||
assert_eq!(d.focused_window(), Some(1));
|
||
}
|
||
|
||
#[test]
|
||
fn focus_window_jumps_to_the_workspace_that_holds_it() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2); // enfocada
|
||
// Manda la 2 al escritorio 3; seguimos en el 1.
|
||
d.on_event(BodyEvent::Keybind("Super+Shift+3".into()));
|
||
assert_eq!(d.active_index(), 0);
|
||
// Enfocar la 2 nos lleva a su escritorio.
|
||
d.apply(DesktopAction::FocusWindow(2));
|
||
assert_eq!(d.active_index(), 2);
|
||
assert_eq!(d.focused_window(), Some(2));
|
||
}
|
||
|
||
#[test]
|
||
fn window_lines_cover_every_window_with_its_workspace() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2);
|
||
d.on_event(BodyEvent::Keybind("Super+Shift+3".into())); // la 2 al esc. 3
|
||
let lines = d.window_lines();
|
||
assert_eq!(lines.len(), 2);
|
||
let w1 = lines.iter().find(|l| l.id == 1).unwrap();
|
||
let w2 = lines.iter().find(|l| l.id == 2).unwrap();
|
||
assert_eq!(w1.workspace, 1);
|
||
assert_eq!(w2.workspace, 3);
|
||
// La 1 quedó enfocada en el escritorio activo (el 1).
|
||
assert!(w1.focused);
|
||
assert!(!w2.focused);
|
||
}
|
||
|
||
#[test]
|
||
fn toggle_float_marks_the_focused_window_and_floats_it_last() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2); // enfocada
|
||
let cmds = d.apply(DesktopAction::ToggleFloat);
|
||
let p = places(&cmds);
|
||
assert!(p.iter().find(|x| x.id == 2).unwrap().floating);
|
||
// La flotante va al final de la lista — orden de pintado.
|
||
assert_eq!(p.last().unwrap().id, 2);
|
||
// Alternar de nuevo la devuelve al teselado.
|
||
let cmds = d.apply(DesktopAction::ToggleFloat);
|
||
assert!(!places(&cmds).iter().find(|x| x.id == 2).unwrap().floating);
|
||
}
|
||
|
||
#[test]
|
||
fn toggle_fullscreen_covers_the_screen_and_hides_the_rest() {
|
||
let mut d = desktop_with_screen();
|
||
for id in [1, 2, 3] {
|
||
open(&mut d, id);
|
||
}
|
||
let cmds = d.apply(DesktopAction::ToggleFullscreen); // sobre la 3
|
||
let p = places(&cmds);
|
||
let fs = p.iter().find(|x| x.id == 3).unwrap();
|
||
assert!(fs.fullscreen && fs.visible);
|
||
assert_eq!(fs.rect, d.screen().unwrap());
|
||
assert!(p.iter().filter(|x| x.id != 3).all(|x| !x.visible));
|
||
// Alternar de nuevo restaura el teselado: las tres visibles.
|
||
let cmds = d.apply(DesktopAction::ToggleFullscreen);
|
||
assert_eq!(places(&cmds).iter().filter(|x| x.visible).count(), 3);
|
||
}
|
||
|
||
#[test]
|
||
fn a_rule_sends_a_new_window_to_its_workspace() {
|
||
let mut d = desktop_with_screen();
|
||
d.set_rules(Rules::from_ron(r#"( rules: [ (app_id: "app2", workspace: 3) ] )"#).unwrap());
|
||
open(&mut d, 1); // app1 → sin regla → escritorio activo (1)
|
||
open(&mut d, 2); // app2 → regla → escritorio 3
|
||
assert_eq!(d.workspace_loads()[0], 1);
|
||
assert_eq!(d.workspace_loads()[2], 1);
|
||
}
|
||
|
||
#[test]
|
||
fn a_rule_can_open_a_window_floating() {
|
||
let mut d = desktop_with_screen();
|
||
d.set_rules(Rules::from_ron(r#"( rules: [ (app_id: "app1", floating: true) ] )"#).unwrap());
|
||
let cmds = open(&mut d, 1);
|
||
assert!(places(&cmds).iter().find(|p| p.id == 1).unwrap().floating);
|
||
}
|
||
|
||
#[test]
|
||
fn without_a_screen_nothing_is_placed() {
|
||
let mut d = Desktop::new();
|
||
assert!(open(&mut d, 1).is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn opening_a_window_places_it() {
|
||
let mut d = desktop_with_screen();
|
||
let cmds = open(&mut d, 1);
|
||
assert_eq!(places(&cmds).len(), 1);
|
||
assert_eq!(d.focused_window(), Some(1));
|
||
}
|
||
|
||
#[test]
|
||
fn closing_a_window_removes_it_everywhere() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2);
|
||
let cmds = d.on_event(BodyEvent::WindowClosed { id: 1 });
|
||
assert_eq!(places(&cmds).len(), 1);
|
||
assert!(d.window_info(1).is_none());
|
||
assert_eq!(d.focused_window(), Some(2));
|
||
}
|
||
|
||
#[test]
|
||
fn focus_keybind_cycles_within_the_active_workspace() {
|
||
let mut d = desktop_with_screen();
|
||
for id in [1, 2, 3] {
|
||
open(&mut d, id);
|
||
}
|
||
assert_eq!(d.focused_window(), Some(3));
|
||
d.on_event(BodyEvent::Keybind("Super+j".into())); // next, da la vuelta
|
||
assert_eq!(d.focused_window(), Some(1));
|
||
d.on_event(BodyEvent::Keybind("Super+k".into())); // prev
|
||
assert_eq!(d.focused_window(), Some(3));
|
||
}
|
||
|
||
#[test]
|
||
fn close_focused_keybind_asks_to_close_the_focused_window() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 7);
|
||
let cmds = d.on_event(BodyEvent::Keybind("Super+q".into()));
|
||
assert_eq!(cmds, vec![BrainCommand::Close(7)]);
|
||
// No se elimina hasta que el Cuerpo confirme con WindowClosed.
|
||
assert!(d.window_info(7).is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn cycle_layout_walks_every_mode_and_returns() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
let start = d.active_workspace().params().mode;
|
||
for _ in 0..LayoutMode::ALL.len() {
|
||
let before = d.active_workspace().params().mode;
|
||
d.on_event(BodyEvent::Keybind("Super+space".into()));
|
||
assert_eq!(d.active_workspace().params().mode, before.next());
|
||
}
|
||
// Una vuelta completa devuelve al modo inicial.
|
||
assert_eq!(d.active_workspace().params().mode, start);
|
||
}
|
||
|
||
#[test]
|
||
fn grow_and_shrink_master_adjust_the_ratio() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
let r0 = d.active_workspace().params().master_ratio;
|
||
d.apply(DesktopAction::GrowMaster);
|
||
assert!(d.active_workspace().params().master_ratio > r0);
|
||
d.apply(DesktopAction::ShrinkMaster);
|
||
assert!((d.active_workspace().params().master_ratio - r0).abs() < 1e-6);
|
||
}
|
||
|
||
#[test]
|
||
fn inc_and_dec_master_adjust_nmaster() {
|
||
let mut d = desktop_with_screen();
|
||
for id in [1, 2, 3] {
|
||
open(&mut d, id);
|
||
}
|
||
assert_eq!(d.active_workspace().params().master_count, 1);
|
||
d.apply(DesktopAction::IncMaster);
|
||
assert_eq!(d.active_workspace().params().master_count, 2);
|
||
d.apply(DesktopAction::DecMaster);
|
||
d.apply(DesktopAction::DecMaster); // no baja de 1
|
||
assert_eq!(d.active_workspace().params().master_count, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn promote_to_master_brings_the_focused_window_to_the_front() {
|
||
let mut d = desktop_with_screen();
|
||
for id in [1, 2, 3] {
|
||
open(&mut d, id);
|
||
}
|
||
d.apply(DesktopAction::FocusWindow(3));
|
||
d.apply(DesktopAction::PromoteToMaster);
|
||
assert_eq!(d.active_workspace().windows()[0], 3);
|
||
assert_eq!(d.focused_window(), Some(3));
|
||
}
|
||
|
||
#[test]
|
||
fn master_ratio_stays_within_bounds() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
for _ in 0..50 {
|
||
d.apply(DesktopAction::GrowMaster);
|
||
}
|
||
assert!(d.active_workspace().params().master_ratio <= 0.95);
|
||
for _ in 0..50 {
|
||
d.apply(DesktopAction::ShrinkMaster);
|
||
}
|
||
assert!(d.active_workspace().params().master_ratio >= 0.05);
|
||
}
|
||
|
||
#[test]
|
||
fn monocle_keybind_hides_all_but_the_focused_window() {
|
||
let mut d = desktop_with_screen();
|
||
for id in [1, 2, 3] {
|
||
open(&mut d, id);
|
||
}
|
||
let cmds = d.on_event(BodyEvent::Keybind("Super+m".into()));
|
||
let visible = places(&cmds).iter().filter(|p| p.visible).count();
|
||
assert_eq!(visible, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn switching_workspace_changes_what_is_placed() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2);
|
||
// Escritorio 2 (índice 1) está vacío.
|
||
let cmds = d.on_event(BodyEvent::Keybind("Super+2".into()));
|
||
assert!(places(&cmds).is_empty());
|
||
assert_eq!(d.active_index(), 1);
|
||
// Volver al 1 reaparece las dos ventanas.
|
||
let cmds = d.on_event(BodyEvent::Keybind("Super+1".into()));
|
||
assert_eq!(places(&cmds).len(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn send_to_workspace_moves_the_focused_window() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2); // enfocada
|
||
d.on_event(BodyEvent::Keybind("Super+Shift+3".into()));
|
||
assert_eq!(d.workspace_loads()[0], 1); // sólo queda la 1
|
||
assert_eq!(d.workspace_loads()[2], 1); // la 2 viajó al escritorio 3
|
||
// La ventana 2 sigue registrada — sólo cambió de escritorio.
|
||
assert!(d.window_info(2).is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn pointer_focuses_a_window_in_the_active_workspace() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2); // enfocada
|
||
d.on_event(BodyEvent::PointerEntered { id: 1 });
|
||
assert_eq!(d.focused_window(), Some(1));
|
||
}
|
||
|
||
#[test]
|
||
fn dragging_floats_a_window_at_the_given_rect() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2);
|
||
assert!(!d.active_workspace().is_floating(2));
|
||
let target = Rect::new(300, 200, 640, 480);
|
||
let cmds = d.on_event(BodyEvent::WindowFloatTo { id: 2, rect: target });
|
||
// La 2 ahora flota exactamente en el rectángulo pedido.
|
||
assert!(d.active_workspace().is_floating(2));
|
||
let p = places(&cmds).iter().find(|p| p.id == 2).unwrap();
|
||
assert!(p.floating);
|
||
assert_eq!(p.rect, target);
|
||
}
|
||
|
||
#[test]
|
||
fn resizing_an_output_retiles_without_losing_the_workspace() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
d.on_event(BodyEvent::Keybind("Super+2".into())); // escritorio activo → 2
|
||
assert_eq!(d.active_index(), 1);
|
||
let cmds = d.on_event(BodyEvent::OutputResized {
|
||
id: 0,
|
||
width: 1920,
|
||
height: 1040,
|
||
});
|
||
// A diferencia de quitar y volver a añadir la salida, el
|
||
// escritorio activo se conserva.
|
||
assert_eq!(d.active_index(), 1);
|
||
assert!(matches!(cmds.as_slice(), [BrainCommand::Place(_)]));
|
||
}
|
||
|
||
#[test]
|
||
fn a_spawn_keybind_becomes_a_spawn_command() {
|
||
let mut d = desktop_with_screen();
|
||
let cmds = d.on_event(BodyEvent::Keybind("Super+Shift+Return".into()));
|
||
assert_eq!(cmds, vec![BrainCommand::Spawn("foot".into())]);
|
||
}
|
||
|
||
#[test]
|
||
fn dragging_an_unknown_window_does_nothing() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
let cmds = d.on_event(BodyEvent::WindowFloatTo {
|
||
id: 99,
|
||
rect: Rect::new(0, 0, 100, 100),
|
||
});
|
||
assert!(cmds.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn retitling_updates_the_registry_without_relayout() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
let cmds = d.on_event(BodyEvent::WindowRetitled {
|
||
id: 1,
|
||
title: "nuevo".into(),
|
||
});
|
||
assert!(cmds.is_empty());
|
||
assert_eq!(d.window_info(1).unwrap().title, "nuevo");
|
||
}
|
||
|
||
#[test]
|
||
fn an_unknown_keybind_does_nothing() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
assert!(d.on_event(BodyEvent::Keybind("Super+F12".into())).is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn quit_emits_a_shutdown() {
|
||
let mut d = desktop_with_screen();
|
||
assert_eq!(
|
||
d.on_event(BodyEvent::Keybind("Super+Shift+e".into())),
|
||
vec![BrainCommand::Shutdown]
|
||
);
|
||
}
|
||
|
||
// --- Multi-monitor -------------------------------------------------
|
||
|
||
/// Un escritorio con dos salidas 1920×1080.
|
||
fn desktop_with_two_outputs() -> Desktop {
|
||
let mut d = Desktop::new();
|
||
d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
|
||
d.on_event(BodyEvent::OutputAdded { id: 1, width: 1920, height: 1080 });
|
||
d
|
||
}
|
||
|
||
#[test]
|
||
fn outputs_lay_side_by_side() {
|
||
let mut d = Desktop::new();
|
||
d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
|
||
d.on_event(BodyEvent::OutputAdded { id: 1, width: 2560, height: 1440 });
|
||
assert_eq!(d.outputs().len(), 2);
|
||
// La segunda salida arranca donde acaba la primera.
|
||
assert_eq!(d.outputs()[1].rect.x, 1920);
|
||
}
|
||
|
||
#[test]
|
||
fn each_output_shows_a_distinct_workspace() {
|
||
let d = desktop_with_two_outputs();
|
||
assert_eq!(d.outputs()[0].workspace, 0);
|
||
assert_eq!(d.outputs()[1].workspace, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn switching_to_a_workspace_shown_on_another_output_swaps_them() {
|
||
let mut d = desktop_with_two_outputs();
|
||
// La salida enfocada (0, ws 0) pide el ws 1, que muestra la 1 → swap.
|
||
d.apply(DesktopAction::SwitchWorkspace(1));
|
||
assert_eq!(d.outputs()[0].workspace, 1);
|
||
assert_eq!(d.outputs()[1].workspace, 0);
|
||
}
|
||
|
||
#[test]
|
||
fn focus_output_next_moves_the_focus_between_outputs() {
|
||
let mut d = desktop_with_two_outputs();
|
||
assert_eq!(d.active_index(), 0); // salida 0 → ws 0
|
||
d.apply(DesktopAction::FocusOutputNext);
|
||
assert_eq!(d.active_index(), 1); // salida 1 → ws 1
|
||
d.apply(DesktopAction::FocusOutputNext); // envuelve
|
||
assert_eq!(d.active_index(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn relayout_places_windows_on_every_output() {
|
||
let mut d = Desktop::new();
|
||
d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
|
||
d.on_event(BodyEvent::OutputAdded { id: 1, width: 1280, height: 720 });
|
||
open(&mut d, 1); // en la salida 0 (ws 0)
|
||
d.apply(DesktopAction::FocusOutputNext);
|
||
let cmds = open(&mut d, 2); // en la salida 1 (ws 1)
|
||
let p = places(&cmds);
|
||
assert_eq!(p.len(), 2);
|
||
// Cada ventana cae en el rectángulo de su salida.
|
||
assert_eq!(p.iter().find(|x| x.id == 1).unwrap().rect.x, 0);
|
||
assert_eq!(p.iter().find(|x| x.id == 2).unwrap().rect.x, 1920);
|
||
}
|
||
|
||
#[test]
|
||
fn keyboard_focus_is_unique_across_outputs() {
|
||
let mut d = desktop_with_two_outputs();
|
||
open(&mut d, 1);
|
||
d.apply(DesktopAction::FocusOutputNext);
|
||
let cmds = open(&mut d, 2);
|
||
// Sólo una ventana con foco de teclado en todo el Place.
|
||
assert_eq!(places(&cmds).iter().filter(|p| p.focused).count(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn removing_an_output_keeps_its_windows_in_their_workspace() {
|
||
let mut d = desktop_with_two_outputs();
|
||
d.apply(DesktopAction::FocusOutputNext); // foco en la salida 1 (ws 1)
|
||
open(&mut d, 1); // en ws 1
|
||
d.on_event(BodyEvent::OutputRemoved { id: 1 });
|
||
// La ventana sigue registrada, en el ws 1.
|
||
assert!(d.window_info(1).is_some());
|
||
assert_eq!(d.workspace_loads()[1], 1);
|
||
assert_eq!(d.outputs().len(), 1);
|
||
}
|
||
|
||
// --- Scratchpad ----------------------------------------------------
|
||
|
||
#[test]
|
||
fn send_to_scratchpad_hides_the_focused_window() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2); // enfocada
|
||
d.apply(DesktopAction::SendToScratchpad);
|
||
assert_eq!(d.workspace_loads()[0], 1); // sólo queda la 1
|
||
assert!(d.window_info(2).is_some()); // sigue registrada
|
||
}
|
||
|
||
#[test]
|
||
fn toggle_scratchpad_shows_then_hides_the_stashed_window() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2);
|
||
d.apply(DesktopAction::SendToScratchpad); // guarda la 2
|
||
assert_eq!(d.workspace_loads()[0], 1);
|
||
// Toggle la invoca, flotando.
|
||
let cmds = d.apply(DesktopAction::ToggleScratchpad);
|
||
assert!(places(&cmds).iter().find(|x| x.id == 2).unwrap().floating);
|
||
assert_eq!(d.workspace_loads()[0], 2);
|
||
// Toggle de nuevo la oculta.
|
||
d.apply(DesktopAction::ToggleScratchpad);
|
||
assert_eq!(d.workspace_loads()[0], 1);
|
||
}
|
||
|
||
#[test]
|
||
fn a_scratchpad_window_follows_you_across_workspaces() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
d.apply(DesktopAction::SendToScratchpad);
|
||
d.apply(DesktopAction::ToggleScratchpad); // mostrada en el escritorio 1
|
||
assert_eq!(d.workspace_loads()[0], 1);
|
||
d.apply(DesktopAction::SwitchWorkspace(1)); // al escritorio 2
|
||
d.apply(DesktopAction::ToggleScratchpad); // estaba en el 1 → la trae al 2
|
||
assert_eq!(d.workspace_loads()[1], 1);
|
||
assert_eq!(d.workspace_loads()[0], 0);
|
||
}
|
||
|
||
#[test]
|
||
fn closing_a_stashed_window_drops_it_from_the_scratchpad() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
d.apply(DesktopAction::SendToScratchpad);
|
||
d.on_event(BodyEvent::WindowClosed { id: 1 });
|
||
// Ya no hay nada que invocar.
|
||
assert!(d.apply(DesktopAction::ToggleScratchpad).is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn a_client_fullscreen_request_is_honoured() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
open(&mut d, 2);
|
||
let cmds = d.on_event(BodyEvent::FullscreenRequest { id: 1, fullscreen: true });
|
||
assert!(places(&cmds).iter().find(|x| x.id == 1).unwrap().fullscreen);
|
||
// El cliente la suelta.
|
||
let cmds = d.on_event(BodyEvent::FullscreenRequest { id: 1, fullscreen: false });
|
||
assert!(!places(&cmds).iter().find(|x| x.id == 1).unwrap().fullscreen);
|
||
}
|
||
|
||
#[test]
|
||
fn a_fullscreen_request_for_an_unknown_window_does_nothing() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
assert!(d
|
||
.on_event(BodyEvent::FullscreenRequest { id: 99, fullscreen: true })
|
||
.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn window_lines_show_a_stashed_window_as_workspace_zero() {
|
||
let mut d = desktop_with_screen();
|
||
open(&mut d, 1);
|
||
d.apply(DesktopAction::SendToScratchpad);
|
||
let line = d.window_lines().into_iter().find(|l| l.id == 1).unwrap();
|
||
assert_eq!(line.workspace, 0);
|
||
}
|
||
}
|