From 730f5d82ece6cbeba9bf275b3a03f85d549d1db8 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Wed, 28 Oct 2026 12:54:39 -0700 Subject: [PATCH] feat(animation): a workspace animation prefix Workspaces are going to slide in and out of the monitor when they are switched, and that is its own kind of animation (workspace) next to moving and transparency: it is turned on, and given a duration and an easing, the same way (animation.enabled, duration and style take a workspace entry, as do komorebic animation, animation-duration and animation-style with --animation-type workspace), with the duration and the easing of the other animations when it has none of its own. Animations being enabled for everything enables it too, and it can be turned off in particular (animation.enabled with workspace set to false), so that enabled: true keeps meaning everything. It needs ghost movement, as the windows slide as ghosts. Nothing uses it yet. --- docs/cli/animation-duration.md | 2 +- docs/cli/animation-style.md | 2 +- docs/cli/animation.md | 2 +- docs/common-workflows/animations.md | 65 ++++++++++++- komorebi/src/animation/mod.rs | 136 ++++++++++++++++++++++++++++ komorebi/src/animation/prefix.rs | 2 + schema.bar.json | 17 +++- schema.json | 42 +++------ 8 files changed, 229 insertions(+), 39 deletions(-) diff --git a/docs/cli/animation-duration.md b/docs/cli/animation-duration.md index dd6f98ed..e0ec8d34 100644 --- a/docs/cli/animation-duration.md +++ b/docs/cli/animation-duration.md @@ -13,7 +13,7 @@ Options: -a, --animation-type Animation type to apply the duration to. If not specified, sets global duration - [possible values: movement, transparency] + [possible values: movement, transparency, workspace] -h, --help Print help diff --git a/docs/cli/animation-style.md b/docs/cli/animation-style.md index f5f9efe9..11cc3eae 100644 --- a/docs/cli/animation-style.md +++ b/docs/cli/animation-style.md @@ -16,7 +16,7 @@ Options: -a, --animation-type Animation type to apply the style to. If not specified, sets global style - [possible values: movement, transparency] + [possible values: movement, transparency, workspace] -h, --help Print help diff --git a/docs/cli/animation.md b/docs/cli/animation.md index 135e57be..530bcaa8 100644 --- a/docs/cli/animation.md +++ b/docs/cli/animation.md @@ -13,7 +13,7 @@ Options: -a, --animation-type Animation type to apply the state to. If not specified, sets global state - [possible values: movement, transparency] + [possible values: movement, transparency, workspace] -h, --help Print help diff --git a/docs/common-workflows/animations.md b/docs/common-workflows/animations.md index 52792091..e6740583 100644 --- a/docs/common-workflows/animations.md +++ b/docs/common-workflows/animations.md @@ -25,4 +25,67 @@ It is important to note that higher `fps` and a longer `duration` settings will in increased CPU usage. This feature is not considered stable, and you may encounter visual artifacts -from time to time. \ No newline at end of file +from time to time. + +## Workspace transitions + +Workspaces can slide in and out of the monitor when you switch between them: the +windows of the workspace you leave slide out of the monitor, and the windows of the +workspace you switch to slide in from the other side, from the right when you go to +a workspace with a higher index (or move right across the edge of a workspace when +`cross_boundary_behaviour` is `Workspace`) and from the left when you go to one with +a lower index (or move left). + +It is on when animations are enabled for everything (`"enabled": true`) and ghost +movement is on, and can be turned off in particular with its own animation kind. It +can also be turned on by itself, without the other kinds: + +```json +{ + "animation": { + "enabled": { + "movement": true, + "workspace": true + }, + "ghost_movement": true, + "duration": { + "movement": 250, + "workspace": 350 + }, + "style": { + "movement": "EaseOutSine", + "workspace": "EaseOutExpo" + } + } +} +``` + +To turn it off and keep the rest: + +```json +{ + "animation": { + "enabled": true, + "ghost_movement": true + } +} +``` + +becomes + +```json +{ + "animation": { + "enabled": { "movement": true, "workspace": false }, + "ghost_movement": true + } +} +``` + +Without `ghost_movement` workspaces switch instantly, as they do without this setting. +The windows never leave the monitor they are on, so no part of them is shown on a +monitor next to it. + +The same can be set with `komorebic animation enable --animation-type workspace`, +`komorebic animation-duration 350 --animation-type workspace` and +`komorebic animation-style --style ease-out-expo --animation-type workspace`. \ No newline at end of file diff --git a/komorebi/src/animation/mod.rs b/komorebi/src/animation/mod.rs index f22ad148..86578fac 100644 --- a/komorebi/src/animation/mod.rs +++ b/komorebi/src/animation/mod.rs @@ -7,6 +7,8 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::time::Duration; use parking_lot::Mutex; @@ -82,5 +84,139 @@ lazy_static! { Arc::new(Mutex::new(HashMap::new())); } +/// Whether the windows of a workspace slide out of a monitor and those of the +/// workspace that is switched to slide in. Animations being enabled for all +/// kinds enables this one too, unless it is turned off for this kind in +/// particular. The windows are slid as ghosts, so ghosts are needed. +fn transitions_enabled_by( + ghost_movement: bool, + enabled_globally: bool, + enabled: &HashMap, +) -> bool { + ghost_movement + && enabled + .get(&AnimationPrefix::Workspace) + .copied() + .unwrap_or(enabled_globally) +} + +/// How long the animation of a kind takes, which is what it is set to or else +/// what animations take +fn duration_for( + prefix: AnimationPrefix, + per_animation: &HashMap, + global: u64, +) -> Duration { + Duration::from_millis(per_animation.get(&prefix).copied().unwrap_or(global)) +} + +/// How the animation of a kind eases, which is what it is set to or else what +/// animations do +fn style_for( + prefix: AnimationPrefix, + per_animation: &HashMap, + global: AnimationStyle, +) -> AnimationStyle { + per_animation.get(&prefix).copied().unwrap_or(global) +} + +/// Whether workspaces slide in and out of the monitor when they are switched +pub fn workspace_transitions_enabled() -> bool { + transitions_enabled_by( + GHOST_MOVEMENT_ENABLED.load(Ordering::Relaxed), + ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst), + &ANIMATION_ENABLED_PER_ANIMATION.lock(), + ) +} + +/// How long a workspace takes to slide in and out of a monitor +pub fn workspace_transition_duration() -> Duration { + duration_for( + AnimationPrefix::Workspace, + &ANIMATION_DURATION_PER_ANIMATION.lock(), + ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst), + ) +} + +/// How a workspace eases as it slides in and out of a monitor +pub fn workspace_transition_style() -> AnimationStyle { + style_for( + AnimationPrefix::Workspace, + &ANIMATION_STYLE_PER_ANIMATION.lock(), + *ANIMATION_STYLE_GLOBAL.lock(), + ) +} + pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS); pub static GHOST_MOVEMENT_ENABLED: AtomicBool = AtomicBool::new(DEFAULT_GHOST_MOVEMENT); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn workspaces_slide_when_animations_are_on_unless_turned_off_for_them() { + let none = HashMap::new(); + let workspace = HashMap::from([(AnimationPrefix::Workspace, true)]); + let movement = HashMap::from([(AnimationPrefix::Movement, true)]); + let off = HashMap::from([(AnimationPrefix::Workspace, false)]); + + // all animations on is this one on + assert!(transitions_enabled_by(true, true, &none)); + assert!(transitions_enabled_by(true, true, &movement)); + + // unless it is turned off in particular + assert!(!transitions_enabled_by(true, true, &off)); + + // or on in particular, without the rest + assert!(transitions_enabled_by(true, false, &workspace)); + assert!(!transitions_enabled_by(true, false, &none)); + assert!(!transitions_enabled_by(true, false, &movement)); + + // and it needs ghosts + assert!(!transitions_enabled_by(false, true, &none)); + assert!(!transitions_enabled_by(false, false, &workspace)); + } + + #[test] + fn the_duration_and_style_are_the_ones_of_the_kind_or_else_the_global_ones() { + let durations = HashMap::from([(AnimationPrefix::Workspace, 400)]); + + assert_eq!( + duration_for(AnimationPrefix::Workspace, &durations, 250), + Duration::from_millis(400) + ); + assert_eq!( + duration_for(AnimationPrefix::Movement, &durations, 250), + Duration::from_millis(250) + ); + assert_eq!( + duration_for(AnimationPrefix::Workspace, &HashMap::new(), 250), + Duration::from_millis(250) + ); + + let styles = HashMap::from([(AnimationPrefix::Workspace, AnimationStyle::EaseOutSine)]); + + assert_eq!( + style_for(AnimationPrefix::Workspace, &styles, AnimationStyle::Linear), + AnimationStyle::EaseOutSine + ); + assert_eq!( + style_for(AnimationPrefix::Movement, &styles, AnimationStyle::Linear), + AnimationStyle::Linear + ); + } + + #[test] + fn the_workspace_kind_is_named_in_the_configuration() { + assert_eq!(AnimationPrefix::Workspace.to_string(), "workspace"); + assert_eq!( + "workspace".parse::().unwrap(), + AnimationPrefix::Workspace + ); + assert_eq!( + serde_json::from_str::("\"workspace\"").unwrap(), + AnimationPrefix::Workspace + ); + } +} diff --git a/komorebi/src/animation/prefix.rs b/komorebi/src/animation/prefix.rs index 6e91d176..47132db7 100644 --- a/komorebi/src/animation/prefix.rs +++ b/komorebi/src/animation/prefix.rs @@ -14,6 +14,8 @@ use strum::EnumString; pub enum AnimationPrefix { Movement, Transparency, + /// A workspace sliding in and out of a monitor when it is switched to + Workspace, } pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String { diff --git a/schema.bar.json b/schema.bar.json index 2058c8a0..63bac232 100644 --- a/schema.bar.json +++ b/schema.bar.json @@ -187,10 +187,19 @@ ], "$defs": { "AnimationPrefix": { - "type": "string", - "enum": [ - "movement", - "transparency" + "oneOf": [ + { + "type": "string", + "enum": [ + "movement", + "transparency" + ] + }, + { + "description": "A workspace sliding in and out of a monitor when it is switched to", + "type": "string", + "const": "workspace" + } ] }, "AnimationStyle": { diff --git a/schema.json b/schema.json index de0d6070..3c9b20cb 100644 --- a/schema.json +++ b/schema.json @@ -3653,15 +3653,9 @@ { "description": "Animation configuration prefixed for a specific animation kind", "type": "object", - "properties": { - "movement": { - "type": "boolean" - }, - "transparency": { - "type": "boolean" - } - }, - "additionalProperties": false + "additionalProperties": { + "type": "boolean" + } }, { "description": "Animation configuration for all animation kinds", @@ -3675,19 +3669,11 @@ { "description": "Animation configuration prefixed for a specific animation kind", "type": "object", - "properties": { - "movement": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "transparency": { - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - "additionalProperties": false + "additionalProperties": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } }, { "description": "Animation configuration for all animation kinds", @@ -3703,15 +3689,9 @@ { "description": "Animation configuration prefixed for a specific animation kind", "type": "object", - "properties": { - "movement": { - "$ref": "#/$defs/AnimationStyle" - }, - "transparency": { - "$ref": "#/$defs/AnimationStyle" - } - }, - "additionalProperties": false + "additionalProperties": { + "$ref": "#/$defs/AnimationStyle" + } }, { "description": "Animation configuration for all animation kinds",