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.
This commit is contained in:
LGUG2Z committed 2026-10-30 15:45:37 -07:00
1 parent 94cd37f6d0
commit 0b17aae86b
3 files changed
+92 -7

No files matched your search

+11
View File
@@ -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)
+78 -7
View File
@@ -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<isize>),
ForceUpdate,
@@ -105,6 +107,27 @@ fn event_rx() -> Receiver<Notification> {
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<HashSet<isize>> = 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<BorderInfo> {
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<Mutex<WindowManager>>) -> 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<Mutex<WindowManager>>) -> 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<ZOrder> 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));
}
}
+3
View File
@@ -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();
}