Unify per-monitor workspace numbering and lifecycle policy

This commit is contained in:
Samuel Onoja committed 2026-10-01 18:24:12 +01:00
1 parent 07bda5a21c
commit 1441875096
20 files changed
+707 -316

No files matched your search

+7 -28
View File
@@ -781,39 +781,18 @@ impl Ferese {
}
}
pub(crate) fn relative_workspace_target(&mut self, next: bool) -> Option<ferese_core::WorkspaceId> {
pub(crate) fn relative_workspace_target(&self, next: bool) -> Option<ferese_core::WorkspaceId> {
let output = self.output_workspaces.focused_output()?;
let current = self.output_workspaces.active_workspace(output)?;
let mut candidates = self.workspaces.ordered();
candidates.retain(|workspace| {
self.output_workspaces
.output_for_workspace(workspace.id)
.is_none_or(|owner| owner == output)
});
let index = candidates.iter().position(|workspace| workspace.id == current)?;
let candidates = self.output_workspaces.ordered_workspaces(&self.workspaces, output);
let index = candidates.iter().position(|id| *id == current)?;
let neighbor = if next {
index.checked_add(1)
index.checked_add(1)?
} else {
index.checked_sub(1)
index.checked_sub(1)?
};
if let Some(workspace) = neighbor.and_then(|index| candidates.get(index)) {
return Some(workspace.id);
}
if next
&& self.workspaces.workspace(current).is_some_and(|workspace| {
workspace.layout.window_ids().next().is_some() || !workspace.floating.is_empty()
})
{
let number = self
.workspaces
.iter()
.filter_map(|workspace| workspace.name.parse::<u32>().ok())
.max()
.unwrap_or(1)
.checked_add(1)?;
return self.workspaces.ensure_numeric(number).ok();
}
None
candidates.get(neighbor).copied()
}
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
+11 -6
View File
@@ -987,20 +987,25 @@ impl Ferese {
}
fn workspaces_json(&self) -> Value {
let active = self.workspaces.active_id();
let workspaces = self
.workspaces
.ordered()
.output_workspaces
.workspace_views(&self.workspaces)
.into_iter()
.map(|workspace| {
.map(|view| {
let workspace = self.workspaces.workspace(view.id).expect("workspace view exists");
let mode = match workspace.layout.mode() {
LayoutMode::Scrolling => "scrolling",
LayoutMode::Tree => "tree",
};
json!({
"id": workspace.id.0,
"name": workspace.name,
"active": workspace.id == active,
"name": view.index.to_string(),
"index": view.index,
"output": view.output.0,
"window_count": view.window_count,
"visible": view.visible,
"focused": view.focused,
"active": view.focused,
"layout": mode,
"focused_window": workspace.last_focused.map(|window| window.0),
"fullscreen_window": workspace.fullscreen.map(|window| window.0),
+15 -7
View File
@@ -125,6 +125,7 @@ impl Default for OverviewState {
#[derive(Clone, Debug)]
pub(crate) struct WorkspaceCard {
pub workspace: WorkspaceId,
pub index: u32,
pub rect: Rect,
pub selected: bool,
pub windows: Vec<(WindowId, Rect)>,
@@ -459,12 +460,11 @@ impl Ferese {
pub(crate) fn overview_workspace_label(
&mut self,
workspace: WorkspaceId,
index: u32,
scale: f64,
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
let name = self.workspaces.workspace(workspace)?.name.clone();
self.overview
.label(&name, scale, self.theme_settings.text_primary_color.0)
.label(&index.to_string(), scale, self.theme_settings.text_primary_color.0)
}
pub(crate) fn set_overview_active(&mut self, active: bool) {
@@ -606,7 +606,12 @@ impl Ferese {
let selected = self.output_workspaces.active_workspace(output_id);
let strip = workspace_strip(bounds);
let capacity = strip_capacity(strip);
let workspaces = self.workspaces.ordered();
let workspaces = self
.output_workspaces
.workspace_views(&self.workspaces)
.into_iter()
.filter(|view| view.output == output_id)
.collect::<Vec<_>>();
let selected_index = workspaces
.iter()
.position(|workspace| Some(workspace.id) == selected)
@@ -629,7 +634,9 @@ impl Ferese {
.skip(offset)
.take(capacity)
.enumerate()
.map(|(index, workspace)| {
.map(|(index, view)| {
let workspace = self.workspaces.workspace(view.id).expect("workspace view exists");
let rect = Rect::new(
start_x + index as f64 * (card_width + WORKSPACE_CARD_GAP),
strip.y + 12.0,
@@ -659,6 +666,7 @@ impl Ferese {
let windows = preview_layout(preview_bounds, &windows, 4.0, 4.0).into_iter().collect();
WorkspaceCard {
workspace: workspace.id,
index: view.index,
rect,
selected: Some(workspace.id) == selected,
windows,
@@ -721,10 +729,10 @@ impl Ferese {
};
let capacity = strip_capacity(workspace_strip(bounds));
let cards = self.overview_workspace_cards(&output);
let ordered = self.workspaces.ordered();
let ordered = self.output_workspaces.ordered_workspaces(&self.workspaces, output_id);
let current = cards
.first()
.and_then(|card| ordered.iter().position(|workspace| workspace.id == card.workspace))
.and_then(|card| ordered.iter().position(|id| *id == card.workspace))
.unwrap_or(0);
let next = if delta > 0.0 {
current.saturating_add(1)
+1 -1
View File
@@ -237,7 +237,7 @@ pub(super) fn overview_strip_elements(
}
}
}
if let Some((buffer, size)) = state.overview_workspace_label(card.workspace, scale) {
if let Some((buffer, size)) = state.overview_workspace_label(card.index, scale) {
let location = Point::<i32, Physical>::from((
((card.rect.x - f64::from(output_geometry.loc.x) + card.rect.width * 0.5) * scale).round() as i32
- size.w / 2,
+26 -17
View File
@@ -318,20 +318,17 @@ impl Ferese {
}
fn workspace_snapshots(&self) -> Vec<WorkspaceSnapshot> {
self.workspaces
.ordered()
self.output_workspaces
.workspace_views(&self.workspaces)
.into_iter()
.map(|workspace| {
let output = self.output_workspaces.output_for_workspace(workspace.id);
let active =
output.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(workspace.id));
WorkspaceSnapshot {
id: workspace.id,
output,
name: workspace.name.clone(),
active,
}
.map(|view| WorkspaceSnapshot {
id: view.id,
output: Some(view.output),
name: view.index.to_string(),
index: view.index,
window_count: view.window_count as u32,
visible: view.visible,
focused: view.focused,
})
.collect()
}
@@ -456,7 +453,10 @@ impl ShellSnapshot {
output_hi,
output_lo,
workspace.name.clone(),
u32::from(workspace.active),
workspace.index,
workspace.window_count,
u32::from(workspace.visible),
u32::from(workspace.focused),
);
}
for snapshot in &self.windows {
@@ -499,7 +499,10 @@ struct WorkspaceSnapshot {
id: WorkspaceId,
output: Option<OutputId>,
name: String,
active: bool,
index: u32,
window_count: u32,
visible: bool,
focused: bool,
}
fn split_id(id: u64) -> (u32, u32) {
@@ -559,7 +562,10 @@ mod tests {
id: WorkspaceId(1),
output: Some(OutputId(1)),
name: "1".into(),
active: true,
index: 1,
window_count: 1,
visible: true,
focused: true,
}],
windows: vec![ManagedWindowSnapshot {
id: WindowId(1),
@@ -575,7 +581,10 @@ mod tests {
|s| s.outputs[0].active_workspace = WorkspaceId(2),
|s| s.outputs.clear(),
|s| s.workspaces[0].output = None,
|s| s.workspaces[0].active = false,
|s| s.workspaces[0].visible = false,
|s| s.workspaces[0].focused = false,
|s| s.workspaces[0].index = 2,
|s| s.workspaces[0].window_count = 0,
|s| s.workspaces[0].name = "renamed".into(),
|s| s.windows[0].workspace = 2,
|s| s.windows[0].title = "new title".into(),
+2 -2
View File
@@ -1016,7 +1016,7 @@ mod tests {
let mut workspaces = WorkspaceSet::default();
let first = workspaces.active_id();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
let second = workspaces.ensure_numeric(2).unwrap();
let second = workspaces.create_workspace();
workspaces.activate(second).unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces.insert_window(WindowId(3), Axis::Horizontal, 0.5).unwrap();
@@ -1027,7 +1027,7 @@ mod tests {
.connect(OutputId(1), OutputGeometry::new(0, 0, 1920, 1080), first)
.unwrap();
let owner = if assignment == Some(OutputId(2)) {
let third = workspaces.ensure_numeric(3).unwrap();
let third = workspaces.create_workspace();
outputs
.connect(OutputId(2), OutputGeometry::new(1920, 0, 1920, 1080), third)
.unwrap();
+1 -1
View File
@@ -313,7 +313,7 @@ impl Ferese {
})
.collect::<HashSet<_>>();
self.unmap_invisible_windows(&visible_windows);
self.prune_empty_workspaces(&visible);
self.reconcile_workspaces(&visible);
self.send_shell_snapshots();
active_animation = true;
}
+6 -6
View File
@@ -15,11 +15,11 @@ impl Ferese {
visible
}
pub(super) fn prune_empty_workspaces(&mut self, visible: &HashSet<WorkspaceId>) {
let mut protected = visible.clone();
protected.extend(self.output_workspaces.history_workspaces());
for workspace in self.workspaces.prune_empty(&protected) {
self.output_workspaces.forget_workspace(workspace);
pub(super) fn reconcile_workspaces(&mut self, visible: &HashSet<WorkspaceId>) {
for workspace in self
.output_workspaces
.reconcile_workspaces(&mut self.workspaces, visible)
{
self.viewport_animations.remove(&workspace);
}
}
@@ -52,7 +52,7 @@ impl Ferese {
pub fn relayout(&mut self) {
self.refresh_input_capture_zones();
let visible_workspaces = self.visible_workspace_ids();
self.prune_empty_workspaces(&visible_workspaces);
self.reconcile_workspaces(&visible_workspaces);
if self.session_lock.active {
self.configure_lock_surfaces();
}
+14 -20
View File
@@ -394,16 +394,13 @@ impl Ferese {
slide_direction: Option<SwipeDirection>,
auto_back_and_forth: bool,
) {
let workspace = match self.workspaces.ensure_numeric(index) {
Ok(workspace) => workspace,
Err(error) => {
tracing::error!(%error, index, "failed to switch workspace");
return;
}
};
let Some(output) = self.output_workspaces.focused_output() else {
return;
};
let Some(workspace) = self.output_workspaces.workspace_at(&self.workspaces, output, index) else {
return;
};
let previous = self.output_workspaces.active_workspace(output);
let owner = match self
.output_workspaces
@@ -526,26 +523,23 @@ impl Ferese {
let Some(window) = self.focused_window else {
return;
};
let Some(output) = self.output_workspaces.focused_output() else {
return;
};
let Some(destination) = self.output_workspaces.workspace_at(&self.workspaces, output, index) else {
return;
};
let axis = self
.output_bounds()
.and_then(|bounds| {
let workspace = self.workspaces.ensure_numeric(index).ok()?;
let target = self.workspaces.workspace(workspace)?;
let target = self.workspaces.workspace(destination)?;
target.layout.automatic_axis(target.last_focused, bounds).ok()
})
.unwrap_or(Axis::Horizontal);
let destination = match self.workspaces.move_window_to_numeric(window, index, axis, 0.5) {
Ok(destination) => destination,
Err(error) => {
tracing::error!(%error, ?window, index, "failed to move window to workspace");
return;
}
};
if let Some(output) = self.output_workspaces.focused_output()
&& let Err(error) = self.output_workspaces.assign_workspace(output, destination)
{
tracing::error!(%error, ?destination, "failed to assign destination workspace");
if let Err(error) = self.workspaces.move_window_to_workspace(window, destination, axis, 0.5) {
tracing::error!(%error, ?window, index, "failed to move window to workspace");
return;
}
self.focused_window = self.workspaces.active().last_focused;
+11 -13
View File
@@ -15,25 +15,23 @@ impl Ferese {
self.next_output_id = self.next_output_id.saturating_add(1);
id
});
let fallback_workspace = (1..)
.find_map(|index| {
let workspace = self.workspaces.ensure_numeric(index).ok()?;
self.output_workspaces
.output_for_workspace(workspace)
.is_none()
.then_some(workspace)
})
.expect("numeric workspaces are inexhaustible");
let fallback_workspace = self
.workspaces
.iter()
.filter(|workspace| self.output_workspaces.output_for_workspace(workspace.id).is_none())
.map(|workspace| workspace.id)
.min_by_key(|id| id.0)
.unwrap_or_else(|| self.workspaces.create_workspace());
let geometry = OutputGeometry::new(geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h);
let registration = self.output_workspaces.connect(output_id, geometry, fallback_workspace);
match registration {
Ok(workspace) => {
Ok(_) => {
self.output_ids.insert(output.clone(), output_id);
if self.output_workspaces.focused_output() == Some(output_id) {
self.activate_output_workspace(output_id, workspace);
}
self.restore_output_focus();
let visible = self.visible_workspace_ids();
self.reconcile_workspaces(&visible);
self.send_shell_snapshots();
}
Err(error) => {
+8 -3
View File
@@ -287,10 +287,15 @@ impl Ferese {
.automatic_axis(self.focused_window, bounds)
.unwrap_or(Axis::Horizontal);
if let Some(workspace) = rule.workspace
&& let Err(error) = self.workspaces.move_window_to_numeric(id, workspace, axis, 0.5)
if let Some(index) = rule.workspace
&& let Some(output) = self
.workspaces
.workspace_for_window(id)
.and_then(|workspace| self.output_workspaces.output_for_workspace(workspace))
&& let Some(workspace) = self.output_workspaces.workspace_at(&self.workspaces, output, index)
&& let Err(error) = self.workspaces.move_window_to_workspace(id, workspace, axis, 0.5)
{
tracing::warn!(%error, ?id, workspace, "failed to apply window workspace rule");
tracing::warn!(%error, ?id, ?workspace, "failed to apply window workspace rule");
}
let bounds = self.floating_bounds_for_window(id).unwrap_or(bounds);