Motion system to make Zed feel fluid and intentional - #48295
Conversation
Add smooth entrance animation to popover menus with opacity fade-in (0.4 to 1.0) and quadratic height unfold effect. Animation uses ease-out-quint easing over 150ms (AnimationDuration::Fast) for polished UI feel. Uses centralized AnimationDuration constant and named constants for animation parameters to maintain consistency with existing codebase patterns (matching animation.rs fade-in behavior).
Replace the previous height-based unfold animation with a simpler opacity fade and vertical slide animation to better match the smooth, polished feel of modern UI patterns like Linear's dropdown menus. Previous approach used progressive height revealing (max-height with quadratic easing) which created a jarring clipping effect. New approach uses full opacity fade (0→1) combined with subtle 6px upward slide for natural, fluid menu entrance. Changes: - Remove height-unfold logic (max-height, overflow-hidden) - Remove partial opacity start (0.4 → 1.0) - Add clean fade-in (0 → 1) with -6px vertical slide - Use .into() for AnimationDuration consistency
Dock panels (agent, notification, terminal, debug) now smoothly slide into view when toggled. Right dock panels slide in from the right, bottom dock panels slide in from the bottom, and left dock panels slide in from the left. Animation includes 16px slide with fade-in effect over 150ms using ease_out_quint easing. Implementation uses an open_generation counter to ensure animations replay on each toggle, preventing stale animation state.
Changed dock panel animations from opacity fade + position offset to physical width/height transitions for a true slide-in/slide-out effect. Panels now expand from 0 to full size when opening and collapse from full to 0 when closing. Added close animation state tracking (is_closing, close_generation, _close_task) to keep panels visible during the 150ms close animation. Opening a panel while closing cancels the close animation task. This provides the expected drawer behavior: right/left docks slide horizontally, bottom dock slides vertically, all matching their edge position.
Changed easing from ease_out_quint to ease_out_cubic to eliminate visual fade artifacts caused by the aggressive deceleration curve. Quint reaches 83% progress at 30% duration, making the tail appear to fade in. Cubic distributes motion more evenly for a clean slide. Asymmetric timing: 150ms open, 100ms close. Faster exit feels snappier and more responsive when dismissing panels.
Use direction-specific animation ID prefixes ("dock-open" vs "dock-close")
to prevent GPUI from treating close animations as already-completed open
animations. Previously both open_generation and close_generation would
reach the same value after one cycle, causing identical animation IDs
and skipped close transitions.
Replace dual-counter approach (open_generation + close_generation with direction-specific prefixes) with a single monotonically increasing animation_generation counter. This achieves the same uniqueness guarantee more simply: every state transition increments one counter, naturally producing unique animation IDs without branching logic or prefix disambiguation. The previous fix prevented animation skipping by using different prefixes for open vs close. This refactor solves it at the root by ensuring the generation value itself is always unique per transition.
Implements smooth fade and slide animations when modals open and close. Modals now animate in over 150ms with an ease-out-cubic curve (opacity 0->1, slide from -6px) and animate out over 100ms with the reverse. The animation state machine prevents flickering during rapid open/close/reopen sequences by tracking animation generations and managing a closing modal state. Focus is restored immediately on dismiss so users can type right away, while the visual animation completes asynchronously. This change affects all 51 ModalView implementors since it modifies the shared ModalLayer render path. Key modals to verify: - Command Palette (Cmd+Shift+P) - File Finder (Cmd+P) - Outline (Cmd+Shift+O) - Theme Selector (Cmd+K Cmd+T) - Tab Switcher (Ctrl+Tab) - Go to Line (Ctrl+G) - Branch Picker, Git Commit Modal, Recent Projects - Onboarding modals (agent, debugger, edit prediction) Modals that override render_bare() (e.g. DisconnectedOverlay) bypass the animation and render directly, so they should be unaffected.
Adds a tri-state `reduce_motion` setting that controls UI animations: - "system" (default): follows macOS accessibility preference - "on": always skip animations - "off": always animate Implementation details: - Integrates NSWorkspace.accessibilityDisplayShouldReduceMotion on macOS - Exposes Platform.should_reduce_motion() through App context - Gates animations in popovers, dock panels, and modal dialogs - Adds setting to Appearance page in Settings UI - Always registers animations but skips visual effects when reduced, preventing false re-animation on setting changes
Adds a convenience function `should_reduce_motion(cx)` that replaces the verbose `ReduceMotionSetting::get_global(cx).should_reduce_motion(cx)` pattern at all call sites. Additional cleanups: - Removed redundant if/else branch in dock close logic - Renamed abbreviated variable `close_gen` to `close_generation` - Inlined single-use variable in modal layer - Fixed alphabetical ordering of pub use exports
Extracts duration constants and eliminates redundant logic: - Adds MODAL_OPEN_DURATION and MODAL_CLOSE_DURATION constants to centralize the timing values used in both the close timer and animation duration, preventing drift between the two - Simplifies DismissDecision::Dismiss match to eliminate tautology where `!should_dismiss` was checked twice - Merges two active_modal borrows in render() into one, deriving is_closing from which branch was taken rather than computing it separately before the early return
Utility panes (used by Agent V2 for thread detail views) now animate when appearing and disappearing, matching the existing dock panel animation pattern. Animations: - Slide in from left/right (150ms, ease_out_cubic) on open - Slide out to width 0 (100ms, ease_out_cubic) on close - Respect macOS accessibility reduce-motion setting Implementation follows the dock animation pattern exactly: - Animation generation counter prevents state conflicts - Async close task waits for animation before removing pane - New utility_pane_frame() helper simplifies all 10 render call sites - Resize handle hidden during close animation for polish
Centralized Left/Right slot dispatch into UtilityPaneState::slot() and slot_mut() accessor methods, eliminating 7 duplicated match blocks throughout the file. Additional improvements: - Flattened clear_utility_pane() with early-return for reduce_motion - Simplified resize handle creation by extracting shared styling - Renamed single-letter variable to full word (e → event) - Combined generation check in async close task into single expression No behavior changes - same animation parameters and logic.
The selection-overlay-animation.patch was accidentally included in commit fdf706f. This patch file was a development artifact and should not have been committed to the repository.
Implements a smooth sliding animation for the selection indicator in uniform_list pickers (e.g., command palette). When navigating with arrow keys, the selection now smoothly slides between items instead of jumping abruptly. Implementation details: - Created SelectionIndicator decoration using UniformListDecoration trait to render an animated overlay behind list items - Added tracking for previous selection index to enable smooth transitions between arbitrary positions - Clamped animation distance to 3 items max to prevent jarring long- distance animations when jumping via search results changes - Used 80ms cubic ease-out timing for natural, responsive feel - Suppressed per-item selection backgrounds in uniform lists to avoid dual-highlight visual conflict - Added reduce-motion support for accessibility - Fixed paint order in uniform_list so decorations render behind items The animation uses a wrapper div positioning approach to avoid layout issues with absolute positioning in decoration contexts.
Improved the selection indicator animation to feel smoother and more polished: - Changed easing from cubic ease-out to ease-in-out for visible motion throughout the transition instead of frontloaded movement - Increased duration from 80ms to 150ms to give the easing curve room to express its character - Fixed overlay width to match ListItem hover background by using horizontal insets (Base04 spacing) instead of full-width - Skip animation when previous selection is outside visible range to prevent jarring motion during scroll jumps The ease-in-out curve starts slowly, accelerates through the middle, and decelerates at the end, creating a natural gliding motion similar to selection animations in polished applications like VS Code and macOS system UI.
Fixed jarring motion when navigating past visible edges of the picker list in both directions. Previously, animation would play while scrolling at the top and bottom edges, causing a jittery up-down or down-up visual effect. Now tracks the visible item range and excludes partially visible edge items from the "safe to animate" zone. When the new selection is outside the fully-visible range, animation is skipped and the indicator stays stationary while list content scrolls underneath. This creates smooth scrolling behavior at both boundaries: the selection indicator remains fixed at the edge position while the list content slides beneath it, matching natural list navigation UX.
Extract helper methods and eliminate duplication in picker selection indicator rendering: - Add SelectionIndicator::animated_origin() to encapsulate animation eligibility checks and remove unwrap() call that could panic - Deduplicate indicator element styling by extracting shared base div - Use idiomatic match instead of if/else for Option handling - Extract is_fully_visible() method to clarify visibility logic and eliminate variable shadowing - Rename use_overlay to has_selection_overlay for clearer intent - Add explanatory comments for non-obvious behavior These changes improve code clarity and follow project guidelines (no unwrap, full words for variable names, explaining "why" in comments) while preserving all existing functionality.
Deduplicate cubic easing function across dock, modal, and utility pane animations by adding ease_out_cubic to gpui's easing module. Skip animation infrastructure entirely when reduce_motion is enabled, avoiding unnecessary element wrapping and frame scheduling. Previously animations ran but returned unchanged elements. Fix unwrap() in reduce_motion_setting to use unwrap_or_default(), preventing potential panic if the setting is missing.
Add exhaustive unit tests for all animation state machines and pure logic functions introduced in the animations branch. Tests focus on behavior correctness, not visual rendering. Test coverage: - reduce_motion_setting: 6 tests for On/Off/System behavior, defaults, settings integration, and global function - picker: 14 tests for SelectionIndicator::animated_origin (reduce motion, visibility, clamping, boundaries) and is_fully_visible logic (safe range, scroll boundaries, list edges, empty cases) - dock: 10 tests for open/close state machine (set_open, reduce_motion skip, animation completion/cancellation, double operations, panel activation, generation tracking) - modal_layer: 11 tests for toggle/hide/show operations, animation lifecycle, reduce_motion skip, type safety, and state queries - utility_pane: 3 tests for slot mapping and dock position conversion All 44 tests pass. Uses GPUI test framework with proper async executor clock advancement for timer-based animations.
Refactor test code added in the previous commit to improve clarity and maintainability while preserving all functionality: - Remove organizational and descriptive comments per CLAUDE.md (comments should explain "why", not "what") - Extract repeated test setup patterns into helper functions (init_test, set_reduce_motion, add_panel_to_dock) - Replace duplicate TestModal structs with define_test_modal! macro to eliminate ~40 lines of boilerplate - Rename abbreviated variables to full words (ind→indicator, gen→generation, cx→_cx when unused) - Inline single-use intermediate variables into assertions - Use std::ptr::eq() for pointer identity checks instead of raw casts - Add doc comment explaining non-obvious test helper design All 44 tests still pass.
Reduce test count from 44 to 22 by merging redundant test cases while maintaining complete coverage. Improves maintainability without sacrificing test quality. Consolidations: - picker: Merge 3 "returns None" cases into 1, merge 4 "computes position" cases into 1, merge visibility boundary tests (7→2) - settings: Merge On/Off/System/Default variants into single test - dock: Merge open+close lifecycle, merge animation completion with visible_entry check, merge double-open and double-close noop tests - modal_layer: Merge toggle open+close, fold hide_empty into hide_animation, drop redundant type-query tests - utility_pane: Merge left+right slot tests into single test Additional improvements: - Extract dock test boilerplate into add_dock_with_panel helper - Add new_modal_layer helper to reduce repetition - Remove comments that restate code (keep only "why" comments) All 22 tests pass with same coverage as original 44 tests.
Fixes three critical issues in animation state management: 1. Silent error handling: Changed modal_layer.rs spawn_in pattern from .ok() (which discarded errors) to upgrade() pattern that properly handles entity lifecycle 2. Integer overflow safety: Replaced += 1 with wrapping_add(1) for animation_generation counters across modal_layer, dock, and utility_pane to prevent panic on usize overflow 3. Magic number elimination: Extracted animation duration constants (OPEN_DURATION: 150ms, CLOSE_DURATION: 100ms) and MODAL_SLIDE_OFFSET (-6.0px) for maintainability Also removed summary doc comments per CLAUDE.md guidelines (comments should explain "why", not "what") and added explanatory comments for non-obvious animation_generation increment pattern that prevents stale close tasks from interfering with new animation cycles.
Adds comprehensive test coverage for previously untested code: - ease_out_cubic easing function: boundary value tests (0.0, 1.0), monotonicity verification across 100 steps, and midpoint behavior validation to ensure proper ease-out deceleration curve - UtilityPaneState slot accessors: pointer identity tests verifying slot() and slot_mut() return references to correct internal fields, default state validation, and slot independence verification Note: Animation lifecycle tests (is_closing, animation_generation, _close_task transitions) require full Workspace fixture and are better suited for integration tests following dock.rs patterns.
Removes summary doc comments that described "what" the code does (which is clear from reading the code itself) from picker.rs. Adds explanatory comment in uniform_list.rs clarifying why decorations paint before items: backgrounds like selection highlights must render behind item content for proper visual layering. Per CLAUDE.md: comments should only explain "why", not "what".
Fixed documentation for reduce_motion setting to show correct values "on"/"off" instead of incorrect "true"/"false". The ReduceMotion enum uses "on", "off", and "system" variants, not boolean values. Documentation now matches actual implementation.
When a user re-expands a utility pane during the 100ms close animation window, the pending close task would fire and clear the slot because toggle_utility_pane didn't cancel the close animation or bump the generation counter. Now when expanding, we reset is_closing, increment animation_generation to invalidate the stale close task, and clear _close_task.
|
Hey @danilo-leal I'm applying for the Product Designer role and wanted to show rather than tell. I applied about a month ago as well, but I've been building a lot since then and felt it was worth re-applying. Would love your thoughts on the motion approach if you get a chance. |
|
Heya @srbsingh3, thanks for taking the time to work on this! This looks good and well done; appreciate the tests and the respect for the reduced motion configuration; very important for accessibility. However, even though I find this super sweet, and might even want to explore having some of these animation capabilities in Zed's tool belt for specific moments/areas, I don't think we should merge this as is. Zed's main feature is performance and speed, and I think overtime, these animations end up significantly hurting the perception of speed—it's a power-use product that should feel absurdly snappy, and having main workspace elements frequently animate compounds to a feeling of slight slowness, even if the animations are fast and have good curves. I'll close this one for now, but I'd be open to smaller PRs, like you mentioned in the description, potentially adding one primitive at a time, so we can evaluate their use and implementation and see whether they make sense bringing in! Thanks again & good luck on the process! |
Copied from zed-industries#48295, just trimmed down
* Remove smooth open/close animation from all docks Reverts the custom dock opening/closing animations that were added from PR zed-industries#48295 and the threads sidebar animation: - bottom/left/right panel docks (workspace render_dock + Dock::set_open) - threads sidebar dock (MultiWorkspace) Docks now open and close instantly again. Also drops the now-unused ease_out_cubic easing helper and its tests from gpui, and removes the corresponding 'Motion system' section from the README. The left/right agent utility panes this originally touched no longer exist upstream (removed in zed-industries#49038), so only dock.rs/multi_workspace.rs/ workspace.rs/animation.rs needed changes. * cargo fmt
Problem
Zed feels snappy but mechanical - panels and modals appear instantly, losing spatial context.
Demo
All five surfaces with reduce_motion toggle:
demo.mp4
Changes
Plus a
reduce_motionsetting that respects macOS accessibility preferences by default.Motion principles
Accessibility
New setting:
reduce_motion: "system" | "on" | "off"Defaults to
system, which reads the macOS preference. When enabled, animations skip gracefully.Implementation notes
Animationand easing primitives