use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::types::state::Frame;
pub type WinID = i32;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LayoutOp {
Focus(WinID),
Swap(WinID, WinID),
MoveToWorkspace {
window: WinID,
workspace: u32,
follow: bool,
},
View { workspace: u32 },
SetFloating { window: WinID, floating: bool },
SetManaged { window: WinID, managed: bool },
SetWidth { window: WinID, ratio: f64 },
SetFrame { window: WinID, frame: Frame },
Stack {
window: WinID,
onto: WinID,
tabs: bool,
},
Unstack(WinID),
}
impl LayoutOp {
#[must_use]
pub fn target(&self) -> Option<WinID> {
match self {
LayoutOp::Focus(window)
| LayoutOp::Swap(window, _)
| LayoutOp::Unstack(window)
| LayoutOp::MoveToWorkspace { window, .. }
| LayoutOp::SetFloating { window, .. }
| LayoutOp::SetManaged { window, .. }
| LayoutOp::SetWidth { window, .. }
| LayoutOp::SetFrame { window, .. }
| LayoutOp::Stack { window, .. } => Some(*window),
LayoutOp::View { .. } => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RelativeRect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl RelativeRect {
#[must_use]
pub fn resolve(self, display: Frame) -> Frame {
#[allow(clippy::cast_possible_truncation)]
let scale = |fraction: f64, extent: i32| -> i32 {
let scaled = fraction * f64::from(extent);
if scaled.is_finite() {
scaled
.round()
.clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
} else {
0
}
};
Frame {
x: display.x + scale(self.x, display.width),
y: display.y + scale(self.y, display.height),
width: scale(self.width, display.width).max(1),
height: scale(self.height, display.height).max(1),
}
}
}
#[derive(Debug)]
struct OpNode {
op: LayoutOp,
prev: Option<Arc<OpNode>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ColumnKind {
Single,
Stack,
Tabs,
Fullscreen,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WindowRec {
pub id: WinID,
pub app_name: String,
pub bundle_id: String,
pub title: String,
pub frame: Option<Frame>,
pub floating: bool,
pub managed: bool,
pub visible: bool,
pub focused: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ColumnSet {
pub kind: ColumnKind,
pub width_ratio: f64,
pub selected: usize,
pub windows: Arc<Vec<WindowRec>>,
}
impl ColumnSet {
#[must_use]
pub fn single(window: WindowRec, width_ratio: f64) -> Self {
Self {
kind: ColumnKind::Single,
width_ratio,
selected: 0,
windows: Arc::new(vec![window]),
}
}
#[must_use]
pub fn top(&self) -> Option<&WindowRec> {
self.windows
.get(self.selected)
.or_else(|| self.windows.first())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WorkspaceSet {
pub number: u32,
pub native_id: u64,
pub active: bool,
pub columns: Arc<Vec<ColumnSet>>,
pub floating: Arc<Vec<WindowRec>>,
}
impl WorkspaceSet {
pub fn windows(&self) -> impl Iterator<Item = &WindowRec> {
self.columns
.iter()
.flat_map(|column| column.windows.iter())
.chain(self.floating.iter())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DisplaySet {
pub id: u32,
pub frame: Frame,
pub active: bool,
pub workspaces: Arc<Vec<WorkspaceSet>>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct WindowSet {
displays: Arc<Vec<DisplaySet>>,
focused: Option<WinID>,
#[serde(skip)]
ops: Option<Arc<OpNode>>,
}
impl PartialEq for WindowSet {
fn eq(&self, other: &Self) -> bool {
self.displays == other.displays && self.focused == other.focused
}
}
impl WindowSet {
#[must_use]
pub fn new(displays: Vec<DisplaySet>, focused: Option<WinID>) -> Self {
Self {
displays: Arc::new(displays),
focused,
ops: None,
}
}
#[must_use]
pub fn displays(&self) -> &[DisplaySet] {
&self.displays
}
#[must_use]
pub fn focused(&self) -> Option<WinID> {
self.focused
}
pub fn workspaces(&self) -> impl Iterator<Item = &WorkspaceSet> {
self.displays
.iter()
.flat_map(|display| display.workspaces.iter())
}
pub fn windows(&self) -> impl Iterator<Item = &WindowRec> {
self.workspaces().flat_map(WorkspaceSet::windows)
}
#[must_use]
pub fn window(&self, id: WinID) -> Option<&WindowRec> {
self.windows().find(|window| window.id == id)
}
#[must_use]
pub fn workspace(&self, number: u32) -> Option<&WorkspaceSet> {
self.workspaces()
.find(|workspace| workspace.number == number)
}
#[must_use]
pub fn current(&self) -> Option<&WorkspaceSet> {
self.displays
.iter()
.find(|display| display.active)
.or_else(|| self.displays.first())?
.workspaces
.iter()
.find(|workspace| workspace.active)
}
#[must_use]
pub fn display_of(&self, id: WinID) -> Option<&DisplaySet> {
self.displays.iter().find(|display| {
display
.workspaces
.iter()
.any(|workspace| workspace.windows().any(|window| window.id == id))
})
}
#[must_use]
pub fn workspace_of(&self, id: WinID) -> Option<&WorkspaceSet> {
self.workspaces()
.find(|workspace| workspace.windows().any(|window| window.id == id))
}
#[must_use]
pub fn column_of(&self, id: WinID) -> Option<usize> {
let workspace = self.workspace_of(id)?;
workspace
.columns
.iter()
.position(|column| column.windows.iter().any(|window| window.id == id))
}
#[must_use]
pub fn east(&self, id: WinID) -> Option<WinID> {
self.neighbour(id, 1)
}
#[must_use]
pub fn west(&self, id: WinID) -> Option<WinID> {
self.neighbour(id, -1)
}
#[must_use]
pub fn next(&self, id: WinID) -> Option<WinID> {
self.cycle(id, 1)
}
#[must_use]
pub fn prev(&self, id: WinID) -> Option<WinID> {
self.cycle(id, -1)
}
fn neighbour(&self, id: WinID, offset: isize) -> Option<WinID> {
let workspace = self.workspace_of(id)?;
let column = self.column_of(id)?;
let target = usize::try_from(isize::try_from(column).ok()? + offset).ok()?;
workspace.columns.get(target)?.top().map(|window| window.id)
}
fn cycle(&self, id: WinID, offset: isize) -> Option<WinID> {
let workspace = self.workspace_of(id)?;
let ids: Vec<WinID> = workspace.windows().map(|window| window.id).collect();
if ids.is_empty() {
return None;
}
let at = ids.iter().position(|&window| window == id)?;
let length = isize::try_from(ids.len()).ok()?;
let index = (isize::try_from(at).ok()? + offset).rem_euclid(length);
ids.get(usize::try_from(index).ok()?).copied()
}
#[must_use]
pub fn ops(&self) -> Vec<LayoutOp> {
let mut ops = Vec::new();
let mut node = self.ops.as_ref();
while let Some(current) = node {
ops.push(current.op);
node = current.prev.as_ref();
}
ops.reverse();
ops
}
#[must_use]
pub fn is_transformed(&self) -> bool {
self.ops.is_some()
}
}
impl WindowSet {
fn with(&self, op: LayoutOp, edit: impl FnOnce(&mut [DisplaySet])) -> Self {
let mut next = self.recording(op);
let displays: &mut Vec<DisplaySet> = Arc::make_mut(&mut next.displays);
edit(displays);
next
}
fn recording(&self, op: LayoutOp) -> Self {
Self {
displays: Arc::clone(&self.displays),
focused: self.focused,
ops: Some(Arc::new(OpNode {
op,
prev: self.ops.clone(),
})),
}
}
#[must_use]
pub fn focus(&self, window: WinID) -> Self {
let mut next = self.with(LayoutOp::Focus(window), |displays| {
for_each_window(displays, |record| record.focused = record.id == window);
});
next.focused = Some(window);
next
}
#[must_use]
pub fn swap(&self, first: WinID, second: WinID) -> Self {
self.with(LayoutOp::Swap(first, second), |displays| {
let (Some(left), Some(right)) = (
find_window(displays, first).cloned(),
find_window(displays, second).cloned(),
) else {
return;
};
for_each_window(displays, |record| {
if record.id == first {
let focused = record.focused;
*record = right.clone();
record.focused = focused;
} else if record.id == second {
let focused = record.focused;
*record = left.clone();
record.focused = focused;
}
});
})
}
#[must_use]
pub fn shift(&self, window: WinID, workspace: u32) -> Self {
self.shift_following(window, workspace, false)
}
#[must_use]
pub fn shift_following(&self, window: WinID, workspace: u32, follow: bool) -> Self {
self.with(
LayoutOp::MoveToWorkspace {
window,
workspace,
follow,
},
|displays| {
if !displays.iter().any(|display| {
display
.workspaces
.iter()
.any(|candidate| candidate.number == workspace)
}) {
return;
}
let Some(record) = take_window(displays, window) else {
return;
};
if let Some(target) = find_workspace_mut(displays, workspace) {
Arc::make_mut(&mut target.columns).push(ColumnSet::single(record, 0.5));
}
},
)
}
#[must_use]
pub fn view(&self, workspace: u32) -> Self {
self.with(LayoutOp::View { workspace }, |displays| {
let on_display = displays.iter().position(|display| {
display
.workspaces
.iter()
.any(|candidate| candidate.number == workspace)
});
let Some(index) = on_display else {
return;
};
for candidate in Arc::make_mut(&mut displays[index].workspaces) {
candidate.active = candidate.number == workspace;
}
})
}
#[must_use]
pub fn float(&self, window: WinID) -> Self {
self.set_floating(window, true)
}
#[must_use]
pub fn float_at(&self, window: WinID, rect: RelativeRect) -> Self {
let display = self
.display_of(window)
.or_else(|| self.displays.iter().find(|display| display.active))
.or_else(|| self.displays.first());
let Some(frame) = display.map(|display| rect.resolve(display.frame)) else {
return self.float(window);
};
self.float(window).set_frame(window, frame)
}
#[must_use]
pub fn set_frame(&self, window: WinID, frame: Frame) -> Self {
self.with(LayoutOp::SetFrame { window, frame }, |displays| {
for_each_window(displays, |record| {
if record.id == window {
record.frame = Some(frame);
}
});
})
}
#[must_use]
pub fn sink(&self, window: WinID) -> Self {
self.set_floating(window, false)
}
fn set_floating(&self, window: WinID, floating: bool) -> Self {
self.with(LayoutOp::SetFloating { window, floating }, |displays| {
let Some(mut record) = take_window(displays, window) else {
return;
};
record.floating = floating;
let target = displays.iter_mut().find_map(|display| {
Arc::make_mut(&mut display.workspaces)
.iter_mut()
.find(|workspace| workspace.active)
});
let Some(target) = target else {
return;
};
if floating {
Arc::make_mut(&mut target.floating).push(record);
} else {
Arc::make_mut(&mut target.columns).push(ColumnSet::single(record, 0.5));
}
})
}
#[must_use]
pub fn manage(&self, window: WinID) -> Self {
self.set_managed(window, true)
}
#[must_use]
pub fn unmanage(&self, window: WinID) -> Self {
self.set_managed(window, false)
}
fn set_managed(&self, window: WinID, managed: bool) -> Self {
self.with(LayoutOp::SetManaged { window, managed }, |displays| {
for_each_window(displays, |record| {
if record.id == window {
record.managed = managed;
}
});
})
}
#[must_use]
pub fn width(&self, window: WinID, ratio: f64) -> Self {
self.with(LayoutOp::SetWidth { window, ratio }, |displays| {
for_each_column(displays, |column| {
if column.windows.iter().any(|record| record.id == window) {
column.width_ratio = ratio;
}
});
})
}
#[must_use]
pub fn stack(&self, window: WinID, onto: WinID) -> Self {
self.stack_as(window, onto, false)
}
#[must_use]
pub fn tab(&self, window: WinID, onto: WinID) -> Self {
self.stack_as(window, onto, true)
}
fn stack_as(&self, window: WinID, onto: WinID, tabs: bool) -> Self {
self.with(LayoutOp::Stack { window, onto, tabs }, |displays| {
if find_window(displays, onto).is_none() {
return;
}
let Some(record) = take_window(displays, window) else {
return;
};
let mut record = Some(record);
for_each_column(displays, |column| {
if record.is_none() || !column.windows.iter().any(|held| held.id == onto) {
return;
}
column.kind = if tabs {
ColumnKind::Tabs
} else {
ColumnKind::Stack
};
Arc::make_mut(&mut column.windows).push(record.take().expect("checked above"));
});
})
}
#[must_use]
pub fn unstack(&self, window: WinID) -> Self {
self.with(LayoutOp::Unstack(window), |displays| {
let Some(record) = take_window(displays, window) else {
return;
};
let target = displays.iter_mut().find_map(|display| {
Arc::make_mut(&mut display.workspaces)
.iter_mut()
.find(|workspace| workspace.active)
});
if let Some(target) = target {
Arc::make_mut(&mut target.columns).push(ColumnSet::single(record, 0.5));
}
})
}
}
fn for_each_window(displays: &mut [DisplaySet], mut visit: impl FnMut(&mut WindowRec)) {
for display in displays.iter_mut() {
for workspace in Arc::make_mut(&mut display.workspaces) {
for column in Arc::make_mut(&mut workspace.columns) {
for window in Arc::make_mut(&mut column.windows) {
visit(window);
}
}
for window in Arc::make_mut(&mut workspace.floating) {
visit(window);
}
}
}
}
fn for_each_column(displays: &mut [DisplaySet], mut visit: impl FnMut(&mut ColumnSet)) {
for display in displays.iter_mut() {
for workspace in Arc::make_mut(&mut display.workspaces) {
for column in Arc::make_mut(&mut workspace.columns) {
visit(column);
}
}
}
}
fn find_window(displays: &[DisplaySet], id: WinID) -> Option<&WindowRec> {
displays
.iter()
.flat_map(|display| display.workspaces.iter())
.flat_map(WorkspaceSet::windows)
.find(|window| window.id == id)
}
fn find_workspace_mut(displays: &mut [DisplaySet], number: u32) -> Option<&mut WorkspaceSet> {
displays.iter_mut().find_map(|display| {
Arc::make_mut(&mut display.workspaces)
.iter_mut()
.find(|workspace| workspace.number == number)
})
}
fn take_window(displays: &mut [DisplaySet], id: WinID) -> Option<WindowRec> {
for display in displays.iter_mut() {
for workspace in Arc::make_mut(&mut display.workspaces) {
let columns = Arc::make_mut(&mut workspace.columns);
for index in 0..columns.len() {
let windows = Arc::make_mut(&mut columns[index].windows);
if let Some(at) = windows.iter().position(|window| window.id == id) {
let taken = windows.remove(at);
if windows.is_empty() {
columns.remove(index);
} else {
let column = &mut columns[index];
column.selected = column.selected.min(column.windows.len() - 1);
if column.windows.len() == 1 {
column.kind = ColumnKind::Single;
}
}
return Some(taken);
}
}
let floating = Arc::make_mut(&mut workspace.floating);
if let Some(at) = floating.iter().position(|window| window.id == id) {
return Some(floating.remove(at));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn window(id: WinID, name: &str) -> WindowRec {
WindowRec {
id,
app_name: name.to_string(),
bundle_id: format!("com.example.{name}"),
title: format!("{name} window"),
frame: None,
floating: false,
managed: true,
visible: true,
focused: false,
}
}
fn fixture() -> WindowSet {
let columns: Vec<ColumnSet> = [(1, "alpha"), (2, "beta"), (3, "gamma")]
.into_iter()
.map(|(id, name)| {
let mut record = window(id, name);
record.focused = id == 1;
ColumnSet::single(record, 0.33)
})
.collect();
WindowSet::new(
vec![DisplaySet {
id: 1,
frame: Frame {
x: 0,
y: 0,
width: 1920,
height: 1080,
},
active: true,
workspaces: Arc::new(vec![
WorkspaceSet {
number: 1,
native_id: 10,
active: true,
columns: Arc::new(columns),
floating: Arc::new(Vec::new()),
},
WorkspaceSet {
number: 2,
native_id: 11,
active: false,
columns: Arc::new(Vec::new()),
floating: Arc::new(Vec::new()),
},
]),
}],
Some(1),
)
}
#[test]
fn a_fresh_window_set_has_asked_for_nothing() {
let set = fixture();
assert!(set.ops().is_empty());
assert!(!set.is_transformed());
}
#[test]
fn transforms_leave_the_original_alone() {
let before = fixture();
let after = before.focus(3);
assert_eq!(
before.focused(),
Some(1),
"the original still has its focus"
);
assert!(
before.ops().is_empty(),
"and has still asked for nothing: {:?}",
before.ops()
);
assert_eq!(after.focused(), Some(3));
assert_eq!(after.ops(), vec![LayoutOp::Focus(3)]);
assert!(after.window(3).unwrap().focused);
assert!(!after.window(1).unwrap().focused);
}
#[test]
fn chained_transforms_record_in_order() {
let set = fixture().focus(2).width(2, 0.75).shift(2, 2);
assert_eq!(
set.ops(),
vec![
LayoutOp::Focus(2),
LayoutOp::SetWidth {
window: 2,
ratio: 0.75
},
LayoutOp::MoveToWorkspace {
window: 2,
workspace: 2,
follow: false
},
]
);
}
#[test]
fn branches_do_not_see_each_others_ops() {
let base = fixture();
let left = base.focus(2);
let right = base.focus(3);
assert_eq!(left.ops(), vec![LayoutOp::Focus(2)]);
assert_eq!(right.ops(), vec![LayoutOp::Focus(3)]);
assert!(base.ops().is_empty());
assert_eq!(base.focused(), Some(1));
}
#[test]
fn untouched_subtrees_are_shared_not_copied() {
let base = fixture();
let clone = base.clone();
assert!(
Arc::ptr_eq(&base.displays, &clone.displays),
"cloning should share, not copy"
);
let edited = base.width(1, 0.9);
let before = &base.displays()[0].workspaces[1];
let after = &edited.displays()[0].workspaces[1];
assert_eq!(before, after, "the untouched workspace keeps its contents");
}
#[test]
fn shift_moves_the_window_between_workspaces() {
let set = fixture().shift(2, 2);
assert!(
set.workspace(1).unwrap().windows().all(|w| w.id != 2),
"the window has left its old workspace"
);
assert!(
set.workspace(2).unwrap().windows().any(|w| w.id == 2),
"...and arrived at the new one"
);
assert_eq!(
set.workspace(1).unwrap().columns.len(),
2,
"its emptied column is gone"
);
}
#[test]
fn swap_exchanges_positions_not_focus() {
let set = fixture().swap(1, 3);
let workspace = set.workspace(1).unwrap();
let order: Vec<WinID> = workspace
.columns
.iter()
.filter_map(|column| column.top().map(|window| window.id))
.collect();
assert_eq!(order, vec![3, 2, 1]);
assert_eq!(set.focused(), Some(1), "swapping does not move the focus");
}
#[test]
fn stacking_merges_columns_and_unstacking_splits_them() {
let stacked = fixture().stack(2, 1);
let workspace = stacked.workspace(1).unwrap();
assert_eq!(workspace.columns.len(), 2, "two columns became one");
assert_eq!(workspace.columns[0].kind, ColumnKind::Stack);
assert_eq!(workspace.columns[0].windows.len(), 2);
let split = stacked.unstack(2);
let workspace = split.workspace(1).unwrap();
assert_eq!(workspace.columns.len(), 3);
assert_eq!(
workspace.columns[0].kind,
ColumnKind::Single,
"the column it left is single again"
);
}
#[test]
fn floating_moves_a_window_off_the_strip_and_back() {
let floated = fixture().float(2);
let workspace = floated.workspace(1).unwrap();
assert_eq!(workspace.columns.len(), 2);
assert_eq!(workspace.floating.len(), 1);
assert!(workspace.floating[0].floating);
let sunk = floated.sink(2);
let workspace = sunk.workspace(1).unwrap();
assert_eq!(workspace.columns.len(), 3);
assert!(workspace.floating.is_empty());
}
#[test]
fn float_at_resolves_fractions_against_the_display() {
let set = fixture().float_at(
2,
RelativeRect {
x: 0.25,
y: 0.5,
width: 0.5,
height: 0.25,
},
);
let placed = Frame {
x: 480,
y: 540,
width: 960,
height: 270,
};
assert_eq!(
set.ops(),
vec![
LayoutOp::SetFloating {
window: 2,
floating: true
},
LayoutOp::SetFrame {
window: 2,
frame: placed
},
],
"customFloating is float-then-place"
);
let window = set.window(2).expect("the window is still there");
assert_eq!(window.frame, Some(placed));
assert!(window.floating);
}
#[test]
fn a_relative_rect_is_offset_by_the_display_origin() {
let second = Frame {
x: 1920,
y: -200,
width: 1000,
height: 800,
};
let full = RelativeRect {
x: 0.0,
y: 0.0,
width: 1.0,
height: 1.0,
};
assert_eq!(full.resolve(second), second, "a full rect is the display");
let half = RelativeRect {
x: 0.5,
y: 0.0,
width: 0.5,
height: 1.0,
};
assert_eq!(
half.resolve(second),
Frame {
x: 2420,
y: -200,
width: 500,
height: 800
}
);
}
#[test]
fn a_degenerate_rect_still_yields_a_usable_frame() {
let display = Frame {
x: 0,
y: 0,
width: 1920,
height: 1080,
};
let zero = RelativeRect {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
};
let resolved = zero.resolve(display);
assert_eq!((resolved.width, resolved.height), (1, 1));
let nonsense = RelativeRect {
x: f64::NAN,
y: f64::INFINITY,
width: 1e30,
height: -1.0,
};
let resolved = nonsense.resolve(display);
assert!(resolved.width >= 1 && resolved.height >= 1);
}
#[test]
fn view_switches_which_workspace_is_active() {
let set = fixture().view(2);
assert!(!set.workspace(1).unwrap().active);
assert!(set.workspace(2).unwrap().active);
assert_eq!(set.ops(), vec![LayoutOp::View { workspace: 2 }]);
}
#[test]
fn navigation_follows_the_strip_and_wraps() {
let set = fixture();
assert_eq!(set.east(1), Some(2));
assert_eq!(set.west(2), Some(1));
assert_eq!(set.west(1), None, "nothing west of the first column");
assert_eq!(set.next(3), Some(1), "next wraps around");
assert_eq!(set.prev(1), Some(3), "and so does prev");
}
#[test]
fn lookups_find_where_a_window_lives() {
let set = fixture();
assert_eq!(set.column_of(2), Some(1));
assert_eq!(set.workspace_of(2).map(|w| w.number), Some(1));
assert_eq!(set.display_of(2).map(|d| d.id), Some(1));
assert_eq!(set.current().map(|w| w.number), Some(1));
assert_eq!(set.window(2).map(|w| w.app_name.as_str()), Some("beta"));
assert_eq!(set.window(99), None);
}
#[test]
fn ops_naming_a_window_expose_it_for_resolution() {
assert_eq!(LayoutOp::Focus(4).target(), Some(4));
assert_eq!(
LayoutOp::Stack {
window: 4,
onto: 5,
tabs: true
}
.target(),
Some(4)
);
assert_eq!(LayoutOp::View { workspace: 2 }.target(), None);
}
#[test]
fn shifting_to_a_workspace_the_snapshot_lacks_keeps_the_window() {
let set = fixture().shift(2, 42);
assert!(
set.window(2).is_some(),
"the window should still be somewhere"
);
assert_eq!(set.workspace_of(2).map(|w| w.number), Some(1));
assert_eq!(
set.ops(),
vec![LayoutOp::MoveToWorkspace {
window: 2,
workspace: 42,
follow: false
}]
);
}
#[test]
fn stacking_onto_a_window_the_snapshot_lacks_keeps_the_window() {
let set = fixture().stack(2, 99);
assert!(
set.window(2).is_some(),
"the window should still be somewhere"
);
assert_eq!(set.workspace(1).unwrap().columns.len(), 3);
}
#[test]
fn transforming_a_missing_window_still_records_the_intent() {
let set = fixture().focus(99);
assert_eq!(set.ops(), vec![LayoutOp::Focus(99)]);
}
}