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()?)?; }