From 42ad4e1f689766a06ac508ed2f2809526f5519ce Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 25 Sep 2026 01:41:51 +0100 Subject: [PATCH] feat: track focused output workspaces --- compositor/src/backends/direct.rs | 11 +++- compositor/src/state.rs | 96 +++++++++++++++++++++++++++-- compositor/src/winit.rs | 1 + crates/ferese-core/src/workspace.rs | 17 +++-- 4 files changed, 114 insertions(+), 11 deletions(-) diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 20206ba..7decd9e 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -477,6 +477,7 @@ fn select_output(drm: &DrmDevice) -> io::Result<(connector::Info, crtc::Handle, fn create_output(state: &mut Ferese, connector: &connector::Info, mode: DrmMode) -> Output { let name = connector.to_string(); + let identity = format!("drm:{name}"); let physical_size = connector.size().unwrap_or((0, 0)); let output = Output::new( name, @@ -497,7 +498,15 @@ fn create_output(state: &mut Ferese, connector: &connector::Info, mode: DrmMode) Some(Scale::Integer(1)), Some((0, 0).into()), ); - state.space.map_output(&output, (0, 0)); + let x = state + .space + .outputs() + .filter_map(|output| state.space.output_geometry(output)) + .map(|geometry| geometry.loc.x + geometry.size.w) + .max() + .unwrap_or(0); + state.space.map_output(&output, (x, 0)); + state.register_output(&output, identity); output } diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 3c09faf..8f0fa66 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -7,7 +7,7 @@ use std::{ }; use ferese_animation::{ClientSize, SpringConfig, WindowGeometry}; -use ferese_core::{WindowPlacement, WorkspaceSet}; +use ferese_core::{OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceSet}; use ferese_layout::{Axis, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId}; use smithay::{ @@ -69,6 +69,9 @@ pub struct Ferese { pub loop_signal: LoopSignal, pub space: Space, pub workspaces: WorkspaceSet, + pub output_workspaces: OutputWorkspaceMap, + output_ids: HashMap, + output_identity_ids: HashMap, pub window_ids: HashMap, pub window_geometry: HashMap, pub focused_window: Option, @@ -82,6 +85,7 @@ pub struct Ferese { pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>, pub(crate) pending_screencopies: Vec, next_window_id: u64, + next_output_id: u64, last_animation_tick: Instant, pub popups: PopupManager, pub seat: Seat, @@ -112,6 +116,67 @@ pub struct Ferese { } impl Ferese { + pub fn register_output(&mut self, output: &Output, identity: String) { + let Some(geometry) = self.space.output_geometry(output) else { + tracing::error!(output = %output.name(), "cannot register an unmapped output"); + return; + }; + let output_id = *self.output_identity_ids.entry(identity).or_insert_with(|| { + let id = OutputId(self.next_output_id); + 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 geometry = OutputGeometry::new( + geometry.loc.x, + geometry.loc.y, + geometry.size.w, + geometry.size.h, + ); + + match self + .output_workspaces + .connect(output_id, geometry, fallback_workspace) + { + Ok(workspace) => { + self.output_ids.insert(output.clone(), output_id); + if self.output_workspaces.focused_output() == Some(output_id) { + self.activate_output_workspace(output_id, workspace); + } + } + Err(error) => { + tracing::error!(%error, output = %output.name(), "failed to register output") + } + } + } + + fn activate_output_workspace(&mut self, output: OutputId, workspace: ferese_core::WorkspaceId) { + if let Err(error) = self.output_workspaces.focus_output(output) { + tracing::error!(%error, "failed to focus output"); + return; + } + + match self.workspaces.activate(workspace) { + Ok(focus) => self.focused_window = focus, + Err(error) => tracing::error!(%error, "failed to activate output workspace"), + } + } + + fn focused_output(&self) -> Option<&Output> { + let focused = self.output_workspaces.focused_output()?; + self.output_ids + .iter() + .find_map(|(output, id)| (*id == focused).then_some(output)) + } + pub(crate) fn queue_dmabuf_import(&mut self, dmabuf: Dmabuf, notifier: ImportNotifier) { self.pending_dmabuf_imports.push((dmabuf, notifier)); } @@ -183,6 +248,9 @@ impl Ferese { loop_signal: event_loop.get_signal(), space: Space::default(), workspaces: WorkspaceSet::default(), + output_workspaces: OutputWorkspaceMap::default(), + output_ids: HashMap::new(), + output_identity_ids: HashMap::new(), window_ids: HashMap::new(), window_geometry: HashMap::new(), focused_window: None, @@ -196,6 +264,7 @@ impl Ferese { pending_dmabuf_imports: Vec::new(), pending_screencopies: Vec::new(), next_window_id: 1, + next_output_id: 1, last_animation_tick: start_time, popups: PopupManager::default(), seat, @@ -864,15 +933,26 @@ impl Ferese { } pub fn switch_workspace(&mut self, index: u32) { - let focus = match self.workspaces.switch_to_numeric(index) { - Ok(focus) => focus, + 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 owner = match self.output_workspaces.switch_workspace(output, workspace) { + Ok(ferese_core::WorkspaceSwitch::Activated(output)) + | Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output, + Err(error) => { + tracing::error!(%error, index, "failed to assign workspace to output"); + return; + } + }; + self.activate_output_workspace(owner, workspace); - self.focused_window = focus; self.relayout(); self.restore_keyboard_focus(); } @@ -923,7 +1003,9 @@ impl Ferese { } fn output_bounds(&self) -> Option { - let output = self.space.outputs().next()?; + let output = self + .focused_output() + .or_else(|| self.space.outputs().next())?; let geometry = self.space.output_geometry(output)?; let zone = layer_map_for_output(output).non_exclusive_zone(); @@ -936,7 +1018,9 @@ impl Ferese { } fn full_output_bounds(&self) -> Option { - let output = self.space.outputs().next()?; + let output = self + .focused_output() + .or_else(|| self.space.outputs().next())?; let geometry = self.space.output_geometry(output)?; Some(Rect::new( diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 5254728..5930952 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -82,6 +82,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() ); output.set_preferred(mode); state.space.map_output(&output, (0, 0)); + state.register_output(&output, "nested-primary".to_owned()); let mut damage_tracker = OutputDamageTracker::from_output(&output); let clock = Clock::::new(); diff --git a/crates/ferese-core/src/workspace.rs b/crates/ferese-core/src/workspace.rs index d7a9a44..cfd975e 100644 --- a/crates/ferese-core/src/workspace.rs +++ b/crates/ferese-core/src/workspace.rs @@ -115,6 +115,17 @@ impl WorkspaceSet { .expect("active workspace always exists") } + pub fn activate(&mut self, workspace: WorkspaceId) -> Result, WorkspaceError> { + if !self.workspaces.contains_key(&workspace) { + return Err(WorkspaceError::UnknownWorkspace(workspace)); + } + + self.active = workspace; + + debug_assert!(self.validate().is_ok()); + Ok(self.active().fullscreen.or(self.active().last_focused)) + } + pub fn workspace(&self, id: WorkspaceId) -> Option<&Workspace> { self.workspaces.get(&id) } @@ -236,10 +247,8 @@ impl WorkspaceSet { } pub fn switch_to_numeric(&mut self, index: u32) -> Result, WorkspaceError> { - self.active = self.ensure_numeric(index)?; - - debug_assert!(self.validate().is_ok()); - Ok(self.active().fullscreen.or(self.active().last_focused)) + let workspace = self.ensure_numeric(index)?; + self.activate(workspace) } pub fn insert_window(