From e2f2d6b919ef890e4bbcf983eeb267e5e1e4ebfe Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sun, 25 Feb 2024 14:36:12 -0800 Subject: [PATCH] feat(animation): add window animations Work on this feature was first started by @thearturca in November 2023 before komorebi v0.1.21 in #597 and has undergone numerous revisions to reach the point of this commit. Although this is a single squashed commit, almost all of the heavy lifting for this feature was done by @thearturca, which is where all of the kudos and gratitude should be directed. This commit adds a new static configuration block for animations, where they can be enabled, and have their style, fps and duration set. Corresponding SocketMessages and komorebic cli commands have also been exposed. There are some caveats to the use of this feature, which revolve around the quality of the Windows compositor (it is not very good): * There will be visual artifacts with various apps when animations are taking place - komorebi can't do anything about this as it is a limitation of the Windows compositor * Since komorebi's borders are implemented as independent windows are are not a part of the windows they are drawn around, these borders will be hidden while animations are in progress * If you wish to use borders with this feature, you'll probably better off using BorderImplementation::Windows, which uses the native thin "accent" borders, which are part of the windows they are drawn around, and can be moved with those windows during animations As a result of these and other caveats, this feature will be marked as "experimental" for the foreseeable future and will be off-by-default. Below, a number of now-squashed commits that contributed to the stabilization of this feature are referenced to help with code archeology in the future. fix(animation): Fixed cancelling logic (57e9b2f4bcaedb4fdfa71adf785d661690d81dfc) Added static animation state manager for tracking "in_progress" and "is_cancelled" states. The idea is not to have states in Animation struct but to keep them in HashMap behind reference (Arc>). So we each animation frame we have access to state and can cancel animation if we have to. Need review and testings refactor(animation): avoid unwrap (fa6d5bbc77c1882f85ee1ce73733ff7e53b39eaa) fix(animation): Move cancel call to Animation struct (306513f5dbe5f6bd6ce817f3edca0bfda13d9442) Only focused window was cancelling its animation because we call cancel in window::set_position and waiting for its cancelling. And because we waiting for cancelling second window is still moving. Second window will stop moving only after the first window. So I moved `cancel` call to Animation struct so its happening in its own thread and doesn't block others animation moves and cancels. refactor(animation): renamed args parameters and variables names (8abb4b9618bbb3823b868fc37551f0a70b98281e) refactor(animation): inverse if-statement in `window::animate_position` (3de2c6e932614651892da4a8c626946e427375dd) There is was a bug when ease function generates `t` greater the `SetWindowPos` function will be called instead of `move_window`. `SetWindowPos` is only for last frame of animation. fix(wm): add shadow rect to `move_window` calls (b58620fb4de36d8e422a80541bedf9c1c1579a31) This fixes a bug when windows get shunk during the animation --- komorebi-core/src/animation.rs | 42 +++ komorebi-core/src/lib.rs | 6 + komorebi-core/src/rect.rs | 10 + komorebi-gui/src/main.rs | 6 +- komorebi/src/animation.rs | 503 +++++++++++++++++++++++++++ komorebi/src/animation_manager.rs | 79 +++++ komorebi/src/lib.rs | 14 + komorebi/src/process_command.rs | 17 + komorebi/src/process_event.rs | 2 +- komorebi/src/stackbar_manager/mod.rs | 8 +- komorebi/src/static_config.rs | 29 ++ komorebi/src/window.rs | 71 +++- komorebi/src/windows_api.rs | 19 + komorebic/src/main.rs | 50 +++ 14 files changed, 845 insertions(+), 11 deletions(-) create mode 100644 komorebi-core/src/animation.rs create mode 100644 komorebi/src/animation.rs create mode 100644 komorebi/src/animation_manager.rs diff --git a/komorebi-core/src/animation.rs b/komorebi-core/src/animation.rs new file mode 100644 index 00000000..19e410b0 --- /dev/null +++ b/komorebi-core/src/animation.rs @@ -0,0 +1,42 @@ +use clap::ValueEnum; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use strum::Display; +use strum::EnumString; + +#[derive( + Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema, +)] +pub enum AnimationStyle { + Linear, + EaseInSine, + EaseOutSine, + EaseInOutSine, + EaseInQuad, + EaseOutQuad, + EaseInOutQuad, + EaseInCubic, + EaseInOutCubic, + EaseInQuart, + EaseOutQuart, + EaseInOutQuart, + EaseInQuint, + EaseOutQuint, + EaseInOutQuint, + EaseInExpo, + EaseOutExpo, + EaseInOutExpo, + EaseInCirc, + EaseOutCirc, + EaseInOutCirc, + EaseInBack, + EaseOutBack, + EaseInOutBack, + EaseInElastic, + EaseOutElastic, + EaseInOutElastic, + EaseInBounce, + EaseOutBounce, + EaseInOutBounce, +} diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index ab3535a9..ca73e2d3 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -14,6 +14,7 @@ use serde::Serialize; use strum::Display; use strum::EnumString; +pub use animation::AnimationStyle; pub use arrangement::Arrangement; pub use arrangement::Axis; pub use custom_layout::CustomLayout; @@ -24,6 +25,7 @@ pub use layout::Layout; pub use operation_direction::OperationDirection; pub use rect::Rect; +pub mod animation; pub mod arrangement; pub mod config_generation; pub mod custom_layout; @@ -134,6 +136,10 @@ pub enum SocketMessage { WatchConfiguration(bool), CompleteConfiguration, AltFocusHack(bool), + Animation(bool), + AnimationDuration(u64), + AnimationFps(u64), + AnimationStyle(AnimationStyle), #[serde(alias = "ActiveWindowBorder")] Border(bool), #[serde(alias = "ActiveWindowBorderColour")] diff --git a/komorebi-core/src/rect.rs b/komorebi-core/src/rect.rs index 164b6e3a..1378dc1d 100644 --- a/komorebi-core/src/rect.rs +++ b/komorebi-core/src/rect.rs @@ -84,4 +84,14 @@ impl Rect { bottom: (self.bottom * rect_dpi) / system_dpi, } } + + #[must_use] + pub const fn rect(&self) -> RECT { + RECT { + left: self.left, + top: self.top, + right: self.left + self.right, + bottom: self.top + self.bottom, + } + } } diff --git a/komorebi-gui/src/main.rs b/komorebi-gui/src/main.rs index 093cc333..00a85608 100644 --- a/komorebi-gui/src/main.rs +++ b/komorebi-gui/src/main.rs @@ -218,7 +218,7 @@ extern "system" fn enum_window( lparam: windows::Win32::Foundation::LPARAM, ) -> windows::Win32::Foundation::BOOL { let windows = unsafe { &mut *(lparam.0 as *mut Vec) }; - let window = Window { hwnd: hwnd.0 }; + let window = Window::from(hwnd.0); if window.is_window() && !window.is_miminized() @@ -246,9 +246,7 @@ impl eframe::App for KomorebiGui { ui.set_width(ctx.screen_rect().width()); ui.collapsing("Debugging", |ui| { ui.collapsing("Window Rules", |ui| { - let window = Window { - hwnd: self.debug_hwnd, - }; + let window = Window::from(self.debug_hwnd); let label = if let (Ok(title), Ok(exe)) = (window.title(), window.exe()) { format!("{title} ({exe})") diff --git a/komorebi/src/animation.rs b/komorebi/src/animation.rs new file mode 100644 index 00000000..a9e81207 --- /dev/null +++ b/komorebi/src/animation.rs @@ -0,0 +1,503 @@ +use color_eyre::Result; +use komorebi_core::AnimationStyle; +use komorebi_core::Rect; + +use schemars::JsonSchema; + +use serde::Deserialize; +use serde::Serialize; +use std::f64::consts::PI; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::time::Duration; +use std::time::Instant; + +use crate::ANIMATION_DURATION; +use crate::ANIMATION_MANAGER; +use crate::ANIMATION_STYLE; + +pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(60); + +pub trait Ease { + fn evaluate(t: f64) -> f64; +} + +pub struct Linear; + +impl Ease for Linear { + fn evaluate(t: f64) -> f64 { + t + } +} + +pub struct EaseInSine; + +impl Ease for EaseInSine { + fn evaluate(t: f64) -> f64 { + 1.0 - f64::cos((t * PI) / 2.0) + } +} + +pub struct EaseOutSine; + +impl Ease for EaseOutSine { + fn evaluate(t: f64) -> f64 { + f64::sin((t * PI) / 2.0) + } +} + +pub struct EaseInOutSine; + +impl Ease for EaseInOutSine { + fn evaluate(t: f64) -> f64 { + -(f64::cos(PI * t) - 1.0) / 2.0 + } +} + +pub struct EaseInQuad; + +impl Ease for EaseInQuad { + fn evaluate(t: f64) -> f64 { + t * t + } +} + +pub struct EaseOutQuad; + +impl Ease for EaseOutQuad { + fn evaluate(t: f64) -> f64 { + (1.0 - t).mul_add(-1.0 - t, 1.0) + } +} + +pub struct EaseInOutQuad; + +impl Ease for EaseInOutQuad { + fn evaluate(t: f64) -> f64 { + if t < 0.5 { + 2.0 * t * t + } else { + 1.0 - (-2.0f64).mul_add(t, 2.0).powi(2) / 2.0 + } + } +} + +pub struct EaseInCubic; + +impl Ease for EaseInCubic { + fn evaluate(t: f64) -> f64 { + t * t * t + } +} + +pub struct EaseOutCubic; + +impl Ease for EaseOutCubic { + fn evaluate(t: f64) -> f64 { + 1.0 - (1.0 - t).powi(3) + } +} + +pub struct EaseInOutCubic; + +impl Ease for EaseInOutCubic { + fn evaluate(t: f64) -> f64 { + if t < 0.5 { + 4.0 * t * t * t + } else { + 1.0 - (-2.0f64).mul_add(t, 2.0).powi(3) / 2.0 + } + } +} + +pub struct EaseInQuart; + +impl Ease for EaseInQuart { + fn evaluate(t: f64) -> f64 { + t * t * t * t + } +} + +pub struct EaseOutQuart; + +impl Ease for EaseOutQuart { + fn evaluate(t: f64) -> f64 { + 1.0 - (1.0 - t).powi(4) + } +} + +pub struct EaseInOutQuart; + +impl Ease for EaseInOutQuart { + fn evaluate(t: f64) -> f64 { + if t < 0.5 { + 8.0 * t * t * t * t + } else { + 1.0 - (-2.0f64).mul_add(t, 2.0).powi(4) / 2.0 + } + } +} + +pub struct EaseInQuint; + +impl Ease for EaseInQuint { + fn evaluate(t: f64) -> f64 { + t * t * t * t * t + } +} + +pub struct EaseOutQuint; + +impl Ease for EaseOutQuint { + fn evaluate(t: f64) -> f64 { + 1.0 - (1.0 - t).powi(5) + } +} + +pub struct EaseInOutQuint; + +impl Ease for EaseInOutQuint { + fn evaluate(t: f64) -> f64 { + if t < 0.5 { + 16.0 * t * t * t * t + } else { + 1.0 - (-2.0f64).mul_add(t, 2.0).powi(5) / 2.0 + } + } +} + +pub struct EaseInExpo; + +impl Ease for EaseInExpo { + fn evaluate(t: f64) -> f64 { + if t == 0.0 { + return t; + } + + 10.0f64.mul_add(t, -10.0).exp2() + } +} + +pub struct EaseOutExpo; + +impl Ease for EaseOutExpo { + fn evaluate(t: f64) -> f64 { + if (t - 1.0).abs() < f64::EPSILON { + return t; + } + + 1.0 - (-10.0 * t).exp2() + } +} + +pub struct EaseInOutExpo; + +impl Ease for EaseInOutExpo { + fn evaluate(t: f64) -> f64 { + if t == 0.0 || (t - 1.0).abs() < f64::EPSILON { + return t; + } + + if t < 0.5 { + 20.0f64.mul_add(t, -10.0).exp2() / 2.0 + } else { + (2.0 - (-20.0f64).mul_add(t, 10.0).exp2()) / 2.0 + } + } +} + +pub struct EaseInCirc; + +impl Ease for EaseInCirc { + fn evaluate(t: f64) -> f64 { + 1.0 - f64::sqrt(t.mul_add(-t, 1.0)) + } +} + +pub struct EaseOutCirc; + +impl Ease for EaseOutCirc { + fn evaluate(t: f64) -> f64 { + f64::sqrt((t - 1.0).mul_add(-(t - 1.0), 1.0)) + } +} + +pub struct EaseInOutCirc; + +impl Ease for EaseInOutCirc { + fn evaluate(t: f64) -> f64 { + if t < 0.5 { + (1.0 - f64::sqrt((2.0 * t).mul_add(-(2.0 * t), 1.0))) / 2.0 + } else { + (f64::sqrt( + (-2.0f64) + .mul_add(t, 2.0) + .mul_add(-(-2.0f64).mul_add(t, 2.0), 1.0), + ) + 1.0) + / 2.0 + } + } +} + +pub struct EaseInBack; + +impl Ease for EaseInBack { + fn evaluate(t: f64) -> f64 { + let c1 = 1.70158; + let c3 = c1 + 1.0; + + (c3 * t * t).mul_add(t, -c1 * t * t) + } +} + +pub struct EaseOutBack; + +impl Ease for EaseOutBack { + fn evaluate(t: f64) -> f64 { + let c1: f64 = 1.70158; + let c3: f64 = c1 + 1.0; + + c1.mul_add((t - 1.0).powi(2), c3.mul_add((t - 1.0).powi(3), 1.0)) + } +} + +pub struct EaseInOutBack; + +impl Ease for EaseInOutBack { + fn evaluate(t: f64) -> f64 { + let c1: f64 = 1.70158; + let c2: f64 = c1 * 1.525; + + if t < 0.5 { + ((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0).mul_add(t, -c2)) / 2.0 + } else { + ((2.0f64.mul_add(t, -2.0)) + .powi(2) + .mul_add((c2 + 1.0).mul_add(t.mul_add(2.0, -2.0), c2), 2.0)) + / 2.0 + } + } +} + +pub struct EaseInElastic; + +impl Ease for EaseInElastic { + fn evaluate(t: f64) -> f64 { + if (t - 1.0).abs() < f64::EPSILON || t == 0.0 { + return t; + } + + let c4 = (2.0 * PI) / 3.0; + + -(10.0f64.mul_add(t, -10.0).exp2()) * f64::sin(t.mul_add(10.0, -10.75) * c4) + } +} + +pub struct EaseOutElastic; + +impl Ease for EaseOutElastic { + fn evaluate(t: f64) -> f64 { + if (t - 1.0).abs() < f64::EPSILON || t == 0.0 { + return t; + } + + let c4 = (2.0 * PI) / 3.0; + + (-10.0 * t) + .exp2() + .mul_add(f64::sin(t.mul_add(10.0, -0.75) * c4), 1.0) + } +} + +pub struct EaseInOutElastic; + +impl Ease for EaseInOutElastic { + fn evaluate(t: f64) -> f64 { + if (t - 1.0).abs() < f64::EPSILON || t == 0.0 { + return t; + } + + let c5 = (2.0 * PI) / 4.5; + + if t < 0.5 { + -(20.0f64.mul_add(t, -10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0 + } else { + ((-20.0f64).mul_add(t, 10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0 + + 1.0 + } + } +} + +pub struct EaseInBounce; + +impl Ease for EaseInBounce { + fn evaluate(t: f64) -> f64 { + 1.0 - EaseOutBounce::evaluate(1.0 - t) + } +} + +pub struct EaseOutBounce; + +impl Ease for EaseOutBounce { + fn evaluate(t: f64) -> f64 { + let mut time = t; + let n1 = 7.5625; + let d1 = 2.75; + + if t < 1.0 / d1 { + n1 * time * time + } else if time < 2.0 / d1 { + time -= 1.5 / d1; + (n1 * time).mul_add(time, 0.75) + } else if time < 2.5 / d1 { + time -= 2.25 / d1; + (n1 * time).mul_add(time, 0.9375) + } else { + time -= 2.625 / d1; + (n1 * time).mul_add(time, 0.984_375) + } + } +} + +pub struct EaseInOutBounce; + +impl Ease for EaseInOutBounce { + fn evaluate(t: f64) -> f64 { + if t < 0.5 { + (1.0 - EaseOutBounce::evaluate(2.0f64.mul_add(-t, 1.0))) / 2.0 + } else { + (1.0 + EaseOutBounce::evaluate(2.0f64.mul_add(t, -1.0))) / 2.0 + } + } +} +fn apply_ease_func(t: f64) -> f64 { + let style = *ANIMATION_STYLE.lock(); + + match style { + AnimationStyle::Linear => Linear::evaluate(t), + AnimationStyle::EaseInSine => EaseInSine::evaluate(t), + AnimationStyle::EaseOutSine => EaseOutSine::evaluate(t), + AnimationStyle::EaseInOutSine => EaseInOutSine::evaluate(t), + AnimationStyle::EaseInQuad => EaseInQuad::evaluate(t), + AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t), + AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t), + AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t), + AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t), + AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t), + AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t), + AnimationStyle::EaseInOutQuart => EaseInOutQuart::evaluate(t), + AnimationStyle::EaseInQuint => EaseInQuint::evaluate(t), + AnimationStyle::EaseOutQuint => EaseOutQuint::evaluate(t), + AnimationStyle::EaseInOutQuint => EaseInOutQuint::evaluate(t), + AnimationStyle::EaseInExpo => EaseInExpo::evaluate(t), + AnimationStyle::EaseOutExpo => EaseOutExpo::evaluate(t), + AnimationStyle::EaseInOutExpo => EaseInOutExpo::evaluate(t), + AnimationStyle::EaseInCirc => EaseInCirc::evaluate(t), + AnimationStyle::EaseOutCirc => EaseOutCirc::evaluate(t), + AnimationStyle::EaseInOutCirc => EaseInOutCirc::evaluate(t), + AnimationStyle::EaseInBack => EaseInBack::evaluate(t), + AnimationStyle::EaseOutBack => EaseOutBack::evaluate(t), + AnimationStyle::EaseInOutBack => EaseInOutBack::evaluate(t), + AnimationStyle::EaseInElastic => EaseInElastic::evaluate(t), + AnimationStyle::EaseOutElastic => EaseOutElastic::evaluate(t), + AnimationStyle::EaseInOutElastic => EaseInOutElastic::evaluate(t), + AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t), + AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t), + AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t), + } +} + +#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct Animation { + pub hwnd: isize, +} + +impl Animation { + pub fn new(hwnd: isize) -> Self { + Self { hwnd } + } + pub fn cancel(&mut self) { + if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) { + return; + } + + ANIMATION_MANAGER.lock().cancel(self.hwnd); + let max_duration = Duration::from_secs(1); + let spent_duration = Instant::now(); + + while ANIMATION_MANAGER.lock().in_progress(self.hwnd) { + if spent_duration.elapsed() >= max_duration { + ANIMATION_MANAGER.lock().end(self.hwnd); + } + + std::thread::sleep(Duration::from_millis( + ANIMATION_DURATION.load(Ordering::SeqCst) / 2, + )); + } + } + + #[allow(clippy::cast_possible_truncation)] + pub fn lerp(start: i32, end: i32, t: f64) -> i32 { + let time = apply_ease_func(t); + f64::from(end - start) + .mul_add(time, f64::from(start)) + .round() as i32 + } + + pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect { + Rect { + left: Self::lerp(start_rect.left, end_rect.left, t), + top: Self::lerp(start_rect.top, end_rect.top, t), + right: Self::lerp(start_rect.right, end_rect.right, t), + bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t), + } + } + + #[allow(clippy::cast_precision_loss)] + pub fn animate( + &mut self, + duration: Duration, + mut render_callback: impl FnMut(f64) -> Result<()>, + ) -> Result<()> { + if ANIMATION_MANAGER.lock().in_progress(self.hwnd) { + self.cancel(); + } + + ANIMATION_MANAGER.lock().start(self.hwnd); + + let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed)); + let mut progress = 0.0; + let animation_start = Instant::now(); + + // start animation + while progress < 1.0 { + // check if animation is cancelled + if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) { + // cancel animation + // set all flags + ANIMATION_MANAGER.lock().end(self.hwnd); + return Ok(()); + } + + let frame_start = Instant::now(); + // calculate progress + progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64; + render_callback(progress).ok(); + + // sleep until next frame + if frame_start.elapsed() < target_frame_time { + std::thread::sleep(target_frame_time - frame_start.elapsed()); + } + } + + ANIMATION_MANAGER.lock().end(self.hwnd); + + // limit progress to 1.0 if animation took longer + if progress > 1.0 { + progress = 1.0; + } + + // process animation for 1.0 to set target position + render_callback(progress) + } +} diff --git a/komorebi/src/animation_manager.rs b/komorebi/src/animation_manager.rs new file mode 100644 index 00000000..2fd3f746 --- /dev/null +++ b/komorebi/src/animation_manager.rs @@ -0,0 +1,79 @@ +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0); + +#[derive(Debug, Clone, Copy)] +struct AnimationState { + pub in_progress: bool, + pub is_cancelled: bool, +} + +#[derive(Debug)] +pub struct AnimationManager { + animations: HashMap, +} + +impl Default for AnimationManager { + fn default() -> Self { + Self::new() + } +} + +impl AnimationManager { + pub fn new() -> Self { + Self { + animations: HashMap::new(), + } + } + + pub fn is_cancelled(&self, hwnd: isize) -> bool { + if let Some(animation_state) = self.animations.get(&hwnd) { + animation_state.is_cancelled + } else { + false + } + } + + pub fn in_progress(&self, hwnd: isize) -> bool { + if let Some(animation_state) = self.animations.get(&hwnd) { + animation_state.in_progress + } else { + false + } + } + + pub fn cancel(&mut self, hwnd: isize) { + if let Some(animation_state) = self.animations.get_mut(&hwnd) { + animation_state.is_cancelled = true; + } + } + + pub fn start(&mut self, hwnd: isize) { + if let Entry::Vacant(e) = self.animations.entry(hwnd) { + e.insert(AnimationState { + in_progress: true, + is_cancelled: false, + }); + + ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release); + return; + } + + if let Some(animation_state) = self.animations.get_mut(&hwnd) { + animation_state.in_progress = true; + } + } + + pub fn end(&mut self, hwnd: isize) { + if let Some(animation_state) = self.animations.get_mut(&hwnd) { + animation_state.in_progress = false; + animation_state.is_cancelled = false; + + self.animations.remove(&hwnd); + ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release); + } + } +} diff --git a/komorebi/src/lib.rs b/komorebi/src/lib.rs index dd91d7ae..6eeb7d9d 100644 --- a/komorebi/src/lib.rs +++ b/komorebi/src/lib.rs @@ -1,5 +1,7 @@ #![warn(clippy::all)] +pub mod animation; +pub mod animation_manager; pub mod border_manager; pub mod com; #[macro_use] @@ -38,9 +40,12 @@ use std::process::Command; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicI32; use std::sync::atomic::AtomicU32; +use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::sync::Arc; +pub use animation::*; +pub use animation_manager::*; pub use colour::*; pub use process_command::*; pub use process_event::*; @@ -55,6 +60,7 @@ use color_eyre::Result; use komorebi_core::config_generation::IdWithIdentifier; use komorebi_core::config_generation::MatchingRule; use komorebi_core::config_generation::MatchingStrategy; +use komorebi_core::AnimationStyle; use komorebi_core::ApplicationIdentifier; use komorebi_core::HidingBehaviour; use komorebi_core::Rect; @@ -197,6 +203,12 @@ lazy_static! { ) }; + static ref ANIMATION_STYLE: Arc> = + Arc::new(Mutex::new(AnimationStyle::Linear)); + + static ref ANIMATION_MANAGER: Arc> = + Arc::new(Mutex::new(AnimationManager::new())); + // Use app-specific titlebar removal options where possible // eg. Windows Terminal, IntelliJ IDEA, Firefox static ref NO_TITLEBAR: Arc>> = Arc::new(Mutex::new(vec![])); @@ -214,6 +226,8 @@ pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false); pub static SESSION_ID: AtomicU32 = AtomicU32::new(0); pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false); +pub static ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false); +pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250); #[must_use] pub fn current_virtual_desktop() -> Option> { diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 4336369e..d00b3fcb 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -58,6 +58,10 @@ use crate::windows_api::WindowsApi; use crate::GlobalState; use crate::Notification; use crate::NotificationEvent; +use crate::ANIMATION_DURATION; +use crate::ANIMATION_ENABLED; +use crate::ANIMATION_FPS; +use crate::ANIMATION_STYLE; use crate::CUSTOM_FFM; use crate::DATA_DIR; use crate::DISPLAY_INDEX_PREFERENCES; @@ -561,6 +565,7 @@ impl WindowManager { self.update_focused_workspace(self.mouse_follows_focus, true)?; } SocketMessage::Retile => { + border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst); border_manager::destroy_all_borders()?; self.retile_all(false)? } @@ -1322,6 +1327,18 @@ impl WindowManager { SocketMessage::BorderOffset(offset) => { border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst); } + SocketMessage::Animation(enable) => { + ANIMATION_ENABLED.store(enable, Ordering::SeqCst); + } + SocketMessage::AnimationDuration(duration) => { + ANIMATION_DURATION.store(duration, Ordering::SeqCst); + } + SocketMessage::AnimationFps(fps) => { + ANIMATION_FPS.store(fps, Ordering::SeqCst); + } + SocketMessage::AnimationStyle(style) => { + *ANIMATION_STYLE.lock() = style; + } SocketMessage::Transparency(enable) => { transparency_manager::TRANSPARENCY_ENABLED.store(enable, Ordering::SeqCst); } diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index d545ef1a..bba4a100 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -606,7 +606,7 @@ impl WindowManager { }; // If we unmanaged a window, it shouldn't be immediately hidden behind managed windows - if let WindowManagerEvent::Unmanage(window) = event { + if let WindowManagerEvent::Unmanage(mut window) = event { window.center(&self.focused_monitor_work_area()?)?; } diff --git a/komorebi/src/stackbar_manager/mod.rs b/komorebi/src/stackbar_manager/mod.rs index b824ae1f..06efd04c 100644 --- a/komorebi/src/stackbar_manager/mod.rs +++ b/komorebi/src/stackbar_manager/mod.rs @@ -15,8 +15,10 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicI32; use std::sync::atomic::AtomicU32; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::OnceLock; use windows::Win32::Foundation::HWND; @@ -30,6 +32,8 @@ pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200); pub static STACKBAR_LABEL: AtomicCell = AtomicCell::new(StackbarLabel::Process); pub static STACKBAR_MODE: AtomicCell = AtomicCell::new(StackbarMode::OnStack); +pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false); + lazy_static! { pub static ref STACKBAR_STATE: Mutex> = Mutex::new(HashMap::new()); pub static ref STACKBAR_FONT_FAMILY: Mutex> = Mutex::new(None); @@ -93,7 +97,9 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result let mut state = wm.lock(); // If stackbars are disabled - if matches!(STACKBAR_MODE.load(), StackbarMode::Never) { + if matches!(STACKBAR_MODE.load(), StackbarMode::Never) + || STACKBAR_TEMPORARILY_DISABLED.load(Ordering::SeqCst) + { for (_, stackbar) in stackbars.iter() { stackbar.destroy()?; } diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 0bfc1e1c..d81f5313 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -23,6 +23,10 @@ use crate::window_manager::WindowManager; use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; use crate::workspace::Workspace; +use crate::ANIMATION_DURATION; +use crate::ANIMATION_ENABLED; +use crate::ANIMATION_FPS; +use crate::ANIMATION_STYLE; use crate::DATA_DIR; use crate::DEFAULT_CONTAINER_PADDING; use crate::DEFAULT_WORKSPACE_PADDING; @@ -52,6 +56,7 @@ use komorebi_core::config_generation::IdWithIdentifier; use komorebi_core::config_generation::MatchingRule; use komorebi_core::config_generation::MatchingStrategy; use komorebi_core::resolve_home_path; +use komorebi_core::AnimationStyle; use komorebi_core::ApplicationIdentifier; use komorebi_core::BorderStyle; use komorebi_core::DefaultLayout; @@ -346,6 +351,21 @@ pub struct StaticConfig { /// Stackbar configuration options #[serde(skip_serializing_if = "Option::is_none")] pub stackbar: Option, + /// Animations configuration options + #[serde(skip_serializing_if = "Option::is_none")] + pub animation: Option, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct AnimationsConfig { + /// Enable or disable animations (default: false) + enabled: bool, + /// Set the animation duration in ms (default: 250) + duration: Option, + /// Set the animation style (default: Linear) + style: Option, + /// Set the animation FPS (default: 60) + fps: Option, } impl StaticConfig { @@ -584,6 +604,7 @@ impl From<&WindowManager> for StaticConfig { monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()), display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()), stackbar: None, + animation: None, } } } @@ -614,6 +635,14 @@ impl StaticConfig { window::MINIMUM_WIDTH.store(width, Ordering::SeqCst); } + if let Some(animations) = &self.animation { + ANIMATION_ENABLED.store(animations.enabled, Ordering::SeqCst); + ANIMATION_DURATION.store(animations.duration.unwrap_or(250), Ordering::SeqCst); + ANIMATION_FPS.store(animations.fps.unwrap_or(60), Ordering::SeqCst); + let mut animation_style = ANIMATION_STYLE.lock(); + *animation_style = animations.style.unwrap_or(AnimationStyle::Linear); + } + if let Some(container) = self.default_container_padding { DEFAULT_CONTAINER_PADDING.store(container, Ordering::SeqCst); } diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index e835613f..c2df6624 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -1,4 +1,9 @@ +use crate::border_manager; use crate::com::SetCloak; +use crate::stackbar_manager; +use crate::ANIMATIONS_IN_PROGRESS; +use crate::ANIMATION_DURATION; +use crate::ANIMATION_ENABLED; use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::Display; @@ -26,6 +31,7 @@ use komorebi_core::ApplicationIdentifier; use komorebi_core::HidingBehaviour; use komorebi_core::Rect; +use crate::animation::Animation; use crate::styles::ExtendedWindowStyle; use crate::styles::WindowStyle; use crate::transparency_manager; @@ -47,17 +53,24 @@ pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0); #[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)] pub struct Window { pub hwnd: isize, + animation: Animation, } impl From for Window { fn from(value: isize) -> Self { - Self { hwnd: value } + Self { + hwnd: value, + animation: Animation::new(value), + } } } impl From for Window { fn from(value: HWND) -> Self { - Self { hwnd: value.0 } + Self { + hwnd: value.0, + animation: Animation::new(value.0), + } } } @@ -141,7 +154,7 @@ impl Window { HWND(self.hwnd) } - pub fn center(&self, work_area: &Rect) -> Result<()> { + pub fn center(&mut self, work_area: &Rect) -> Result<()> { let half_width = work_area.right / 2; let half_weight = work_area.bottom / 2; @@ -156,13 +169,61 @@ impl Window { ) } + pub fn animate_position(&self, layout: &Rect, top: bool) -> Result<()> { + let hwnd = self.hwnd(); + let curr_rect = WindowsApi::window_rect(hwnd).unwrap(); + + let target_rect = *layout; + let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst)); + let mut animation = self.animation; + + border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst); + border_manager::send_notification(); + + stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst); + stackbar_manager::send_notification(); + + std::thread::spawn(move || { + animation.animate(duration, |progress: f64| { + let new_rect = Animation::lerp_rect(&curr_rect, &target_rect, progress); + + if progress == 1.0 { + WindowsApi::position_window(hwnd, &new_rect, top)?; + + if ANIMATIONS_IN_PROGRESS.load(Ordering::Acquire) == 0 { + border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst); + stackbar_manager::STACKBAR_TEMPORARILY_DISABLED + .store(false, Ordering::SeqCst); + + border_manager::send_notification(); + stackbar_manager::send_notification(); + transparency_manager::send_notification(); + } + } else { + // using MoveWindow because it runs faster than SetWindowPos + // so animation have more fps and feel smoother + WindowsApi::move_window(hwnd, &new_rect, false)?; + // WindowsApi::position_window(hwnd, &new_rect, top)?; + WindowsApi::invalidate_rect(hwnd, None, false); + } + + Ok(()) + }) + }); + + Ok(()) + } + pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> { if WindowsApi::window_rect(self.hwnd())?.eq(layout) { return Ok(()); } - let rect = *layout; - WindowsApi::position_window(self.hwnd(), &rect, top) + if ANIMATION_ENABLED.load(Ordering::SeqCst) { + self.animate_position(layout, top) + } else { + WindowsApi::position_window(self.hwnd(), layout, top) + } } pub fn is_maximized(self) -> bool { diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index db488627..137e316f 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -18,6 +18,7 @@ use windows::Win32::Foundation::HMODULE; use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::LPARAM; use windows::Win32::Foundation::POINT; +use windows::Win32::Foundation::RECT; use windows::Win32::Foundation::WPARAM; use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute; use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute; @@ -34,6 +35,7 @@ use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL; use windows::Win32::Graphics::Gdi::CreateSolidBrush; use windows::Win32::Graphics::Gdi::EnumDisplayMonitors; use windows::Win32::Graphics::Gdi::GetMonitorInfoW; +use windows::Win32::Graphics::Gdi::InvalidateRect; use windows::Win32::Graphics::Gdi::MonitorFromPoint; use windows::Win32::Graphics::Gdi::MonitorFromWindow; use windows::Win32::Graphics::Gdi::Rectangle; @@ -84,6 +86,7 @@ use windows::Win32::UI::WindowsAndMessaging::IsIconic; use windows::Win32::UI::WindowsAndMessaging::IsWindow; use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible; use windows::Win32::UI::WindowsAndMessaging::IsZoomed; +use windows::Win32::UI::WindowsAndMessaging::MoveWindow; use windows::Win32::UI::WindowsAndMessaging::PostMessageW; use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW; use windows::Win32::UI::WindowsAndMessaging::RegisterClassW; @@ -429,6 +432,17 @@ impl WindowsApi { .process() } + pub fn move_window(hwnd: HWND, layout: &Rect, repaint: bool) -> Result<()> { + let shadow_rect = Self::shadow_rect(hwnd).unwrap_or_default(); + let rect = Rect { + left: layout.left + shadow_rect.left, + top: layout.top + shadow_rect.top, + right: layout.right + shadow_rect.right, + bottom: layout.bottom + shadow_rect.bottom, + }; + unsafe { MoveWindow(hwnd, rect.left, rect.top, rect.right, rect.bottom, repaint) }.process() + } + pub fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) { // BOOL is returned but does not signify whether or not the operation was succesful // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow @@ -1023,6 +1037,11 @@ impl WindowsApi { .process() } + pub fn invalidate_rect(hwnd: HWND, rect: Option<&Rect>, erase: bool) -> bool { + let rect = rect.map(|rect| &rect.rect() as *const RECT); + unsafe { InvalidateRect(hwnd, rect, erase) }.as_bool() + } + pub fn alt_is_pressed() -> bool { let state = unsafe { GetKeyState(i32::from(VK_MENU.0)) }; #[allow(clippy::cast_sign_loss)] diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index b2623c45..f07319ba 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -714,6 +714,31 @@ struct BorderImplementation { style: komorebi_core::BorderImplementation, } +#[derive(Parser)] +struct Animation { + #[clap(value_enum)] + boolean_state: BooleanState, +} + +#[derive(Parser)] +struct AnimationDuration { + /// Desired animation durations in ms + duration: u64, +} + +#[derive(Parser)] +struct AnimationFps { + /// Desired animation frames per second + fps: u64, +} + +#[derive(Parser)] +struct AnimationStyle { + /// Desired ease function for animation + #[clap(value_enum, short, long, default_value = "linear")] + style: komorebi_core::AnimationStyle, +} + #[derive(Parser)] #[allow(clippy::struct_excessive_bools)] struct Start { @@ -1225,6 +1250,18 @@ enum SubCommand { /// Set the alpha value for unfocused window transparency #[clap(arg_required_else_help = true)] TransparencyAlpha(TransparencyAlpha), + /// Enable or disable the window move animation + #[clap(arg_required_else_help = true)] + Animation(Animation), + /// Set the duration for the window move animation in ms + #[clap(arg_required_else_help = true)] + AnimationDuration(AnimationDuration), + /// Set the frames per second for the window move animation + #[clap(arg_required_else_help = true)] + AnimationFps(AnimationFps), + /// Set the ease function for the window move animation + #[clap(arg_required_else_help = true)] + AnimationStyle(AnimationStyle), /// Enable or disable focus follows mouse for the operating system #[clap(arg_required_else_help = true)] FocusFollowsMouse(FocusFollowsMouse), @@ -2335,6 +2372,19 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue SubCommand::TransparencyAlpha(arg) => { send_message(&SocketMessage::TransparencyAlpha(arg.alpha).as_bytes()?)?; } + SubCommand::Animation(arg) => { + send_message(&SocketMessage::Animation(arg.boolean_state.into()).as_bytes()?)?; + } + SubCommand::AnimationDuration(arg) => { + send_message(&SocketMessage::AnimationDuration(arg.duration).as_bytes()?)?; + } + SubCommand::AnimationFps(arg) => { + send_message(&SocketMessage::AnimationFps(arg.fps).as_bytes()?)?; + } + SubCommand::AnimationStyle(arg) => { + send_message(&SocketMessage::AnimationStyle(arg.style).as_bytes()?)?; + } + SubCommand::ResizeDelta(arg) => { send_message(&SocketMessage::ResizeDelta(arg.pixels).as_bytes()?)?; }