Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions komorebi-layouts/src/rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ impl Rect {
}
}

/// Returns true if this rect overlaps horizontally with the given area.
/// Both rects use `right` as width (not as right edge).
#[must_use]
pub const fn overlaps_horizontally(&self, area: &Rect) -> bool {
let self_right_edge = self.left + self.right;
let area_right_edge = area.left + area.right;
self.left < area_right_edge && self_right_edge > area.left
}

#[cfg(feature = "darwin")]
#[must_use]
pub fn percentage_within_horizontal_bounds(&self, other: &Rect) -> f64 {
Expand Down
12 changes: 12 additions & 0 deletions komorebi/src/process_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,18 @@ impl WindowManager {
{
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
self.uncloack_to_ignore = self.uncloack_to_ignore.saturating_sub(1);
} else if matches!(event, WindowManagerEvent::Uncloak(_, _))
&& self
.focused_workspace()
.map(|ws| ws.scrolling_cloaked_hwnds.contains(&window.hwnd))
.unwrap_or(false)
{
// A scrolling-cloaked window was uncloaked externally (e.g. via
// Alt-Tab). Scroll the layout to reveal the container holding it.
tracing::info!("scrolling to reveal uncloaked window on scrolling workspace");
self.focused_workspace_mut()?
.focus_container_by_window(window.hwnd)?;
self.update_focused_workspace(self.mouse_follows_focus, false)?;
} else {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx =
Expand Down
3 changes: 3 additions & 0 deletions komorebi/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;

use crate::BorderColours;
use crate::BorderStyle;
use crate::CURRENT_VIRTUAL_DESKTOP;
Expand Down Expand Up @@ -274,6 +276,7 @@ impl From<&WindowManager> for State {
workspace_config: None,
preselected_container_idx: None,
promotion_swap_container_idx: None,
scrolling_cloaked_hwnds: HashSet::new(),
})
.collect::<VecDeque<_>>();
ws.focus(monitor.workspaces.focused_idx());
Expand Down
11 changes: 0 additions & 11 deletions komorebi/src/static_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1344,8 +1344,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 = {
Expand Down Expand Up @@ -1394,15 +1392,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)?;
}
}
Expand Down
15 changes: 15 additions & 0 deletions komorebi/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,21 @@ impl Window {
self.restore_with_border(true);
}

/// Cloak this window directly via SetCloak without touching HIDDEN_HWNDS.
/// Used for scrolling layout visibility management where windows are not
/// "hidden" in the workspace-switching sense, just scrolled off-screen.
pub fn cloak(self) {
SetCloak(self.hwnd(), 1, 2);
border_manager::hide_border(self.hwnd);
}

/// Uncloak this window directly via SetCloak without touching HIDDEN_HWNDS.
/// Counterpart to [`Self::cloak`] for scrolling layout visibility.
pub fn uncloak(self) {
SetCloak(self.hwnd(), 1, 0);
border_manager::show_border(self.hwnd);
}

pub fn minimize(self) {
let exe = self.exe().unwrap_or_default();
if !exe.contains("komorebi-bar") {
Expand Down
8 changes: 0 additions & 8 deletions komorebi/src/window_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3198,16 +3198,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) => {
Expand Down
88 changes: 88 additions & 0 deletions komorebi/src/workspace.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::collections::VecDeque;
use std::ffi::OsStr;
use std::fmt::Display;
Expand Down Expand Up @@ -83,6 +84,11 @@ pub struct Workspace {
pub preselected_container_idx: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub promotion_swap_container_idx: Option<usize>,
/// HWNDs of windows currently cloaked because they are scrolled off-screen.
/// Tracked separately from HIDDEN_HWNDS to avoid interfering with workspace
/// hide/restore cycles.
#[serde(skip)]
pub scrolling_cloaked_hwnds: HashSet<isize>,
}

#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
Expand Down Expand Up @@ -136,6 +142,7 @@ impl Default for Workspace {
wallpaper: None,
preselected_container_idx: None,
promotion_swap_container_idx: None,
scrolling_cloaked_hwnds: HashSet::new(),
}
}
}
Expand Down Expand Up @@ -419,13 +426,29 @@ impl Workspace {
let idx = self.focused_container_idx();
let mut to_focus = None;

let is_scrolling = matches!(self.layout, Layout::Default(DefaultLayout::Scrolling));
// Clone the set before the mutable borrow on containers
let scrolling_cloaked = self.scrolling_cloaked_hwnds.clone();

for (i, container) in self.containers_mut().iter_mut().enumerate() {
if let Some(window) = container.focused_window_mut()
&& idx == i
{
to_focus = Option::from(*window);
}

// For scrolling layout, skip restoring containers whose windows are
// scrolling-cloaked (off-screen). The subsequent update() call will
// handle correct visibility so they don't flash on neighbouring monitors.
if is_scrolling
&& container
.windows()
.iter()
.any(|w| scrolling_cloaked.contains(&w.hwnd))
{
continue;
}

container.restore();
}

Expand Down Expand Up @@ -543,6 +566,24 @@ impl Workspace {
self.window_container_behaviour = updated_behaviour;
}

// If we are no longer in a state where scrolling cloaking should apply
// (e.g. layout changed from Scrolling, monocle toggled on, etc.),
// uncloak any previously scrolling-cloaked windows.
if !self.scrolling_cloaked_hwnds.is_empty() {
let needs_scrolling_cloaking =
matches!(self.layout, Layout::Default(DefaultLayout::Scrolling))
&& self.tile
&& self.monocle_container.is_none()
&& self.maximized_window.is_none()
&& !self.containers().is_empty();

if !needs_scrolling_cloaking {
for hwnd in self.scrolling_cloaked_hwnds.drain() {
Window { hwnd }.uncloak();
}
}
}

let managed_maximized_window = self.maximized_window.is_some();

if self.tile {
Expand Down Expand Up @@ -578,6 +619,17 @@ 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));

// For scrolling layout: take the old cloaked set so we can reconcile
// which windows need to be cloaked/uncloaked after this update.
let old_scrolling_cloaked = if is_scrolling {
std::mem::take(&mut self.scrolling_cloaked_hwnds)
} else {
HashSet::new()
};
let mut new_scrolling_cloaked = HashSet::new();

let containers = self.containers_mut();

for (i, container) in containers.iter_mut().enumerate() {
Expand All @@ -595,7 +647,26 @@ impl Workspace {
layout.bottom -= total_height;
}

// For scrolling layout, check if this container is within the
// visible workspace area. Off-screen containers are cloaked so
// they don't appear on neighbouring monitors.
if is_scrolling && !layout.overlaps_horizontally(&adjusted_work_area) {
for window in container.windows() {
if !old_scrolling_cloaked.contains(&window.hwnd) {
window.cloak();
}
new_scrolling_cloaked.insert(window.hwnd);
}
continue;
}

for window in container.windows() {
// Uncloak windows that were scrolling-cloaked but are now
// back in the visible area
if is_scrolling && old_scrolling_cloaked.contains(&window.hwnd) {
window.uncloak();
}

if container
.focused_window()
.is_some_and(|w| w.hwnd == window.hwnd)
Expand Down Expand Up @@ -627,6 +698,17 @@ impl Workspace {
}
}

// Uncloak windows that were scrolling-cloaked but have been removed
// from the workspace (e.g. moved to another workspace while off-screen)
if is_scrolling {
for hwnd in &old_scrolling_cloaked {
if !new_scrolling_cloaked.contains(hwnd) {
Window { hwnd: *hwnd }.uncloak();
}
}
}
self.scrolling_cloaked_hwnds = new_scrolling_cloaked;

self.latest_layout = layouts;
}
}
Expand Down Expand Up @@ -918,6 +1000,12 @@ impl Workspace {
}

pub fn remove_window(&mut self, hwnd: isize) -> eyre::Result<()> {
// If this window was scrolling-cloaked, uncloak it before removal so it
// is visible when managed elsewhere (e.g. moved to another workspace).
if self.scrolling_cloaked_hwnds.remove(&hwnd) {
Window { hwnd }.uncloak();
}

border_manager::delete_border(hwnd);

if self.floating_windows().iter().any(|w| w.hwnd == hwnd) {
Expand Down
Loading