Skip to content

Conversation

@sagezaugg
Copy link

@sagezaugg sagezaugg commented Oct 17, 2025

Add Window Animation System

Preamble

Hey all!! Very excited to introduce my first PR here. I am a huge fan of GlazeWM and use it daily. However, I wanted to add a bit of visual magic to window management. So, I cracked open my IDE and implemented an Animation Engine with many different configuration possibilities! I am new to Rust, so critiques are definitely welcome.

Additionally, I tried to see if this feature was purposefully omitted or not, but couldn't find anything. If this is against GlazeWM's philosophy, more than happy to close.

Anyways, thanks for reviewing!

📝 Description

This PR introduces a comprehensive animation system for GlazeWM, bringing smooth, polished visual feedback to window management operations. The system provides fluid transitions for window movements, resizing, opening, and closing with extensive configuration options and intelligent adaptive behavior.

🎥 Demo

GlazeWM.Animations2.mp4

✨ Features

Core Capabilities

  • Smooth Window Movement - Fluid transitions when windows move or resize
  • Opening Animations - Windows fade in and scale up when opened
  • Closing Animations - Windows fade out and scale down when closed
  • Configurable Profiles - Three presets (Fast, Balanced, Smooth) plus custom configuration
  • Adaptive Timing - Automatically adjusts animation speed based on operation type:
    • Fast (80ms) for window expansions to minimize visual artifacts
    • Medium (100ms) for window shrinking
    • Normal (150ms) for pure movement
    • Scales duration based on magnitude of change
  • Refresh Rate Aware - Automatically uses monitor's actual refresh rate (60/144/240 Hz)
  • Cancel-and-Replace - Smooth transitions when animation targets change mid-flight
  • Effect Composition - Combine multiple effects (fade, scale, slide) simultaneously

Configuration

  • Global Enable/Disable - Toggle all animations on or off
  • Per-Type Configuration - Separate settings for movement, open, and close animations
  • Customizable Easing - Multiple easing functions (linear, ease-in, ease-out, cubic variants)
  • Adjustable Thresholds - Configure minimum movement distance to trigger animations
  • Runtime Toggle - Enable/disable animations without restarting via keybinding/IPC

IPC Integration

  • Query Command - query animations-enabled to check animation state
  • Toggle Command - wm-toggle-animations to enable/disable
  • Subscribe Event - animations-changed event for monitoring animation state

🏗️ Architecture

Component Structure

┌─────────────────────────────────────────────────────────┐
│ animation_engine.rs                                     │
│ - Pure mathematical functions                           │
│ - Easing functions (7 variants)                         │
│ - Interpolation (rects, opacity)                        │
│ - Scaling and transformation utilities                  │
└─────────────────────────────────────────────────────────┘
                          ▲
                          │
┌─────────────────────────────────────────────────────────┐
│ animation_state.rs                                      │
│ - WindowAnimationState: Per-window animation tracking  │
│ - AnimationManager: Central registry (HashMap-based)   │
│ - Animation types: Movement, Open, Close               │
└─────────────────────────────────────────────────────────┘
                          ▲
                          │
┌─────────────────────────────────────────────────────────┐
│ platform_sync.rs                                        │
│ - Determines when animations should start              │
│ - Tracks window target positions                       │
│ - Applies animated rects to native windows             │
│ - Handles cancel-and-replace logic                     │
└─────────────────────────────────────────────────────────┐
                          ▲
                          │
┌─────────────────────────────────────────────────────────┐
│ wm.rs + main.rs                                         │
│ - Animation timer management (tokio interval)           │
│ - Thread-safe timer coordination (Arc<AtomicBool>)     │
│ - Event loop integration                                │
└─────────────────────────────────────────────────────────┘

Key Design Decisions

  1. Stateless Animation Engine - Pure functions for easy testing and reusability
  2. Target Position Tracking - Prevents animation restart loops by comparing targets, not current positions
  3. Thread-Safe Timer - Uses Arc<AtomicBool> to prevent multiple concurrent timers
  4. Adaptive Behavior - Dynamically adjusts timing based on operation type and magnitude
  5. Proper Cleanup - Removes animation state when windows are destroyed to prevent memory leaks

⚙️ Configuration Options

Basic Configuration

animations:
  enabled: true
  profile: "balanced" # fast, balanced, smooth, or custom

Animation Profiles

Fast Profile

  • Movement: 100ms, ease-in-out
  • Open: 120ms fade + scale + slide
  • Close: 100ms fade + scale
  • Best for: Snappy, responsive feel

Balanced Profile (Default)

  • Movement: 150ms, ease-in-out
  • Open: 200ms fade + scale + slide
  • Close: 150ms fade + scale
  • Best for: Good balance between speed and smoothness

Smooth Profile

  • Movement: 200ms, ease-in-out-cubic
  • Open: 250ms fade + scale + slide
  • Close: 200ms fade + scale
  • Best for: Most visually pleasing, cinematic feel

Custom Configuration

animations:
  enabled: true
  profile: "custom"
  movement_threshold_px: 10 # Minimum distance to trigger animation

  window_movement:
    enabled: true
    duration_ms: 150
    easing:
      "ease_in_out" # linear, ease_in, ease_out, ease_in_out,
      # ease_in_cubic, ease_out_cubic, ease_in_out_cubic

  window_open:
    enabled: true
    duration_ms: 200
    easing: "ease_out"
    fade: true # Fade in from transparent
    slide: true # Slide in from slightly offset position
    scale: true # Scale up from 90% size

  window_close:
    enabled: true
    duration_ms: 150
    easing: "ease_in"
    fade: true # Fade out to transparent
    slide: false # No slide effect on close
    scale: true # Scale down to 90% size

Keybinding Example

binding_modes:
  - name: "normal"
    keybindings:
      - commands: ["wm-toggle-animations"]
        bindings: ["Alt+Shift+N"]

IPC Commands

# Query animation state
glazewm-cli query animations-enabled

# Toggle animations
glazewm-cli invoke wm-toggle-animations

# Subscribe to animation changes
glazewm-cli subscribe -e animations-changed

🎯 Technical Highlights

Performance Optimizations

  1. Efficient Timer Management

    • Single tokio::interval per animation loop (not one per frame)
    • MissedTickBehavior::Skip prevents tick accumulation
    • Atomic flag prevents multiple concurrent timers
  2. Smart Redraw Queuing

    • Only queues windows with active animations
    • Filters out completed animations before processing
    • No redundant redraw operations
  3. Adaptive Frame Rate

    • Automatically detects monitor refresh rate
    • Adjusts frame timing accordingly (60/144/240 Hz)
    • Defaults to 60 Hz if detection fails
  4. Context-Aware Timing

    • Faster animations for window expansions (80ms)
    • Medium speed for shrinking (100ms)
    • Normal speed for movement (150ms)
    • Scales based on change magnitude

Memory Management

  • Automatic Cleanup - Animation state removed when windows are destroyed
  • No Leaks - Target positions cleaned up with animations
  • Efficient Storage - ~200 bytes per animated window

Thread Safety

  • Atomic Operations - Uses Arc<AtomicBool> for timer coordination
  • Channel-Based - Animation ticks sent via mpsc::unbounded_channel
  • No Race Conditions - Proper synchronization throughout

🧪 Testing

Manual Testing Performed

  • ✅ Window movement animations smooth and responsive
  • ✅ Window open/close with fade and scale effects
  • ✅ Rapid workspace switching doesn't stutter
  • ✅ Cancel-and-replace works correctly
  • ✅ Window destruction during animation (no crashes)
  • ✅ Toggle animations via keybinding works
  • ✅ Multiple concurrent animations don't interfere

🔧 Implementation Details

Key Files Added

  • packages/wm/src/animation_engine.rs - Core math and interpolation functions
  • packages/wm/src/animation_state.rs - Animation state management
  • packages/wm/src/commands/general/toggle_animations.rs - Toggle command implementation

Key Files Modified

  • packages/wm/src/wm.rs - Timer management and animation updates
  • packages/wm/src/wm_state.rs - Animation manager and target position tracking
  • packages/wm/src/commands/general/platform_sync.rs - Animation integration and logic
  • packages/wm/src/commands/window/unmanage_window.rs - Animation cleanup
  • packages/wm-common/src/parsed_config.rs - Animation configuration structs
  • packages/wm-common/src/app_command.rs - IPC commands and queries
  • packages/wm-common/src/wm_event.rs - Animation events
  • packages/wm-platform/src/native_monitor.rs - Refresh rate detection

📋 Breaking Changes

None - This is a purely additive feature with sensible defaults. Existing configurations will work without modification.

🔄 Migration Guide

No migration needed! Animations are disabled by default. To enable:

animations:
  enabled: true
  profile: "balanced"

🐛 Known Issues

  • Expanding windows causes black bars and/or the desktop to show briefly. This is a limitation of how fast windows can redraw.

🎯 For Reviewers

Focus Areas

  1. Animation Logic (platform_sync.rs:268-430) - Target position tracking prevents restart loops
  2. Timer Management (wm.rs:189-223) - Thread-safe interval with atomic coordination
  3. Cleanup (unmanage_window.rs:27-29) - Proper resource cleanup
  4. Configuration (parsed_config.rs:393-610) - Profile system and easing functions

What to Test

  1. Open and close windows - should fade and scale smoothly
  2. Move and resize windows - should animate smoothly
  3. Rapid workspace switches - should not stutter
  4. Toggle animations with keybinding - should work instantly
  5. Destroy window during animation - should not crash

Expected Behavior

  • Default animations are subtle and professional
  • No performance impact when animations disabled
  • Smooth 60+ FPS on modern hardware
  • Adaptive timing prevents visual artifacts
  • Cancel-and-replace creates fluid transitions

Ready for review!

…ations

Adds a new animation engine with functions for calculating animation progress, applying easing functions, and interpolating between rectangles and opacity values. Introduces `WindowAnimationState` to manage individual window animations, including movement, opening, and closing animations. Integrates animation management into the `WindowManager` and updates the configuration to support animation settings.
Introduces a new `window_target_positions` HashMap in `WmState` to track target positions for each window, preventing animation restart loops. Updates the `redraw_containers` function to utilize previous target positions for smoother animations and cleans up tracking data when a window is unmanaged.
Enhances the window animation system by introducing adaptive timing based on the size change of the target window. The animation duration is now adjusted for expansions and shrinkages, with specific easing functions applied to improve visual fluidity. This update aims to create smoother transitions and reduce visual artifacts during window movements.
…ance during workspace switches

Introduces a new `skip_animations` field in `PendingSync` to control animation behavior during sync operations. Updates the `redraw_containers` function to respect this setting, allowing for smoother transitions when switching workspaces. This enhancement aims to optimize performance by reducing unnecessary animations.
…uration

Adds a new command to toggle animations on and off, updating the configuration and state management accordingly. Introduces an animation profile system to control timing values for window animations, allowing users to choose between different profiles (fast, balanced, smooth, custom). Updates the IPC server to respond with the current animation state and modifies the redraw logic to respect the new configuration settings.
When multiple movement commands were issued rapidly for the same window,
animations would queue up and slow down significantly. Each new animation
was starting from the previous target position rather than the current
animated position, causing a buildup of slow back-and-forth movements.

This fix implements a "cancel and replace" approach:
- When a new movement command arrives during an active animation, the
  current animation is immediately cancelled
- A new animation starts from the window's current animated position
  (not the previous target) to the new target position
- Threshold for detecting target changes kept at 10 pixels to avoid
  excessive restarts from minor position adjustments

The window now feels responsive and snappy when toggling layouts or
resizing rapidly, with no animation buildup or slowdown.

Changes:
- Store existing animation state before checking if new animation needed
- Use existing_animation.current_rect() as start position when cancelling
- Calculate adaptive timing based on current position, not previous target
Addresses inconsistent animation timing when rapidly toggling window layouts.
Previously, interrupted animations would calculate duration based on the
current animated position (which varies frame-by-frame), causing wildly
inconsistent speeds - sometimes fast, sometimes slow.
Now uses a fixed 100ms duration for all cancel-and-replace animations
(when interrupting an existing animation), while preserving adaptive
timing for new animations starting from stable positions. This ensures:
- Rapid toggling feels smooth and consistent
- No more random slowdowns or speed variations
- Predictable animation behavior regardless of when toggle occurs
- Adaptive timing benefits preserved for initial movements
The fix splits animation config logic in platform_sync.rs to detect
cancel-and-replace scenarios and apply fixed timing only when needed.
Implements Issues glzr-io#1, glzr-io#2, and glzr-io#4 from animation system review to address
performance inefficiencies and add user configurability. Also reduces
excessive logging during normal operation.

Changes:

1. Fix Timer Management (Issue glzr-io#1)
   - Add animation_timer_running atomic flag to WindowManager
   - Replace task-per-tick spawning with proper tokio::time::interval
   - Prevents multiple concurrent timer tasks from running
   - Timer exits cleanly when channel closes
   - Significantly reduces task overhead during animations

2. Fix Redundant Redraw Queuing (Issue glzr-io#2)
   - Filter completed animations from active window list
   - Ensures windows are only queued once for final redraw
   - Removes duplicate redraw operations that could impact performance
   - Simplifies animation completion logic

3. Add Configurable Movement Threshold (Issue glzr-io#4)
   - Add movement_threshold_px field to AnimationsConfig (default: 10)
   - Replace hardcoded pixel thresholds with configurable value
   - Allows users to adjust sensitivity based on DPI and preference
   - Document in sample config with guidance for high-DPI displays

4. Reduce Excessive Logging
   - Change window position update log from info! to debug! level
   - Prevents log spam during animation frames
   - Keeps information available when debug logging is enabled

Issue glzr-io#3 (window cleanup) was already properly implemented in
unmanage_window.rs, so no changes were needed.

The animation system is now more efficient, prevents unnecessary work,
and provides better configurability for end users.
@github-project-automation github-project-automation bot moved this to 📬 Needs triage in glazewm Oct 17, 2025
@sagezaugg sagezaugg changed the title refactor: improve animation system efficiency and configurability feat: add animations to window movements Oct 17, 2025
@sagezaugg sagezaugg changed the title feat: add animations to window movements feat: add window animation system Oct 17, 2025
Copy link
Contributor

@Video-Nomad Video-Nomad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really great! However, there are some issues I've found after the initial testing:

  • Full screen video in a browser doesn't work properly when another app tiled in a workspace with the browser. Same with toggle-fullscreen command in GlazeWM if maximized: false.

  • When moving floating windows using hotkeys it animates as well and sort of bounces weirdly. Also has a really hard time getting to the edge of the screen for some reason 😅

  • Floating windows reset their size when trying to move them with hotkeys. For example a window was resized and then moved using GlazeWM hotkeys and it will snap back to the size it was originally before resizing.

  • On multi-monitor setup using move-workspace --direction left/right will move the windows to another monitor. If that target monitor already had a workspace with windows, they will become semi-transparent when you then switch back to their workspace. This only happens with animations enabled. Same can happen with another apps in workspaces in the original monitor. So move-workspace --direction should be tested more.

Amazing job so far!

@sagezaugg
Copy link
Author

Looks really great! However, there are some issues I've found after the initial testing:

  • Full screen video in a browser doesn't work properly when another app tiled in a workspace with the browser. Same with toggle-fullscreen command in GlazeWM if maximized: false.

  • When moving floating windows using hotkeys it animates as well and sort of bounces weirdly. Also has a really hard time getting to the edge of the screen for some reason 😅

  • Floating windows reset their size when trying to move them with hotkeys. For example a window was resized and then moved using GlazeWM hotkeys and it will snap back to the size it was originally before resizing.

  • On multi-monitor setup using move-workspace --direction left/right will move the windows to another monitor. If that target monitor already had a workspace with windows, they will become semi-transparent when you then switch back to their workspace. This only happens with animations enabled. Same can happen with another apps in workspaces in the original monitor. So move-workspace --direction should be tested more.

Amazing job so far!

Thank you so much for taking a look! I will be on vacation for a few days, but will take a look at those issues mentioned!

Question for you as well, I didn't see any testing unless I missed it (again, new to Rust). Any harm in adding some basic unit tests? The math on the animation engine is pretty precise 😅

@Video-Nomad
Copy link
Contributor

@sagezaugg No idea about the tests to be honest, it's better to ask core maintainers about that. I'm just a GlazeWM user and very new to rust too.

Just to add one additional thing:

  • When moving windows manually (just dragging the titlebar or via AltSnap) the window sort of jumps back a bit and then snaps to place. It's the same with all easing modes.
pZrDHXEt7r.mp4

@lars-berger
Copy link
Member

Whoa, that's insane. Would not have guessed this was written by someone pretty new to Rust. Thanks for the PR 🙏

I've got a partial list of feedback noted so far but will have to continue with the review tomorrow.

Any harm in adding some basic unit tests?

Nope, not at all! Actually just started adding tests to some wm-platform stuff with the macOS port. There isn't a set code style or setup for testing, so what you've got there currently all looks perfectly fine 👍


Also ty @Video-Nomad for the testing and PR feedback!

/// Outputs whether the window manager is paused.
Paused,
/// Outputs whether animations are enabled.
AnimationsEnabled,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reckon we should leave the IPC event, query, and command as a separate change. especially since I think a lot of people would confuse it with whether system animations are enabled:

image

there's also use cases for getting/setting the specific animation config rather than just enabled state. could potentially do something what this PR did but i'd rather that be tackled in a separate PR

} else {
60
})
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove the 60Hz default value in the case that it's invalid. instead return an err, and default to 60Hz where it's consumed


/// Interpolates between two rectangles.
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
pub fn interpolate_rect(start: &Rect, end: &Rect, progress: f32) -> Rect {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would recommend moving interpolate_rect and the other fns below as methods on Rect/OpacityValue

e.g.

impl Rect {
  /// Linearly interpolates between this rect and another rect.
  /// `t` should be in the range [0.0, 1.0] where:
  /// - 0.0 returns `self`
  /// - 1.0 returns `other`
  /// - 0.5 returns the midpoint
  #[must_use]
  pub fn interpolate(&self, other: &Rect, t: f32) -> Self {
    let t = t.clamp(0.0, 1.0);
    
    Self::from_ltrb(
      self.left + ((other.left - self.left) as f32 * t) as i32,
      self.top + ((other.top - self.top) as f32 * t) as i32,
      self.right + ((other.right - self.right) as f32 * t) as i32,
      self.bottom + ((other.bottom - self.bottom) as f32 * t) as i32,
    )
  }
}

}

/// Updates all active animations and redraws windows that are animating.
pub fn update_animations(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the business logic for animations is spread out a bit too much atm (wm.rs, platform_sync.rs, and animation_state.rs) - wdyt of moving these functions (and possibly animation_tick_rx, animation_tick_tx, animation_timer_running fields) into AnimationManager?

let existing_animation_clone = existing_animation.clone();

// Decide whether to start a new animation
let should_start_new_animation = if is_opening && config.value.animations.effective_window_open().enabled {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to in wm.rs, would suggest moving out parts of the animation logic here to AnimationManager - e.g. could have a should_start_new_animation getter

# - 'balanced': Default, smooth but responsive (150-200ms)
# - 'smooth': Slower, more fluid animations (200-250ms)
# - 'custom': Use the detailed settings below
profile: "balanced"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the "animation profiles" add more complexity than they're worth - would you be on board with removing this? since we're already specifying explicit duration and easings in the sample config anyways, animation profiles seem unnecessary

animations:
# Whether to enable window animations (opt-in feature).
# Default: false
enabled: false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a bit overkill with both the top-level enabled flag as well as the individual enabled flags under window_open etc.

we can remove this top-level one and let the individual flags determine whether an animation is enabled. plus it'd be more inline with how the window_effects config is set up

# Helps prevent animations from starting on very small position changes.
# Increase this value on high-DPI displays to reduce sensitivity.
# Default: 10
movement_threshold_px: 10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could be under the window_movement config


# Animation settings for window movement (tiling, resizing, etc).
# Only used when profile is set to 'custom'.
window_movement:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we rename this to window_move?

# Whether to slide windows into position.
slide: true
# Whether to scale windows from a smaller size.
scale: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regarding:

    # Whether to fade in windows.
    fade: true
    # Whether to slide windows into position.
    slide: true
    # Whether to scale windows from a smaller size.
    scale: true

dm'd you on discord about some thoughts on this. it'd be nice if there was a flexible enough way to represent combinations of opacity, positional, and scale transforms similar to how it's done in CSS. but after a lot of experimenting with syntax, i unfortunately don't think there's a user-friendly way to allow for this.

i'm thinking the best approach would be similar to hyprland's config where they have preset animations with customizable easing curves, something like:

  window_move:
    enabled: true
    animation_type: "slide"
    duration_ms: 200
    easing: "ease_out"

lmk what you think 👍

(also should every animation type be allowed for window_move + window_open? might make sense to have an enum of window move animations and separate window open animations. will leave that to you)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 📬 Needs triage

Development

Successfully merging this pull request may close these issues.

3 participants