From 1f9a66d5dca69273e028846e8c402c0c33919853 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Wed, 29 Oct 2025 21:09:37 -0700 Subject: [PATCH] feat(wm): allow scrolling layout with >1 monitors This commit takes some inspiration from a trick I implemented in the komorebi for Mac codebase to hide windows that are not within the horizontal bounds of the work space area when the scrolling layer is active. Doing this ensures that such windows will not spill over into adjacent monitors (or at least, not for very long). There are some additional changes required in komorebi for Windows to try and make sure this plays nicely with the animations feature, which is not present in komorebi for Mac. --- komorebi-bar/src/bar.rs | 2 +- komorebi/src/core/rect.rs | 9 +++++ komorebi/src/stackbar_manager/stackbar.rs | 2 +- komorebi/src/static_config.rs | 11 ------ komorebi/src/window.rs | 42 +++++++++++++++++++---- komorebi/src/window_manager.rs | 8 ----- komorebi/src/workspace.rs | 20 +++++++++-- 7 files changed, 64 insertions(+), 30 deletions(-) diff --git a/komorebi-bar/src/bar.rs b/komorebi-bar/src/bar.rs index e9e337ee..a8e0cae7 100644 --- a/komorebi-bar/src/bar.rs +++ b/komorebi-bar/src/bar.rs @@ -802,7 +802,7 @@ impl Komobar { pub fn position_bar(&self) { if let Some(hwnd) = self.hwnd { let window = komorebi_client::Window::from(hwnd); - match window.set_position(&self.size_rect, false) { + match window.set_position(&self.size_rect, false, false) { Ok(_) => { tracing::info!("updated bar position"); } diff --git a/komorebi/src/core/rect.rs b/komorebi/src/core/rect.rs index 04ad653d..35d08e2a 100644 --- a/komorebi/src/core/rect.rs +++ b/komorebi/src/core/rect.rs @@ -85,6 +85,15 @@ impl Rect { && point.1 <= self.top + self.bottom } + pub fn contains_within_horizontal_bounds(&self, other: &Rect) -> bool { + let left_corner_is_within_bounds = + other.left >= self.left && other.left < self.left + self.right; + let right_corner_is_within_bounds = other.left + other.right >= self.left + && other.left + other.right < self.left + self.right; + + left_corner_is_within_bounds || right_corner_is_within_bounds + } + #[must_use] pub const fn scale(&self, system_dpi: i32, rect_dpi: i32) -> Rect { Rect { diff --git a/komorebi/src/stackbar_manager/stackbar.rs b/komorebi/src/stackbar_manager/stackbar.rs index ac51e335..b07ba6a4 100644 --- a/komorebi/src/stackbar_manager/stackbar.rs +++ b/komorebi/src/stackbar_manager/stackbar.rs @@ -355,7 +355,7 @@ impl Stackbar { // tile if index != focused_window_idx && let Err(err) = - window.set_position(&focused_window_rect, false) + window.set_position(&focused_window_rect, false, false) { tracing::error!( "stackbar WM_LBUTTONDOWN repositioning error: hwnd {} ({})", diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index b268e93a..4ef57960 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -1361,8 +1361,6 @@ impl StaticConfig { workspace_matching_rules.clear(); drop(workspace_matching_rules); - let monitor_count = wm.monitors().len(); - let offset = wm.work_area_offset; for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() { let preferred_config_idx = { @@ -1411,15 +1409,6 @@ impl StaticConfig { monitor.update_workspaces_globals(offset); for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() { if let Some(workspace_config) = monitor_config.workspaces.get_mut(j) { - if monitor_count > 1 - && matches!(workspace_config.layout, Some(DefaultLayout::Scrolling)) - { - tracing::warn!( - "scrolling layout is only supported for a single monitor; falling back to columns layout" - ); - workspace_config.layout = Some(DefaultLayout::Columns); - } - ws.load_static_config(workspace_config)?; } } diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index cc431e0f..b3e6b015 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -165,6 +165,7 @@ struct MovementRenderDispatcher { target_rect: Rect, top: bool, style: AnimationStyle, + end_with_hide: bool, } impl MovementRenderDispatcher { @@ -176,6 +177,7 @@ impl MovementRenderDispatcher { target_rect: Rect, top: bool, style: AnimationStyle, + end_with_hide: bool, ) -> Self { Self { hwnd, @@ -183,6 +185,7 @@ impl MovementRenderDispatcher { target_rect, top, style, + end_with_hide, } } } @@ -193,6 +196,11 @@ impl RenderDispatcher for MovementRenderDispatcher { } fn pre_render(&self) -> eyre::Result<()> { + let window = Window::from(self.hwnd); + if window.is_cloaked().unwrap_or(true) { + window.restore() + } + stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst); stackbar_manager::send_notification(); @@ -213,7 +221,13 @@ impl RenderDispatcher for MovementRenderDispatcher { fn post_render(&self) -> eyre::Result<()> { // we don't add the async_window_pos flag here because animations // are always run on a separate thread - WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false)?; + if self.end_with_hide { + let window = Window::from(self.hwnd); + window.hide(); + } else { + WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false)?; + } + if ANIMATION_MANAGER .lock() .count_in_progress(MovementRenderDispatcher::PREFIX) @@ -383,7 +397,7 @@ impl Window { let anim_count = ANIMATION_MANAGER .lock() .count_in_progress(MovementRenderDispatcher::PREFIX); - self.set_position(&new_rect, true)?; + self.set_position(&new_rect, true, false)?; let hwnd = self.hwnd; // Wait for the animation to finish before maximizing the window again, otherwise // we would be maximizing the window on the current monitor anyway @@ -402,11 +416,11 @@ impl Window { windows_api::WindowsApi::maximize_window(hwnd); }); } else { - self.set_position(&new_rect, true)?; + self.set_position(&new_rect, true, false)?; windows_api::WindowsApi::maximize_window(self.hwnd); } } else { - self.set_position(&new_rect, true)?; + self.set_position(&new_rect, true, false)?; } Ok(()) @@ -436,10 +450,11 @@ impl Window { bottom: target_height, }, true, + false, ) } - pub fn set_position(&self, layout: &Rect, top: bool) -> eyre::Result<()> { + pub fn set_position(&self, layout: &Rect, top: bool, end_with_hide: bool) -> eyre::Result<()> { let window_rect = WindowsApi::window_rect(self.hwnd)?; if window_rect.eq(layout) { @@ -463,11 +478,24 @@ impl Window { .get(&MovementRenderDispatcher::PREFIX) .unwrap_or(&ANIMATION_STYLE_GLOBAL.lock()); - let render_dispatcher = - MovementRenderDispatcher::new(self.hwnd, window_rect, *layout, top, style); + let render_dispatcher = MovementRenderDispatcher::new( + self.hwnd, + window_rect, + *layout, + top, + style, + end_with_hide, + ); AnimationEngine::animate(render_dispatcher, duration) + } else if end_with_hide { + self.hide(); + Ok(()) } else { + if self.is_cloaked().unwrap_or(true) { + self.restore() + } + WindowsApi::position_window(self.hwnd, layout, top, true) } } diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index a9f2aeef..31348e4f 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -3088,16 +3088,8 @@ impl WindowManager { pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> eyre::Result<()> { tracing::info!("changing layout"); - let monitor_count = self.monitors().len(); let workspace = self.focused_workspace_mut()?; - if monitor_count > 1 && matches!(layout, DefaultLayout::Scrolling) { - tracing::warn!( - "scrolling layout is only supported for a single monitor; not changing layout" - ); - return Ok(()); - } - match &workspace.layout { Layout::Default(_) => {} Layout::Custom(layout) => { diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index fafadeb9..e92b8268 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -531,7 +531,7 @@ impl Workspace { adjusted_work_area.add_padding(container_padding); adjusted_work_area.add_padding(border_offset); adjusted_work_area.add_padding(border_width); - window.set_position(&adjusted_work_area, true)?; + window.set_position(&adjusted_work_area, true, false)?; }; } else if let Some(window) = &mut self.maximized_window { window.maximize(); @@ -553,6 +553,8 @@ impl Workspace { let no_titlebar = NO_TITLEBAR.lock().clone(); let regex_identifiers = REGEX_IDENTIFIERS.lock().clone(); + let is_scrolling = matches!(self.layout, Layout::Default(DefaultLayout::Scrolling)); + let containers = self.containers_mut(); for (i, container) in containers.iter_mut().enumerate() { @@ -597,7 +599,13 @@ impl Workspace { WindowsApi::restore_window(window.hwnd); } } - window.set_position(layout, false)?; + + window.set_position( + layout, + false, + is_scrolling + && !work_area.contains_within_horizontal_bounds(layout), + )?; } } } @@ -1581,6 +1589,14 @@ impl Workspace { tracing::info!("focusing container"); self.containers.focus(idx); + + if matches!(self.layout, Layout::Default(DefaultLayout::Scrolling)) + && let Some(container) = self.focused_container() + && let Some(window) = container.focused_window() + && window.is_cloaked().unwrap_or(true) + { + window.restore(); + } } pub fn swap_containers(&mut self, i: usize, j: usize) {