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 d5f94229..8a6de5c9 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; @@ -138,6 +140,9 @@ pub enum SocketMessage { BorderColour(WindowKind, u32, u32, u32), #[serde(alias = "ActiveWindowBorderStyle")] BorderStyle(BorderStyle), + Animation(bool), + AnimationDuration(u64), + AnimationStyle(AnimationStyle), BorderWidth(i32), BorderOffset(i32), InvisibleBorders(Rect), 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/src/animation.rs b/komorebi/src/animation.rs new file mode 100644 index 00000000..a154e324 --- /dev/null +++ b/komorebi/src/animation.rs @@ -0,0 +1,501 @@ +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::Ordering; +use std::time::Duration; +use std::time::Instant; + +use crate::ANIMATION_DURATION; +use crate::ANIMATION_MANAGER; +use crate::ANIMATION_STYLE; + +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)] +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(x: i32, new_x: i32, t: f64) -> i32 { + let time = apply_ease_func(t); + f64::from(new_x - x).mul_add(time, f64::from(x)) as i32 + } + + pub fn lerp_rect(original_rect: &Rect, new_rect: &Rect, t: f64) -> Rect { + Rect { + left: Self::lerp(original_rect.left, new_rect.left, t), + top: Self::lerp(original_rect.top, new_rect.top, t), + right: Self::lerp(original_rect.right, new_rect.right, t), + bottom: Self::lerp(original_rect.bottom, new_rect.bottom, t), + } + } + + #[allow(clippy::cast_precision_loss)] + pub fn animate( + &mut self, + duration: Duration, + mut f: impl FnMut(f64) -> Result<()>, + ) -> Result<()> { + if ANIMATION_MANAGER.lock().in_progress(self.hwnd) { + self.cancel(); + } + + ANIMATION_MANAGER.lock().start(self.hwnd); + + // set target frame time to match 240 fps (my max refresh rate of monitor) + // probably the best way to do it is take actual monitor refresh rate + // or make it configurable + let target_frame_time = Duration::from_millis(1000 / 240); + 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 tick_start = Instant::now(); + // calculate progress + progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64; + f(progress).ok(); + + // sleep until next frame + while tick_start.elapsed() < target_frame_time { + std::thread::sleep(target_frame_time - tick_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 + f(progress) + } +} diff --git a/komorebi/src/animation_manager.rs b/komorebi/src/animation_manager.rs new file mode 100644 index 00000000..838a9fb0 --- /dev/null +++ b/komorebi/src/animation_manager.rs @@ -0,0 +1,68 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy)] +struct AnimationState { + pub in_progress: bool, + pub is_cancelled: bool, +} + +#[derive(Debug)] +pub struct AnimationManager { + animations: HashMap, +} + +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 !self.animations.contains_key(&hwnd) { + self.animations.insert( + hwnd, + AnimationState { + in_progress: true, + is_cancelled: false, + }, + ); + 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); + } + } +} diff --git a/komorebi/src/lib.rs b/komorebi/src/lib.rs index 675c8bfa..db6ab52e 100644 --- a/komorebi/src/lib.rs +++ b/komorebi/src/lib.rs @@ -1,3 +1,5 @@ +pub mod animation; +pub mod animation_manager; pub mod border_manager; pub mod com; #[macro_use] @@ -35,9 +37,12 @@ use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicI32; use std::sync::atomic::AtomicIsize; 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 hidden::*; pub use process_command::*; @@ -55,6 +60,7 @@ use crossbeam_utils::atomic::AtomicCell; 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; @@ -199,6 +205,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![])); @@ -216,7 +228,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); pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0); pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 34a23b4e..873c27ea 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -52,6 +52,9 @@ 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_STYLE; use crate::CUSTOM_FFM; use crate::DATA_DIR; use crate::DISPLAY_INDEX_PREFERENCES; @@ -1246,6 +1249,15 @@ 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::AnimationStyle(style) => { + *ANIMATION_STYLE.lock() = style; + } SocketMessage::StackbarMode(mode) => { STACKBAR_MODE.store(mode); @@ -1326,7 +1338,7 @@ impl WindowManager { self.update_focused_workspace(false, false)?; } SocketMessage::DebugWindow(hwnd) => { - let window = Window { hwnd }; + let window = Window::new(hwnd); let mut rule_debug = RuleDebug::default(); let _ = window.should_manage(None, &mut rule_debug); let schema = serde_json::to_string_pretty(&rule_debug)?; diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 698c5805..7123df96 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -608,11 +608,12 @@ impl WindowManager { } WindowManagerEvent::DisplayChange(..) | WindowManagerEvent::MouseCapture(..) - | WindowManagerEvent::Cloak(..) => {} + | WindowManagerEvent::Cloak(..) + | WindowManagerEvent::UpdateFocusedWindowBorder(..) => {} }; // 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.rs b/komorebi/src/stackbar.rs index 5a5d0e16..acfcc44f 100644 --- a/komorebi/src/stackbar.rs +++ b/komorebi/src/stackbar.rs @@ -95,7 +95,7 @@ impl Stackbar { let bottom = height; if x >= left && x <= right && y >= top && y <= bottom { - let window = Window { hwnd: *win_hwnd }; + let window = Window::new(*win_hwnd); window.restore(); if let Err(err) = window.focus(false) { tracing::error!("Stackbar focus error: HWND:{} {}", *win_hwnd, err); diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index b1b33849..0d5c725d 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -10,6 +10,9 @@ 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_STYLE; use crate::DATA_DIR; use crate::DEFAULT_CONTAINER_PADDING; use crate::DEFAULT_WORKSPACE_PADDING; @@ -44,6 +47,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; @@ -319,6 +323,19 @@ 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, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -445,6 +462,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, } } } @@ -467,6 +485,13 @@ impl StaticConfig { *window_hiding_behaviour = behaviour; } + if let Some(animations) = &self.animation { + ANIMATION_ENABLED.store(animations.enabled, Ordering::SeqCst); + ANIMATION_DURATION.store(animations.duration.unwrap_or(250), 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 1379721a..7b604977 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -1,9 +1,13 @@ use crate::com::SetCloak; +use crate::winevent_listener; +use crate::ANIMATION_DURATION; +use crate::ANIMATION_ENABLED; use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::Display; use std::fmt::Formatter; use std::fmt::Write as _; +use std::sync::atomic::Ordering; use std::time::Duration; use color_eyre::eyre; @@ -24,6 +28,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::window_manager_event::WindowManagerEvent; @@ -41,6 +46,7 @@ use crate::WSL2_UI_PROCESSES; #[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema)] pub struct Window { pub hwnd: isize, + animation: Animation, } #[allow(clippy::module_name_repetitions)] @@ -119,11 +125,19 @@ impl Serialize for Window { } impl Window { + // for instantiation of animation struct + pub fn new(hwnd: isize) -> Self { + Self { + hwnd, + animation: Animation::new(hwnd), + } + } + pub const fn hwnd(self) -> HWND { 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; @@ -138,13 +152,50 @@ impl Window { ) } - pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> { + 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; + let self_copied = *self; + + std::thread::spawn(move || { + animation.animate(duration, |progress: f64| { + let new_rect = Animation::lerp_rect(&curr_rect, &target_rect, progress); + + if progress < 1.0 { + // using MoveWindow because it runs faster than SetWindowPos + // so animation have more fps and feel smoother + WindowsApi::move_window(hwnd, &new_rect, true)?; + WindowsApi::invalidate_rect(hwnd, None, false); + } else { + WindowsApi::position_window(hwnd, &new_rect, top)?; + + if WindowsApi::foreground_window()? == self_copied.hwnd { + winevent_listener::event_tx() + .send(WindowManagerEvent::UpdateFocusedWindowBorder(self_copied))?; + } + } + + Ok(()) + }) + }); + + Ok(()) + } + + pub fn set_position(&mut 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(&rect, top) + } else { + WindowsApi::position_window(self.hwnd(), &rect, top) + } } pub fn is_maximized(self) -> bool { diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 1428d97f..0b19f671 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -666,7 +666,7 @@ impl WindowManager { // Hide the window we are about to remove if it is on the currently focused workspace if op.is_origin(focused_monitor_idx, focused_workspace_idx) { - Window { hwnd: op.hwnd }.hide(); + Window::new(op.hwnd).hide(); should_update_focused_workspace = true; } @@ -696,7 +696,7 @@ impl WindowManager { .get_mut(op.target_workspace_idx) .ok_or_else(|| anyhow!("there is no workspace with that index"))?; - target_workspace.new_container_for_window(Window { hwnd: op.hwnd }); + target_workspace.new_container_for_window(Window::new(op.hwnd)); } // Only re-tile the focused workspace if we need to @@ -744,14 +744,14 @@ impl WindowManager { #[tracing::instrument(skip(self))] pub fn manage_focused_window(&mut self) -> Result<()> { let hwnd = WindowsApi::foreground_window()?; - let event = WindowManagerEvent::Manage(Window { hwnd }); + let event = WindowManagerEvent::Manage(Window::new(hwnd)); Ok(winevent_listener::event_tx().send(event)?) } #[tracing::instrument(skip(self))] pub fn unmanage_focused_window(&mut self) -> Result<()> { let hwnd = WindowsApi::foreground_window()?; - let event = WindowManagerEvent::Unmanage(Window { hwnd }); + let event = WindowManagerEvent::Unmanage(Window::new(hwnd)); Ok(winevent_listener::event_tx().send(event)?) } @@ -779,15 +779,13 @@ impl WindowManager { return Ok(()); } - let event = WindowManagerEvent::Raise(Window { hwnd }); + let event = WindowManagerEvent::Raise(Window::new(hwnd)); self.has_pending_raise_op = true; winevent_listener::event_tx().send(event)?; } else { tracing::debug!( "not raising unknown window: {}", - Window { - hwnd: WindowsApi::window_at_cursor_pos()? - } + Window::new(WindowsApi::window_at_cursor_pos()?) ); } @@ -911,9 +909,7 @@ impl WindowManager { window.focus(self.mouse_follows_focus)?; } } else { - let desktop_window = Window { - hwnd: WindowsApi::desktop_window()?, - }; + let desktop_window = Window::new(WindowsApi::desktop_window()?); let rect = self.focused_monitor_size()?; WindowsApi::center_cursor_in_rect(&rect)?; diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs index 2726bc3d..37c2b16a 100644 --- a/komorebi/src/window_manager_event.rs +++ b/komorebi/src/window_manager_event.rs @@ -28,6 +28,7 @@ pub enum WindowManagerEvent { Unmanage(Window), Raise(Window), DisplayChange(Window), + UpdateFocusedWindowBorder(Window), ForceUpdate(Window), } @@ -79,6 +80,9 @@ impl Display for WindowManagerEvent { Self::DisplayChange(window) => { write!(f, "DisplayChange (Window: {window})") } + Self::UpdateFocusedWindowBorder(window) => { + write!(f, "UpdateFocusedBorderWindow (Window: {window})") + } Self::ForceUpdate(window) => { write!(f, "ForceUpdate (Window: {window})") } @@ -103,6 +107,7 @@ impl WindowManagerEvent { | Self::Manage(window) | Self::DisplayChange(window) | Self::Unmanage(window) + | Self::UpdateFocusedWindowBorder(window) | Self::ForceUpdate(window) => window, } } diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index 01cfca92..a0b6991f 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; @@ -33,6 +34,7 @@ use windows::Win32::Graphics::Gdi::CreateSolidBrush; use windows::Win32::Graphics::Gdi::EnumDisplayDevicesW; 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; @@ -83,6 +85,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; @@ -412,6 +415,20 @@ impl WindowsApi { .process() } + pub fn move_window(hwnd: HWND, layout: &Rect, repaint: bool) -> Result<()> { + unsafe { + MoveWindow( + hwnd, + layout.left, + layout.top, + layout.right, + layout.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 @@ -982,6 +999,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/komorebi/src/windows_callbacks.rs b/komorebi/src/windows_callbacks.rs index 89ddd5ee..e2a1b05b 100644 --- a/komorebi/src/windows_callbacks.rs +++ b/komorebi/src/windows_callbacks.rs @@ -134,7 +134,7 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL { let is_maximized = WindowsApi::is_zoomed(hwnd); if is_visible && is_window && !is_minimized { - let window = Window { hwnd: hwnd.0 }; + let window = Window::new(hwnd.0); if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) { if should_manage { @@ -186,7 +186,7 @@ pub extern "system" fn win_event_hook( return; } - let window = Window { hwnd: hwnd.0 }; + let window = Window::new(hwnd.0); let winevent = match WinEvent::try_from(event) { Ok(event) => event, @@ -211,7 +211,7 @@ pub extern "system" fn hidden_window( unsafe { match message { WM_DISPLAYCHANGE => { - let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 }); + let event_type = WindowManagerEvent::DisplayChange(Window::new(window.0)); winevent_listener::event_tx() .send(event_type) .expect("could not send message on winevent_listener::event_tx"); @@ -224,7 +224,7 @@ pub extern "system" fn hidden_window( if wparam.0 as u32 == SPI_SETWORKAREA.0 || wparam.0 as u32 == SPI_ICONVERTICALSPACING.0 { - let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 }); + let event_type = WindowManagerEvent::DisplayChange(Window::new(window.0)); winevent_listener::event_tx() .send(event_type) .expect("could not send message on winevent_listener::event_tx"); @@ -235,7 +235,7 @@ pub extern "system" fn hidden_window( WM_DEVICECHANGE => { #[allow(clippy::cast_possible_truncation)] if wparam.0 as u32 == DBT_DEVNODES_CHANGED { - let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 }); + let event_type = WindowManagerEvent::DisplayChange(Window::new(window.0)); winevent_listener::event_tx() .send(event_type) .expect("could not send message on winevent_listener::event_tx"); diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 37bb7863..ef06a31c 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -286,6 +286,13 @@ impl Workspace { let managed_maximized_window = self.maximized_window().is_some(); if *self.tile() { + // TODO: Figure out what to do with this + // if ANIMATION_ENABLED.load(Ordering::SeqCst) { + // let border = Border::from(BORDER_HWND.load(Ordering::SeqCst)); + // border.hide()?; + // BORDER_HIDDEN.store(true, Ordering::SeqCst); + // } + if let Some(container) = self.monocle_container_mut() { if let Some(window) = container.focused_window_mut() { adjusted_work_area.add_padding(container_padding.unwrap_or_default()); diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index a78d1234..e2b3e2fd 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -683,6 +683,25 @@ struct BorderOffset { offset: i32, } +#[derive(Parser, AhkFunction)] +struct Animation { + #[clap(value_enum)] + boolean_state: BooleanState, +} + +#[derive(Parser, AhkFunction)] +struct AnimationDuration { + /// Desired animation durations in ms + duration: u64, +} + +#[derive(Parser, AhkFunction)] +struct AnimationStyle { + /// Desired ease function for animation + #[clap(value_enum, short, long, default_value = "linear")] + style: komorebi_core::AnimationStyle, +} + #[derive(Parser, AhkFunction)] #[allow(clippy::struct_excessive_bools)] struct Start { @@ -1165,6 +1184,15 @@ enum SubCommand { #[clap(arg_required_else_help = true)] #[clap(alias = "active-window-border-offset")] BorderOffset(BorderOffset), + /// 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 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), @@ -2248,6 +2276,15 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue SubCommand::BorderOffset(arg) => { send_message(&SocketMessage::BorderOffset(arg.offset).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::AnimationStyle(arg) => { + send_message(&SocketMessage::AnimationStyle(arg.style).as_bytes()?)?; + } SubCommand::ResizeDelta(arg) => { send_message(&SocketMessage::ResizeDelta(arg.pixels).as_bytes()?)?; }