Format Rust sources at 100 columns
This commit is contained in:
1 parent
4661f595f8
commit
610478f8c7
336 files changed
+23497
-7892
No files matched your search
@@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user