Prune unused empty workspaces while preserving visible outputs
This commit is contained in:
1 parent
0295b5be3e
commit
f87039562b
5 files changed
+148
-2
No files matched your search
@@ -507,7 +507,11 @@ impl Ferese {
|
|||||||
.map(|workspace| workspace.id)
|
.map(|workspace| workspace.id)
|
||||||
{
|
{
|
||||||
self.activate_managed_workspace(workspace);
|
self.activate_managed_workspace(workspace);
|
||||||
} else if next {
|
} else if next
|
||||||
|
&& self.workspaces.workspace(current).is_some_and(|workspace| {
|
||||||
|
workspace.layout.window_ids().next().is_some() || !workspace.floating.is_empty()
|
||||||
|
})
|
||||||
|
{
|
||||||
let next_number = self
|
let next_number = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -1263,6 +1263,15 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn relayout(&mut self) {
|
pub fn relayout(&mut self) {
|
||||||
|
let protected = self
|
||||||
|
.output_workspaces
|
||||||
|
.connected_outputs()
|
||||||
|
.filter_map(|output| self.output_workspaces.active_workspace(output))
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
for workspace in self.workspaces.prune_empty(&protected) {
|
||||||
|
self.output_workspaces.forget_workspace(workspace);
|
||||||
|
self.viewport_animations.remove(&workspace);
|
||||||
|
}
|
||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
self.configure_lock_surfaces();
|
self.configure_lock_surfaces();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,37 @@ impl OutputWorkspaceMap {
|
|||||||
self.assignments.get(&workspace).copied()
|
self.assignments.get(&workspace).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn forget_workspace(&mut self, workspace: WorkspaceId) -> bool {
|
||||||
|
if self
|
||||||
|
.outputs
|
||||||
|
.values()
|
||||||
|
.any(|output| output.active == workspace)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(owner) = self.assignments.remove(&workspace)
|
||||||
|
&& let Some(output) = self.outputs.get_mut(&owner)
|
||||||
|
{
|
||||||
|
output.workspaces.remove(&workspace);
|
||||||
|
}
|
||||||
|
self.revisions.remove(&workspace);
|
||||||
|
self.evacuations.retain(|_, record| {
|
||||||
|
record
|
||||||
|
.workspaces
|
||||||
|
.retain(|entry| entry.workspace != workspace);
|
||||||
|
if record.active == workspace {
|
||||||
|
if let Some(entry) = record.workspaces.first() {
|
||||||
|
record.active = entry.workspace;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
debug_assert!(self.validate());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub fn connected_outputs(&self) -> impl Iterator<Item = OutputId> + '_ {
|
pub fn connected_outputs(&self) -> impl Iterator<Item = OutputId> + '_ {
|
||||||
self.outputs.keys().copied()
|
self.outputs.keys().copied()
|
||||||
}
|
}
|
||||||
@@ -379,6 +410,27 @@ impl OutputWorkspaceMap {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn forgetting_an_inactive_workspace_prevents_hotplug_resurrection() {
|
||||||
|
let mut outputs = super::OutputWorkspaceMap::default();
|
||||||
|
let display = super::OutputId(1);
|
||||||
|
let geometry = super::OutputGeometry::new(0, 0, 1920, 1080);
|
||||||
|
let first = super::WorkspaceId(1);
|
||||||
|
let second = super::WorkspaceId(2);
|
||||||
|
outputs.connect(display, geometry, first).unwrap();
|
||||||
|
assert!(!outputs.forget_workspace(first));
|
||||||
|
outputs.switch_workspace(display, second).unwrap();
|
||||||
|
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();
|
||||||
|
assert_eq!(outputs.active_workspace(display), Some(second));
|
||||||
|
assert_eq!(outputs.output_for_workspace(first), None);
|
||||||
|
assert!(outputs.validate());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn changing_output_geometry_preserves_workspace_ownership_and_focus() {
|
fn changing_output_geometry_preserves_workspace_ownership_and_focus() {
|
||||||
let mut outputs = super::OutputWorkspaceMap::default();
|
let mut outputs = super::OutputWorkspaceMap::default();
|
||||||
|
|||||||
@@ -523,6 +523,38 @@ impl WorkspaceSet {
|
|||||||
self.workspaces.values()
|
self.workspaces.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prune_empty(&mut self, protected: &HashSet<WorkspaceId>) -> Vec<WorkspaceId> {
|
||||||
|
let mut empty = self
|
||||||
|
.workspaces
|
||||||
|
.values()
|
||||||
|
.filter(|workspace| {
|
||||||
|
workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty()
|
||||||
|
})
|
||||||
|
.map(|workspace| workspace.id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
empty.sort_by_key(|id| id.0);
|
||||||
|
let visible_empty = empty
|
||||||
|
.iter()
|
||||||
|
.any(|id| *id == self.active || protected.contains(id));
|
||||||
|
let spare = if visible_empty {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
empty.first().copied()
|
||||||
|
};
|
||||||
|
let mut removed = Vec::new();
|
||||||
|
for id in empty {
|
||||||
|
if id == self.active || protected.contains(&id) || Some(id) == spare {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(workspace) = self.workspaces.remove(&id) {
|
||||||
|
self.names.remove(&workspace.name);
|
||||||
|
removed.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug_assert!(self.validate().is_ok());
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
|
||||||
pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> {
|
pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> {
|
||||||
self.window_workspaces.get(&window).copied()
|
self.window_workspaces.get(&window).copied()
|
||||||
}
|
}
|
||||||
@@ -1068,6 +1100,52 @@ fn finite_or_zero(value: f64) -> f64 {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn pruning_keeps_windows_and_one_empty_workspace() {
|
||||||
|
let mut set = super::WorkspaceSet::default();
|
||||||
|
let first = set.active_id();
|
||||||
|
set.insert_window(super::WindowId(1), super::Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
let floating = set.ensure_numeric(2).unwrap();
|
||||||
|
set.insert_floating_window(
|
||||||
|
super::WindowId(2),
|
||||||
|
floating,
|
||||||
|
super::Rect::new(0., 0., 100., 100.),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let spare = set.ensure_numeric(3).unwrap();
|
||||||
|
let excess = set.ensure_numeric(4).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
set.prune_empty(&std::collections::HashSet::new()),
|
||||||
|
vec![excess]
|
||||||
|
);
|
||||||
|
assert!(set.workspace(first).is_some());
|
||||||
|
assert!(set.workspace(floating).is_some());
|
||||||
|
assert!(set.workspace(spare).is_some());
|
||||||
|
set.activate(spare).unwrap();
|
||||||
|
set.remove_window(super::WindowId(1)).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
set.prune_empty(&std::collections::HashSet::new()),
|
||||||
|
vec![first]
|
||||||
|
);
|
||||||
|
assert!(set.validate().is_ok());
|
||||||
|
assert_ne!(set.ensure_numeric(1).unwrap(), first);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pruning_never_removes_another_displays_active_workspace() {
|
||||||
|
let mut set = super::WorkspaceSet::default();
|
||||||
|
let first = set.active_id();
|
||||||
|
let second = set.ensure_numeric(2).unwrap();
|
||||||
|
let third = set.ensure_numeric(3).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
set.prune_empty(&std::collections::HashSet::from([first, second])),
|
||||||
|
vec![third]
|
||||||
|
);
|
||||||
|
assert!(set.workspace(second).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() {
|
fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() {
|
||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
|
|||||||
@@ -393,7 +393,10 @@ Settings → Notifications controls popups, Do Not Disturb, and the default time
|
|||||||
|
|
||||||
A single-output session starts with workspace 1. Numbered workspace shortcuts create
|
A single-output session starts with workspace 1. Numbered workspace shortcuts create
|
||||||
workspaces on demand; switching forward past the last workspace also creates the
|
workspaces on demand; switching forward past the last workspace also creates the
|
||||||
next one. Additional displays receive their own workspace.
|
next one when the current workspace contains windows. Empty workspaces are
|
||||||
|
cleaned up automatically, keeping at most one empty workspace in addition to
|
||||||
|
any empty workspace currently shown on another display. Additional displays
|
||||||
|
receive their own workspace.
|
||||||
|
|
||||||
Shell rounding is controlled in Settings → Appearance → Shell corner radius.
|
Shell rounding is controlled in Settings → Appearance → Shell corner radius.
|
||||||
Small controls cap the radius to fit their size. Window rounding remains under
|
Small controls cap the radius to fit their size. Window rounding remains under
|
||||||
|
|||||||
Reference in new issue
Block a user