From 0b17aae86bed95ecb13e8e80fe413eb1f5b14f98 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Wed, 28 Oct 2026 11:14:24 -0700 Subject: [PATCH] fix(border): leave the border of a ghosted window to the animation While a ghost stands in for a window the window is hidden and not where the ghost is, and an animation moves its border along with the ghost. But the border also follows the real window: every event komorebi handles ends with a border update that reads the rect of the real window, and so does every location change of it, and komorebi's own cloak, uncloak and resize of the window produce both. Each of them put the border where the window is going to be, or was, for a frame, until the next frame of the animation moved it back: the border jumped ahead of the window, by as much as the whole width of the screen when moves overlapped. Say which windows a ghost stands in for, from when the ghost is made until it is gone (or the animation is cancelled), and leave the borders of those to the animation. Recorded over 16 moves of mixed pace, a border was moved by something other than its animation 21 times before; on a swap in the right column, 6 times before (up to 127px off) and none after. --- komorebi/src/border_manager/border.rs | 11 ++++ komorebi/src/border_manager/mod.rs | 85 ++++++++++++++++++++++++--- komorebi/src/window.rs | 3 + 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/komorebi/src/border_manager/border.rs b/komorebi/src/border_manager/border.rs index db7ee180..7360ee95 100644 --- a/komorebi/src/border_manager/border.rs +++ b/komorebi/src/border_manager/border.rs @@ -539,6 +539,17 @@ impl Border { } let reference_hwnd = (*border_pointer).tracking_hwnd; + + // The window is hidden while a ghost stands in for it, and + // is not where the ghost is, which is where the border + // belongs: the animation moves it. + if crate::border_manager::is_ghosted(reference_hwnd) { + tracing::trace!( + "border: left the border of hwnd {reference_hwnd} to its ghost's animation" + ); + return LRESULT(0); + } + let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default(); apply_tracked_rect(border_pointer, rect); LRESULT(0) diff --git a/komorebi/src/border_manager/mod.rs b/komorebi/src/border_manager/mod.rs index 55af0638..9f527119 100644 --- a/komorebi/src/border_manager/mod.rs +++ b/komorebi/src/border_manager/mod.rs @@ -23,6 +23,7 @@ use parking_lot::Mutex; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; +use std::collections::HashSet; use std::collections::hash_map::Entry; use std::ops::Deref; use std::sync::Arc; @@ -74,6 +75,7 @@ impl Deref for RenderTarget { } } +#[derive(Debug)] pub enum Notification { Update(Option), ForceUpdate, @@ -105,6 +107,27 @@ fn event_rx() -> Receiver { channel().1.clone() } +lazy_static! { + /// The windows that a ghost is standing in for, which are hidden and not + /// where the ghost is. The animation moves their borders. + static ref GHOSTED: Mutex> = Mutex::new(HashSet::new()); +} + +/// Say that a ghost is, or has stopped being, what is seen of a window: from +/// then until then the border of the window follows the animation of the ghost +/// (see `animate_to`) and not the window, which is not where it is seen. +pub fn set_ghosted(hwnd: isize, ghosted: bool) { + if ghosted { + GHOSTED.lock().insert(hwnd); + } else { + GHOSTED.lock().remove(&hwnd); + } +} + +pub(crate) fn is_ghosted(hwnd: isize) -> bool { + GHOSTED.lock().contains(&hwnd) +} + pub fn window_border(hwnd: isize) -> Option { let id = WINDOWS_BORDERS.lock().get(&hwnd)?.clone(); BORDER_STATE.lock().get(&id).map(|b| BorderInfo { @@ -457,7 +480,14 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result // Update the border's monitor idx in case it changed border.monitor_idx = Some(monitor_idx); - let rect = WindowsApi::window_rect(focused_window_hwnd)?; + let rect = if !new_border && is_ghosted(focused_window_hwnd) { + tracing::trace!( + "border: left the border of hwnd {focused_window_hwnd} to its ghost's animation" + ); + border.window_rect + } else { + WindowsApi::window_rect(focused_window_hwnd)? + }; border.window_rect = rect; if new_border { @@ -615,11 +645,18 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result // avoid getting into a thread restart loop if we try to look up // rect info for a window that has been destroyed by the time // we get here - let rect = match WindowsApi::window_rect(focused_window_hwnd) { - Ok(rect) => rect, - Err(_) => { - remove_border(&c.id, &mut borders, &mut windows_borders)?; - continue 'containers; + let rect = if !new_border && is_ghosted(focused_window_hwnd) { + tracing::trace!( + "border: left the border of hwnd {focused_window_hwnd} to its ghost's animation" + ); + border.window_rect + } else { + match WindowsApi::window_rect(focused_window_hwnd) { + Ok(rect) => rect, + Err(_) => { + remove_border(&c.id, &mut borders, &mut windows_borders)?; + continue 'containers; + } } }; border.window_rect = rect; @@ -709,7 +746,15 @@ fn handle_floating_borders( // Update the border's monitor idx in case it changed border.monitor_idx = Some(monitor_idx); - let rect = WindowsApi::window_rect(window.hwnd)?; + let rect = if !new_border && is_ghosted(window.hwnd) { + tracing::trace!( + "border: left the border of hwnd {} to its ghost's animation", + window.hwnd + ); + border.window_rect + } else { + WindowsApi::window_rect(window.hwnd)? + }; border.window_rect = rect; let should_invalidate = @@ -870,3 +915,29 @@ impl From for isize { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_window_is_ghosted_from_when_it_is_said_to_be_until_it_is_not() { + // a window handle nothing else in the tests uses + let hwnd = 0x7e57_0001; + + assert!(!is_ghosted(hwnd)); + + set_ghosted(hwnd, true); + assert!(is_ghosted(hwnd)); + assert!(!is_ghosted(hwnd + 1)); + + // saying it twice does not make it take two to undo + set_ghosted(hwnd, true); + set_ghosted(hwnd, false); + assert!(!is_ghosted(hwnd)); + + // and undoing what was never done is fine + set_ghosted(hwnd, false); + assert!(!is_ghosted(hwnd)); + } +} diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index 6661d6ac..f3600b6c 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -338,6 +338,7 @@ impl RenderDispatcher for MovementRenderDispatcher { match GhostWindow::create(self.hwnd, self.start_rect) { Ok(ghost) => { *self.ghost.lock() = Some(ghost); + border_manager::set_ghosted(self.hwnd, true); // DwmSetWindowAttribute(DWMWA_CLOAK) is rejected with // E_ACCESSDENIED for foreign HWNDs; the undocumented @@ -428,6 +429,7 @@ impl RenderDispatcher for MovementRenderDispatcher { ghost.fade_out(self.target_rect); } + border_manager::set_ghosted(self.hwnd, false); let _ = ghost.dispose(); self.finalise_managers(); @@ -455,6 +457,7 @@ impl RenderDispatcher for MovementRenderDispatcher { self.uncloak(); } + border_manager::set_ghosted(self.hwnd, false); if let Some(ghost) = self.ghost.lock().take() { let _ = ghost.dispose(); }