Compare commits

...

7 Commits

Author SHA1 Message Date
LGUG2Z
7435089010 handle azin's empty device_path output (maybe?) 2024-07-13 17:03:21 -07:00
LGUG2Z
faa7786979 docs(mkdocs): add updates for v0.1.28 features 2024-07-13 16:12:35 -07:00
LGUG2Z
3c8a6cb7bd chore(deps): bump win32-display-data 2024-07-13 16:12:34 -07:00
LGUG2Z
50a279239a fix(animation): disable on cross-monitor ops
There are quite a lot of janky animation bugs when moving window
containers across monitor and workspace boundaries.

This commit disables animation on all of the main cross-border window
container operations, meaning that animations should now only happen
within the context of a single workspace.

fix #912
2024-07-12 09:21:50 -07:00
LGUG2Z
2c8f25ef82 fix(animation): add async focus manager for mff
This commit adds a new focus manager module to be used to trigger async
focus changes with mouse follows focus updates. Currently this should
only need to be used with animations as all other focus calls are
synchronous.

fix #910
2024-07-11 18:29:46 -07:00
LGUG2Z
bdc1cad597 feat(config): add "color-hex" format to jsonschema
This commit specifies "color-hex" as the "format" when implementing
JsonSchema for Hex.

resolve #911
2024-07-11 15:20:20 -07:00
LGUG2Z
e2f2d6b919 feat(animation): add window animations
Work on this feature was first started by @thearturca in November 2023
before komorebi v0.1.21 in #597 and has undergone numerous revisions
to reach the point of this commit.

Although this is a single squashed commit, almost all of the heavy
lifting for this feature was done by @thearturca, which is where all of
the kudos and gratitude should be directed.

This commit adds a new static configuration block for animations, where
they can be enabled, and have their style, fps and duration set.
Corresponding SocketMessages and komorebic cli commands have also been
exposed.

There are some caveats to the use of this feature, which revolve around
the quality of the Windows compositor (it is not very good):

* There will be visual artifacts with various apps when animations are
  taking place - komorebi can't do anything about this as it is a
  limitation of the Windows compositor
* Since komorebi's borders are implemented as independent windows are
  are not a part of the windows they are drawn around, these borders
  will be hidden while animations are in progress
* If you wish to use borders with this feature, you'll probably better
  off using BorderImplementation::Windows, which uses the native thin
  "accent" borders, which are part of the windows they are drawn around,
  and can be moved with those windows during animations

As a result of these and other caveats, this feature will be marked as
"experimental" for the foreseeable future and will be off-by-default.

Below, a number of now-squashed commits that contributed to the
stabilization of this feature are referenced to help with code
archeology in the future.

fix(animation): Fixed cancelling logic
(57e9b2f4bcaedb4fdfa71adf785d661690d81dfc)

Added static animation state manager for tracking "in_progress" and
"is_cancelled" states. The idea is not to have states in Animation
struct but to keep them in HashMap<hwnd, AnimationState> behind
reference (Arc<Mutex<>>). So we each animation frame we have access to
state and can cancel animation if we have to.

Need review and testings

refactor(animation): avoid unwrap
(fa6d5bbc77c1882f85ee1ce73733ff7e53b39eaa)

fix(animation): Move cancel call to Animation struct
(306513f5dbe5f6bd6ce817f3edca0bfda13d9442)

Only focused window was cancelling its animation because we call cancel
in window::set_position and waiting for its cancelling. And because we
waiting for cancelling second window is still moving. Second window will stop
moving only after the first window. So I moved `cancel` call to
Animation struct so its happening in its own thread and doesn't block
others animation moves and cancels.

refactor(animation): renamed args parameters and variables names
(8abb4b9618bbb3823b868fc37551f0a70b98281e)

refactor(animation): inverse if-statement in `window::animate_position`
(3de2c6e932614651892da4a8c626946e427375dd)

There is was a bug when ease function generates `t` greater the
`SetWindowPos` function will be called instead of `move_window`.
`SetWindowPos` is only for last frame of animation.

fix(wm): add shadow rect to `move_window` calls
(b58620fb4de36d8e422a80541bedf9c1c1579a31)

This fixes a bug when windows get shunk during the animation
2024-07-10 13:28:31 -07:00
33 changed files with 1231 additions and 56 deletions

2
Cargo.lock generated
View File

@@ -5098,7 +5098,7 @@ checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
[[package]]
name = "win32-display-data"
version = "0.1.0"
source = "git+https://github.com/LGUG2Z/win32-display-data?rev=2a0f7166da154880a1750b91829b1186d9c6a00c#2a0f7166da154880a1750b91829b1186d9c6a00c"
source = "git+https://github.com/LGUG2Z/win32-display-data?rev=32a45cebf132c3d651ee22c0c40033a6b7edc945#32a45cebf132c3d651ee22c0c40033a6b7edc945"
dependencies = [
"itertools",
"thiserror",

View File

@@ -18,7 +18,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
sysinfo = "0.30"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "2a0f7166da154880a1750b91829b1186d9c6a00c" }
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "32a45cebf132c3d651ee22c0c40033a6b7edc945" }
windows-implement = { version = "0.53" }
windows-interface = { version = "0.53" }
shadow-rs = "0.29"

View File

@@ -358,7 +358,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.25"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28"}
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -1,12 +0,0 @@
# ahk-library
```
Generate a library of AutoHotKey helper functions
Usage: komorebic.exe komorebic.exe ahk-library
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,20 @@
# border-implementation
```
Set the border implementation
Usage: komorebic.exe border-implementation <STYLE>
Arguments:
<STYLE>
Desired border implementation
Possible values:
- komorebi: Use the adjustable komorebi border implementation
- windows: Use the thin Windows accent border implementation
Options:
-h, --help
Print help (see a summary with '-h')
```

21
docs/cli/border-style.md Normal file
View File

@@ -0,0 +1,21 @@
# border-style
```
Set the border style
Usage: komorebic.exe border-style <STYLE>
Arguments:
<STYLE>
Desired border style
Possible values:
- system: Use the system border style
- rounded: Use the Windows 11-style rounded borders
- square: Use the Windows 10-style square borders
Options:
-h, --help
Print help (see a summary with '-h')
```

View File

@@ -0,0 +1,12 @@
# clear-all-workspace-rules
```
Remove all application association rules for all workspaces
Usage: komorebic.exe clear-all-workspace-rules
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,16 @@
# clear-named-workspace-rules
```
Remove all application association rules for a named workspace
Usage: komorebic.exe clear-named-workspace-rules <WORKSPACE>
Arguments:
<WORKSPACE>
Name of a workspace
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,19 @@
# clear-workspace-rules
```
Remove all application association rules for a workspace by monitor and workspace index
Usage: komorebic.exe clear-workspace-rules <MONITOR> <WORKSPACE>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index on the specified monitor (zero-indexed)
Options:
-h, --help
Print help
```

View File

@@ -10,7 +10,7 @@ Arguments:
Possible values:
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
Options:
-h, --help

View File

@@ -0,0 +1,25 @@
# Animations
If you would like to add window movement animations, ensure the following options are
defined in the `komorebi.json` configuration file.
```json
{
"animation": {
"enabled": true
}
}
```
Window movement animations only apply to actions taking place within the same monitor
workspace.
You can optionally set a custom duration in ms with `animation.duration` (default: `250`),
a custom style with `animation.style` (default: `Linear`), and a custom FPS value with
`animation.fps` (default: `60`).
It is important to note that higher `fps` and a longer `duration` settings will result
in increased CPU usage.
This feature is not considered stable, and you may encounter visual artifacts
from time to time.

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.25/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.28/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",

View File

@@ -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,
}

View File

@@ -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")]

View File

@@ -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,
}
}
}

View File

@@ -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<Window>) };
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})")

503
komorebi/src/animation.rs Normal file
View File

@@ -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)
}
}

View File

@@ -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<isize, AnimationState>,
}
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);
}
}
}

View File

@@ -39,6 +39,7 @@ impl JsonSchema for Hex {
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("color-hex".to_string()),
..Default::default()
}
.into()

View File

@@ -0,0 +1,70 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use parking_lot::Mutex;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::OnceLock;
use crate::Window;
use crate::WindowManager;
pub struct Notification(isize);
impl Deref for Notification {
type Target = isize;
fn deref(&self) -> &Self::Target {
&self.0
}
}
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
}
fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
// Currently this should only be used for async focus updates, such as
// when an animation finishes and we need to focus to set the cursor
// position if the user has mouse follows focus enabled
pub fn send_notification(hwnd: isize) {
if event_tx().try_send(Notification(hwnd)).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
for notification in receiver {
let mouse_follows_focus = wm.lock().mouse_follows_focus;
let _ = Window::from(*notification).focus(mouse_follows_focus);
}
Ok(())
}

View File

@@ -1,11 +1,14 @@
#![warn(clippy::all)]
pub mod animation;
pub mod animation_manager;
pub mod border_manager;
pub mod com;
#[macro_use]
pub mod ring;
pub mod colour;
pub mod container;
pub mod focus_manager;
pub mod monitor;
pub mod monitor_reconciliator;
pub mod process_command;
@@ -38,9 +41,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 +61,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 +204,12 @@ lazy_static! {
)
};
static ref ANIMATION_STYLE: Arc<Mutex<AnimationStyle >> =
Arc::new(Mutex::new(AnimationStyle::Linear));
static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
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<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
@@ -214,6 +227,9 @@ 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_TEMPORARY_DISABLED: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {

View File

@@ -25,6 +25,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use komorebi::border_manager;
use komorebi::focus_manager;
use komorebi::load_configuration;
use komorebi::monitor_reconciliator;
use komorebi::process_command::listen_for_commands;
@@ -265,6 +266,7 @@ fn main() -> Result<()> {
workspace_reconciliator::listen_for_notifications(wm.clone());
monitor_reconciliator::listen_for_notifications(wm.clone())?;
reaper::watch_for_orphans(wm.clone());
focus_manager::listen_for_notifications(wm.clone());
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {

View File

@@ -67,11 +67,17 @@ pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
.flatten()
.map(|display| {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let (device, device_id) = if path.is_empty() {
(String::from("UNKNOWN"), String::from("UNKNOWN"))
} else {
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
(device, device_id)
};
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();

View File

@@ -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);
}

View File

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

View File

@@ -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<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::OnStack);
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
lazy_static! {
pub static ref STACKBAR_STATE: Mutex<HashMap<String, Stackbar>> = Mutex::new(HashMap::new());
pub static ref STACKBAR_FONT_FAMILY: Mutex<Option<String>> = Mutex::new(None);
@@ -93,7 +97,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> 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()?;
}

View File

@@ -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<StackbarConfig>,
/// Animations configuration options
#[serde(skip_serializing_if = "Option::is_none")]
pub animation: Option<AnimationsConfig>,
}
#[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<u64>,
/// Set the animation style (default: Linear)
style: Option<AnimationStyle>,
/// Set the animation FPS (default: 60)
fps: Option<u64>,
}
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);
}

View File

@@ -1,4 +1,11 @@
use crate::border_manager;
use crate::com::SetCloak;
use crate::focus_manager;
use crate::stackbar_manager;
use crate::ANIMATIONS_IN_PROGRESS;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_TEMPORARY_DISABLED;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::Display;
@@ -26,6 +33,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 +55,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<isize> for Window {
fn from(value: isize) -> Self {
Self { hwnd: value }
Self {
hwnd: value,
animation: Animation::new(value),
}
}
}
impl From<HWND> for Window {
fn from(value: HWND) -> Self {
Self { hwnd: value.0 }
Self {
hwnd: value.0,
animation: Animation::new(value.0),
}
}
}
@@ -141,7 +156,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 +171,66 @@ 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 WindowsApi::foreground_window().unwrap_or_default() == hwnd.0 {
focus_manager::send_notification(hwnd.0)
}
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)
&& !ANIMATION_TEMPORARY_DISABLED.load(Ordering::SeqCst)
{
self.animate_position(layout, top)
} else {
WindowsApi::position_window(self.hwnd(), layout, top)
}
}
pub fn is_maximized(self) -> bool {

View File

@@ -65,6 +65,7 @@ use crate::BorderColours;
use crate::Colour;
use crate::Rgb;
use crate::WorkspaceRule;
use crate::ANIMATION_TEMPORARY_DISABLED;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
@@ -1108,6 +1109,7 @@ impl WindowManager {
follow: bool,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
ANIMATION_TEMPORARY_DISABLED.store(true, Ordering::SeqCst);
tracing::info!("moving container");
@@ -1177,12 +1179,17 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
}
self.update_focused_workspace(self.mouse_follows_focus, true)
self.update_focused_workspace(self.mouse_follows_focus, true)?;
ANIMATION_TEMPORARY_DISABLED.store(false, Ordering::SeqCst);
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn move_container_to_workspace(&mut self, idx: usize, follow: bool) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
ANIMATION_TEMPORARY_DISABLED.store(true, Ordering::SeqCst);
tracing::info!("moving container");
@@ -1194,7 +1201,11 @@ impl WindowManager {
monitor.move_container_to_workspace(idx, follow)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
self.update_focused_workspace(mouse_follows_focus, true)
self.update_focused_workspace(mouse_follows_focus, true)?;
ANIMATION_TEMPORARY_DISABLED.store(false, Ordering::SeqCst);
Ok(())
}
pub fn remove_focused_workspace(&mut self) -> Option<Workspace> {
@@ -1297,6 +1308,13 @@ impl WindowManager {
let origin_monitor_idx = self.focused_monitor_idx();
let target_container_idx = workspace.new_idx_for_direction(direction);
let animation_temporarily_disabled = if target_container_idx.is_none() {
ANIMATION_TEMPORARY_DISABLED.store(true, Ordering::SeqCst);
true
} else {
false
};
match target_container_idx {
// If there is nowhere to move on the current workspace, try to move it onto the monitor
// in that direction if there is one
@@ -1421,7 +1439,13 @@ impl WindowManager {
}
}
self.update_focused_workspace(self.mouse_follows_focus, true)
self.update_focused_workspace(self.mouse_follows_focus, true)?;
if animation_temporarily_disabled {
ANIMATION_TEMPORARY_DISABLED.store(false, Ordering::SeqCst);
}
Ok(())
}
#[tracing::instrument(skip(self))]

View File

@@ -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;
@@ -236,11 +239,17 @@ impl WindowsApi {
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
'read: for display in win32_display_data::connected_displays_all().flatten() {
let path = display.device_path.clone();
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let (device, device_id) = if path.is_empty() {
(String::from("UNKNOWN"), String::from("UNKNOWN"))
} else {
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
(device, device_id)
};
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
@@ -429,6 +438,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
@@ -795,11 +815,17 @@ impl WindowsApi {
for display in win32_display_data::connected_displays_all().flatten() {
if display.hmonitor == hmonitor {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let (device, device_id) = if path.is_empty() {
(String::from("UNKNOWN"), String::from("UNKNOWN"))
} else {
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
(device, device_id)
};
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
@@ -1023,6 +1049,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)]

View File

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

View File

@@ -57,6 +57,7 @@ nav:
- Troubleshooting: troubleshooting.md
- Common workflows:
- common-workflows/komorebi-config-home.md
- common-workflows/animations.md
- common-workflows/autohotkey.md
- common-workflows/borders.md
- common-workflows/stackbar.md
@@ -191,6 +192,9 @@ nav:
- cli/initial-named-workspace-rule.md
- cli/workspace-rule.md
- cli/named-workspace-rule.md
- cli/clear-workspace-rules.md
- cli/clear-named-workspace-rules.md
- cli/clear-all-workspace-rules.md
- cli/identify-object-name-change-application.md
- cli/identify-tray-application.md
- cli/identify-layered-application.md
@@ -200,6 +204,8 @@ nav:
- cli/border-colour.md
- cli/border-width.md
- cli/border-offset.md
- cli/border-style.md
- cli/border-implementation.md
- cli/transparency.md
- cli/transparency-alpha.md
- cli/focus-follows-mouse.md

View File

@@ -1,9 +1,70 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.27`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.28`",
"type": "object",
"properties": {
"animation": {
"description": "Animations configuration options",
"type": "object",
"required": [
"enabled"
],
"properties": {
"duration": {
"description": "Set the animation duration in ms (default: 250)",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"enabled": {
"description": "Enable or disable animations (default: false)",
"type": "boolean"
},
"fps": {
"description": "Set the animation FPS (default: 60)",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"style": {
"description": "Set the animation style (default: Linear)",
"type": "string",
"enum": [
"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"
]
}
}
},
"app_specific_configuration_path": {
"description": "Path to applications.yaml from komorebi-application-specific-configurations (default: None)",
"type": "string"
@@ -50,7 +111,8 @@
},
{
"description": "Colour represented as Hex",
"type": "string"
"type": "string",
"format": "color-hex"
}
]
},
@@ -88,7 +150,8 @@
},
{
"description": "Colour represented as Hex",
"type": "string"
"type": "string",
"format": "color-hex"
}
]
},
@@ -126,7 +189,8 @@
},
{
"description": "Colour represented as Hex",
"type": "string"
"type": "string",
"format": "color-hex"
}
]
},
@@ -164,12 +228,32 @@
},
{
"description": "Colour represented as Hex",
"type": "string"
"type": "string",
"format": "color-hex"
}
]
}
}
},
"border_implementation": {
"description": "Display an active window border (default: false)",
"oneOf": [
{
"description": "Use the adjustable komorebi border implementation",
"type": "string",
"enum": [
"Komorebi"
]
},
{
"description": "Use the thin Windows accent border implementation",
"type": "string",
"enum": [
"Windows"
]
}
]
},
"border_offset": {
"description": "Offset of the window border (default: -1)",
"type": "integer",
@@ -674,6 +758,16 @@
]
}
},
"minimum_window_height": {
"description": "DISCOURAGED: Minimum height for a window to be eligible for tiling",
"type": "integer",
"format": "int32"
},
"minimum_window_width": {
"description": "DISCOURAGED: Minimum width for a window to be eligible for tiling",
"type": "integer",
"format": "int32"
},
"monitor_index_preferences": {
"description": "Set monitor index preferences",
"type": "object",
@@ -796,6 +890,10 @@
"name"
],
"properties": {
"apply_window_based_work_area_offset": {
"description": "Apply this monitor's window-based work area offset (default: true)",
"type": "boolean"
},
"container_padding": {
"description": "Container padding (default: global)",
"type": "integer",
@@ -1094,7 +1192,8 @@
},
{
"description": "Colour represented as Hex",
"type": "string"
"type": "string",
"format": "color-hex"
}
]
},
@@ -1132,10 +1231,20 @@
},
{
"description": "Colour represented as Hex",
"type": "string"
"type": "string",
"format": "color-hex"
}
]
},
"font_family": {
"description": "Font family",
"type": "string"
},
"font_size": {
"description": "Font size",
"type": "integer",
"format": "int32"
},
"unfocused_text": {
"description": "Unfocused tab text colour",
"anyOf": [
@@ -1170,7 +1279,8 @@
},
{
"description": "Colour represented as Hex",
"type": "string"
"type": "string",
"format": "color-hex"
}
]
},
@@ -1315,7 +1425,7 @@
]
},
"window_hiding_behaviour": {
"description": "Which Windows signal to use when hiding windows (default: minimize)",
"description": "Which Windows signal to use when hiding windows (default: Cloak)",
"oneOf": [
{
"description": "Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)",
@@ -1332,7 +1442,7 @@
]
},
{
"description": "Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)",
"description": "Use the undocumented SetCloak Win32 function to hide windows when switching workspaces",
"type": "string",
"enum": [
"Cloak"