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.
This commit is contained in:
LGUG2Z
2025-10-29 21:09:37 -07:00
parent 18ee667896
commit 1f9a66d5dc
7 changed files with 64 additions and 30 deletions

View File

@@ -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");
}

View File

@@ -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 {

View File

@@ -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 {} ({})",

View File

@@ -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)?;
}
}

View File

@@ -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)
}
}

View File

@@ -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) => {

View File

@@ -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) {