Compare commits

..

1 Commits

Author SHA1 Message Date
LGUG2Z 1f9a66d5dc 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.
2025-10-30 13:57:55 -07:00
8 changed files with 64 additions and 41 deletions
+1 -1
View File
@@ -802,7 +802,7 @@ impl Komobar {
pub fn position_bar(&self) { pub fn position_bar(&self) {
if let Some(hwnd) = self.hwnd { if let Some(hwnd) = self.hwnd {
let window = komorebi_client::Window::from(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(_) => { Ok(_) => {
tracing::info!("updated bar position"); tracing::info!("updated bar position");
} }
+9
View File
@@ -85,6 +85,15 @@ impl Rect {
&& point.1 <= self.top + self.bottom && 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] #[must_use]
pub const fn scale(&self, system_dpi: i32, rect_dpi: i32) -> Rect { pub const fn scale(&self, system_dpi: i32, rect_dpi: i32) -> Rect {
Rect { Rect {
+1 -1
View File
@@ -355,7 +355,7 @@ impl Stackbar {
// tile // tile
if index != focused_window_idx if index != focused_window_idx
&& let Err(err) = && let Err(err) =
window.set_position(&focused_window_rect, false) window.set_position(&focused_window_rect, false, false)
{ {
tracing::error!( tracing::error!(
"stackbar WM_LBUTTONDOWN repositioning error: hwnd {} ({})", "stackbar WM_LBUTTONDOWN repositioning error: hwnd {} ({})",
-11
View File
@@ -1361,8 +1361,6 @@ impl StaticConfig {
workspace_matching_rules.clear(); workspace_matching_rules.clear();
drop(workspace_matching_rules); drop(workspace_matching_rules);
let monitor_count = wm.monitors().len();
let offset = wm.work_area_offset; let offset = wm.work_area_offset;
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() { for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let preferred_config_idx = { let preferred_config_idx = {
@@ -1411,15 +1409,6 @@ impl StaticConfig {
monitor.update_workspaces_globals(offset); monitor.update_workspaces_globals(offset);
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() { for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get_mut(j) { 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)?; ws.load_static_config(workspace_config)?;
} }
} }
-1
View File
@@ -17,7 +17,6 @@ use crate::WindowsApi;
use crate::should_act; use crate::should_act;
pub static TRANSPARENCY_ENABLED: AtomicBool = AtomicBool::new(false); pub static TRANSPARENCY_ENABLED: AtomicBool = AtomicBool::new(false);
pub static TRANSPARENCY_ENABLED_OVERRIDE: AtomicBool = AtomicBool::new(false);
pub static TRANSPARENCY_ALPHA: AtomicU8 = AtomicU8::new(200); pub static TRANSPARENCY_ALPHA: AtomicU8 = AtomicU8::new(200);
static KNOWN_HWNDS: OnceLock<Mutex<Vec<isize>>> = OnceLock::new(); static KNOWN_HWNDS: OnceLock<Mutex<Vec<isize>>> = OnceLock::new();
+35 -7
View File
@@ -165,6 +165,7 @@ struct MovementRenderDispatcher {
target_rect: Rect, target_rect: Rect,
top: bool, top: bool,
style: AnimationStyle, style: AnimationStyle,
end_with_hide: bool,
} }
impl MovementRenderDispatcher { impl MovementRenderDispatcher {
@@ -176,6 +177,7 @@ impl MovementRenderDispatcher {
target_rect: Rect, target_rect: Rect,
top: bool, top: bool,
style: AnimationStyle, style: AnimationStyle,
end_with_hide: bool,
) -> Self { ) -> Self {
Self { Self {
hwnd, hwnd,
@@ -183,6 +185,7 @@ impl MovementRenderDispatcher {
target_rect, target_rect,
top, top,
style, style,
end_with_hide,
} }
} }
} }
@@ -193,6 +196,11 @@ impl RenderDispatcher for MovementRenderDispatcher {
} }
fn pre_render(&self) -> eyre::Result<()> { 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::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification(); stackbar_manager::send_notification();
@@ -213,7 +221,13 @@ impl RenderDispatcher for MovementRenderDispatcher {
fn post_render(&self) -> eyre::Result<()> { fn post_render(&self) -> eyre::Result<()> {
// we don't add the async_window_pos flag here because animations // we don't add the async_window_pos flag here because animations
// are always run on a separate thread // 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 if ANIMATION_MANAGER
.lock() .lock()
.count_in_progress(MovementRenderDispatcher::PREFIX) .count_in_progress(MovementRenderDispatcher::PREFIX)
@@ -383,7 +397,7 @@ impl Window {
let anim_count = ANIMATION_MANAGER let anim_count = ANIMATION_MANAGER
.lock() .lock()
.count_in_progress(MovementRenderDispatcher::PREFIX); .count_in_progress(MovementRenderDispatcher::PREFIX);
self.set_position(&new_rect, true)?; self.set_position(&new_rect, true, false)?;
let hwnd = self.hwnd; let hwnd = self.hwnd;
// Wait for the animation to finish before maximizing the window again, otherwise // Wait for the animation to finish before maximizing the window again, otherwise
// we would be maximizing the window on the current monitor anyway // we would be maximizing the window on the current monitor anyway
@@ -402,11 +416,11 @@ impl Window {
windows_api::WindowsApi::maximize_window(hwnd); windows_api::WindowsApi::maximize_window(hwnd);
}); });
} else { } else {
self.set_position(&new_rect, true)?; self.set_position(&new_rect, true, false)?;
windows_api::WindowsApi::maximize_window(self.hwnd); windows_api::WindowsApi::maximize_window(self.hwnd);
} }
} else { } else {
self.set_position(&new_rect, true)?; self.set_position(&new_rect, true, false)?;
} }
Ok(()) Ok(())
@@ -436,10 +450,11 @@ impl Window {
bottom: target_height, bottom: target_height,
}, },
true, 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)?; let window_rect = WindowsApi::window_rect(self.hwnd)?;
if window_rect.eq(layout) { if window_rect.eq(layout) {
@@ -463,11 +478,24 @@ impl Window {
.get(&MovementRenderDispatcher::PREFIX) .get(&MovementRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock()); .unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher = let render_dispatcher = MovementRenderDispatcher::new(
MovementRenderDispatcher::new(self.hwnd, window_rect, *layout, top, style); self.hwnd,
window_rect,
*layout,
top,
style,
end_with_hide,
);
AnimationEngine::animate(render_dispatcher, duration) AnimationEngine::animate(render_dispatcher, duration)
} else if end_with_hide {
self.hide();
Ok(())
} else { } else {
if self.is_cloaked().unwrap_or(true) {
self.restore()
}
WindowsApi::position_window(self.hwnd, layout, top, true) WindowsApi::position_window(self.hwnd, layout, top, true)
} }
} }
-18
View File
@@ -2682,11 +2682,6 @@ impl WindowManager {
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub fn stack_all(&mut self) -> eyre::Result<()> { pub fn stack_all(&mut self) -> eyre::Result<()> {
if transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst) {
transparency_manager::TRANSPARENCY_ENABLED.store(false, Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ENABLED_OVERRIDE.store(true, Ordering::SeqCst);
}
self.handle_unmanaged_window_behaviour()?; self.handle_unmanaged_window_behaviour()?;
tracing::info!("stacking all windows on workspace"); tracing::info!("stacking all windows on workspace");
@@ -2754,11 +2749,6 @@ impl WindowManager {
workspace.focus_container_by_window(hwnd)?; workspace.focus_container_by_window(hwnd)?;
} }
if transparency_manager::TRANSPARENCY_ENABLED_OVERRIDE.load(Ordering::SeqCst) {
transparency_manager::TRANSPARENCY_ENABLED.store(true, Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ENABLED_OVERRIDE.store(false, Ordering::SeqCst);
}
if update_workspace { if update_workspace {
self.update_focused_workspace(self.mouse_follows_focus, true)?; self.update_focused_workspace(self.mouse_follows_focus, true)?;
} }
@@ -3098,16 +3088,8 @@ impl WindowManager {
pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> eyre::Result<()> { pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> eyre::Result<()> {
tracing::info!("changing layout"); tracing::info!("changing layout");
let monitor_count = self.monitors().len();
let workspace = self.focused_workspace_mut()?; 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 { match &workspace.layout {
Layout::Default(_) => {} Layout::Default(_) => {}
Layout::Custom(layout) => { Layout::Custom(layout) => {
+18 -2
View File
@@ -531,7 +531,7 @@ impl Workspace {
adjusted_work_area.add_padding(container_padding); adjusted_work_area.add_padding(container_padding);
adjusted_work_area.add_padding(border_offset); adjusted_work_area.add_padding(border_offset);
adjusted_work_area.add_padding(border_width); 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 { } else if let Some(window) = &mut self.maximized_window {
window.maximize(); window.maximize();
@@ -553,6 +553,8 @@ impl Workspace {
let no_titlebar = NO_TITLEBAR.lock().clone(); let no_titlebar = NO_TITLEBAR.lock().clone();
let regex_identifiers = REGEX_IDENTIFIERS.lock().clone(); let regex_identifiers = REGEX_IDENTIFIERS.lock().clone();
let is_scrolling = matches!(self.layout, Layout::Default(DefaultLayout::Scrolling));
let containers = self.containers_mut(); let containers = self.containers_mut();
for (i, container) in containers.iter_mut().enumerate() { for (i, container) in containers.iter_mut().enumerate() {
@@ -597,7 +599,13 @@ impl Workspace {
WindowsApi::restore_window(window.hwnd); 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"); tracing::info!("focusing container");
self.containers.focus(idx); 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) { pub fn swap_containers(&mut self, i: usize, j: usize) {