mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-13 12:23:38 +01:00
refactor(animation): move the ghost dispatcher out of window.rs
The animation of a window by a ghost is not about windows in general, and the workspace transitions that follow need to use it for more than moving a window from one place in a layout to another. No change in behaviour.
This commit is contained in:
1 parent
699f31f8bc
commit
8bb6fed332
3 files changed
+340
-319
No files matched your search
@@ -0,0 +1,330 @@
|
||||
//! The animation of one window by a ghost: a picture of the window is moved
|
||||
//! and scaled in its place while the window itself, cloaked, is laid out at
|
||||
//! its new size, and then shown again under the ghost as it dissolves.
|
||||
|
||||
use crate::AnimationStyle;
|
||||
use crate::animation::ANIMATION_MANAGER;
|
||||
use crate::animation::GHOST_MOVEMENT_ENABLED;
|
||||
use crate::animation::RenderDispatcher;
|
||||
use crate::animation::ghost;
|
||||
use crate::animation::ghost::GhostWindow;
|
||||
use crate::animation::lerp::Lerp;
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::animation::prefix::new_animation_key;
|
||||
use crate::animation::snapshot;
|
||||
use crate::border_manager;
|
||||
use crate::com::SetCloak;
|
||||
use crate::core::Rect;
|
||||
use crate::focus_manager;
|
||||
use crate::stackbar_manager;
|
||||
use crate::transparency_manager;
|
||||
use crate::window::Window;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use color_eyre::eyre;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
|
||||
pub(crate) struct GhostDispatcher {
|
||||
hwnd: isize,
|
||||
start_rect: Rect,
|
||||
target_rect: Rect,
|
||||
top: bool,
|
||||
style: AnimationStyle,
|
||||
/// Some between successful pre_render and post_render/cleanup_on_cancel when
|
||||
/// ghost movement is active. None for the legacy code path.
|
||||
ghost: Mutex<Option<GhostWindow>>,
|
||||
/// Tracks whether the source has been cloaked so cleanup can uncloak idempotently.
|
||||
cloaked: AtomicBool,
|
||||
/// Last lerped logical rect actually applied; used by cleanup_on_cancel to
|
||||
/// snap the real window to the position the user was last seeing.
|
||||
last_animated_rect: Mutex<Rect>,
|
||||
/// Resizing the cloaked source to its target, which is done while the
|
||||
/// animation runs because it can take a while.
|
||||
resize: Mutex<Option<thread::JoinHandle<eyre::Result<()>>>>,
|
||||
/// Whether the source has been shown again, under the fading ghost
|
||||
revealed: AtomicBool,
|
||||
/// How far through the animation the source was shown again, which is
|
||||
/// when the ghost starts to fade
|
||||
revealed_at: Mutex<f64>,
|
||||
}
|
||||
|
||||
impl GhostDispatcher {
|
||||
pub(crate) const PREFIX: AnimationPrefix = AnimationPrefix::Movement;
|
||||
|
||||
pub(crate) fn new(
|
||||
hwnd: isize,
|
||||
start_rect: Rect,
|
||||
target_rect: Rect,
|
||||
top: bool,
|
||||
style: AnimationStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
hwnd,
|
||||
start_rect,
|
||||
target_rect,
|
||||
top,
|
||||
style,
|
||||
ghost: Mutex::new(None),
|
||||
cloaked: AtomicBool::new(false),
|
||||
last_animated_rect: Mutex::new(start_rect),
|
||||
resize: Mutex::new(None),
|
||||
revealed: AtomicBool::new(false),
|
||||
revealed_at: Mutex::new(ghost::REVEAL_AT),
|
||||
}
|
||||
}
|
||||
|
||||
fn use_ghost(&self) -> bool {
|
||||
// DWM has no thumbnail to show of a window that is minimized or gone
|
||||
GHOST_MOVEMENT_ENABLED.load(Ordering::Relaxed)
|
||||
&& WindowsApi::is_window(self.hwnd)
|
||||
&& !WindowsApi::is_iconic(self.hwnd)
|
||||
}
|
||||
|
||||
fn finalise_managers(&self) {
|
||||
if ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(GhostDispatcher::PREFIX)
|
||||
== 0
|
||||
{
|
||||
if WindowsApi::foreground_window().unwrap_or_default() == self.hwnd {
|
||||
focus_manager::send_notification(self.hwnd)
|
||||
}
|
||||
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
|
||||
stackbar_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
}
|
||||
}
|
||||
|
||||
fn uncloak(&self) {
|
||||
if self.cloaked.swap(false, Ordering::SeqCst) {
|
||||
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Start resizing the cloaked source to its target rect, on a thread of
|
||||
/// its own: a window that is slow to take a new size would otherwise hold
|
||||
/// up the animation, and the ghost is on screen in its place meanwhile.
|
||||
fn start_resize(&self) {
|
||||
let hwnd = self.hwnd;
|
||||
let target = self.target_rect;
|
||||
let top = self.top;
|
||||
|
||||
*self.resize.lock() = Some(thread::spawn(move || {
|
||||
let resized = WindowsApi::position_window(hwnd, &target, top, false);
|
||||
|
||||
if resized.is_ok() {
|
||||
// A window that is hidden has no reason to lay itself out again
|
||||
// before it is shown, and Explorer takes its time over it when
|
||||
// it is: have it do it now, while the ghost is on screen.
|
||||
snapshot::paint_now(hwnd);
|
||||
|
||||
// Windows that render through a swap chain are given a real
|
||||
// size change on the way (see `ghost::nudge_swap_chain_window`)
|
||||
// so that their compositor rebuilds its render target at the
|
||||
// final size. It takes as long as the window takes to answer
|
||||
// twice, which is why it is done here, while the window is
|
||||
// hidden, and not in the middle of the animation. It has to be
|
||||
// the last thing done to the size of the window before it is
|
||||
// shown: rendering the window leaves its compositor as stale as
|
||||
// it was, and the window then takes over a hundred
|
||||
// milliseconds to show anything.
|
||||
if ghost::is_swap_chain_window(hwnd) {
|
||||
ghost::nudge_swap_chain_window(hwnd, &target, top);
|
||||
}
|
||||
}
|
||||
|
||||
resized
|
||||
}));
|
||||
}
|
||||
|
||||
/// Whether the resize started by `start_resize` is done, or never was
|
||||
fn resize_is_done(&self) -> bool {
|
||||
self.resize
|
||||
.lock()
|
||||
.as_ref()
|
||||
.is_none_or(thread::JoinHandle::is_finished)
|
||||
}
|
||||
|
||||
/// Wait for the resize started by `start_resize` to be done
|
||||
fn finish_resize(&self) -> eyre::Result<()> {
|
||||
match self.resize.lock().take() {
|
||||
Some(resize) => resize
|
||||
.join()
|
||||
.unwrap_or_else(|_| Err(eyre::eyre!("the resize of hwnd {} panicked", self.hwnd))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the source again, at its target, under the ghost, which fades out
|
||||
/// over it from here on. By now it has had part of the animation to lay
|
||||
/// itself out at its new size, which is why this is not done at the end.
|
||||
fn reveal(&self, progress: f64) {
|
||||
if self.revealed.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
*self.revealed_at.lock() = progress.clamp(ghost::REVEAL_AT, 1.0);
|
||||
|
||||
if let Err(error) = self.finish_resize() {
|
||||
// the window cannot be left hidden, and the next layout pass
|
||||
// puts it where it belongs
|
||||
tracing::warn!(
|
||||
"ghost movement: failed to resize hwnd {} to its target: {error}",
|
||||
self.hwnd
|
||||
);
|
||||
}
|
||||
|
||||
self.uncloak();
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderDispatcher for GhostDispatcher {
|
||||
fn get_animation_key(&self) -> String {
|
||||
new_animation_key(GhostDispatcher::PREFIX, self.hwnd.to_string())
|
||||
}
|
||||
|
||||
fn pre_render(&self) -> eyre::Result<()> {
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
if self.use_ghost() {
|
||||
// The ghost is a snapshot of the window as it is now, which is
|
||||
// taken before anything is done to the window, and shown at the
|
||||
// LOGICAL rect (visible content area) the window is in. It does
|
||||
// not follow the window, so it shows the old frame for as long as
|
||||
// the window takes to lay itself out again, which is what the
|
||||
// animation is for.
|
||||
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
|
||||
// IApplicationView::SetCloak path used elsewhere does not
|
||||
// have that restriction.
|
||||
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 2);
|
||||
self.cloaked.store(true, Ordering::SeqCst);
|
||||
|
||||
// The window is resized now rather than when the animation
|
||||
// is over, so that it has the animation to paint at its new
|
||||
// size in, while it cannot be seen.
|
||||
self.start_resize();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"ghost movement: failed to create ghost for hwnd {}: {error}; \
|
||||
falling back to legacy path",
|
||||
self.hwnd
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn paces_with_compositor(&self) -> bool {
|
||||
self.ghost.lock().is_some()
|
||||
}
|
||||
|
||||
fn render(&self, progress: f64) -> eyre::Result<()> {
|
||||
let logical = self.start_rect.lerp(self.target_rect, progress, self.style);
|
||||
*self.last_animated_rect.lock() = logical;
|
||||
|
||||
let ghost_active = self.ghost.lock().is_some();
|
||||
if ghost_active {
|
||||
// A window that is slow to take its new size is not waited for: the
|
||||
// animation goes on with the ghost as it is, and the window is
|
||||
// shown when it is ready, or once the animation is over.
|
||||
if progress >= ghost::REVEAL_AT
|
||||
&& !self.revealed.load(Ordering::SeqCst)
|
||||
&& self.resize_is_done()
|
||||
{
|
||||
self.reveal(progress);
|
||||
}
|
||||
|
||||
let opacity = if self.revealed.load(Ordering::SeqCst) {
|
||||
ghost::ghost_opacity(progress, *self.revealed_at.lock())
|
||||
} else {
|
||||
255
|
||||
};
|
||||
|
||||
if let Some(ghost) = self.ghost.lock().as_ref()
|
||||
&& let Err(error) = ghost.update(logical, opacity)
|
||||
{
|
||||
tracing::trace!("ghost update failed: {error}");
|
||||
}
|
||||
border_manager::animate_to(self.hwnd, logical);
|
||||
} else {
|
||||
// Legacy path: animations always run on a separate thread, so we don't
|
||||
// gate on WINDOW_HANDLING_BEHAVIOUR here.
|
||||
WindowsApi::move_window(self.hwnd, &logical, false)?;
|
||||
WindowsApi::invalidate_rect(self.hwnd, None, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_render(&self) -> eyre::Result<()> {
|
||||
let Some(ghost) = self.ghost.lock().take() else {
|
||||
// Legacy path: the final reposition, then one DWM frame's wait so
|
||||
// the app's first post-move paint lands.
|
||||
let landed = WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false);
|
||||
self.uncloak();
|
||||
unsafe {
|
||||
let _ = windows::Win32::Graphics::Dwm::DwmFlush();
|
||||
}
|
||||
self.finalise_managers();
|
||||
return landed;
|
||||
};
|
||||
|
||||
// Normally done a good while ago, but not for a window that was slow to
|
||||
// take its new size, which is waited for now, with the ghost on screen
|
||||
// over it, and then faded out of the way a little more quickly than
|
||||
// it otherwise would be.
|
||||
if !self.revealed.load(Ordering::SeqCst) {
|
||||
self.reveal(1.0);
|
||||
ghost.fade_out(self.target_rect);
|
||||
}
|
||||
|
||||
border_manager::set_ghosted(self.hwnd, false);
|
||||
let _ = ghost.dispose();
|
||||
|
||||
self.finalise_managers();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup_on_cancel(&self) {
|
||||
// Snap the real window to wherever the ghost was last drawn so the next
|
||||
// dispatcher can capture an accurate start_rect. Then uncloak and tear
|
||||
// down the ghost.
|
||||
let _ = self.finish_resize();
|
||||
|
||||
if WindowsApi::is_window(self.hwnd) {
|
||||
let target = *self.last_animated_rect.lock();
|
||||
|
||||
if let Err(error) = WindowsApi::position_window(self.hwnd, &target, false, false) {
|
||||
tracing::warn!(
|
||||
"ghost movement cancel: failed to snap hwnd {} to last rect: {error}",
|
||||
self.hwnd
|
||||
);
|
||||
}
|
||||
|
||||
self.revealed.store(true, Ordering::SeqCst);
|
||||
self.uncloak();
|
||||
}
|
||||
|
||||
border_manager::set_ghosted(self.hwnd, false);
|
||||
if let Some(ghost) = self.ghost.lock().take() {
|
||||
let _ = ghost.dispose();
|
||||
}
|
||||
|
||||
self.finalise_managers();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ pub use engine::AnimationEngine;
|
||||
pub mod animation_manager;
|
||||
pub mod engine;
|
||||
pub mod ghost;
|
||||
pub mod ghost_dispatcher;
|
||||
pub mod lerp;
|
||||
pub mod prefix;
|
||||
pub mod render_dispatcher;
|
||||
|
||||
+9
-319
@@ -20,14 +20,11 @@ use crate::animation::ANIMATION_MANAGER;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
use crate::animation::AnimationEngine;
|
||||
use crate::animation::GHOST_MOVEMENT_ENABLED;
|
||||
use crate::animation::RenderDispatcher;
|
||||
use crate::animation::ghost;
|
||||
use crate::animation::ghost::GhostWindow;
|
||||
use crate::animation::ghost_dispatcher::GhostDispatcher;
|
||||
use crate::animation::lerp::Lerp;
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::animation::prefix::new_animation_key;
|
||||
use crate::animation::snapshot;
|
||||
use crate::border_manager;
|
||||
use crate::cloaked;
|
||||
use crate::com::SetCloak;
|
||||
@@ -37,8 +34,6 @@ use crate::core::Rect;
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::focus_manager;
|
||||
use crate::stackbar_manager;
|
||||
use crate::styles::ExtendedWindowStyle;
|
||||
use crate::styles::WindowStyle;
|
||||
use crate::transparency_manager;
|
||||
@@ -47,7 +42,6 @@ use crate::windows_api;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use color_eyre::eyre;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -58,7 +52,6 @@ use std::convert::TryFrom;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
@@ -166,309 +159,6 @@ impl Serialize for Window {
|
||||
}
|
||||
}
|
||||
|
||||
struct MovementRenderDispatcher {
|
||||
hwnd: isize,
|
||||
start_rect: Rect,
|
||||
target_rect: Rect,
|
||||
top: bool,
|
||||
style: AnimationStyle,
|
||||
/// Some between successful pre_render and post_render/cleanup_on_cancel when
|
||||
/// ghost movement is active. None for the legacy code path.
|
||||
ghost: Mutex<Option<GhostWindow>>,
|
||||
/// Tracks whether the source has been cloaked so cleanup can uncloak idempotently.
|
||||
cloaked: AtomicBool,
|
||||
/// Last lerped logical rect actually applied; used by cleanup_on_cancel to
|
||||
/// snap the real window to the position the user was last seeing.
|
||||
last_animated_rect: Mutex<Rect>,
|
||||
/// Resizing the cloaked source to its target, which is done while the
|
||||
/// animation runs because it can take a while.
|
||||
resize: Mutex<Option<thread::JoinHandle<eyre::Result<()>>>>,
|
||||
/// Whether the source has been shown again, under the fading ghost
|
||||
revealed: AtomicBool,
|
||||
/// How far through the animation the source was shown again, which is
|
||||
/// when the ghost starts to fade
|
||||
revealed_at: Mutex<f64>,
|
||||
}
|
||||
|
||||
impl MovementRenderDispatcher {
|
||||
const PREFIX: AnimationPrefix = AnimationPrefix::Movement;
|
||||
|
||||
pub fn new(
|
||||
hwnd: isize,
|
||||
start_rect: Rect,
|
||||
target_rect: Rect,
|
||||
top: bool,
|
||||
style: AnimationStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
hwnd,
|
||||
start_rect,
|
||||
target_rect,
|
||||
top,
|
||||
style,
|
||||
ghost: Mutex::new(None),
|
||||
cloaked: AtomicBool::new(false),
|
||||
last_animated_rect: Mutex::new(start_rect),
|
||||
resize: Mutex::new(None),
|
||||
revealed: AtomicBool::new(false),
|
||||
revealed_at: Mutex::new(ghost::REVEAL_AT),
|
||||
}
|
||||
}
|
||||
|
||||
fn use_ghost(&self) -> bool {
|
||||
// DWM has no thumbnail to show of a window that is minimized or gone
|
||||
GHOST_MOVEMENT_ENABLED.load(Ordering::Relaxed)
|
||||
&& WindowsApi::is_window(self.hwnd)
|
||||
&& !WindowsApi::is_iconic(self.hwnd)
|
||||
}
|
||||
|
||||
fn finalise_managers(&self) {
|
||||
if ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX)
|
||||
== 0
|
||||
{
|
||||
if WindowsApi::foreground_window().unwrap_or_default() == self.hwnd {
|
||||
focus_manager::send_notification(self.hwnd)
|
||||
}
|
||||
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
|
||||
stackbar_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
}
|
||||
}
|
||||
|
||||
fn uncloak(&self) {
|
||||
if self.cloaked.swap(false, Ordering::SeqCst) {
|
||||
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Start resizing the cloaked source to its target rect, on a thread of
|
||||
/// its own: a window that is slow to take a new size would otherwise hold
|
||||
/// up the animation, and the ghost is on screen in its place meanwhile.
|
||||
fn start_resize(&self) {
|
||||
let hwnd = self.hwnd;
|
||||
let target = self.target_rect;
|
||||
let top = self.top;
|
||||
|
||||
*self.resize.lock() = Some(thread::spawn(move || {
|
||||
let resized = WindowsApi::position_window(hwnd, &target, top, false);
|
||||
|
||||
if resized.is_ok() {
|
||||
// A window that is hidden has no reason to lay itself out again
|
||||
// before it is shown, and Explorer takes its time over it when
|
||||
// it is: have it do it now, while the ghost is on screen.
|
||||
snapshot::paint_now(hwnd);
|
||||
|
||||
// Windows that render through a swap chain are given a real
|
||||
// size change on the way (see `ghost::nudge_swap_chain_window`)
|
||||
// so that their compositor rebuilds its render target at the
|
||||
// final size. It takes as long as the window takes to answer
|
||||
// twice, which is why it is done here, while the window is
|
||||
// hidden, and not in the middle of the animation. It has to be
|
||||
// the last thing done to the size of the window before it is
|
||||
// shown: rendering the window leaves its compositor as stale as
|
||||
// it was, and the window then takes over a hundred
|
||||
// milliseconds to show anything.
|
||||
if ghost::is_swap_chain_window(hwnd) {
|
||||
ghost::nudge_swap_chain_window(hwnd, &target, top);
|
||||
}
|
||||
}
|
||||
|
||||
resized
|
||||
}));
|
||||
}
|
||||
|
||||
/// Whether the resize started by `start_resize` is done, or never was
|
||||
fn resize_is_done(&self) -> bool {
|
||||
self.resize
|
||||
.lock()
|
||||
.as_ref()
|
||||
.is_none_or(thread::JoinHandle::is_finished)
|
||||
}
|
||||
|
||||
/// Wait for the resize started by `start_resize` to be done
|
||||
fn finish_resize(&self) -> eyre::Result<()> {
|
||||
match self.resize.lock().take() {
|
||||
Some(resize) => resize
|
||||
.join()
|
||||
.unwrap_or_else(|_| Err(eyre::eyre!("the resize of hwnd {} panicked", self.hwnd))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the source again, at its target, under the ghost, which fades out
|
||||
/// over it from here on. By now it has had part of the animation to lay
|
||||
/// itself out at its new size, which is why this is not done at the end.
|
||||
fn reveal(&self, progress: f64) {
|
||||
if self.revealed.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
*self.revealed_at.lock() = progress.clamp(ghost::REVEAL_AT, 1.0);
|
||||
|
||||
if let Err(error) = self.finish_resize() {
|
||||
// the window cannot be left hidden, and the next layout pass
|
||||
// puts it where it belongs
|
||||
tracing::warn!(
|
||||
"ghost movement: failed to resize hwnd {} to its target: {error}",
|
||||
self.hwnd
|
||||
);
|
||||
}
|
||||
|
||||
self.uncloak();
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderDispatcher for MovementRenderDispatcher {
|
||||
fn get_animation_key(&self) -> String {
|
||||
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
|
||||
}
|
||||
|
||||
fn pre_render(&self) -> eyre::Result<()> {
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
if self.use_ghost() {
|
||||
// The ghost is a snapshot of the window as it is now, which is
|
||||
// taken before anything is done to the window, and shown at the
|
||||
// LOGICAL rect (visible content area) the window is in. It does
|
||||
// not follow the window, so it shows the old frame for as long as
|
||||
// the window takes to lay itself out again, which is what the
|
||||
// animation is for.
|
||||
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
|
||||
// IApplicationView::SetCloak path used elsewhere does not
|
||||
// have that restriction.
|
||||
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 2);
|
||||
self.cloaked.store(true, Ordering::SeqCst);
|
||||
|
||||
// The window is resized now rather than when the animation
|
||||
// is over, so that it has the animation to paint at its new
|
||||
// size in, while it cannot be seen.
|
||||
self.start_resize();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"ghost movement: failed to create ghost for hwnd {}: {error}; \
|
||||
falling back to legacy path",
|
||||
self.hwnd
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn paces_with_compositor(&self) -> bool {
|
||||
self.ghost.lock().is_some()
|
||||
}
|
||||
|
||||
fn render(&self, progress: f64) -> eyre::Result<()> {
|
||||
let logical = self.start_rect.lerp(self.target_rect, progress, self.style);
|
||||
*self.last_animated_rect.lock() = logical;
|
||||
|
||||
let ghost_active = self.ghost.lock().is_some();
|
||||
if ghost_active {
|
||||
// A window that is slow to take its new size is not waited for: the
|
||||
// animation goes on with the ghost as it is, and the window is
|
||||
// shown when it is ready, or once the animation is over.
|
||||
if progress >= ghost::REVEAL_AT
|
||||
&& !self.revealed.load(Ordering::SeqCst)
|
||||
&& self.resize_is_done()
|
||||
{
|
||||
self.reveal(progress);
|
||||
}
|
||||
|
||||
let opacity = if self.revealed.load(Ordering::SeqCst) {
|
||||
ghost::ghost_opacity(progress, *self.revealed_at.lock())
|
||||
} else {
|
||||
255
|
||||
};
|
||||
|
||||
if let Some(ghost) = self.ghost.lock().as_ref()
|
||||
&& let Err(error) = ghost.update(logical, opacity)
|
||||
{
|
||||
tracing::trace!("ghost update failed: {error}");
|
||||
}
|
||||
border_manager::animate_to(self.hwnd, logical);
|
||||
} else {
|
||||
// Legacy path: animations always run on a separate thread, so we don't
|
||||
// gate on WINDOW_HANDLING_BEHAVIOUR here.
|
||||
WindowsApi::move_window(self.hwnd, &logical, false)?;
|
||||
WindowsApi::invalidate_rect(self.hwnd, None, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_render(&self) -> eyre::Result<()> {
|
||||
let Some(ghost) = self.ghost.lock().take() else {
|
||||
// Legacy path: the final reposition, then one DWM frame's wait so
|
||||
// the app's first post-move paint lands.
|
||||
let landed = WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false);
|
||||
self.uncloak();
|
||||
unsafe {
|
||||
let _ = windows::Win32::Graphics::Dwm::DwmFlush();
|
||||
}
|
||||
self.finalise_managers();
|
||||
return landed;
|
||||
};
|
||||
|
||||
// Normally done a good while ago, but not for a window that was slow to
|
||||
// take its new size, which is waited for now, with the ghost on screen
|
||||
// over it, and then faded out of the way a little more quickly than
|
||||
// it otherwise would be.
|
||||
if !self.revealed.load(Ordering::SeqCst) {
|
||||
self.reveal(1.0);
|
||||
ghost.fade_out(self.target_rect);
|
||||
}
|
||||
|
||||
border_manager::set_ghosted(self.hwnd, false);
|
||||
let _ = ghost.dispose();
|
||||
|
||||
self.finalise_managers();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup_on_cancel(&self) {
|
||||
// Snap the real window to wherever the ghost was last drawn so the next
|
||||
// dispatcher can capture an accurate start_rect. Then uncloak and tear
|
||||
// down the ghost.
|
||||
let _ = self.finish_resize();
|
||||
|
||||
if WindowsApi::is_window(self.hwnd) {
|
||||
let target = *self.last_animated_rect.lock();
|
||||
|
||||
if let Err(error) = WindowsApi::position_window(self.hwnd, &target, false, false) {
|
||||
tracing::warn!(
|
||||
"ghost movement cancel: failed to snap hwnd {} to last rect: {error}",
|
||||
self.hwnd
|
||||
);
|
||||
}
|
||||
|
||||
self.revealed.store(true, Ordering::SeqCst);
|
||||
self.uncloak();
|
||||
}
|
||||
|
||||
border_manager::set_ghosted(self.hwnd, false);
|
||||
if let Some(ghost) = self.ghost.lock().take() {
|
||||
let _ = ghost.dispose();
|
||||
}
|
||||
|
||||
self.finalise_managers();
|
||||
}
|
||||
}
|
||||
|
||||
struct TransparencyRenderDispatcher {
|
||||
hwnd: isize,
|
||||
start_opacity: u8,
|
||||
@@ -615,14 +305,14 @@ impl Window {
|
||||
windows_api::WindowsApi::unmaximize_window(self.hwnd);
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let move_enabled = animation_enabled
|
||||
.get(&MovementRenderDispatcher::PREFIX)
|
||||
.get(&GhostDispatcher::PREFIX)
|
||||
.is_some_and(|v| *v);
|
||||
drop(animation_enabled);
|
||||
|
||||
if move_enabled || ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst) {
|
||||
let anim_count = ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX);
|
||||
.count_in_progress(GhostDispatcher::PREFIX);
|
||||
self.set_position(&new_rect, true)?;
|
||||
let hwnd = self.hwnd;
|
||||
// Wait for the animation to finish before maximizing the window again, otherwise
|
||||
@@ -630,13 +320,13 @@ impl Window {
|
||||
thread::spawn(move || {
|
||||
let mut new_anim_count = ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX);
|
||||
.count_in_progress(GhostDispatcher::PREFIX);
|
||||
let mut max_wait = 2000; // Max waiting time. No one will be using an animation longer than 2s, right? RIGHT??? WHY?
|
||||
while new_anim_count > anim_count && max_wait > 0 {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
new_anim_count = ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX);
|
||||
.count_in_progress(GhostDispatcher::PREFIX);
|
||||
max_wait -= 1;
|
||||
}
|
||||
windows_api::WindowsApi::maximize_window(hwnd);
|
||||
@@ -687,7 +377,7 @@ impl Window {
|
||||
}
|
||||
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let move_enabled = animation_enabled.get(&MovementRenderDispatcher::PREFIX);
|
||||
let move_enabled = animation_enabled.get(&GhostDispatcher::PREFIX);
|
||||
|
||||
if move_enabled.is_some_and(|enabled| *enabled)
|
||||
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
|
||||
@@ -695,16 +385,16 @@ impl Window {
|
||||
let duration = Duration::from_millis(
|
||||
*ANIMATION_DURATION_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&MovementRenderDispatcher::PREFIX)
|
||||
.get(&GhostDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
|
||||
);
|
||||
let style = *ANIMATION_STYLE_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&MovementRenderDispatcher::PREFIX)
|
||||
.get(&GhostDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
|
||||
|
||||
let render_dispatcher =
|
||||
MovementRenderDispatcher::new(self.hwnd, window_rect, *layout, top, style);
|
||||
GhostDispatcher::new(self.hwnd, window_rect, *layout, top, style);
|
||||
|
||||
AnimationEngine::animate(render_dispatcher, duration)
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user