diff --git a/src/vs/base/browser/ui/motion/motion.ts b/src/vs/base/browser/ui/motion/motion.ts index 455825510eb..817ad0b0213 100644 --- a/src/vs/base/browser/ui/motion/motion.ts +++ b/src/vs/base/browser/ui/motion/motion.ts @@ -11,23 +11,23 @@ import './motion.css'; * Duration in milliseconds for panel open (entrance) animations. * Per Fluent 2 Enter/Exit pattern - entrance should feel smooth but not sluggish. */ -export const PANEL_OPEN_DURATION = 200; +export const PANEL_OPEN_DURATION = 150; /** * Duration in milliseconds for panel close (exit) animations. * Exits are faster than entrances - feels snappy and responsive. */ -export const PANEL_CLOSE_DURATION = 75; +export const PANEL_CLOSE_DURATION = 50; /** * Duration in milliseconds for quick input open (entrance) animations. */ -export const QUICK_INPUT_OPEN_DURATION = 200; +export const QUICK_INPUT_OPEN_DURATION = 150; /** * Duration in milliseconds for quick input close (exit) animations. */ -export const QUICK_INPUT_CLOSE_DURATION = 75; +export const QUICK_INPUT_CLOSE_DURATION = 50; //#endregion @@ -111,6 +111,40 @@ function bezierComponentDerivative(u: number, p1: number, p2: number): number { //#endregion +//#region Duration Scaling + +/** + * Reference pixel distance at which the base duration constants apply. + * Duration scales linearly: a 600px animation takes twice as long as a 300px + * one, keeping perceived velocity constant. + */ +const REFERENCE_DISTANCE = 300; + +/** Minimum animation duration in milliseconds (avoids sub-frame flickers). */ +const MIN_DURATION = 50; + +/** Maximum animation duration in milliseconds (avoids sluggish feel). */ +const MAX_DURATION = 300; + +/** + * Scales a base animation duration proportionally to the pixel distance + * being animated, so that perceived velocity stays constant regardless of + * panel width. + * + * @param baseDuration The duration (ms) that applies at {@link REFERENCE_DISTANCE} pixels. + * @param pixelDistance The actual number of pixels the view will resize. + * @returns The scaled duration, clamped to [{@link MIN_DURATION}, {@link MAX_DURATION}]. + */ +export function scaleDuration(baseDuration: number, pixelDistance: number): number { + if (pixelDistance <= 0) { + return baseDuration; + } + const scaled = baseDuration * (pixelDistance / REFERENCE_DISTANCE); + return Math.round(Math.max(MIN_DURATION, Math.min(MAX_DURATION, scaled))); +} + +//#endregion + //#region Utility Functions /** diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 8a457624e3e..d83124decb2 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -14,7 +14,7 @@ import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } fr import { clamp } from '../../../common/numbers.js'; import { Scrollable, ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js'; import * as types from '../../../common/types.js'; -import { isMotionReduced, parseCubicBezier, solveCubicBezier } from '../motion/motion.js'; +import { isMotionReduced, parseCubicBezier, scaleDuration, solveCubicBezier } from '../motion/motion.js'; import './splitview.css'; export { Orientation } from '../sash/sash.js'; @@ -896,10 +896,17 @@ export class SplitView cleanup(false); - // 9. Animate via requestAnimationFrame + // 10. Animate via requestAnimationFrame const startTime = performance.now(); const totalSize = this.size; @@ -945,6 +953,9 @@ export class SplitView void) | undefined; private readonly inQuickInputContext: IContextKey; private readonly quickInputTypeContext: IContextKey; @@ -712,15 +713,19 @@ export class QuickInputController extends Disposable { const backKeybindingLabel = this.options.backKeybindingLabel(); backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back"); + const wasVisible = ui.container.style.display !== 'none'; ui.container.style.display = ''; + // Cancel any in-flight exit animation that would set display:none + this._cancelExitAnimation?.(); + this._cancelExitAnimation = undefined; this.updateLayout(); this.dndController?.setEnabled(!controller.anchor); this.dndController?.layoutContainer(); ui.inputBox.setFocus(); this.quickInputTypeContext.set(controller.type); - // Animate entrance: fade in + slide down - if (!isMotionReduced(ui.container)) { + // Animate entrance: fade in + slide down (only when first appearing) + if (!wasVisible && !isMotionReduced(ui.container)) { // Set initial state without transition ui.container.style.transition = 'none'; ui.container.style.opacity = '0'; @@ -806,10 +811,15 @@ export class QuickInputController extends Disposable { if (container) { // Animate exit: fade out + slide up (faster than open) if (!isMotionReduced(container)) { + let exitCancelled = false; container.style.transition = `opacity ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}, transform ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}`; container.style.opacity = '0'; container.style.transform = 'translateY(-8px)'; const onDone = () => { + if (exitCancelled) { + return; + } + exitCancelled = true; container.style.display = 'none'; container.style.transition = ''; container.style.opacity = ''; @@ -817,6 +827,14 @@ export class QuickInputController extends Disposable { container.style.willChange = ''; container.removeEventListener('transitionend', onDone); }; + this._cancelExitAnimation = () => { + exitCancelled = true; + container.style.transition = ''; + container.style.opacity = ''; + container.style.transform = ''; + container.style.willChange = ''; + container.removeEventListener('transitionend', onDone); + }; container.addEventListener('transitionend', onDone); // Safety timeout in case transitionend doesn't fire const window = dom.getWindow(container); diff --git a/src/vs/workbench/browser/media/motion.css b/src/vs/workbench/browser/media/motion.css index 79730b812c7..182d420add4 100644 --- a/src/vs/workbench/browser/media/motion.css +++ b/src/vs/workbench/browser/media/motion.css @@ -5,10 +5,10 @@ /* Motion custom properties -- only active when motion is enabled */ .monaco-workbench.monaco-enable-motion { - --vscode-motion-panel-open-duration: 200ms; - --vscode-motion-panel-close-duration: 75ms; - --vscode-motion-quick-input-open-duration: 200ms; - --vscode-motion-quick-input-close-duration: 75ms; + --vscode-motion-panel-open-duration: 150ms; + --vscode-motion-panel-close-duration: 50ms; + --vscode-motion-quick-input-open-duration: 150ms; + --vscode-motion-quick-input-close-duration: 50ms; --vscode-motion-ease-out: cubic-bezier(0.1, 0.9, 0.2, 1); --vscode-motion-ease-in: cubic-bezier(0.9, 0.1, 1, 0.2); }