feat: integrate numeric workspaces

This commit is contained in:
Samuel Onoja committed 2026-09-24 11:08:42 +01:00
1 parent 19abb033c5
commit 3d8c7b0c4f
4 files changed
+187 -16

No files matched your search

+6
View File
@@ -48,6 +48,12 @@ impl XdgActivationHandler for Ferese {
};
self.focused_window = self.window_ids.get(&window).copied();
if let Some(focused) = self.focused_window
&& let Err(error) = self.workspaces.focus_window(focused)
{
tracing::error!(%error, ?focused, "failed to update workspace focus");
return;
}
self.space.raise_element(&window, true);
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
keyboard.set_focus(self, Some(surface), SERIAL_COUNTER.next_serial());
+36 -1
View File
@@ -37,8 +37,9 @@ impl Ferese {
let supported_modifiers = modifiers.logo
&& !modifiers.alt
&& !(modifiers.ctrl && modifiers.shift);
let symbol = keysym.modified_sym().raw();
let direction = if supported_modifiers {
match keysym.modified_sym().raw() {
match symbol {
keysyms::KEY_h | keysyms::KEY_H => Some(Direction::Left),
keysyms::KEY_j | keysyms::KEY_J => Some(Direction::Down),
keysyms::KEY_k | keysyms::KEY_K => Some(Direction::Up),
@@ -63,6 +64,35 @@ impl Ferese {
}
FilterResult::Intercept(())
} else if modifiers.logo && !modifiers.ctrl && !modifiers.alt {
let workspace = match symbol {
keysyms::KEY_1 => Some(1),
keysyms::KEY_2 => Some(2),
keysyms::KEY_3 => Some(3),
keysyms::KEY_4 => Some(4),
keysyms::KEY_5 => Some(5),
keysyms::KEY_6 => Some(6),
keysyms::KEY_7 => Some(7),
keysyms::KEY_8 => Some(8),
keysyms::KEY_9 => Some(9),
_ => None,
};
if let Some(workspace) = workspace {
if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode);
if modifiers.shift {
data.move_focused_to_workspace(workspace);
} else {
data.switch_workspace(workspace);
}
}
FilterResult::Intercept(())
} else {
FilterResult::Forward
}
} else {
FilterResult::Forward
}
@@ -175,6 +205,11 @@ impl Ferese {
let window = window.clone();
self.focused_window = self.window_ids.get(&window).copied();
if let Some(focused) = self.focused_window
&& let Err(error) = self.workspaces.focus_window(focused)
{
tracing::error!(%error, ?focused, "failed to update workspace focus");
}
self.space.raise_element(&window, true);
let surface = window
.toplevel()
+113 -15
View File
@@ -6,7 +6,7 @@ use std::{
time::Instant,
};
use ferese_layout::{Axis, Direction, GapConfig, LayoutTree, Rect, WindowId};
use ferese_layout::{Axis, Direction, GapConfig, Rect, WindowId, WorkspaceSet};
use smithay::{
desktop::{PopupManager, Space, Window, WindowSurfaceType},
@@ -43,7 +43,7 @@ pub struct Ferese {
pub display_handle: DisplayHandle,
pub loop_signal: LoopSignal,
pub space: Space<Window>,
pub layout: LayoutTree,
pub workspaces: WorkspaceSet,
pub window_ids: HashMap<Window, WindowId>,
pub focused_window: Option<WindowId>,
pub intercepted_keys: HashSet<smithay::input::keyboard::Keycode>,
@@ -98,7 +98,7 @@ impl Ferese {
display_handle,
loop_signal: event_loop.get_signal(),
space: Space::default(),
layout: LayoutTree::default(),
workspaces: WorkspaceSet::default(),
window_ids: HashMap::new(),
focused_window: None,
intercepted_keys: HashSet::new(),
@@ -170,10 +170,16 @@ impl Ferese {
let axis = self
.output_bounds()
.and_then(|bounds| self.layout.automatic_axis(self.focused_window, bounds).ok())
.and_then(|bounds| {
self.workspaces
.active()
.layout
.automatic_axis(self.focused_window, bounds)
.ok()
})
.unwrap_or(Axis::Horizontal);
if let Err(error) = self.layout.insert(id, self.focused_window, axis, 0.5) {
if let Err(error) = self.workspaces.insert_window(id, axis, 0.5) {
tracing::error!(%error, ?id, "failed to insert window into layout");
return;
}
@@ -190,15 +196,11 @@ impl Ferese {
};
self.space.unmap_elem(window);
if let Err(error) = self.layout.remove(id) {
if let Err(error) = self.workspaces.remove_window(id) {
tracing::error!(%error, ?id, "failed to remove window from layout");
}
if self.focused_window == Some(id) {
self.focused_window = self
.space
.elements()
.rev()
.find_map(|window| self.window_ids.get(window).copied());
self.focused_window = self.workspaces.active().last_focused;
}
self.relayout();
@@ -208,7 +210,24 @@ impl Ferese {
let Some(bounds) = self.output_bounds() else {
return;
};
let geometry = match self.layout.geometry_with_gaps(bounds, GapConfig::default()) {
let active = self.workspaces.active_id();
let inactive = self
.window_ids
.iter()
.filter(|(_, id)| self.workspaces.workspace_for_window(**id) != Some(active))
.map(|(window, _)| window.clone())
.collect::<Vec<_>>();
for window in inactive {
self.space.unmap_elem(&window);
}
let geometry = match self
.workspaces
.active()
.layout
.geometry_with_gaps(bounds, GapConfig::default())
{
Ok(geometry) => geometry,
Err(error) => {
tracing::error!(%error, "failed to compute tiled geometry");
@@ -243,9 +262,19 @@ impl Ferese {
let Some(bounds) = self.output_bounds() else {
return;
};
let Ok(Some(next)) = self.layout.directional_neighbor(current, direction, bounds) else {
let Ok(Some(next)) = self
.workspaces
.active()
.layout
.directional_neighbor(current, direction, bounds)
else {
return;
};
if let Err(error) = self.workspaces.focus_window(next) {
tracing::error!(%error, ?next, "failed to update workspace focus");
return;
}
let Some(window) = self
.window_ids
.iter()
@@ -286,7 +315,12 @@ impl Ferese {
return;
};
match self.layout.move_window(current, direction, bounds) {
match self
.workspaces
.active_mut()
.layout
.move_window(current, direction, bounds)
{
Ok(true) => self.relayout(),
Ok(false) => {}
Err(error) => tracing::error!(%error, ?current, "failed to move tiled window"),
@@ -300,13 +334,77 @@ impl Ferese {
return;
};
match self.layout.resize_window(current, direction, RESIZE_STEP) {
match self
.workspaces
.active_mut()
.layout
.resize_window(current, direction, RESIZE_STEP)
{
Ok(true) => self.relayout(),
Ok(false) => {}
Err(error) => tracing::error!(%error, ?current, "failed to resize tiled window"),
}
}
pub fn switch_workspace(&mut self, index: u32) {
let focus = match self.workspaces.switch_to_numeric(index) {
Ok(focus) => focus,
Err(error) => {
tracing::error!(%error, index, "failed to switch workspace");
return;
}
};
self.focused_window = focus;
self.relayout();
self.restore_keyboard_focus();
}
pub fn move_focused_to_workspace(&mut self, index: u32) {
let Some(window) = self.focused_window else {
return;
};
let axis = self
.output_bounds()
.and_then(|bounds| {
let workspace = self.workspaces.ensure_numeric(index).ok()?;
let target = self.workspaces.workspace(workspace)?;
target
.layout
.automatic_axis(target.last_focused, bounds)
.ok()
})
.unwrap_or(Axis::Horizontal);
if let Err(error) = self
.workspaces
.move_window_to_numeric(window, index, axis, 0.5)
{
tracing::error!(%error, ?window, index, "failed to move window to workspace");
return;
}
self.focused_window = self.workspaces.active().last_focused;
self.relayout();
self.restore_keyboard_focus();
}
fn restore_keyboard_focus(&mut self) {
let surface = self.focused_window.and_then(|focused| {
self.window_ids.iter().find_map(|(window, id)| {
(*id == focused)
.then(|| window.toplevel())
.flatten()
.map(|toplevel| toplevel.wl_surface().clone())
})
});
self.seat
.get_keyboard()
.expect("seat has a keyboard")
.set_focus(self, surface, smithay::utils::SERIAL_COUNTER.next_serial());
}
fn output_bounds(&self) -> Option<Rect> {
let output = self.space.outputs().next()?;
let geometry = self.space.output_geometry(output)?;
+32
View File
@@ -111,6 +111,19 @@ impl WorkspaceSet {
self.window_workspaces.get(&window).copied()
}
pub fn focus_window(&mut self, window: WindowId) -> Result<(), WorkspaceError> {
if self.workspace_for_window(window) != Some(self.active) {
return Err(WorkspaceError::InvalidState(
"focused window is not on the active workspace",
));
}
self.active_mut().last_focused = Some(window);
debug_assert!(self.validate().is_ok());
Ok(())
}
pub fn ensure_numeric(&mut self, index: u32) -> Result<WorkspaceId, WorkspaceError> {
if index == 0 {
return Err(WorkspaceError::InvalidNumericName(index));
@@ -360,4 +373,23 @@ mod tests {
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
assert!(workspaces.validate().is_ok());
}
#[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
.move_window_to_numeric(WindowId(1), 2, Axis::Horizontal, 0.5)
.unwrap();
assert_eq!(
workspaces.focus_window(WindowId(1)),
Err(WorkspaceError::InvalidState(
"focused window is not on the active workspace"
))
);
assert!(workspaces.validate().is_ok());
}
}