Add per-monitor workspace back-and-forth
This commit is contained in:
1 parent
cb6fe73e44
commit
17b8482433
12 files changed
+405
-20
No files matched your search
@@ -59,9 +59,19 @@ pub enum WorkspaceSwitch {
|
||||
struct OutputState {
|
||||
geometry: OutputGeometry,
|
||||
active: WorkspaceId,
|
||||
previous: Option<WorkspaceId>,
|
||||
workspaces: HashSet<WorkspaceId>,
|
||||
}
|
||||
|
||||
impl OutputState {
|
||||
fn activate(&mut self, workspace: WorkspaceId) {
|
||||
if self.active != workspace {
|
||||
self.previous = self.workspaces.contains(&self.active).then_some(self.active);
|
||||
self.active = workspace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct EvacuatedWorkspace {
|
||||
workspace: WorkspaceId,
|
||||
@@ -93,6 +103,14 @@ impl OutputWorkspaceMap {
|
||||
self.outputs.get(&output).map(|state| state.active)
|
||||
}
|
||||
|
||||
pub fn previous_workspace(&self, output: OutputId) -> Option<WorkspaceId> {
|
||||
self.outputs.get(&output).and_then(|state| state.previous)
|
||||
}
|
||||
|
||||
pub fn history_workspaces(&self) -> impl Iterator<Item = WorkspaceId> + '_ {
|
||||
self.outputs.values().filter_map(|state| state.previous)
|
||||
}
|
||||
|
||||
pub fn output_for_workspace(&self, workspace: WorkspaceId) -> Option<OutputId> {
|
||||
self.assignments.get(&workspace).copied()
|
||||
}
|
||||
@@ -105,6 +123,9 @@ impl OutputWorkspaceMap {
|
||||
&& let Some(output) = self.outputs.get_mut(&owner)
|
||||
{
|
||||
output.workspaces.remove(&workspace);
|
||||
if output.previous == Some(workspace) {
|
||||
output.previous = None;
|
||||
}
|
||||
}
|
||||
self.revisions.remove(&workspace);
|
||||
self.evacuations.retain(|_, record| {
|
||||
@@ -154,6 +175,9 @@ impl OutputWorkspaceMap {
|
||||
&& let Some(target_state) = self.outputs.get_mut(&target)
|
||||
{
|
||||
target_state.workspaces.remove(&evacuated.workspace);
|
||||
if target_state.previous == Some(evacuated.workspace) {
|
||||
target_state.previous = None;
|
||||
}
|
||||
if target_state.active == evacuated.workspace
|
||||
&& let Some(active) = target_state
|
||||
.workspaces
|
||||
@@ -161,7 +185,7 @@ impl OutputWorkspaceMap {
|
||||
.copied()
|
||||
.min_by_key(|workspace| workspace.0)
|
||||
{
|
||||
target_state.active = active;
|
||||
target_state.activate(active);
|
||||
}
|
||||
}
|
||||
self.assignments.insert(evacuated.workspace, output);
|
||||
@@ -194,6 +218,7 @@ impl OutputWorkspaceMap {
|
||||
OutputState {
|
||||
geometry,
|
||||
active,
|
||||
previous: None,
|
||||
workspaces: reclaimed.into_iter().collect(),
|
||||
},
|
||||
);
|
||||
@@ -229,7 +254,7 @@ impl OutputWorkspaceMap {
|
||||
self.outputs
|
||||
.get_mut(&target)
|
||||
.expect("migration target is connected")
|
||||
.active = removed.active;
|
||||
.activate(removed.active);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,14 +301,14 @@ impl OutputWorkspaceMap {
|
||||
self.outputs
|
||||
.get_mut(&owner)
|
||||
.expect("workspace owner is connected")
|
||||
.active = workspace;
|
||||
.activate(workspace);
|
||||
return Ok(WorkspaceSwitch::FocusedExisting(owner));
|
||||
}
|
||||
|
||||
let reassigned = self.assignments.insert(workspace, output) != Some(output);
|
||||
let state = self.outputs.get_mut(&output).expect("output was checked above");
|
||||
state.workspaces.insert(workspace);
|
||||
state.active = workspace;
|
||||
state.activate(workspace);
|
||||
self.focused = Some(output);
|
||||
if reassigned {
|
||||
self.bump_revision(workspace);
|
||||
@@ -293,6 +318,20 @@ impl OutputWorkspaceMap {
|
||||
Ok(WorkspaceSwitch::Activated(output))
|
||||
}
|
||||
|
||||
pub fn select_workspace(
|
||||
&mut self,
|
||||
output: OutputId,
|
||||
workspace: WorkspaceId,
|
||||
auto_back_and_forth: bool,
|
||||
) -> Result<WorkspaceSwitch, OutputError> {
|
||||
let workspace = if auto_back_and_forth && self.active_workspace(output) == Some(workspace) {
|
||||
self.previous_workspace(output).unwrap_or(workspace)
|
||||
} else {
|
||||
workspace
|
||||
};
|
||||
self.switch_workspace(output, workspace)
|
||||
}
|
||||
|
||||
pub fn assign_workspace(&mut self, output: OutputId, workspace: WorkspaceId) -> Result<OutputId, OutputError> {
|
||||
if !self.outputs.contains_key(&output) {
|
||||
return Err(OutputError::UnknownOutput(output));
|
||||
@@ -351,6 +390,12 @@ impl OutputWorkspaceMap {
|
||||
if !state.workspaces.contains(&state.active) {
|
||||
return false;
|
||||
}
|
||||
if state
|
||||
.previous
|
||||
.is_some_and(|previous| previous == state.active || !state.workspaces.contains(&previous))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for workspace in &state.workspaces {
|
||||
if self.assignments.get(workspace) != Some(output) {
|
||||
return false;
|
||||
@@ -406,6 +451,112 @@ mod tests {
|
||||
OutputGeometry::new(x, 0, 1_920, 1_080)
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(outputs.previous_workspace(display), None);
|
||||
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();
|
||||
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();
|
||||
outputs.select_workspace(display, current, true).unwrap();
|
||||
assert_eq!(outputs.active_workspace(display), Some(expected));
|
||||
assert_eq!(outputs.previous_workspace(display), Some(current));
|
||||
}
|
||||
assert!(outputs.validate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_monitors_workspace_updates_only_its_owners_history() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
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.switch_workspace(left, WorkspaceId(3)).unwrap();
|
||||
outputs.switch_workspace(right, WorkspaceId(4)).unwrap();
|
||||
assert_eq!(
|
||||
outputs.select_workspace(left, WorkspaceId(2), true),
|
||||
Ok(WorkspaceSwitch::FocusedExisting(right))
|
||||
);
|
||||
assert_eq!(outputs.active_workspace(left), Some(WorkspaceId(3)));
|
||||
assert_eq!(outputs.previous_workspace(left), Some(WorkspaceId(1)));
|
||||
assert_eq!(outputs.previous_workspace(right), Some(WorkspaceId(4)));
|
||||
let previous = outputs.previous_workspace(right).unwrap();
|
||||
outputs.switch_workspace(right, previous).unwrap();
|
||||
assert_eq!(outputs.active_workspace(right), Some(WorkspaceId(4)));
|
||||
assert_eq!(outputs.previous_workspace(right), Some(WorkspaceId(2)));
|
||||
assert!(outputs.validate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconnect_clears_history_for_workspaces_reclaimed_by_another_output() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
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.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();
|
||||
assert_eq!(outputs.previous_workspace(left), None);
|
||||
assert_eq!(outputs.previous_workspace(right), None);
|
||||
assert!(outputs.validate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
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.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();
|
||||
assert_eq!(outputs.active_workspace(display), Some(WorkspaceId(2)));
|
||||
assert_eq!(
|
||||
outputs.select_workspace(OutputId(99), WorkspaceId(2), true),
|
||||
Err(OutputError::UnknownOutput(OutputId(99)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_previous_workspace_survives_pruning_until_history_moves_on() {
|
||||
let mut workspaces = crate::WorkspaceSet::default();
|
||||
let first = workspaces.active_id();
|
||||
let second = workspaces.ensure_numeric(2).unwrap();
|
||||
let third = workspaces.ensure_numeric(3).unwrap();
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
let display = OutputId(1);
|
||||
outputs.connect(display, geometry(0), first).unwrap();
|
||||
outputs.switch_workspace(display, second).unwrap();
|
||||
workspaces.activate(second).unwrap();
|
||||
let protected = outputs.history_workspaces().chain([second]).collect();
|
||||
let removed = workspaces.prune_empty(&protected);
|
||||
assert!(removed.contains(&third));
|
||||
assert!(workspaces.workspace(first).is_some());
|
||||
let third = workspaces.ensure_numeric(3).unwrap();
|
||||
outputs.switch_workspace(display, third).unwrap();
|
||||
workspaces.activate(third).unwrap();
|
||||
let protected = outputs.history_workspaces().chain([third]).collect();
|
||||
for workspace in workspaces.prune_empty(&protected) {
|
||||
outputs.forget_workspace(workspace);
|
||||
}
|
||||
assert!(workspaces.workspace(first).is_none());
|
||||
assert!(workspaces.workspace(second).is_some());
|
||||
assert_eq!(outputs.previous_workspace(display), Some(second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_outputs_receive_distinct_workspaces() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
|
||||
Reference in new issue
Block a user