Skip to content

feat: slide workspace transitions - #600

Open
Trap101 wants to merge 1 commit into
acsandmann:mainfrom
Trap101:feat/workspace-transition-animation
Open

Trap101 wants to merge 1 commit into
acsandmann:mainfrom
Trap101:feat/workspace-transition-animation

Conversation

@Trap101

@Trap101 Trap101 commented Oct 20, 2026

Copy link
Copy Markdown

What

An optional Spaces-style slide when switching virtual workspaces. The outgoing workspace slides off one edge while the incoming one slides in from the other. The direction follows workspace order, and next/prev keep their natural direction across wrap-around. It's off by default and independent of animate.

workspace_transition = "none"            # "none" | "slide"
workspace_transition_duration = 0.25     # seconds
workspace_transition_axis = "horizontal" # "horizontal" (next enters from the right) | "vertical" (next enters from below)

We're open to changing this. If you'd prefer a different shape, for example a nested [settings.workspace_transition] table, folding it into the existing animation_* keys, or different names and values, we're happy to discuss and rework it.

How it works

  • Built on existing pieces. It uses the presenter and transition engine from perf: smoother animations #551, so there's no new thread or timer. apply_layout routes the switch arrange to a new AnimationManager::workspace_transition_layout when a direction is pending for that space.
  • Which windows move. Windows are classified by geometry, not workspace membership. A window is incoming if its target is visible and it's currently parked, and outgoing if the reverse. Everything else, including the dragged window, goes through the existing instant path.
  • Ending at the normal park. Outgoing windows slide offscreen, keeping 1px revealed, and settle at the usual hidden-corner park on the final frame. The transaction target is the normal park, so the rest of the system sees the normal parked state, and on multi-display setups windows always come to rest at their regular park.
  • Detecting parked windows. A new HiddenWindowPlacement::is_parked treats a window as parked if at most a sliver shows on either axis. macOS pulls parked windows back up by their title bar (a park requested at y=955 lands around y=924), so an exact match fails.
  • Fallbacks. The switch stays instant during a drag and in low-power mode.
  • Not cancelled by follow-up arranges. instant_layout_inner now only cancels presentations whose pending target actually changed, so focus and raise arranges right after a switch don't cut the slide short.

Rapid switching

Toggling workspaces quickly (⌥1 ⌥2 ⌥1 …) was expensive, so two changes affect the shared engine:

  1. A clock per window in ActiveAnimation.
    • Previously every Replace restarted all windows on a fresh full duration, including windows that weren't retargeted.
    • Now a retargeted window continues from its presented frame, takes a share of the duration matching the distance it has left, and eases out instead of starting from rest.
    • Windows the replacement doesn't touch finish on their original schedule.
    • Each window ends its lease as soon as it lands, and finished windows are no longer re-sent every tick.
    • This also affects animate = true layouts, since they share replace_with.
  2. No repair for parked windows.
    • A settled park frame is tagged FrameSource::Park.
    • The app actor skips its end-of-motion repair for it. macOS always clamps a park, so the repair could never succeed.
    • Over 10 slides this removed 15 of 15 pointless set_frame writes, one per outgoing window. Other AX traffic was unchanged.

Testing

  • cargo +nightly fmt --all --check, cargo check --locked, and the full test suite pass.
  • New tests:
    • config defaults, the template, and validation;
    • direction, including wrap-around and no-op switches;
    • incoming and outgoing frames on both axes;
    • a slide from an empty workspace;
    • instant re-arranges not cancelling an in-flight slide;
    • retarget duration scaling;
    • carried windows keeping their own clock;
    • per-window lease end;
    • a landed window restarting from the caller's frame rather than its park;
    • park frames being tagged Park.
  • Manual: used day to day on a single 1470×956 display with the traditional layout, including rapid toggling. I haven't tested a multi-monitor setup yet.

🤖 Generated with Claude Code

https://claude.ai/code/session_018JyfUbLLYEiyoxyQ2vjRsy

Add an optional Spaces-style slide when switching virtual workspaces:
the outgoing workspace slides off one edge while the incoming one
slides in from the other. Direction follows workspace order, and
next/prev keep their natural direction across wrap-around.

New settings, independent of `animate` (off by default):
- workspace_transition = "none" | "slide"
- workspace_transition_duration (seconds, default 0.25)
- workspace_transition_axis = "horizontal" | "vertical"

Outgoing windows slide offscreen, then settle at their usual hidden
park on the final frame, so the rest of the system sees the normal
parked state. Windows are classified by geometry (`is_parked`), since
macOS pulls parked windows up by their title bar. The slide falls back
to the instant switch during drags and in low-power mode.

Rapid switching stays cheap:
- Each transition window keeps its own clock. A window retargeted
  mid-motion continues from its presented frame for a share of the
  duration matching the distance it has left, easing out instead of
  restarting from rest; windows a replacement does not handle finish
  on their original schedule, and each window ends its lease as soon
  as it lands.
- A settled park frame is tagged `FrameSource::Park`, and the app
  actor skips the end-of-motion repair for it: macOS always clamps a
  park, so the rewrite could never succeed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018JyfUbLLYEiyoxyQ2vjRsy
Copilot AI balanced review requested due to automatic review settings October 20, 2026 13:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants