Format Rust sources at 100 columns

This commit is contained in:
Samuel Onoja committed 2026-10-22 01:28:03 +01:00
1 parent 4661f595f8
commit 610478f8c7
336 files changed
+23497 -7892

No files matched your search

+27 -6
View File
@@ -21,7 +21,11 @@ impl FocusHistory {
self.windows.retain(|id| *id != window);
}
pub fn candidates(&self, current: Option<WindowId>, available: impl AsRef<[WindowId]>) -> Vec<WindowId> {
pub fn candidates(
&self,
current: Option<WindowId>,
available: impl AsRef<[WindowId]>,
) -> Vec<WindowId> {
let mut remaining = available.as_ref().iter().copied().collect::<HashSet<_>>();
let mut ordered = Vec::with_capacity(remaining.len());
@@ -71,7 +75,12 @@ impl FocusCycle {
let old = self
.selected
.and_then(|id| self.windows.iter().position(|candidate| *candidate == id));
let before = old.map(|index| self.windows[..index].iter().filter(|id| available.contains(id)).count());
let before = old.map(|index| {
self.windows[..index]
.iter()
.filter(|id| available.contains(id))
.count()
});
let survives = self.selected.is_some_and(|id| available.contains(&id));
self.windows.retain(|id| available.contains(id));
if self.windows.is_empty() {
@@ -142,7 +151,10 @@ mod tests {
[3, 2, 1, 4].map(WindowId)
);
history.record(WindowId(2));
assert_eq!(history.candidates(Some(WindowId(2)), available)[1], WindowId(3));
assert_eq!(
history.candidates(Some(WindowId(2)), available)[1],
WindowId(3)
);
history.remove(WindowId(3));
assert_eq!(
history.candidates(None, vec![WindowId(1), WindowId(4)]),
@@ -158,7 +170,10 @@ mod tests {
assert_eq!(cycle.advance(false, &available), Some(WindowId(1)));
assert_eq!(cycle.advance(false, &available), Some(WindowId(3)));
assert_eq!(cycle.advance(true, &available), Some(WindowId(1)));
assert_eq!(cycle.advance(false, &[WindowId(3), WindowId(2)]), Some(WindowId(3)));
assert_eq!(
cycle.advance(false, &[WindowId(3), WindowId(2)]),
Some(WindowId(3))
);
assert_eq!(cycle.advance(false, &[]), None);
}
@@ -177,7 +192,13 @@ mod tests {
let available = [3, 2, 1].map(WindowId);
let mut cycle = FocusCycle::new(available.to_vec(), Some(WindowId(3)));
assert_eq!(cycle.advance(false, &available), Some(WindowId(2)));
assert_eq!(cycle.reconcile(&[WindowId(3), WindowId(1)]), Some(WindowId(1)));
assert_eq!(cycle.reconcile(&[WindowId(3), WindowId(1)]), Some(WindowId(1)));
assert_eq!(
cycle.reconcile(&[WindowId(3), WindowId(1)]),
Some(WindowId(1))
);
assert_eq!(
cycle.reconcile(&[WindowId(3), WindowId(1)]),
Some(WindowId(1))
);
}
}
+5 -2
View File
@@ -4,8 +4,11 @@ mod workspace;
mod workspace_policy;
pub use focus::{FocusCycle, FocusHistory};
pub use output::{DesktopPlan, OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch};
pub use output::{
DesktopPlan, OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch,
};
pub use workspace::{
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, WorkspaceSet,
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout,
WorkspaceSet,
};
pub use workspace_policy::WorkspaceView;
+345 -98
View File
@@ -17,7 +17,12 @@ pub struct OutputGeometry {
impl OutputGeometry {
pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
Self { x, y, width, height }
Self {
x,
y,
width,
height,
}
}
fn center(self) -> (i64, i64) {
@@ -33,7 +38,10 @@ pub enum OutputError {
AlreadyConnected(OutputId),
UnknownOutput(OutputId),
InvalidGeometry(OutputId),
WorkspaceAlreadyAssigned { workspace: WorkspaceId, output: OutputId },
WorkspaceAlreadyAssigned {
workspace: WorkspaceId,
output: OutputId,
},
}
impl fmt::Display for OutputError {
@@ -41,9 +49,14 @@ impl fmt::Display for OutputError {
match self {
Self::AlreadyConnected(output) => write!(formatter, "output {output:?} is connected"),
Self::UnknownOutput(output) => write!(formatter, "unknown output {output:?}"),
Self::InvalidGeometry(output) => write!(formatter, "invalid geometry for output {output:?}"),
Self::InvalidGeometry(output) => {
write!(formatter, "invalid geometry for output {output:?}")
}
Self::WorkspaceAlreadyAssigned { workspace, output } => {
write!(formatter, "workspace {workspace:?} is assigned to {output:?}")
write!(
formatter,
"workspace {workspace:?} is assigned to {output:?}"
)
}
}
}
@@ -68,7 +81,10 @@ struct OutputState {
impl OutputState {
fn activate(&mut self, workspace: WorkspaceId) {
if self.active != workspace {
self.previous = self.workspaces.contains(&self.active).then_some(self.active);
self.previous = self
.workspaces
.contains(&self.active)
.then_some(self.active);
self.active = workspace;
}
}
@@ -148,12 +164,17 @@ impl OutputWorkspaceMap {
.copied()
.filter(|workspace| {
outputs.output_for_workspace(*workspace).is_none()
&& outputs.home_output(*workspace).is_none_or(|home| home == id)
&& outputs
.home_output(*workspace)
.is_none_or(|home| home == id)
})
.min()
.unwrap_or_else(|| {
let workspace = next_workspace;
next_workspace.0 = next_workspace.0.checked_add(1).expect("workspace IDs exhausted");
next_workspace.0 = next_workspace
.0
.checked_add(1)
.expect("workspace IDs exhausted");
created_workspaces.push(workspace);
workspace
});
@@ -202,7 +223,10 @@ impl OutputWorkspaceMap {
})
}
pub fn assigned_workspaces(&self, output: OutputId) -> impl DoubleEndedIterator<Item = WorkspaceId> + '_ {
pub fn assigned_workspaces(
&self,
output: OutputId,
) -> impl DoubleEndedIterator<Item = WorkspaceId> + '_ {
self.outputs
.get(&output)
.into_iter()
@@ -230,7 +254,11 @@ impl OutputWorkspaceMap {
}
pub fn forget_workspace(&mut self, workspace: WorkspaceId) -> bool {
if self.outputs.values().any(|output| output.active == workspace) {
if self
.outputs
.values()
.any(|output| output.active == workspace)
{
return false;
}
if let Some(owner) = self.assignments.remove(&workspace)
@@ -247,7 +275,9 @@ impl OutputWorkspaceMap {
self.orphaned_focus = None;
}
self.evacuations.retain(|_, record| {
record.workspaces.retain(|entry| entry.workspace != workspace);
record
.workspaces
.retain(|entry| entry.workspace != workspace);
if record.active == workspace {
if let Some(entry) = record.workspaces.first() {
record.active = entry.workspace;
@@ -282,7 +312,11 @@ impl OutputWorkspaceMap {
.assignments
.get(&evacuated.workspace)
.copied()
.or_else(|| evacuated.target.filter(|target| self.outputs.contains_key(target)));
.or_else(|| {
evacuated
.target
.filter(|target| self.outputs.contains_key(target))
});
let assignment_matches = self.homes.get(&evacuated.workspace) == Some(&output);
let revision_matches = self.revision(evacuated.workspace) == evacuated.revision;
@@ -296,7 +330,10 @@ impl OutputWorkspaceMap {
// the reconnecting monitor can use its fresh fallback.
if target
.and_then(|target| self.outputs.get(&target))
.is_some_and(|state| state.workspaces.len() == 1 && state.workspaces.contains(&evacuated.workspace))
.is_some_and(|state| {
state.workspaces.len() == 1
&& state.workspaces.contains(&evacuated.workspace)
})
{
continue;
}
@@ -344,7 +381,12 @@ impl OutputWorkspaceMap {
let active = record
.as_ref()
.and_then(|record| reclaimed.contains(&record.active).then_some(record.active))
.or_else(|| reclaimed.iter().copied().min_by_key(|workspace| workspace.0));
.or_else(|| {
reclaimed
.iter()
.copied()
.min_by_key(|workspace| workspace.0)
});
let mut active = match active {
Some(active) => active,
None => {
@@ -398,7 +440,10 @@ impl OutputWorkspaceMap {
}
pub fn disconnect(&mut self, output: OutputId) -> Result<Option<OutputId>, OutputError> {
let removed = self.outputs.remove(&output).ok_or(OutputError::UnknownOutput(output))?;
let removed = self
.outputs
.remove(&output)
.ok_or(OutputError::UnknownOutput(output))?;
let target = self.evacuate(output, removed);
debug_assert!(self.validate());
Ok(target)
@@ -483,7 +528,10 @@ impl OutputWorkspaceMap {
}
let reassigned = self.assignments.insert(workspace, output) != Some(output);
let state = self.outputs.get_mut(&output).expect("output was checked above");
let state = self
.outputs
.get_mut(&output)
.expect("output was checked above");
state.workspaces.insert(workspace);
state.activate(workspace);
self.focused = Some(output);
@@ -510,7 +558,11 @@ impl OutputWorkspaceMap {
self.switch_workspace(output, workspace)
}
pub fn assign_workspace(&mut self, output: OutputId, workspace: WorkspaceId) -> Result<OutputId, OutputError> {
pub fn assign_workspace(
&mut self,
output: OutputId,
workspace: WorkspaceId,
) -> Result<OutputId, OutputError> {
if !self.outputs.contains_key(&output) {
return Err(OutputError::UnknownOutput(output));
}
@@ -545,7 +597,10 @@ impl OutputWorkspaceMap {
if let Some(owner) = self.assignments.get(&workspace).copied()
&& owner != output
{
let donor = self.outputs.get_mut(&owner).expect("assigned owner is connected");
let donor = self
.outputs
.get_mut(&owner)
.expect("assigned owner is connected");
if donor.workspaces.len() == 1 {
if let Some(assigned) = self.assignments.get(&donor_fallback) {
return Err(OutputError::WorkspaceAlreadyAssigned {
@@ -570,13 +625,21 @@ impl OutputWorkspaceMap {
}
self.assignments.insert(workspace, output);
self.homes.insert(workspace, output);
self.outputs.get_mut(&output).unwrap().workspaces.insert(workspace);
self.outputs
.get_mut(&output)
.unwrap()
.workspaces
.insert(workspace);
self.bump_revision(workspace);
debug_assert!(self.validate());
Ok(())
}
fn migration_target(&self, removed: OutputId, removed_geometry: OutputGeometry) -> Option<OutputId> {
fn migration_target(
&self,
removed: OutputId,
removed_geometry: OutputGeometry,
) -> Option<OutputId> {
if let Some(focused) = self.focused
&& focused != removed
&& self.outputs.contains_key(&focused)
@@ -606,7 +669,10 @@ impl OutputWorkspaceMap {
}
fn validate(&self) -> bool {
if self.focused.is_some_and(|focused| !self.outputs.contains_key(&focused)) {
if self
.focused
.is_some_and(|focused| !self.outputs.contains_key(&focused))
{
return false;
}
@@ -614,10 +680,9 @@ impl OutputWorkspaceMap {
if !state.workspaces.contains(&state.active) {
return false;
}
if state
.previous
.is_some_and(|previous| previous == state.active || !state.workspaces.contains(&previous))
{
if state.previous.is_some_and(|previous| {
previous == state.active || !state.workspaces.contains(&previous)
}) {
return false;
}
for workspace in &state.workspaces {
@@ -662,8 +727,14 @@ mod tests {
assert!(!unchanged.focus_changed);
let removed = plan(&second.outputs, &[1]);
assert_eq!(removed.outputs.focused_output(), Some(OutputId(1)));
assert_eq!(removed.outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(removed.outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
assert_eq!(
removed.outputs.active_workspace(OutputId(1)),
Some(WorkspaceId(1))
);
assert_eq!(
removed.outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(1))
);
}
#[test]
@@ -672,7 +743,10 @@ mod tests {
before.focus_output(OutputId(2)).unwrap();
let after = plan(&before, &[4]);
assert_eq!(after.outputs.focused_output(), Some(OutputId(4)));
assert_eq!(after.outputs.active_workspace(OutputId(4)), Some(WorkspaceId(2)));
assert_eq!(
after.outputs.active_workspace(OutputId(4)),
Some(WorkspaceId(2))
);
for workspace in 1..=3 {
assert_eq!(
after.outputs.output_for_workspace(WorkspaceId(workspace)),
@@ -695,7 +769,10 @@ mod tests {
let before = plan(&OutputWorkspaceMap::default(), &[1, 2]).outputs;
let mut evacuated = plan(&before, &[1]).outputs;
let restored = plan(&evacuated, &[1, 2]);
assert_eq!(restored.outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
assert_eq!(
restored.outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(2))
);
evacuated
.reassign_workspace(OutputId(1), WorkspaceId(2), WorkspaceId(9))
.unwrap();
@@ -711,11 +788,16 @@ mod tests {
let mut before = plan(&OutputWorkspaceMap::default(), &[1, 2, 3]).outputs;
before.focus_output(OutputId(2)).unwrap();
before.disconnect(OutputId(1)).unwrap();
before.switch_workspace(OutputId(2), WorkspaceId(1)).unwrap();
before
.switch_workspace(OutputId(2), WorkspaceId(1))
.unwrap();
assert!(before.forget_workspace(WorkspaceId(2)));
before.focus_output(OutputId(3)).unwrap();
let after = plan(&before, &[1, 3]).outputs;
assert_eq!(after.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
assert_eq!(
after.output_for_workspace(WorkspaceId(1)),
Some(OutputId(1))
);
assert_eq!(after.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(after.focused_output(), Some(OutputId(3)));
}
@@ -735,7 +817,11 @@ mod tests {
);
assert!(
before
.plan_desktop(&[(OutputId(3), OutputGeometry::new(0, 0, 0, 800))], &[], WorkspaceId(3))
.plan_desktop(
&[(OutputId(3), OutputGeometry::new(0, 0, 0, 800))],
&[],
WorkspaceId(3)
)
.is_err()
);
assert_eq!(before, saved);
@@ -749,9 +835,15 @@ mod tests {
assert_eq!(headless.focused_output(), None);
assert!(headless.assignments.is_empty());
let recovered = plan(&headless, &[3]).outputs;
assert_eq!(recovered.active_workspace(OutputId(3)), Some(WorkspaceId(2)));
assert_eq!(
recovered.active_workspace(OutputId(3)),
Some(WorkspaceId(2))
);
for id in [1, 2] {
assert_eq!(recovered.output_for_workspace(WorkspaceId(id)), Some(OutputId(3)));
assert_eq!(
recovered.output_for_workspace(WorkspaceId(id)),
Some(OutputId(3))
);
}
}
@@ -761,7 +853,9 @@ mod tests {
let mut seed = 0x5eed_u64;
for step in 0..512 {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
let ids = (1..=4).filter(|id| seed & (1 << id) != 0).collect::<Vec<_>>();
let ids = (1..=4)
.filter(|id| seed & (1 << id) != 0)
.collect::<Vec<_>>();
let next = plan(&map, &ids);
assert!(next.outputs.validate(), "step {step}");
assert_eq!(plan(&next.outputs, &ids).outputs, next.outputs);
@@ -793,7 +887,10 @@ mod tests {
.filter(|id| seed & (1 << id) != 0)
.map(|id| (OutputId(id), geometry(id as i32 * 800)))
.collect::<Vec<_>>();
let ids = workspaces.iter().map(|workspace| workspace.id).collect::<Vec<_>>();
let ids = workspaces
.iter()
.map(|workspace| workspace.id)
.collect::<Vec<_>>();
let plan = outputs
.plan_desktop(&inventory, &ids, workspaces.next_workspace_id())
.unwrap();
@@ -803,7 +900,9 @@ mod tests {
outputs = plan.outputs;
outputs.reconcile_workspaces(&mut workspaces, &Default::default());
let window = WindowId(step);
workspaces.insert_window(window, Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(window, Axis::Horizontal, 0.5)
.unwrap();
windows.insert(window);
if let Some((output, _)) = inventory.last() {
outputs.focus_output(*output).unwrap();
@@ -817,10 +916,22 @@ mod tests {
workspaces.validate().unwrap();
let actual = workspaces
.iter()
.flat_map(|workspace| workspace.layout.window_ids().chain(workspace.floating.iter().copied()))
.flat_map(|workspace| {
workspace
.layout
.window_ids()
.chain(workspace.floating.iter().copied())
})
.collect::<Vec<_>>();
assert_eq!(actual.len(), windows.len(), "duplicate or lost window at {step}");
assert_eq!(actual.into_iter().collect::<std::collections::HashSet<_>>(), windows);
assert_eq!(
actual.len(),
windows.len(),
"duplicate or lost window at {step}"
);
assert_eq!(
actual.into_iter().collect::<std::collections::HashSet<_>>(),
windows
);
for workspace in workspaces.iter().filter(|workspace| !workspace.is_empty()) {
assert_eq!(
outputs.output_for_workspace(workspace.id).is_some(),
@@ -833,13 +944,19 @@ mod tests {
#[test]
fn reconnect_cannot_reclaim_the_donor_monitors_only_workspace() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1920), WorkspaceId(2))
.unwrap();
outputs.focus_output(OutputId(2)).unwrap();
outputs.disconnect(OutputId(2)).unwrap();
assert!(outputs.forget_workspace(WorkspaceId(1)));
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(3)).unwrap();
outputs
.connect(OutputId(2), geometry(1920), WorkspaceId(3))
.unwrap();
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(2)));
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(3)));
@@ -859,7 +976,9 @@ mod tests {
assert!(outputs.forget_workspace(first));
assert_eq!(outputs.output_for_workspace(first), None);
outputs.disconnect(display).unwrap();
outputs.connect(display, geometry, super::WorkspaceId(3)).unwrap();
outputs
.connect(display, geometry, super::WorkspaceId(3))
.unwrap();
assert_eq!(outputs.active_workspace(display), Some(second));
assert_eq!(outputs.output_for_workspace(first), None);
assert!(outputs.validate());
@@ -888,14 +1007,20 @@ mod tests {
fn history_tracks_actual_switches_and_toggles_without_replacing_it_on_a_noop() {
let mut outputs = OutputWorkspaceMap::default();
let display = OutputId(1);
outputs.connect(display, geometry(0), WorkspaceId(1)).unwrap();
outputs
.connect(display, geometry(0), WorkspaceId(1))
.unwrap();
assert_eq!(outputs.previous_workspace(display), None);
outputs.select_workspace(display, WorkspaceId(1), true).unwrap();
outputs
.select_workspace(display, WorkspaceId(1), true)
.unwrap();
assert_eq!(outputs.previous_workspace(display), None);
outputs.switch_workspace(display, WorkspaceId(2)).unwrap();
outputs.switch_workspace(display, WorkspaceId(2)).unwrap();
assert_eq!(outputs.previous_workspace(display), Some(WorkspaceId(1)));
outputs.select_workspace(display, WorkspaceId(2), false).unwrap();
outputs
.select_workspace(display, WorkspaceId(2), false)
.unwrap();
assert_eq!(outputs.active_workspace(display), Some(WorkspaceId(2)));
for expected in [WorkspaceId(1), WorkspaceId(2), WorkspaceId(1)] {
let current = outputs.active_workspace(display).unwrap();
@@ -912,7 +1037,9 @@ mod tests {
let left = OutputId(1);
let right = OutputId(2);
outputs.connect(left, geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(right, geometry(1920), WorkspaceId(2)).unwrap();
outputs
.connect(right, geometry(1920), WorkspaceId(2))
.unwrap();
outputs.switch_workspace(left, WorkspaceId(3)).unwrap();
outputs.switch_workspace(right, WorkspaceId(4)).unwrap();
assert_eq!(
@@ -935,13 +1062,17 @@ mod tests {
let left = OutputId(1);
let right = OutputId(2);
outputs.connect(left, geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(right, geometry(1920), WorkspaceId(2)).unwrap();
outputs
.connect(right, geometry(1920), WorkspaceId(2))
.unwrap();
outputs.focus_output(right).unwrap();
outputs.disconnect(right).unwrap();
assert_eq!(outputs.previous_workspace(left), Some(WorkspaceId(1)));
outputs.switch_workspace(left, WorkspaceId(1)).unwrap();
assert_eq!(outputs.previous_workspace(left), Some(WorkspaceId(2)));
outputs.connect(right, geometry(1920), WorkspaceId(3)).unwrap();
outputs
.connect(right, geometry(1920), WorkspaceId(3))
.unwrap();
assert_eq!(outputs.previous_workspace(left), None);
assert_eq!(outputs.previous_workspace(right), None);
assert!(outputs.validate());
@@ -951,11 +1082,15 @@ mod tests {
fn forgetting_a_previous_workspace_clears_its_history() {
let mut outputs = OutputWorkspaceMap::default();
let display = OutputId(1);
outputs.connect(display, geometry(0), WorkspaceId(1)).unwrap();
outputs
.connect(display, geometry(0), WorkspaceId(1))
.unwrap();
outputs.switch_workspace(display, WorkspaceId(2)).unwrap();
assert!(outputs.forget_workspace(WorkspaceId(1)));
assert_eq!(outputs.previous_workspace(display), None);
outputs.select_workspace(display, WorkspaceId(2), true).unwrap();
outputs
.select_workspace(display, WorkspaceId(2), true)
.unwrap();
assert_eq!(outputs.active_workspace(display), Some(WorkspaceId(2)));
assert_eq!(
outputs.select_workspace(OutputId(99), WorkspaceId(2), true),
@@ -975,28 +1110,47 @@ mod tests {
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)),
Ok(WorkspaceId(2))
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(1))
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(2))
);
}
#[test]
fn removal_prefers_a_remaining_focused_output_without_replacing_its_workspace() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs.focus_output(OutputId(1)).unwrap();
assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1))));
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(1))
);
}
#[test]
fn removing_the_focused_output_activates_its_workspace_on_the_nearest_output() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs.connect(OutputId(3), geometry(3_840), WorkspaceId(3)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs
.connect(OutputId(3), geometry(3_840), WorkspaceId(3))
.unwrap();
outputs.focus_output(OutputId(2)).unwrap();
assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1))));
@@ -1007,8 +1161,12 @@ mod tests {
#[test]
fn reconnect_reclaims_automatically_evacuated_workspaces() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs.focus_output(OutputId(1)).unwrap();
outputs.disconnect(OutputId(2)).unwrap();
@@ -1016,20 +1174,29 @@ mod tests {
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
Ok(WorkspaceId(2))
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(2))
);
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
}
#[test]
fn reconnect_restores_the_migration_targets_remaining_workspace() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs.focus_output(OutputId(2)).unwrap();
outputs.disconnect(OutputId(2)).unwrap();
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(2)));
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)).unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(3))
.unwrap();
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
@@ -1038,24 +1205,37 @@ mod tests {
#[test]
fn using_an_evacuated_workspace_does_not_prevent_automatic_return() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs.focus_output(OutputId(1)).unwrap();
outputs.disconnect(OutputId(2)).unwrap();
outputs.switch_workspace(OutputId(1), WorkspaceId(2)).unwrap();
outputs
.switch_workspace(OutputId(1), WorkspaceId(2))
.unwrap();
assert_eq!(
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
Ok(WorkspaceId(2))
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(2))
);
}
#[test]
fn repeated_lid_and_monitor_handoffs_restore_both_workspace_owners() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1920), WorkspaceId(2))
.unwrap();
for removed in [OutputId(1), OutputId(2), OutputId(1), OutputId(2)] {
let remaining = if removed == OutputId(1) {
OutputId(2)
@@ -1064,8 +1244,14 @@ mod tests {
};
outputs.focus_output(removed).unwrap();
outputs.disconnect(removed).unwrap();
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(remaining));
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(remaining));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(remaining)
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(remaining)
);
// Switching between handed-off workspaces is normal use, not an
// explicit change of their home output.
outputs.switch_workspace(remaining, WorkspaceId(1)).unwrap();
@@ -1077,8 +1263,14 @@ mod tests {
WorkspaceId(3),
)
.unwrap();
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(1))
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(2))
);
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
}
@@ -1087,8 +1279,12 @@ mod tests {
#[test]
fn selecting_a_workspace_owned_by_another_output_focuses_that_output() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
assert_eq!(
outputs.switch_workspace(OutputId(1), WorkspaceId(2)),
@@ -1100,11 +1296,19 @@ mod tests {
#[test]
fn assigning_an_unused_workspace_does_not_change_the_active_workspace() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
assert_eq!(outputs.assign_workspace(OutputId(1), WorkspaceId(2)), Ok(OutputId(1)));
assert_eq!(
outputs.assign_workspace(OutputId(1), WorkspaceId(2)),
Ok(OutputId(1))
);
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(1))
);
}
#[test]
fn two_hop_evacuation_restores_original_home() {
@@ -1117,51 +1321,94 @@ mod tests {
outputs.focus_output(OutputId(2)).unwrap();
outputs.disconnect(OutputId(1)).unwrap();
outputs.disconnect(OutputId(2)).unwrap();
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(3)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(3))
);
assert_eq!(outputs.home_output(WorkspaceId(1)), Some(OutputId(1)));
outputs.connect(OutputId(1), geometry(0), WorkspaceId(4)).unwrap();
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(4))
.unwrap();
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(1))
);
assert!(outputs.active_workspace(OutputId(3)).is_some());
}
#[test]
fn explicit_reassignment_after_evacuation_prevents_home_steal_back() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1920), WorkspaceId(2))
.unwrap();
outputs.disconnect(OutputId(1)).unwrap();
outputs
.reassign_workspace(OutputId(2), WorkspaceId(1), WorkspaceId(9))
.unwrap();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(3)).unwrap();
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(2)));
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(3))
.unwrap();
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(2))
);
assert_eq!(outputs.home_output(WorkspaceId(1)), Some(OutputId(2)));
}
#[test]
fn reassigning_last_workspace_supplies_donor_fallback() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1920), WorkspaceId(2))
.unwrap();
outputs
.reassign_workspace(OutputId(2), WorkspaceId(1), WorkspaceId(3))
.unwrap();
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(3)));
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(2)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(2))
);
}
#[test]
fn final_output_replacement_keeps_all_workspaces_reachable_without_changing_homes() {
let mut outputs = OutputWorkspaceMap::default();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.assign_workspace(OutputId(1), WorkspaceId(2)).unwrap();
outputs.switch_workspace(OutputId(1), WorkspaceId(2)).unwrap();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.assign_workspace(OutputId(1), WorkspaceId(2))
.unwrap();
outputs
.switch_workspace(OutputId(1), WorkspaceId(2))
.unwrap();
outputs.disconnect(OutputId(1)).unwrap();
outputs.connect(OutputId(3), geometry(0), WorkspaceId(3)).unwrap();
outputs
.connect(OutputId(3), geometry(0), WorkspaceId(3))
.unwrap();
assert_eq!(outputs.active_workspace(OutputId(3)), Some(WorkspaceId(2)));
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(3)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(3))
);
assert_eq!(outputs.home_output(WorkspaceId(1)), Some(OutputId(1)));
outputs.connect(OutputId(1), geometry(1920), WorkspaceId(4)).unwrap();
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
outputs
.connect(OutputId(1), geometry(1920), WorkspaceId(4))
.unwrap();
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(1))
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(1))
);
}
}
+392 -106
View File
@@ -3,8 +3,8 @@ use std::error::Error;
use std::fmt;
use ferese_layout::{
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect, ScrollingLayout,
SizeConstraints, ViewportFocusStrategy, WindowId,
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect,
ScrollingLayout, SizeConstraints, ViewportFocusStrategy, WindowId,
};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
@@ -39,7 +39,11 @@ impl Default for WorkspaceLayout {
}
impl WorkspaceLayout {
pub fn new(mode: LayoutMode, default_column_width: ColumnWidth, focus_strategy: ViewportFocusStrategy) -> Self {
pub fn new(
mode: LayoutMode,
default_column_width: ColumnWidth,
focus_strategy: ViewportFocusStrategy,
) -> Self {
match mode {
LayoutMode::Scrolling => {
let mut layout = ScrollingLayout::with_default_width(default_column_width);
@@ -95,7 +99,10 @@ impl WorkspaceLayout {
Self::Scrolling(_) => None,
};
scrolling.into_iter().flatten().chain(tree.into_iter().flatten())
scrolling
.into_iter()
.flatten()
.chain(tree.into_iter().flatten())
}
pub fn insert(
@@ -158,7 +165,12 @@ impl WorkspaceLayout {
Self::Scrolling(layout) => {
let mut layout = layout.clone();
layout
.resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), None)
.resolve_geometry_with_constraints(
bounds,
GapConfig::default(),
&HashMap::new(),
None,
)
.map(|result| result.geometry)
}
Self::Tree(layout) => layout.geometry(bounds),
@@ -190,17 +202,22 @@ impl WorkspaceLayout {
for id in &group.windows {
let constraint = constraints.entry(*id).or_default();
constraint.min_height += TAB_HEIGHT;
constraint.max_height = constraint.max_height.map(|height| height + TAB_HEIGHT);
constraint.max_height =
constraint.max_height.map(|height| height + TAB_HEIGHT);
}
}
let mut result = layout.geometry_with_constraints(bounds, gaps, &constraints, focused)?;
let mut result =
layout.geometry_with_constraints(bounds, gaps, &constraints, focused)?;
for group in groups {
if let Some(rect) = result.geometry.get_mut(&group.active) {
let height = TAB_HEIGHT.min((rect.height - 1.0).max(0.0));
let header = Rect::new(rect.x, rect.y, rect.width, height);
rect.y += height;
rect.height -= height;
result.tab_bars.push(ferese_layout::TabBar { group, rect: header });
result.tab_bars.push(ferese_layout::TabBar {
group,
rect: header,
});
}
}
Ok(result)
@@ -208,7 +225,11 @@ impl WorkspaceLayout {
}
}
pub fn automatic_axis(&self, focused: Option<WindowId>, bounds: Rect) -> Result<Axis, LayoutError> {
pub fn automatic_axis(
&self,
focused: Option<WindowId>,
bounds: Rect,
) -> Result<Axis, LayoutError> {
match self {
Self::Scrolling(_) => Ok(Axis::Horizontal),
Self::Tree(layout) => layout.automatic_axis(focused, bounds),
@@ -227,14 +248,24 @@ impl WorkspaceLayout {
}
}
pub fn move_window(&mut self, window: WindowId, direction: Direction, bounds: Rect) -> Result<bool, LayoutError> {
pub fn move_window(
&mut self,
window: WindowId,
direction: Direction,
bounds: Rect,
) -> Result<bool, LayoutError> {
match self {
Self::Scrolling(layout) => layout.move_window(window, direction),
Self::Tree(layout) => layout.move_window(window, direction, bounds),
}
}
pub fn resize_window(&mut self, window: WindowId, direction: Direction, amount: f64) -> Result<bool, LayoutError> {
pub fn resize_window(
&mut self,
window: WindowId,
direction: Direction,
amount: f64,
) -> Result<bool, LayoutError> {
match self {
Self::Scrolling(layout) => layout.resize_window(window, direction, amount),
Self::Tree(layout) => layout.resize_window(window, direction, amount),
@@ -256,7 +287,11 @@ impl WorkspaceLayout {
}
}
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, LayoutError> {
pub fn cycle_column_width(
&mut self,
window: WindowId,
presets: &[ColumnWidth],
) -> Result<bool, LayoutError> {
match self {
Self::Scrolling(layout) => layout.cycle_column_width(window, presets),
Self::Tree(_) => Ok(false),
@@ -303,7 +338,11 @@ impl WorkspaceLayout {
for column in columns {
for (row, window) in column.windows.into_iter().enumerate() {
let axis = if row == 0 { Axis::Horizontal } else { Axis::Vertical };
let axis = if row == 0 {
Axis::Horizontal
} else {
Axis::Vertical
};
tree.insert(window, previous, axis, 0.5)?;
previous = Some(window);
}
@@ -414,7 +453,11 @@ impl WorkspaceSet {
let active = WorkspaceId(1);
let workspace = Workspace {
id: active,
layout: WorkspaceLayout::new(default_layout_mode, default_column_width, scrolling_focus_strategy),
layout: WorkspaceLayout::new(
default_layout_mode,
default_column_width,
scrolling_focus_strategy,
),
floating: Vec::new(),
last_focused: None,
fullscreen: None,
@@ -502,7 +545,12 @@ impl WorkspaceSet {
}
/// Update policy without moving windows or replacing existing column widths.
pub fn reconfigure_defaults(&mut self, mode: LayoutMode, width: ColumnWidth, strategy: ViewportFocusStrategy) {
pub fn reconfigure_defaults(
&mut self,
mode: LayoutMode,
width: ColumnWidth,
strategy: ViewportFocusStrategy,
) {
self.default_layout_mode = mode;
self.default_column_width = width;
self.scrolling_focus_strategy = strategy;
@@ -539,8 +587,12 @@ impl WorkspaceSet {
debug_assert!(self.validate().is_ok());
let focused = self.active().last_focused;
let visible_floating =
focused.filter(|window| matches!(self.placement(*window), Some(WindowPlacement::Floating { .. })));
let visible_floating = focused.filter(|window| {
matches!(
self.placement(*window),
Some(WindowPlacement::Floating { .. })
)
});
Ok(visible_floating.or(self.active().fullscreen).or(focused))
}
@@ -564,15 +616,23 @@ impl WorkspaceSet {
self.placements.get(&window).copied()
}
pub fn set_active_layout_mode(&mut self, mode: LayoutMode, bounds: Rect) -> Result<bool, WorkspaceError> {
pub fn set_active_layout_mode(
&mut self,
mode: LayoutMode,
bounds: Rect,
) -> Result<bool, WorkspaceError> {
let default_column_width = self.default_column_width;
let focus_strategy = self.scrolling_focus_strategy;
let display = self.default_column_display;
let workspace = self.active_mut();
let focused = tiled_focus(workspace);
let changed = workspace
.layout
.set_mode(mode, bounds, focused, default_column_width, focus_strategy)?;
let changed = workspace.layout.set_mode(
mode,
bounds,
focused,
default_column_width,
focus_strategy,
)?;
if let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout {
layout.set_default_display(display);
}
@@ -581,7 +641,11 @@ impl WorkspaceSet {
Ok(changed)
}
pub fn set_floating_rect(&mut self, window: WindowId, rect: Rect) -> Result<(), WorkspaceError> {
pub fn set_floating_rect(
&mut self,
window: WindowId,
rect: Rect,
) -> Result<(), WorkspaceError> {
let placement = self
.placements
.get_mut(&window)
@@ -607,23 +671,35 @@ impl WorkspaceSet {
self.focus_window_with_reveal(window, false)
}
fn focus_window_with_reveal(&mut self, window: WindowId, reveal: bool) -> Result<(), WorkspaceError> {
fn focus_window_with_reveal(
&mut self,
window: WindowId,
reveal: bool,
) -> Result<(), WorkspaceError> {
if self.workspace_for_window(window) != Some(self.active) {
return Err(WorkspaceError::InvalidState(
"focused window is not on the active workspace",
));
}
if self.active().fullscreen.is_some_and(|fullscreen| {
fullscreen != window && !matches!(self.placement(window), Some(WindowPlacement::Floating { .. }))
fullscreen != window
&& !matches!(
self.placement(window),
Some(WindowPlacement::Floating { .. })
)
}) {
return Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen"));
return Err(WorkspaceError::InvalidState(
"focused window is hidden by fullscreen",
));
}
if self.placement(window) == Some(WindowPlacement::Tiled) {
if reveal {
self.active_mut().layout.activate_window(window)?;
} else {
self.active_mut().layout.activate_window_without_reveal(window)?;
self.active_mut()
.layout
.activate_window_without_reveal(window)?;
}
}
self.active_mut().last_focused = Some(window);
@@ -632,7 +708,11 @@ impl WorkspaceSet {
Ok(())
}
pub fn stack_window(&mut self, window: WindowId, target: WindowId) -> Result<(), WorkspaceError> {
pub fn stack_window(
&mut self,
window: WindowId,
target: WindowId,
) -> Result<(), WorkspaceError> {
let workspace = self
.workspace_for_window(window)
.ok_or(LayoutError::UnknownWindow(window))?;
@@ -673,7 +753,11 @@ impl WorkspaceSet {
Ok(changed)
}
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, WorkspaceError> {
pub fn cycle_column_width(
&mut self,
window: WindowId,
presets: &[ColumnWidth],
) -> Result<bool, WorkspaceError> {
if self.workspace_for_window(window) != Some(self.active)
|| self.placement(window) != Some(WindowPlacement::Tiled)
{
@@ -682,7 +766,10 @@ impl WorkspaceSet {
));
}
let changed = self.active_mut().layout.cycle_column_width(window, presets)?;
let changed = self
.active_mut()
.layout
.cycle_column_width(window, presets)?;
debug_assert!(self.validate().is_ok());
Ok(changed)
}
@@ -743,7 +830,10 @@ impl WorkspaceSet {
/// Creates an identity; display positions are assigned by the output owner.
pub fn create_workspace(&mut self) -> WorkspaceId {
let id = WorkspaceId(self.next_id);
self.next_id = self.next_id.checked_add(1).expect("workspace IDs exhausted");
self.next_id = self
.next_id
.checked_add(1)
.expect("workspace IDs exhausted");
self.workspaces.insert(
id,
Workspace {
@@ -770,7 +860,12 @@ impl WorkspaceSet {
self.activate(workspace)
}
pub fn insert_window(&mut self, window: WindowId, axis: Axis, ratio: f64) -> Result<(), WorkspaceError> {
pub fn insert_window(
&mut self,
window: WindowId,
axis: Axis,
ratio: f64,
) -> Result<(), WorkspaceError> {
if self.window_workspaces.contains_key(&window) {
return Err(WorkspaceError::DuplicateWindow(window));
}
@@ -980,7 +1075,11 @@ impl WorkspaceSet {
Ok(enabled)
}
pub fn set_fullscreen(&mut self, window: WindowId, enabled: bool) -> Result<bool, WorkspaceError> {
pub fn set_fullscreen(
&mut self,
window: WindowId,
enabled: bool,
) -> Result<bool, WorkspaceError> {
let workspace_id = self
.workspace_for_window(window)
.ok_or(LayoutError::UnknownWindow(window))?;
@@ -1014,7 +1113,9 @@ impl WorkspaceSet {
for (id, workspace) in &self.workspaces {
if workspace.id != *id {
return Err(WorkspaceError::InvalidState("workspace index is inconsistent"));
return Err(WorkspaceError::InvalidState(
"workspace index is inconsistent",
));
}
workspace.layout.validate()?;
@@ -1024,7 +1125,9 @@ impl WorkspaceSet {
|| self.window_workspaces.get(&window) != Some(id)
|| self.placements.get(&window) != Some(&WindowPlacement::Tiled)
{
return Err(WorkspaceError::InvalidState("window ownership is inconsistent"));
return Err(WorkspaceError::InvalidState(
"window ownership is inconsistent",
));
}
}
@@ -1034,7 +1137,10 @@ impl WorkspaceSet {
if !seen_floating.insert(*window)
|| !seen_windows.insert(*window)
|| self.window_workspaces.get(window) != Some(id)
|| !matches!(self.placements.get(window), Some(WindowPlacement::Floating { .. }))
|| !matches!(
self.placements.get(window),
Some(WindowPlacement::Floating { .. })
)
{
return Err(WorkspaceError::InvalidState(
"floating window ownership is inconsistent",
@@ -1046,7 +1152,9 @@ impl WorkspaceSet {
.last_focused
.is_some_and(|window| self.window_workspaces.get(&window) != Some(id))
{
return Err(WorkspaceError::InvalidState("workspace focus is inconsistent"));
return Err(WorkspaceError::InvalidState(
"workspace focus is inconsistent",
));
}
if workspace
@@ -1059,8 +1167,12 @@ impl WorkspaceSet {
}
}
if seen_windows.len() != self.window_workspaces.len() || self.placements.len() != self.window_workspaces.len() {
return Err(WorkspaceError::InvalidState("window index retains stale entries"));
if seen_windows.len() != self.window_workspaces.len()
|| self.placements.len() != self.window_workspaces.len()
{
return Err(WorkspaceError::InvalidState(
"window index retains stale entries",
));
}
Ok(())
@@ -1068,10 +1180,13 @@ impl WorkspaceSet {
}
fn first_window(workspace: &Workspace) -> Option<WindowId> {
workspace
.layout
.preferred_window()
.or_else(|| workspace.floating.iter().copied().min_by_key(|window| window.0))
workspace.layout.preferred_window().or_else(|| {
workspace
.floating
.iter()
.copied()
.min_by_key(|window| window.0)
})
}
fn tiled_focus(workspace: &Workspace) -> Option<WindowId> {
@@ -1110,7 +1225,9 @@ mod tests {
fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() {
let mut workspaces = WorkspaceSet::default();
for id in 1..=3 {
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
}
let WorkspaceLayout::Scrolling(layout) = &mut workspaces.active_mut().layout else {
panic!()
@@ -1149,18 +1266,30 @@ mod tests {
#[test]
fn reconfiguration_preserves_windows_and_current_widths_but_updates_defaults() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
let workspace = workspaces.active_id();
workspaces.reconfigure_defaults(LayoutMode::Tree, ColumnWidth::Full, ViewportFocusStrategy::Minimal);
workspaces.reconfigure_defaults(
LayoutMode::Tree,
ColumnWidth::Full,
ViewportFocusStrategy::Minimal,
);
let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else {
panic!("existing layout must survive");
};
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.5));
assert_eq!(layout.default_width(), ColumnWidth::Full);
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
assert_eq!(
workspaces.workspace_for_window(WindowId(1)),
Some(workspace)
);
workspaces.switch_to_fixture(2).unwrap();
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree);
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
assert_eq!(
workspaces.workspace_for_window(WindowId(1)),
Some(workspace)
);
}
use super::*;
@@ -1179,9 +1308,14 @@ mod tests {
#[test]
fn configured_layout_defaults_apply_to_new_workspaces() {
let mut workspaces =
WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Full, ViewportFocusStrategy::Minimal);
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
let mut workspaces = WorkspaceSet::new(
LayoutMode::Scrolling,
ColumnWidth::Full,
ViewportFocusStrategy::Minimal,
);
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
let first = workspaces
.active_mut()
.layout
@@ -1193,7 +1327,10 @@ mod tests {
)
.unwrap();
assert_eq!(first.geometry[&WindowId(1)], Rect::new(4.0, 4.0, 992.0, 792.0));
assert_eq!(
first.geometry[&WindowId(1)],
Rect::new(4.0, 4.0, 992.0, 792.0)
);
workspaces.switch_to_fixture(2).unwrap();
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
}
@@ -1201,10 +1338,14 @@ mod tests {
#[test]
fn switching_restores_workspace_focus() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
assert_eq!(workspaces.switch_to_fixture(2).unwrap(), None);
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
assert_eq!(workspaces.switch_to_fixture(1).unwrap(), Some(WindowId(1)));
assert_eq!(workspaces.switch_to_fixture(2).unwrap(), Some(WindowId(2)));
@@ -1214,8 +1355,12 @@ mod tests {
#[test]
fn moving_a_window_updates_membership_without_switching() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
let first = workspaces.active_id();
let second = workspaces
.move_window_to_fixture(WindowId(2), 2, Axis::Horizontal, 0.5)
@@ -1223,16 +1368,32 @@ mod tests {
assert_eq!(workspaces.active_id(), first);
assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(second));
assert!(!workspaces.workspace(first).unwrap().layout.contains(WindowId(2)));
assert!(workspaces.workspace(second).unwrap().layout.contains(WindowId(2)));
assert!(
!workspaces
.workspace(first)
.unwrap()
.layout
.contains(WindowId(2))
);
assert!(
workspaces
.workspace(second)
.unwrap()
.layout
.contains(WindowId(2))
);
assert!(workspaces.validate().is_ok());
}
#[test]
fn removing_focus_chooses_a_deterministic_remaining_window() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.remove_window(WindowId(1)).unwrap();
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
@@ -1243,7 +1404,9 @@ mod tests {
fn closing_a_scrolling_column_focuses_its_adjacent_column() {
let mut workspaces = WorkspaceSet::default();
for id in 1..=4 {
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
}
workspaces.focus_window(WindowId(3)).unwrap();
@@ -1255,7 +1418,9 @@ mod tests {
#[test]
fn focus_rejects_a_window_on_an_inactive_workspace() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.move_window_to_fixture(WindowId(1), 2, Axis::Horizontal, 0.5)
.unwrap();
@@ -1272,7 +1437,9 @@ mod tests {
#[test]
fn floating_toggle_removes_and_restores_tiled_membership() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
let rect = Rect::new(10.0, 20.0, 640.0, 480.0);
assert_eq!(
@@ -1298,14 +1465,21 @@ mod tests {
#[test]
fn floating_geometry_updates_are_authoritative_and_validated() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
assert_eq!(
workspaces.set_floating_rect(WindowId(1), Rect::default()),
Err(WorkspaceError::InvalidState("window is not floating"))
);
workspaces
.toggle_floating(WindowId(1), Rect::new(10.0, 20.0, 640.0, 480.0), Axis::Horizontal, 0.5)
.toggle_floating(
WindowId(1),
Rect::new(10.0, 20.0, 640.0, 480.0),
Axis::Horizontal,
0.5,
)
.unwrap();
workspaces
.set_floating_rect(WindowId(1), Rect::new(30.0, 40.0, f64::NAN, -2.0))
@@ -1327,9 +1501,15 @@ mod tests {
ViewportFocusStrategy::Center,
ViewportFocusStrategy::Paged,
] {
let mut workspaces = WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Proportion(0.5), strategy);
let mut workspaces = WorkspaceSet::new(
LayoutMode::Scrolling,
ColumnWidth::Proportion(0.5),
strategy,
);
for id in 1..=3 {
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
}
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
let gaps = GapConfig {
@@ -1379,13 +1559,20 @@ mod tests {
ViewportFocusStrategy::Paged,
);
for id in 1..=4 {
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
}
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
let before = workspaces
.active_mut()
.layout
.resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(4)))
.resolve_geometry_with_constraints(
bounds,
GapConfig::default(),
&HashMap::new(),
Some(WindowId(4)),
)
.unwrap();
let scroll = workspaces.active().layout.viewport_x();
workspaces
@@ -1399,7 +1586,12 @@ mod tests {
let after = workspaces
.active_mut()
.layout
.resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(5)))
.resolve_geometry_with_constraints(
bounds,
GapConfig::default(),
&HashMap::new(),
Some(WindowId(5)),
)
.unwrap();
assert_eq!(before.geometry, after.geometry);
assert_eq!(scroll, workspaces.active().layout.viewport_x());
@@ -1410,7 +1602,9 @@ mod tests {
#[test]
fn floating_window_can_follow_its_parent_workspace_without_focus() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
let parent_workspace = workspaces.active_id();
workspaces.switch_to_fixture(2).unwrap();
workspaces
@@ -1422,7 +1616,10 @@ mod tests {
)
.unwrap();
assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(parent_workspace));
assert_eq!(
workspaces.workspace_for_window(WindowId(2)),
Some(parent_workspace)
);
assert_eq!(
workspaces.workspace(parent_workspace).unwrap().last_focused,
Some(WindowId(1))
@@ -1433,7 +1630,9 @@ mod tests {
#[test]
fn fullscreen_preserves_underlying_placement() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
let before = workspaces.placement(WindowId(1));
assert!(workspaces.toggle_fullscreen(WindowId(1)).unwrap());
@@ -1451,9 +1650,13 @@ mod tests {
#[test]
fn mapping_under_fullscreen_does_not_change_focus() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
assert_eq!(workspaces.active().fullscreen, Some(WindowId(1)));
@@ -1465,10 +1668,17 @@ mod tests {
fn floating_dialog_can_focus_above_fullscreen() {
let mut workspaces = WorkspaceSet::default();
let workspace = workspaces.active_id();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
workspaces
.insert_floating_window(WindowId(2), workspace, Rect::new(100.0, 100.0, 500.0, 400.0), true)
.insert_floating_window(
WindowId(2),
workspace,
Rect::new(100.0, 100.0, 500.0, 400.0),
true,
)
.unwrap();
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
@@ -1482,10 +1692,14 @@ mod tests {
#[test]
fn switching_to_fullscreen_restores_the_visible_window() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
workspaces.switch_to_fixture(2).unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.move_window_to_fixture(WindowId(2), 1, Axis::Horizontal, 0.5)
.unwrap();
@@ -1494,7 +1708,9 @@ mod tests {
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
assert_eq!(
workspaces.focus_window(WindowId(2)),
Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen"))
Err(WorkspaceError::InvalidState(
"focused window is hidden by fullscreen"
))
);
assert!(workspaces.validate().is_ok());
}
@@ -1502,8 +1718,12 @@ mod tests {
#[test]
fn grouping_tiled_windows_preserves_workspace_membership() {
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
workspaces.stack_window(WindowId(2), WindowId(1)).unwrap();
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
@@ -1524,16 +1744,26 @@ mod tests {
fn layout_mode_conversion_preserves_membership_and_focus() {
let mut workspaces = WorkspaceSet::default();
for id in 1..=4 {
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
}
workspaces.focus_window(WindowId(2)).unwrap();
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
assert!(workspaces.set_active_layout_mode(LayoutMode::Tree, bounds).unwrap());
assert!(
workspaces
.set_active_layout_mode(LayoutMode::Tree, bounds)
.unwrap()
);
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree);
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
assert_eq!(
workspaces.active().layout.window_ids().collect::<HashSet<_>>(),
workspaces
.active()
.layout
.window_ids()
.collect::<HashSet<_>>(),
HashSet::from([WindowId(1), WindowId(2), WindowId(3), WindowId(4)])
);
@@ -1553,9 +1783,13 @@ mod tests {
ColumnWidth::Proportion(0.5),
ViewportFocusStrategy::Minimal,
);
workspaces.insert_window(ids[0], Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(ids[0], Axis::Horizontal, 0.5)
.unwrap();
for pair in ids.windows(2) {
workspaces.insert_window(pair[1], Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(pair[1], Axis::Horizontal, 0.5)
.unwrap();
workspaces.stack_window(pair[1], pair[0]).unwrap();
}
workspaces
@@ -1564,7 +1798,13 @@ mod tests {
#[test]
fn stacked_tree_mode_conversions_preserve_membership_focus_and_order() {
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
let all_ids = [WindowId(30), WindowId(10), WindowId(20), WindowId(5), WindowId(40)];
let all_ids = [
WindowId(30),
WindowId(10),
WindowId(20),
WindowId(5),
WindowId(40),
];
for count in [2, 3, 5] {
let ids = &all_ids[..count];
let expected = ids.iter().copied().collect::<HashSet<_>>();
@@ -1583,7 +1823,11 @@ mod tests {
assert_eq!(workspaces.active().last_focused, Some(*focused));
assert!(workspaces.validate().is_ok());
for mode in [LayoutMode::Scrolling, LayoutMode::Tree, LayoutMode::Scrolling] {
for mode in [
LayoutMode::Scrolling,
LayoutMode::Tree,
LayoutMode::Scrolling,
] {
assert!(workspaces.set_active_layout_mode(mode, bounds).unwrap());
let windows = workspaces.active().layout.window_ids().collect::<Vec<_>>();
assert_eq!(windows.len(), ids.len());
@@ -1605,15 +1849,29 @@ mod tests {
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
let mut workspaces = tree_stack_workspaces(&[WindowId(30), WindowId(10), WindowId(20)]);
workspaces.focus_window(WindowId(10)).unwrap();
workspaces.insert_window(WindowId(5), Axis::Vertical, 0.5).unwrap();
workspaces.insert_window(WindowId(40), Axis::Horizontal, 0.5).unwrap();
workspaces.insert_window(WindowId(50), Axis::Vertical, 0.5).unwrap();
workspaces
.insert_window(WindowId(5), Axis::Vertical, 0.5)
.unwrap();
workspaces
.insert_window(WindowId(40), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.insert_window(WindowId(50), Axis::Vertical, 0.5)
.unwrap();
workspaces.stack_window(WindowId(50), WindowId(40)).unwrap();
workspaces.focus_window(WindowId(40)).unwrap();
let before = workspaces.active().layout.window_ids().collect::<HashSet<_>>();
let before = workspaces
.active()
.layout
.window_ids()
.collect::<HashSet<_>>();
assert_eq!(before.len(), 6);
assert!(workspaces.active().layout.geometry(bounds).unwrap().len() < before.len());
for mode in [LayoutMode::Scrolling, LayoutMode::Tree, LayoutMode::Scrolling] {
for mode in [
LayoutMode::Scrolling,
LayoutMode::Tree,
LayoutMode::Scrolling,
] {
assert!(workspaces.set_active_layout_mode(mode, bounds).unwrap());
let windows = workspaces.active().layout.window_ids().collect::<Vec<_>>();
assert_eq!(windows.len(), before.len());
@@ -1631,11 +1889,19 @@ mod tests {
for _ in 0..32 {
let mut workspaces = tree_stack_workspaces(&[WindowId(30), WindowId(20), WindowId(40)]);
workspaces.active_mut().last_focused = None;
assert_eq!(workspaces.active().layout.preferred_window(), Some(WindowId(40)));
workspaces.insert_window(WindowId(10), Axis::Horizontal, 0.5).unwrap();
assert_eq!(
workspaces.active().layout.preferred_window(),
Some(WindowId(40))
);
workspaces
.insert_window(WindowId(10), Axis::Horizontal, 0.5)
.unwrap();
workspaces.remove_window(WindowId(10)).unwrap();
assert_eq!(workspaces.active().last_focused, Some(WindowId(40)));
assert_eq!(workspaces.active().layout.preferred_window(), Some(WindowId(40)));
assert_eq!(
workspaces.active().layout.preferred_window(),
Some(WindowId(40))
);
assert!(workspaces.validate().is_ok());
}
}
@@ -1644,14 +1910,19 @@ mod tests {
fn scrolling_column_commands_preserve_workspace_state() {
let mut workspaces = WorkspaceSet::default();
for id in 1..=3 {
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
}
workspaces.stack_window(WindowId(3), WindowId(2)).unwrap();
assert!(workspaces.extract_window(WindowId(3)).unwrap());
assert!(
workspaces
.cycle_column_width(WindowId(3), &[ColumnWidth::Proportion(0.5), ColumnWidth::Full],)
.cycle_column_width(
WindowId(3),
&[ColumnWidth::Proportion(0.5), ColumnWidth::Full],
)
.unwrap()
);
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
@@ -1679,7 +1950,9 @@ mod tests {
0 if windows.len() < 48 => {
let window = WindowId(next_window);
next_window += 1;
workspaces.insert_window(window, random_axis(random), 0.5).unwrap();
workspaces
.insert_window(window, random_axis(random), 0.5)
.unwrap();
windows.push(window);
}
1 if !windows.is_empty() => {
@@ -1726,7 +1999,8 @@ mod tests {
})
.collect::<Vec<_>>();
if let Some(window) = candidates.get(random as usize % candidates.len().max(1)) {
if let Some(window) = candidates.get(random as usize % candidates.len().max(1))
{
workspaces.focus_window(*window).unwrap();
}
}
@@ -1774,7 +2048,9 @@ mod tab_geometry_tests {
)
.unwrap();
}
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else {
panic!()
};
@@ -1785,9 +2061,13 @@ mod tab_geometry_tests {
fn default_column_display_only_changes_future_columns_and_reaches_new_workspaces() {
use ferese_layout::ColumnDisplay;
let mut workspaces = WorkspaceSet::default();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.set_default_column_display(ColumnDisplay::Tabbed);
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else {
panic!()
};
@@ -1795,13 +2075,17 @@ mod tab_geometry_tests {
assert_eq!(layout.columns()[1].display, ColumnDisplay::Tabbed);
let next = workspaces.create_workspace();
workspaces.activate(next).unwrap();
workspaces.insert_window(WindowId(3), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(3), Axis::Horizontal, 0.5)
.unwrap();
let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else {
panic!()
};
assert_eq!(layout.columns()[0].display, ColumnDisplay::Tabbed);
workspaces.set_default_column_display(ColumnDisplay::Stacked);
workspaces.insert_window(WindowId(4), Axis::Horizontal, 0.5).unwrap();
workspaces
.insert_window(WindowId(4), Axis::Horizontal, 0.5)
.unwrap();
let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else {
panic!()
};
@@ -1812,7 +2096,9 @@ mod tab_geometry_tests {
#[test]
fn tab_header_reserves_space_and_disappears_after_leaving() {
let mut layout = WorkspaceLayout::Tree(LayoutTree::default());
layout.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
layout
.insert(WindowId(1), None, Axis::Horizontal, 0.5)
.unwrap();
layout
.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
.unwrap();
+74 -21
View File
@@ -56,13 +56,22 @@ impl OutputWorkspaceMap {
.filter(|id| workspaces.workspace(*id).is_some())
}
pub fn ordered_workspaces(&self, workspaces: &WorkspaceSet, output: OutputId) -> Vec<WorkspaceId> {
pub fn ordered_workspaces(
&self,
workspaces: &WorkspaceSet,
output: OutputId,
) -> Vec<WorkspaceId> {
self.ordered_workspace_ids(workspaces, output).collect()
}
/// Out-of-range numeric selection resolves to the trailing spare. Looking
/// up a gesture target never creates workspaces or changes history.
pub fn workspace_at(&self, workspaces: &WorkspaceSet, output: OutputId, index: u32) -> Option<WorkspaceId> {
pub fn workspace_at(
&self,
workspaces: &WorkspaceSet,
output: OutputId,
index: u32,
) -> Option<WorkspaceId> {
let index = index.checked_sub(1)? as usize;
self.ordered_workspace_ids(workspaces, output)
.nth(index)
@@ -76,8 +85,13 @@ impl OutputWorkspaceMap {
workspaces: &mut WorkspaceSet,
transitions: &HashSet<WorkspaceId>,
) -> Vec<WorkspaceId> {
if let Some(active) = self.focused_output().and_then(|output| self.active_workspace(output)) {
workspaces.activate(active).expect("output's active workspace exists");
if let Some(active) = self
.focused_output()
.and_then(|output| self.active_workspace(output))
{
workspaces
.activate(active)
.expect("output's active workspace exists");
}
let mut protected = HashSet::new();
@@ -129,7 +143,11 @@ mod tests {
let mut outputs = OutputWorkspaceMap::default();
let output = OutputId(1);
outputs
.connect(output, OutputGeometry::new(0, 0, 1000, 800), set.active_id())
.connect(
output,
OutputGeometry::new(0, 0, 1000, 800),
set.active_id(),
)
.unwrap();
(set, outputs, output)
@@ -142,7 +160,10 @@ mod tests {
for output in outputs.connected_outputs() {
let ids = outputs.ordered_workspaces(set, output);
assert!(set.workspace(*ids.last().unwrap()).unwrap().is_empty());
let empties = ids.iter().filter(|id| set.workspace(**id).unwrap().is_empty()).count();
let empties = ids
.iter()
.filter(|id| set.workspace(**id).unwrap().is_empty())
.count();
let active_middle_empty = outputs
.active_workspace(output)
.is_some_and(|id| Some(&id) != ids.last() && set.workspace(id).unwrap().is_empty());
@@ -156,12 +177,14 @@ mod tests {
let first = set.active_id();
reconcile(&mut set, &mut outputs);
assert_eq!(set.iter().count(), 1);
set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
set.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
reconcile(&mut set, &mut outputs);
let second = outputs.workspace_at(&set, output, 2).unwrap();
outputs.switch_workspace(output, second).unwrap();
reconcile(&mut set, &mut outputs);
set.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
set.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
reconcile(&mut set, &mut outputs);
let spare = outputs.workspace_at(&set, output, 3).unwrap();
@@ -187,7 +210,8 @@ mod tests {
fn selected_empty_survives_until_leaving_but_history_does_not_pin_it() {
let (mut set, mut outputs, output) = setup();
let first = set.active_id();
set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
set.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
reconcile(&mut set, &mut outputs);
let spare = outputs.workspace_at(&set, output, 2).unwrap();
set.remove_window(WindowId(1)).unwrap();
@@ -205,7 +229,8 @@ mod tests {
fn transitions_temporarily_protect_empty_workspaces_and_cancelled_lookup_has_no_effect() {
let (mut set, mut outputs, output) = setup();
let first = set.active_id();
set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
set.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
reconcile(&mut set, &mut outputs);
let spare = outputs.workspace_at(&set, output, 2).unwrap();
let before = outputs.workspace_views(&set);
@@ -246,7 +271,8 @@ mod tests {
fn monitors_have_independent_positions_and_reconnect_restores_occupied_workspaces() {
let (mut set, mut outputs, left) = setup();
let first = set.active_id();
set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
set.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
reconcile(&mut set, &mut outputs);
let right = OutputId(2);
let second = set.create_workspace();
@@ -255,16 +281,25 @@ mod tests {
.unwrap();
outputs.focus_output(right).unwrap();
reconcile(&mut set, &mut outputs);
set.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
set.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
reconcile(&mut set, &mut outputs);
assert_eq!(outputs.workspace_at(&set, left, 1), Some(first));
assert_eq!(outputs.workspace_at(&set, right, 1), Some(second));
assert_eq!(
outputs.workspace_views(&set).iter().filter(|view| view.visible).count(),
outputs
.workspace_views(&set)
.iter()
.filter(|view| view.visible)
.count(),
2
);
assert_eq!(
outputs.workspace_views(&set).iter().filter(|view| view.focused).count(),
outputs
.workspace_views(&set)
.iter()
.filter(|view| view.focused)
.count(),
1
);
@@ -295,7 +330,11 @@ mod sequences {
let mut set = WorkspaceSet::default();
let mut outputs = OutputWorkspaceMap::default();
outputs
.connect(OutputId(1), OutputGeometry::new(0, 0, 1000, 800), set.active_id())
.connect(
OutputId(1),
OutputGeometry::new(0, 0, 1000, 800),
set.active_id(),
)
.unwrap();
let mut next_window = 1;
let mut seed = 0x1234_5678_u64;
@@ -313,11 +352,13 @@ mod sequences {
match seed % 5 {
0 => {
set.insert_window(WindowId(next_window), Axis::Horizontal, 0.5).unwrap();
set.insert_window(WindowId(next_window), Axis::Horizontal, 0.5)
.unwrap();
next_window += 1;
}
1 if !windows.is_empty() => {
set.remove_window(windows[seed as usize % windows.len()]).unwrap();
set.remove_window(windows[seed as usize % windows.len()])
.unwrap();
}
2 => {
let index = (seed % 9 + 1) as u32;
@@ -326,8 +367,13 @@ mod sequences {
}
3 if !windows.is_empty() => {
let target = outputs.workspace_at(&set, output, u32::MAX).unwrap();
set.move_window_to_workspace(windows[seed as usize % windows.len()], target, Axis::Horizontal, 0.5)
.unwrap();
set.move_window_to_workspace(
windows[seed as usize % windows.len()],
target,
Axis::Horizontal,
0.5,
)
.unwrap();
}
_ => {
if outputs.active_workspace(OutputId(2)).is_some() {
@@ -335,7 +381,11 @@ mod sequences {
} else {
let fallback = set.create_workspace();
outputs
.connect(OutputId(2), OutputGeometry::new(1000, 0, 1000, 800), fallback)
.connect(
OutputId(2),
OutputGeometry::new(1000, 0, 1000, 800),
fallback,
)
.unwrap();
outputs.focus_output(OutputId(2)).unwrap();
}
@@ -348,7 +398,10 @@ mod sequences {
assert_eq!(views.iter().filter(|view| view.focused).count(), 1);
assert_eq!(views.len(), set.iter().count());
for output in outputs.connected_outputs() {
let rows = views.iter().filter(|view| view.output == output).collect::<Vec<_>>();
let rows = views
.iter()
.filter(|view| view.output == output)
.collect::<Vec<_>>();
assert_eq!(rows.last().unwrap().window_count, 0);
for (index, row) in rows.iter().enumerate() {
assert_eq!(row.index, index as u32 + 1);