diff --git a/komorebi-core/src/animation.rs b/komorebi-core/src/animation.rs new file mode 100644 index 00000000..5ba904a8 --- /dev/null +++ b/komorebi-core/src/animation.rs @@ -0,0 +1,43 @@ +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, +)] +#[strum(serialize_all = "snake_case")] +pub enum EaseEnum { + 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 f50fe2ab..25b1afef 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::EaseEnum; 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; @@ -127,6 +129,9 @@ pub enum SocketMessage { WatchConfiguration(bool), CompleteConfiguration, AltFocusHack(bool), + Animate(bool), + AnimateDuration(u64), + AnimateEase(EaseEnum), ActiveWindowBorder(bool), ActiveWindowBorderColour(WindowKind, u32, u32, u32), ActiveWindowBorderWidth(i32), diff --git a/komorebi/src/animation.rs b/komorebi/src/animation.rs index 2e24743b..09c2fc44 100644 --- a/komorebi/src/animation.rs +++ b/komorebi/src/animation.rs @@ -1,55 +1,510 @@ use color_eyre::Result; +use komorebi_core::EaseEnum; use komorebi_core::Rect; -use std::thread::sleep; + +use schemars::JsonSchema; + +use std::f64::consts::PI; use std::time::Duration; use std::time::Instant; -pub struct Animation; +use crate::ANIMATE_EASE; + +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 { + 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 { + 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 || 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 || 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 || 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 ease = *ANIMATE_EASE.lock(); + + match ease { + EaseEnum::Linear => Linear::evaluate(t), + EaseEnum::EaseInSine => EaseInSine::evaluate(t), + EaseEnum::EaseOutSine => EaseOutSine::evaluate(t), + EaseEnum::EaseInOutSine => EaseInOutSine::evaluate(t), + EaseEnum::EaseInQuad => EaseInQuad::evaluate(t), + EaseEnum::EaseOutQuad => EaseOutQuad::evaluate(t), + EaseEnum::EaseInOutQuad => EaseInOutQuad::evaluate(t), + EaseEnum::EaseInCubic => EaseInCubic::evaluate(t), + EaseEnum::EaseInOutCubic => EaseInOutCubic::evaluate(t), + EaseEnum::EaseInQuart => EaseInQuart::evaluate(t), + EaseEnum::EaseOutQuart => EaseOutQuart::evaluate(t), + EaseEnum::EaseInOutQuart => EaseInOutQuart::evaluate(t), + EaseEnum::EaseInQuint => EaseInQuint::evaluate(t), + EaseEnum::EaseOutQuint => EaseOutQuint::evaluate(t), + EaseEnum::EaseInOutQuint => EaseInOutQuint::evaluate(t), + EaseEnum::EaseInExpo => EaseInExpo::evaluate(t), + EaseEnum::EaseOutExpo => EaseOutExpo::evaluate(t), + EaseEnum::EaseInOutExpo => EaseInOutExpo::evaluate(t), + EaseEnum::EaseInCirc => EaseInCirc::evaluate(t), + EaseEnum::EaseOutCirc => EaseOutCirc::evaluate(t), + EaseEnum::EaseInOutCirc => EaseInOutCirc::evaluate(t), + EaseEnum::EaseInBack => EaseInBack::evaluate(t), + EaseEnum::EaseOutBack => EaseOutBack::evaluate(t), + EaseEnum::EaseInOutBack => EaseInOutBack::evaluate(t), + EaseEnum::EaseInElastic => EaseInElastic::evaluate(t), + EaseEnum::EaseOutElastic => EaseOutElastic::evaluate(t), + EaseEnum::EaseInOutElastic => EaseInOutElastic::evaluate(t), + EaseEnum::EaseInBounce => EaseInBounce::evaluate(t), + EaseEnum::EaseOutBounce => EaseOutBounce::evaluate(t), + EaseEnum::EaseInOutBounce => EaseInOutBounce::evaluate(t), + } +} + +#[derive(Debug, Clone, Copy, JsonSchema)] +pub struct Animation { + // is_cancel: AtomicBool, + // pub in_progress: AtomicBool, + is_cancel: bool, + pub in_progress: bool, +} + +impl Default for Animation { + fn default() -> Self { + Animation { + // I'm not sure if this is the right way to do it + // I've tried to use Arc> but it dooes not implement Copy trait + // and I dont want to rewrite everything cause I'm not experienced with rust + // Down here you can see the idea I've tried to achive like in any other OOP language + // My thought is that in order to prevent Google Chrome breaking render window + // I need to cancel animation if user starting new window movement. So window stops + // moving at one point and then fires new animation. + // But my approach does not work because of rust borrowing rules and wired pointers + // lifetime annotation that I dont know how to use. + is_cancel: false, + in_progress: false, + // is_cancel: AtomicBool::new(false), + // in_progress: AtomicBool::new(false), + } + } +} impl Animation { + pub fn cancel(&mut self) -> Result<()> { + if !self.in_progress { + return Ok(()); + } + + self.is_cancel = true; + let max_duration = Duration::from_secs(1); + let spent_duration = Instant::now(); + + while self.in_progress { + if spent_duration.elapsed() >= max_duration { + break; + } + + std::thread::sleep(Duration::from_millis(16)); + } + + Ok(()) + } + pub fn lerp(x: i32, new_x: i32, t: f64) -> i32 { - (x as f64 + (new_x - x) as f64 * t) as 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 { - let is_half_way = t > 0.5; - let mut rect = Rect::default(); - rect.top = Animation::lerp(original_rect.top, new_rect.top, t); - rect.left = Animation::lerp(original_rect.left, new_rect.left, t); - rect.bottom = if is_half_way { - new_rect.bottom - } else { - original_rect.bottom - }; - rect.right = if is_half_way { - new_rect.right - } else { - original_rect.right - }; - - 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), + } } - pub fn animate(duration: Duration, mut f: impl FnMut(f64) -> Result<()>) -> bool { + pub fn animate( + &mut self, + duration: Duration, + mut f: impl FnMut(f64) -> Result<()>, + ) -> Result<()> { + self.in_progress = true; + // set target frame time to match 240 fps (my max refresh rate of monitor) + // probably not 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(); + let animation_start = Instant::now(); + // start animation while progress < 1.0 { - let tick_start = Instant::now(); - f(progress).unwrap(); - progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64; - - if progress > 1.0 { - progress = 1.0; + // check if animation is cancelled + if self.is_cancel { + // cancel animation + // set all flags + self.is_cancel = !self.is_cancel; + self.in_progress = false; + 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 { - sleep(target_frame_time - tick_start.elapsed()); + std::thread::sleep(target_frame_time - tick_start.elapsed()); } } - f(progress).unwrap(); - true + self.in_progress = false; + + // 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/main.rs b/komorebi/src/main.rs index 62f436ab..8b041f5e 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -16,6 +16,7 @@ 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; #[cfg(feature = "deadlock_detection")] @@ -26,6 +27,7 @@ use color_eyre::Result; use crossbeam_channel::Receiver; use crossbeam_channel::Sender; use crossbeam_utils::Backoff; +use komorebi_core::EaseEnum; use lazy_static::lazy_static; use os_info::Version; #[cfg(feature = "deadlock_detection")] @@ -215,6 +217,8 @@ lazy_static! { static ref BORDER_OFFSET: Arc>> = Arc::new(Mutex::new(None)); + static ref ANIMATE_EASE: Arc> = Arc::new(Mutex::new(EaseEnum::EaseInOutExpo)); + // Use app-specific titlebar removal options where possible // eg. Windows Terminal, IntelliJ IDEA, Firefox static ref NO_TITLEBAR: Arc>> = Arc::new(Mutex::new(vec![])); @@ -238,6 +242,8 @@ pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(20); // 0 0 0 aka pure black, I doubt anyone will want this as a border colour pub const TRANSPARENCY_COLOUR: u32 = 0; pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false); +pub static ANIMATE_ENABLED: AtomicBool = AtomicBool::new(true); +pub static ANIMATE_DURATION: AtomicU64 = AtomicU64::new(250); pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0); diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 8535cd9e..ac28ae29 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -47,6 +47,9 @@ use crate::windows_api::WindowsApi; use crate::Notification; use crate::NotificationEvent; use crate::ALT_FOCUS_HACK; +use crate::ANIMATE_DURATION; +use crate::ANIMATE_EASE; +use crate::ANIMATE_ENABLED; use crate::BORDER_COLOUR_CURRENT; use crate::BORDER_COLOUR_MONOCLE; use crate::BORDER_COLOUR_SINGLE; @@ -1140,6 +1143,15 @@ impl WindowManager { self.hide_border()?; } } + SocketMessage::Animate(enable) => { + ANIMATE_ENABLED.store(enable, Ordering::SeqCst); + } + SocketMessage::AnimateDuration(duration) => { + ANIMATE_DURATION.store(duration, Ordering::SeqCst); + } + SocketMessage::AnimateEase(ease) => { + *ANIMATE_EASE.lock() = ease; + } SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => { match kind { WindowKind::Single => { @@ -1295,7 +1307,7 @@ impl WindowManager { | SocketMessage::CycleMoveWindow(_) | SocketMessage::MoveWindow(_) => { let foreground = WindowsApi::foreground_window()?; - let foreground_window = Window { hwnd: foreground }; + let foreground_window = Window::new(foreground); let mut rect = WindowsApi::window_rect(foreground_window.hwnd())?; rect.top -= self.invisible_borders.bottom; rect.bottom += self.invisible_borders.bottom; diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index f47f0e92..df1a08e2 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -1,11 +1,12 @@ use crate::com::SetCloak; +use crate::ANIMATE_DURATION; +use crate::ANIMATE_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::thread; use std::time::Duration; use color_eyre::eyre::anyhow; @@ -47,6 +48,7 @@ use crate::WSL2_UI_PROCESSES; #[derive(Debug, Clone, Copy, JsonSchema)] pub struct Window { pub(crate) hwnd: isize, + animation: Animation, } impl Display for Window { @@ -106,6 +108,14 @@ impl Serialize for Window { } impl Window { + // for instantiation of animation struct + pub fn new(hwnd: isize) -> Self { + Window { + hwnd, + animation: Animation::default(), + } + } + pub const fn hwnd(self) -> HWND { HWND(self.hwnd) } @@ -125,21 +135,37 @@ impl Window { true, ) } - pub fn animate_position(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> { - let duration = Duration::from_millis(200); + pub fn animate_position(&mut self, layout: &Rect, top: bool) -> Result<()> { + let hwnd = self.hwnd(); let curr_rect = WindowsApi::window_rect(hwnd).unwrap(); - if assert_eq!(curr_rect, *layout) { - WindowsApi::position_window(hwnd, layout, top); + if curr_rect.left == layout.left + && curr_rect.top == layout.top + && curr_rect.bottom == layout.bottom + && curr_rect.right == layout.right + { + WindowsApi::position_window(hwnd, layout, top) + } else { + let target_rect = *layout; + let duration = Duration::from_millis(ANIMATE_DURATION.load(Ordering::SeqCst)); + let mut animation = self.animation; + 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) + } else { + WindowsApi::position_window(hwnd, &new_rect, top) + } + }) + .unwrap(); + }); + + Ok(()) } - - let animate_window = |progress: f64| { - let new_rect = Animation::lerp_rect(&curr_rect, layout, progress); - WindowsApi::position_window(hwnd, &new_rect, top); - }; - - Animation::animate(duration, animate_window); - Ok(()) } pub fn set_position( @@ -173,14 +199,17 @@ impl Window { rect.bottom += invisible_borders.bottom; } - let hwnd = self.hwnd(); + if ANIMATE_ENABLED.load(Ordering::SeqCst) { + // check if animation is in progress + if self.animation.in_progress { + // wait for cancle animation + self.animation.cancel().unwrap(); + } - thread::spawn(move || { - Window::animate_position(hwnd, &rect, top).unwrap(); - }); - Ok(()) - - // WindowsApi::position_window(self.hwnd(), &rect, top) + self.animate_position(&rect, top) + } else { + WindowsApi::position_window(self.hwnd(), &rect, top) + } } pub fn hide(self) { diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 49130f38..d82da7b1 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -217,7 +217,7 @@ impl WindowManager { #[tracing::instrument(skip(self))] pub fn show_border(&self) -> Result<()> { let foreground = WindowsApi::foreground_window()?; - let foreground_window = Window { hwnd: foreground }; + let foreground_window = Window::new(foreground); let mut rect = WindowsApi::window_rect(foreground_window.hwnd())?; rect.top -= self.invisible_borders.bottom; rect.bottom += self.invisible_borders.bottom; @@ -589,7 +589,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; } @@ -619,7 +619,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 @@ -663,14 +663,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_CALLBACK_CHANNEL.lock().0.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_CALLBACK_CHANNEL.lock().0.send(event)?) } @@ -708,13 +708,14 @@ impl WindowManager { ]; if !known_hwnd { - let class = Window { hwnd }.class()?; + let class = Window::new(hwnd).class()?; // Some applications (Electron/Chromium-based, explorer) have (invisible?) overlays // windows that we need to look beyond to find the actual window to raise if overlay_classes.contains(&class) { for monitor in self.monitors() { for workspace in monitor.workspaces() { - if let Some(exe_hwnd) = workspace.hwnd_from_exe(&Window { hwnd }.exe()?) + if let Some(exe_hwnd) = + workspace.hwnd_from_exe(&Window::new(hwnd).exe()?) { hwnd = exe_hwnd; known_hwnd = true; @@ -725,11 +726,11 @@ impl WindowManager { } if known_hwnd { - let event = WindowManagerEvent::Raise(Window { hwnd }); + let event = WindowManagerEvent::Raise(Window::new(hwnd)); self.has_pending_raise_op = true; Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?) } else { - tracing::debug!("not raising unknown window: {}", Window { hwnd }); + tracing::debug!("not raising unknown window: {}", Window::new(hwnd,)); Ok(()) } } @@ -843,9 +844,7 @@ impl WindowManager { } else if let Ok(window) = self.focused_window_mut() { 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/windows_api.rs b/komorebi/src/windows_api.rs index a5d4a209..22ddf60c 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -81,6 +81,7 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId; use windows::Win32::UI::WindowsAndMessaging::IsIconic; use windows::Win32::UI::WindowsAndMessaging::IsWindow; use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible; +use windows::Win32::UI::WindowsAndMessaging::MoveWindow; use windows::Win32::UI::WindowsAndMessaging::PostMessageW; use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW; use windows::Win32::UI::WindowsAndMessaging::RegisterClassA; @@ -354,6 +355,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() + } + 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 diff --git a/komorebi/src/windows_callbacks.rs b/komorebi/src/windows_callbacks.rs index 6b1529eb..5964a7d0 100644 --- a/komorebi/src/windows_callbacks.rs +++ b/komorebi/src/windows_callbacks.rs @@ -104,7 +104,7 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL { let is_minimized = WindowsApi::is_iconic(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) { if should_manage { @@ -132,7 +132,7 @@ pub extern "system" fn win_event_hook( return; } - let window = Window { hwnd: hwnd.0 }; + let window = Window::new(hwnd.0); let winevent = unsafe { ::std::mem::transmute(event) }; let event_type = match WindowManagerEvent::from_win_event(winevent, window) { @@ -196,7 +196,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_CALLBACK_CHANNEL .lock() .0 @@ -211,7 +211,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_CALLBACK_CHANNEL .lock() .0 @@ -224,7 +224,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_CALLBACK_CHANNEL .lock() .0 diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 246a2d34..3450ddf8 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -21,6 +21,7 @@ use color_eyre::Result; use fs_tail::TailedFile; use heck::ToKebabCase; use komorebi_core::resolve_home_path; +use komorebi_core::EaseEnum; use lazy_static::lazy_static; use paste::paste; use sysinfo::SystemExt; @@ -627,6 +628,25 @@ struct ActiveWindowBorderOffset { offset: i32, } +#[derive(Parser, AhkFunction)] +struct Animate { + #[clap(value_enum)] + boolean_state: BooleanState, +} + +#[derive(Parser, AhkFunction)] +struct AnimateDuration { + /// Desired animation durations in ms + duration: u64, +} + +#[derive(Parser, AhkFunction)] +struct AnimateEase { + /// Desired ease function for animation + #[clap(value_enum, short, long, default_value = "linear")] + ease_func: EaseEnum, +} + #[derive(Parser, AhkFunction)] #[allow(clippy::struct_excessive_bools)] struct Start { @@ -1059,6 +1079,15 @@ enum SubCommand { /// Set the offset for the active window border #[clap(arg_required_else_help = true)] ActiveWindowBorderOffset(ActiveWindowBorderOffset), + /// Enable or disable the window move animation + #[clap(arg_required_else_help = true)] + Animate(Animate), + /// Set the duration for the window move animation in ms + #[clap(arg_required_else_help = true)] + AnimateDuration(AnimateDuration), + /// Set the ease function for the window move animation + #[clap(arg_required_else_help = true)] + AnimateEase(AnimateEase), /// Enable or disable focus follows mouse for the operating system #[clap(arg_required_else_help = true)] FocusFollowsMouse(FocusFollowsMouse), @@ -1986,6 +2015,15 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue SubCommand::ActiveWindowBorderOffset(arg) => { send_message(&SocketMessage::ActiveWindowBorderOffset(arg.offset).as_bytes()?)?; } + SubCommand::Animate(arg) => { + send_message(&SocketMessage::Animate(arg.boolean_state.into()).as_bytes()?)?; + } + SubCommand::AnimateDuration(arg) => { + send_message(&SocketMessage::AnimateDuration(arg.duration).as_bytes()?)?; + } + SubCommand::AnimateEase(arg) => { + send_message(&SocketMessage::AnimateEase(arg.ease_func).as_bytes()?)?; + } SubCommand::ResizeDelta(arg) => { send_message(&SocketMessage::ResizeDelta(arg.pixels).as_bytes()?)?; }