diff --git a/komorebi/src/animation/ghost.rs b/komorebi/src/animation/ghost.rs
index b55b27a6..9c02b612 100644
--- a/komorebi/src/animation/ghost.rs
+++ b/komorebi/src/animation/ghost.rs
@@ -35,6 +35,7 @@ use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Dwm::DWM_THUMBNAIL_PROPERTIES;
use windows::Win32::Graphics::Dwm::DWM_TNP_OPACITY;
use windows::Win32::Graphics::Dwm::DWM_TNP_RECTDESTINATION;
+use windows::Win32::Graphics::Dwm::DWM_TNP_RECTSOURCE;
use windows::Win32::Graphics::Dwm::DWM_TNP_VISIBLE;
use windows::Win32::Graphics::Gdi::AC_SRC_ALPHA;
use windows::Win32::Graphics::Gdi::AC_SRC_OVER;
@@ -81,6 +82,7 @@ use windows::core::PCWSTR;
use crate::WindowsApi;
use crate::animation::snapshot::Snapshot;
use crate::animation::snapshot::SnapshotWindow;
+use crate::animation::transition::clip_thumbnail;
use crate::core::Rect;
const GHOST_CLASS_NAME: &[u16] = &[
@@ -296,7 +298,11 @@ impl GhostHost {
let origin = self.origin();
let registered =
WindowsApi::dwm_register_thumbnail(self.hwnd, source.hwnd).and_then(|hthumb| {
- let props = thumbnail_properties(destination(start_rect, origin), 255);
+ let props = thumbnail_properties(
+ destination(start_rect, origin),
+ full_source(snapshot.width, snapshot.height),
+ 255,
+ );
WindowsApi::dwm_update_thumbnail_properties(hthumb, &props)
.inspect_err(|_| {
let _ = WindowsApi::dwm_unregister_thumbnail(hthumb);
@@ -471,17 +477,49 @@ fn destination(rect: Rect, origin: (i32, i32)) -> RECT {
}
}
-fn thumbnail_properties(destination: RECT, opacity: u8) -> DWM_THUMBNAIL_PROPERTIES {
+/// The whole of a picture, in pixels of it
+fn full_source(width: i32, height: i32) -> RECT {
+ RECT {
+ left: 0,
+ top: 0,
+ right: width,
+ bottom: height,
+ }
+}
+
+/// A rect, in pixels of the picture, as the part of it that is shown
+fn part_of_source(part: Rect) -> RECT {
+ RECT {
+ left: part.left,
+ top: part.top,
+ right: part.left + part.right,
+ bottom: part.top + part.bottom,
+ }
+}
+
+fn thumbnail_properties(destination: RECT, source: RECT, opacity: u8) -> DWM_THUMBNAIL_PROPERTIES {
DWM_THUMBNAIL_PROPERTIES {
- dwFlags: DWM_TNP_VISIBLE | DWM_TNP_RECTDESTINATION | DWM_TNP_OPACITY,
+ dwFlags: DWM_TNP_VISIBLE | DWM_TNP_RECTDESTINATION | DWM_TNP_RECTSOURCE | DWM_TNP_OPACITY,
rcDestination: destination,
- rcSource: RECT::default(),
+ rcSource: source,
opacity,
fVisible: true.into(),
fSourceClientAreaOnly: false.into(),
}
}
+/// A thumbnail that is not shown, because none of it is where it may be
+fn hidden_thumbnail_properties() -> DWM_THUMBNAIL_PROPERTIES {
+ DWM_THUMBNAIL_PROPERTIES {
+ dwFlags: DWM_TNP_VISIBLE,
+ rcDestination: RECT::default(),
+ rcSource: RECT::default(),
+ opacity: 0,
+ fVisible: false.into(),
+ fSourceClientAreaOnly: false.into(),
+ }
+}
+
/// A "ghost" of a window, used during movement animations: a still picture of
/// the window, taken when the animation starts, that is moved and scaled in the
/// window's place while the window itself, cloaked, is laid out again at its
@@ -491,11 +529,21 @@ pub struct GhostWindow {
hthumb: isize,
/// Where the host's client area starts in screen coordinates
origin: (i32, i32),
- /// Where the ghost was last put, and how opaque it was made
- shown: Mutex>,
+ /// How big the picture is, in pixels
+ source_size: (i32, i32),
+ /// What the ghost was last made to show
+ shown: Mutex >,
disposed: bool,
}
+/// What a ghost shows: where it is drawn and which part of its picture goes
+/// there, or nothing, and how opaque it is
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct Shown {
+ drawn: Option<(Rect, Rect)>,
+ opacity: u8,
+}
+
impl GhostWindow {
/// Take a picture of the window and show it where the window is. The
/// picture is taken before the window is touched.
@@ -503,6 +551,7 @@ impl GhostWindow {
let start = std::time::Instant::now();
let snapshot = Snapshot::capture(src_hwnd)?;
let captured = start.elapsed();
+ let source_size = (snapshot.width, snapshot.height);
let (reply_tx, reply_rx) = bounded::>(1);
ghost_owner()
@@ -524,8 +573,21 @@ impl GhostWindow {
Ok(Self {
hthumb,
origin,
- // it is registered where the window is, and opaque
- shown: Mutex::new(Some((start_rect, 255))),
+ source_size,
+ // it is registered where the window is, with all of its picture,
+ // and opaque
+ shown: Mutex::new(Some(Shown {
+ drawn: Some((
+ start_rect,
+ Rect {
+ left: 0,
+ top: 0,
+ right: source_size.0,
+ bottom: source_size.1,
+ },
+ )),
+ opacity: 255,
+ })),
disposed: false,
})
}
@@ -537,13 +599,22 @@ impl GhostWindow {
/// A ghost that is already there is left alone: asking DWM for what is
/// already so still has it compose the frame again.
pub fn update(&self, rect: Rect, opacity: u8) -> eyre::Result<()> {
- if !needs_update(&mut self.shown.lock(), rect, opacity) {
+ self.update_clipped(rect, opacity, None)
+ }
+
+ /// Like `update`, but nothing of the ghost is drawn outside `clip`: the
+ /// part of it that is outside is cut off, and it is not shown at all when
+ /// all of it is.
+ pub fn update_clipped(&self, rect: Rect, opacity: u8, clip: Option) -> eyre::Result<()> {
+ let drawn = drawn_part(rect, self.source_size, clip);
+
+ if !needs_update(&mut self.shown.lock(), Shown { drawn, opacity }) {
return Ok(());
}
WindowsApi::dwm_update_thumbnail_properties(
self.hthumb,
- &thumbnail_properties(destination(rect, self.origin), opacity),
+ &properties_for(drawn, self.origin, opacity),
)
}
@@ -583,14 +654,47 @@ impl Drop for GhostWindow {
}
}
-/// Whether putting a ghost at `rect` with `opacity` changes anything, given
-/// where it was last put, which is remembered if it does
-fn needs_update(shown: &mut Option<(Rect, u8)>, rect: Rect, opacity: u8) -> bool {
- if *shown == Some((rect, opacity)) {
+/// Where a ghost of a picture of `source_size` that is to be at `rect` is to be
+/// drawn and which part of the picture goes there, when nothing of it is to be
+/// drawn outside `clip`, if there is one. Nothing if none of it is inside.
+fn drawn_part(rect: Rect, source_size: (i32, i32), clip: Option) -> Option<(Rect, Rect)> {
+ match clip {
+ Some(clip) => clip_thumbnail(rect, source_size, clip),
+ None => Some((
+ rect,
+ Rect {
+ left: 0,
+ top: 0,
+ right: source_size.0,
+ bottom: source_size.1,
+ },
+ )),
+ }
+}
+
+/// What to ask DWM for to show what `drawn_part` says, on a host whose client
+/// area starts at `origin` on the screen
+fn properties_for(
+ drawn: Option<(Rect, Rect)>,
+ origin: (i32, i32),
+ opacity: u8,
+) -> DWM_THUMBNAIL_PROPERTIES {
+ match drawn {
+ Some((rect, part)) => {
+ thumbnail_properties(destination(rect, origin), part_of_source(part), opacity)
+ }
+ None => hidden_thumbnail_properties(),
+ }
+}
+
+/// Whether showing what a ghost is to show changes anything, given what it
+/// was last made to show, which is remembered if it does
+fn needs_update(shown: &mut Option, next: Shown) -> bool {
+ if *shown == Some(next) {
return false;
}
- *shown = Some((rect, opacity));
+ *shown = Some(next);
true
}
@@ -710,23 +814,59 @@ mod tests {
right: 100,
bottom: 50,
};
- let mut shown = Some((rect(0), 255));
+ let picture = rect(0);
+ let at = |left, opacity| Shown {
+ drawn: Some((rect(left), picture)),
+ opacity,
+ };
+ let mut shown = Some(at(0, 255));
// where it is already
- assert!(!needs_update(&mut shown, rect(0), 255));
+ assert!(!needs_update(&mut shown, at(0, 255)));
// moved, or faded, or both
- assert!(needs_update(&mut shown, rect(10), 255));
- assert!(!needs_update(&mut shown, rect(10), 255));
- assert!(needs_update(&mut shown, rect(10), 200));
- assert!(needs_update(&mut shown, rect(20), 100));
+ assert!(needs_update(&mut shown, at(10, 255)));
+ assert!(!needs_update(&mut shown, at(10, 255)));
+ assert!(needs_update(&mut shown, at(10, 200)));
+ assert!(needs_update(&mut shown, at(20, 100)));
// and back, which is a change from where it was last
- assert!(needs_update(&mut shown, rect(10), 200));
+ assert!(needs_update(&mut shown, at(10, 200)));
// one that was never put anywhere is put somewhere
let mut never = None;
- assert!(needs_update(&mut never, rect(0), 255));
+ assert!(needs_update(&mut never, at(0, 255)));
+ }
+
+ #[test]
+ fn a_ghost_is_updated_when_the_part_of_it_that_is_drawn_changes() {
+ let rect = |left, width| Rect {
+ left,
+ top: 0,
+ right: width,
+ bottom: 50,
+ };
+ let drawn = |part_left, part_width| Shown {
+ drawn: Some((rect(0, part_width), rect(part_left, part_width))),
+ opacity: 255,
+ };
+ let nothing = Shown {
+ drawn: None,
+ opacity: 255,
+ };
+
+ let mut shown = Some(drawn(0, 100));
+
+ // the same place, but a different part of the picture
+ assert!(needs_update(&mut shown, drawn(20, 100)));
+ assert!(!needs_update(&mut shown, drawn(20, 100)));
+
+ // not drawn at all, which is asked for once
+ assert!(needs_update(&mut shown, nothing));
+ assert!(!needs_update(&mut shown, nothing));
+
+ // and drawn again
+ assert!(needs_update(&mut shown, drawn(20, 100)));
}
#[test]
@@ -770,4 +910,211 @@ mod tests {
assert_eq!(ghost_opacity(1.0, 1.0), 0);
assert_eq!(ghost_opacity(0.99, 1.0), 255);
}
+
+ /// Needs a desktop to put windows on, so it is only run when asked to:
+ /// `cargo test -p komorebi ghost -- --ignored`
+ ///
+ /// What is asked of DWM to clip a ghost to a rect, as if the rect were the
+ /// edge of a monitor with another one next to it, draws none of it outside
+ /// the rect, and all of the rest of it where it belongs.
+ ///
+ /// (The host of the ghosts is a layered window, whose thumbnails the screen
+ /// cannot be read back from, so this is done on a host that is not.)
+ #[test]
+ #[ignore = "needs an interactive desktop"]
+ fn dwm_draws_a_clipped_thumbnail_only_inside_its_clip() {
+ use windows::Win32::Graphics::Gdi::BitBlt;
+ use windows::Win32::Graphics::Gdi::CAPTUREBLT;
+ use windows::Win32::Graphics::Gdi::CreateCompatibleBitmap;
+ use windows::Win32::Graphics::Gdi::CreateSolidBrush;
+ use windows::Win32::Graphics::Gdi::DeleteDC;
+ use windows::Win32::Graphics::Gdi::DeleteObject;
+ use windows::Win32::Graphics::Gdi::GetPixel;
+ use windows::Win32::Graphics::Gdi::ROP_CODE;
+ use windows::Win32::Graphics::Gdi::SRCCOPY;
+ use windows::Win32::Graphics::Gdi::SelectObject;
+ use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
+ use windows::Win32::UI::WindowsAndMessaging::DestroyWindow;
+ use windows::Win32::UI::WindowsAndMessaging::HWND_TOPMOST;
+ use windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE;
+ use windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
+ use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
+ use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
+ use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
+ use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
+ use windows::core::w;
+
+ // windows of two colours nothing else on the desktop is: the one that
+ // is thumbnailed (magenta) and the host that it is drawn on (green)
+ let instance = WindowsApi::module_handle_w().unwrap();
+ let window = |class: windows::core::PCWSTR, colour: u32, x, y, width, height| {
+ let window_class = WNDCLASSW {
+ hInstance: instance.into(),
+ lpszClassName: class,
+ lpfnWndProc: Some(ghost_wnd_proc),
+ hbrBackground: unsafe { CreateSolidBrush(COLORREF(colour)) },
+ ..Default::default()
+ };
+ let _ = WindowsApi::register_class_w(&window_class);
+
+ unsafe {
+ CreateWindowExW(
+ WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
+ class,
+ class,
+ WS_POPUP | WS_VISIBLE,
+ x,
+ y,
+ width,
+ height,
+ None,
+ None,
+ Some(instance.into()),
+ None,
+ )
+ .unwrap()
+ }
+ };
+
+ let source = window(
+ w!("komorebi-ghost-test-source"),
+ 0x00FF_00FF,
+ 100,
+ 100,
+ 300,
+ 200,
+ );
+ let host = window(
+ w!("komorebi-ghost-test-host"),
+ 0x0000_FF00,
+ 600,
+ 300,
+ 800,
+ 600,
+ );
+ unsafe {
+ SetWindowPos(host, Some(HWND_TOPMOST), 600, 300, 800, 600, SWP_NOACTIVATE).unwrap();
+ }
+ // paint them now: nothing is pumping the messages of this thread
+ for hwnd in [source, host] {
+ unsafe {
+ use windows::Win32::Graphics::Gdi::RDW_ERASE;
+ use windows::Win32::Graphics::Gdi::RDW_INVALIDATE;
+ use windows::Win32::Graphics::Gdi::RDW_UPDATENOW;
+ use windows::Win32::Graphics::Gdi::RedrawWindow;
+
+ let _ = RedrawWindow(
+ Some(hwnd),
+ None,
+ None,
+ RDW_INVALIDATE | RDW_ERASE | RDW_UPDATENOW,
+ );
+ }
+ }
+ std::thread::sleep(Duration::from_millis(300));
+
+ let hthumb = WindowsApi::dwm_register_thumbnail(host.0 as isize, source.0 as isize)
+ .expect("a thumbnail of the window");
+ let size = unsafe { windows::Win32::Graphics::Dwm::DwmQueryThumbnailSourceSize(hthumb) }
+ .expect("the size of what is thumbnailed");
+ let size = (size.cx, size.cy);
+
+ // shown 400 wide, from 600 on the screen, of which what is right of 800
+ // is inside the clip
+ let dest = Rect {
+ left: 600,
+ top: 400,
+ right: 400,
+ bottom: 300,
+ };
+ let clip = Rect {
+ left: 800,
+ top: 0,
+ right: 1000,
+ bottom: 1500,
+ };
+ let drawn = drawn_part(dest, size, Some(clip));
+ assert!(drawn.is_some());
+ WindowsApi::dwm_update_thumbnail_properties(
+ hthumb,
+ &properties_for(drawn, (600, 300), 255),
+ )
+ .expect("show it clipped");
+ std::thread::sleep(Duration::from_millis(400));
+
+ // the screen is read in pixels, and the windows were put where they are in
+ // the units of a process that the display's scaling is applied to
+ let scale = WindowsApi::window_rect(host.0 as isize)
+ .map(|rect| f64::from(rect.right) / 800.0)
+ .unwrap_or(1.0);
+
+ let screen = unsafe { windows::Win32::Graphics::Gdi::GetDC(None) };
+ let sample = |x: i32, y: i32| unsafe {
+ let (x, y) = (
+ (f64::from(x) * scale).round() as i32,
+ (f64::from(y) * scale).round() as i32,
+ );
+ let dc = CreateCompatibleDC(Some(screen));
+ let bitmap = CreateCompatibleBitmap(screen, 1, 1);
+ let previous = SelectObject(dc, bitmap.into());
+ let _ = BitBlt(
+ dc,
+ 0,
+ 0,
+ 1,
+ 1,
+ Some(screen),
+ x,
+ y,
+ ROP_CODE(SRCCOPY.0 | CAPTUREBLT.0),
+ );
+ let pixel = GetPixel(dc, 0, 0).0 & 0xFF_FFFF;
+ SelectObject(dc, previous);
+ let _ = DeleteObject(bitmap.into());
+ let _ = DeleteDC(dc);
+ pixel
+ };
+
+ // left of the clip, but in the destination: what the host is
+ let outside = sample(650, 450);
+ // right of the edge of the clip, in the destination: the window
+ let inside = sample(950, 450);
+ // in the host and not in the destination at all: what the host is
+ let elsewhere = sample(650, 350);
+
+ // all of it moved out of the clip: hidden
+ let hidden = drawn_part(Rect { left: 100, ..dest }, size, Some(clip));
+ assert!(hidden.is_none());
+ WindowsApi::dwm_update_thumbnail_properties(
+ hthumb,
+ &properties_for(hidden, (600, 300), 255),
+ )
+ .expect("hide it");
+ std::thread::sleep(Duration::from_millis(400));
+ let moved_out = sample(950, 450);
+
+ unsafe {
+ ReleaseDC(None, screen);
+ let _ = WindowsApi::dwm_unregister_thumbnail(hthumb);
+ let _ = DestroyWindow(source);
+ let _ = DestroyWindow(host);
+ }
+
+ assert_eq!(
+ outside, 0x00FF00,
+ "the host outside the clip, got {outside:#08x}"
+ );
+ assert_eq!(
+ inside, 0xFF00FF,
+ "the window inside the clip, got {inside:#08x}"
+ );
+ assert_eq!(
+ elsewhere, 0x00FF00,
+ "the host outside the destination, got {elsewhere:#08x}"
+ );
+ assert_eq!(
+ moved_out, 0x00FF00,
+ "nothing drawn when all of it is outside, got {moved_out:#08x}"
+ );
+ }
}