From dcd707c688994b506ced57348b8b04fed1fadd5f Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 13:00:46 -0700 Subject: [PATCH] feat(core): promote_container, effective_layout_options, new_floating_window and hide are shared Four more functions of the workspace move into komorebi-core, written from the KDE text with three hooks that have defaults: primary_container_idx (the layout's primary position; 0 for the default layouts, Windows overrides it for custom layouts), unmaximize_window (KDE asks the compositor, a no-op on Windows, as new_floating_window does there today) and before_hide_containers (the KDE scrolling prelude of hide, returning the resize adjustments to put back). effective_layout_options needs no hook: a layout that is not one of the defaults has no default options, which is the Windows custom-layout arm and an unreachable pattern on KDE. Tests with the stub platform cover promote, new_floating_window and hide. The daemons still have their own copies; the next revisions make them use these. Verified: cargo test -p komorebi-core --features schemars (native): 23 passed, 0 failed. Not run: the daemons. --- crates/komorebi-core/src/workspace.rs | 196 +++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 1 deletion(-) diff --git a/crates/komorebi-core/src/workspace.rs b/crates/komorebi-core/src/workspace.rs index fdf76ddf..0ee33f22 100644 --- a/crates/komorebi-core/src/workspace.rs +++ b/crates/komorebi-core/src/workspace.rs @@ -96,6 +96,26 @@ pub trait WorkspacePlatform: Sized + 'static { /// Called after a floating window was taken out of the workspace; KDE refocuses a floating /// window, Windows does nothing. fn after_floating_removed(_workspace: &mut Workspace) {} + + /// The index at which [`Workspace::promote_container`] inserts the container: the primary + /// position of the layout. The default layouts have it first. + fn primary_container_idx(_layout: &Layout) -> eyre::Result { + Ok(0) + } + + /// Gives the maximized window back to the layout, for the platforms where that is something + /// to tell the window (KDE asks the compositor; on Windows there is nothing to do). + fn unmaximize_window(_window: &Self::Window) -> eyre::Result<()> { + Ok(()) + } + + /// Called by [`Workspace::hide`] before it hides the containers. A platform that has to put + /// the workspace in order first does it here (KDE tiles a scrolling workspace again without its + /// resize adjustments, so that the windows are hidden where the layout has them) and returns + /// the adjustments to put back. + fn before_hide_containers(_workspace: &mut Workspace) -> Option>> { + None + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -1325,6 +1345,136 @@ impl Workspace

{ pub fn is_scrolling(&self) -> bool { matches!(self.layout, Layout::Default(DefaultLayout::Scrolling)) } + /// Moves the focused container to the primary position of the layout and focuses it. + pub fn promote_container(&mut self) -> eyre::Result<()> { + let focused_idx = self.focused_container_idx(); + let container = self + .containers_mut() + .remove_respecting_locks(focused_idx) + .ok_or_eyre("there is no container")?; + + let primary_idx = P::primary_container_idx(&self.layout)?; + + let insertion_idx = self + .containers_mut() + .insert_respecting_locks(primary_idx, container); + self.focus_container(insertion_idx); + + Ok(()) + } + + /// The layout options in force for the number of containers the workspace has: its own, or + /// those its layout has by default. + pub fn effective_layout_options(&self) -> Option { + let container_count = self.containers().len(); + + let has_workspace_overrides = + self.layout_options.is_some() || !self.layout_options_rules.is_empty(); + + let (effective_base, effective_rules): (Option, &[(usize, LayoutOptions)]) = + if has_workspace_overrides { + (self.layout_options, &self.layout_options_rules) + } else { + match &self.layout { + Layout::Default(dl) => match self.layout_defaults_cache.get(dl) { + Some(entry) => (entry.layout_options, &entry.layout_options_rules), + None => (None, &[]), + }, + // a layout that is not one of the defaults (the Windows custom layouts) has none + #[allow(unreachable_patterns)] + _ => (None, &[]), + } + }; + + resolve_threshold_match(effective_rules, container_count).or(effective_base) + } + + /// Takes the focused window of the workspace (the maximized one, the monocle's or the focused + /// container's) out of its place and makes it a floating window. + pub fn new_floating_window(&mut self) -> eyre::Result<()> { + let window = if let Some(maximized_window) = self.maximized_window { + P::unmaximize_window(&maximized_window)?; + self.maximized_window = None; + self.maximized_window_restore_idx = None; + maximized_window + } else if let Some(monocle_container) = &mut self.monocle_container { + let window = monocle_container + .remove_focused_window() + .ok_or_eyre("there is no window")?; + + if monocle_container.windows().is_empty() { + self.monocle_container = None; + self.monocle_container_restore_idx = None; + } else { + monocle_container.load_focused_window(); + } + + window + } else { + let focused_idx = self.focused_container_idx(); + + let container = self + .focused_container_mut() + .ok_or_eyre("there is no container")?; + + let window = container + .remove_focused_window() + .ok_or_eyre("there is no window")?; + + if container.windows().is_empty() { + self.remove_container_by_idx(focused_idx); + + if focused_idx == self.containers().len() { + self.focus_container(focused_idx.saturating_sub(1)); + } + } else { + container.load_focused_window(); + } + + window + }; + + self.floating_windows_mut().push_back(window); + + Ok(()) + } + + /// Hides every window of the workspace, or every one but `omit`: the floating windows and the + /// windows of every container, the maximized window and the monocle container. + pub fn hide(&mut self, omit: Option>) { + for window in self.floating_windows_mut().iter_mut().rev() { + let mut should_hide = omit.is_none(); + + if !should_hide + && let Some(omit) = omit + && omit != window.id() + { + should_hide = true + } + + if should_hide { + window.hide_window(); + } + } + + let resize_dimensions = P::before_hide_containers(self); + + for container in self.containers_mut() { + container.hide(omit); + } + + if let Some(resize_dimensions) = resize_dimensions { + self.resize_dimensions = resize_dimensions; + } + + if let Some(window) = self.maximized_window { + window.hide_window(); + } + + if let Some(container) = &mut self.monocle_container { + container.hide(omit); + } + } } #[cfg(test)] @@ -1334,6 +1484,14 @@ mod tests { #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] struct W(u32); + thread_local! { + static HIDDEN: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; + } + + fn hidden() -> Vec { + HIDDEN.with(|hidden| std::mem::take(&mut *hidden.borrow_mut())) + } + impl ContainerWindow for W { type Id = u32; @@ -1345,7 +1503,9 @@ mod tests { Some(format!("exe{}", self.0)) } - fn hide_window(&self) {} + fn hide_window(&self) { + HIDDEN.with(|hidden| hidden.borrow_mut().push(self.0)); + } fn restore_window(&self) {} @@ -1465,6 +1625,40 @@ mod tests { assert!(!keys.contains(&"scrolling")); } + #[test] + fn promoting_a_container_puts_it_first_and_focuses_it() { + let mut workspace = workspace_of(&[1, 2, 3]); + + workspace.promote_container().unwrap(); + + assert_eq!(workspace.focused_container_idx(), 0); + assert_eq!(workspace.container_idx_for_window(3), Some(0)); + } + + #[test] + fn a_focused_window_becomes_floating_and_its_empty_container_goes() { + let mut workspace = workspace_of(&[1, 2]); + + workspace.new_floating_window().unwrap(); + + assert_eq!(workspace.containers().len(), 1); + assert_eq!(workspace.floating_windows().len(), 1); + assert!(workspace.contains_window(2)); + } + + #[test] + fn hiding_leaves_out_the_window_it_is_told_to() { + let mut workspace = workspace_of(&[1, 2, 3]); + workspace.floating_windows_mut().push_back(W(9)); + hidden(); + + workspace.hide(Some(2)); + + let mut hidden = hidden(); + hidden.sort(); + assert_eq!(hidden, [1, 3, 9]); + } + #[test] fn a_workspace_over_a_unit_state_is_a_workspace_with_no_padding_by_default() { let workspace = Workspace::::default();