diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 60b2666cf26..a060fdae9fc 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -6,7 +6,7 @@ import { IBoundarySashes, Orientation } from '../sash/sash.js'; import { equals, tail } from '../../../common/arrays.js'; import { Event } from '../../../common/event.js'; -import { Disposable } from '../../../common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../common/lifecycle.js'; import './gridview.css'; import { Box, GridView, IGridViewOptions, IGridViewStyles, IView as IGridViewView, IViewSize, orthogonal, Sizing as GridViewSizing, GridLocation } from './gridview.js'; import type { SplitView, AutoSizing as SplitViewAutoSizing } from '../splitview/splitview.js'; @@ -655,6 +655,20 @@ export class Grid extends Disposable { this.gridview.setViewVisible(location, visible); } + /** + * Set the visibility state of a {@link IView view} with a smooth animation. + * + * @param view The {@link IView view}. + * @param visible Whether the view should be visible. + * @param duration The transition duration in milliseconds. + * @param easing The CSS easing function string. + * @returns A disposable that cancels the animation. + */ + setViewVisibleAnimated(view: T, visible: boolean, duration: number, easing: string, onComplete?: () => void): IDisposable { + const location = this.getViewLocation(view); + return this.gridview.setViewVisibleAnimated(location, visible, duration, easing, onComplete); + } + /** * Returns a descriptor for the entire grid. */ diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 46821d6fcb8..29bb93c67de 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -633,6 +633,24 @@ class BranchNode implements ISplitView, IDisposable { } } + setChildVisibleAnimated(index: number, visible: boolean, duration: number, easing: string, onComplete?: () => void): IDisposable { + index = validateIndex(index, this.children.length); + + if (this.splitview.isViewVisible(index) === visible) { + return { dispose: () => { } }; + } + + const wereAllChildrenHidden = this.splitview.contentSize === 0; + const disposable = this.splitview.setViewVisibleAnimated(index, visible, duration, easing, onComplete); + const areAllChildrenHidden = this.splitview.contentSize === 0; + + if ((visible && wereAllChildrenHidden) || (!visible && areAllChildrenHidden)) { + this._onDidVisibilityChange.fire(visible); + } + + return disposable; + } + getChildCachedVisibleSize(index: number): number | undefined { index = validateIndex(index, this.children.length); @@ -1677,6 +1695,31 @@ export class GridView implements IDisposable { parent.setChildVisible(index, visible); } + /** + * Set the visibility state of a {@link IView view} with a smooth animation. + * + * @param location The {@link GridLocation location} of the view. + * @param visible Whether the view should be visible. + * @param duration The transition duration in milliseconds. + * @param easing The CSS easing function string. + * @returns A disposable that cancels the animation. + */ + setViewVisibleAnimated(location: GridLocation, visible: boolean, duration: number, easing: string, onComplete?: () => void): IDisposable { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + return { dispose: () => { } }; + } + + const [rest, index] = tail(location); + const [, parent] = this.getNode(rest); + + if (!(parent instanceof BranchNode)) { + throw new Error('Invalid from location'); + } + + return parent.setChildVisibleAnimated(index, visible, duration, easing, onComplete); + } + /** * Returns a descriptor for the entire grid. */ diff --git a/src/vs/base/browser/ui/motion/motion.css b/src/vs/base/browser/ui/motion/motion.css new file mode 100644 index 00000000000..69e257be2d3 --- /dev/null +++ b/src/vs/base/browser/ui/motion/motion.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Utility class applied during panel animations to prevent content overflow */ +.monaco-split-view2 > .split-view-container > .split-view-view.motion-animating { + overflow: hidden; +} diff --git a/src/vs/base/browser/ui/motion/motion.ts b/src/vs/base/browser/ui/motion/motion.ts new file mode 100644 index 00000000000..cd7a9a05ab4 --- /dev/null +++ b/src/vs/base/browser/ui/motion/motion.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './motion.css'; + +//#region Duration Constants + +/** + * 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 = 300; + +/** + * Duration in milliseconds for panel close (exit) animations. + * Exits are faster than entrances - feels snappy and responsive. + */ +export const PANEL_CLOSE_DURATION = 200; + +/** + * Duration in milliseconds for quick input open (entrance) animations. + */ +export const QUICK_INPUT_OPEN_DURATION = 250; + +/** + * Duration in milliseconds for quick input close (exit) animations. + */ +export const QUICK_INPUT_CLOSE_DURATION = 150; + +//#endregion + +//#region Easing Curves + +/** + * Fluent 2 ease-out curve - default for entrances and expansions. + * Starts fast and decelerates to a stop. + */ +export const EASE_OUT = 'cubic-bezier(0.1, 0.9, 0.2, 1)'; + +/** + * Fluent 2 ease-in curve - for exits and collapses. + * Starts slow and accelerates out. + */ +export const EASE_IN = 'cubic-bezier(0.9, 0.1, 1, 0.2)'; + +//#endregion + +//#region Cubic Bezier Evaluation + +/** + * Parses a CSS `cubic-bezier(x1, y1, x2, y2)` string into its four control + * point values. Returns `[0, 0, 1, 1]` (linear) on parse failure. + */ +export function parseCubicBezier(css: string): [number, number, number, number] { + const match = css.match(/cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/); + if (!match) { + return [0, 0, 1, 1]; + } + return [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4])]; +} + +/** + * Evaluates a cubic bezier curve at time `t` (0-1). + * + * Given control points `(x1, y1)` and `(x2, y2)` (the CSS `cubic-bezier` + * parameters), this finds the bezier parameter `u` such that `Bx(u) = t` + * using Newton's method, then returns `By(u)`. + */ +export function solveCubicBezier(x1: number, y1: number, x2: number, y2: number, t: number): number { + if (t <= 0) { + return 0; + } + if (t >= 1) { + return 1; + } + + // Newton's method to find u where Bx(u) = t + let u = t; // initial guess + for (let i = 0; i < 8; i++) { + const currentX = bezierComponent(u, x1, x2); + const error = currentX - t; + if (Math.abs(error) < 1e-6) { + break; + } + const dx = bezierComponentDerivative(u, x1, x2); + if (Math.abs(dx) < 1e-6) { + break; + } + u -= error / dx; + } + + u = Math.max(0, Math.min(1, u)); + return bezierComponent(u, y1, y2); +} + +/** Evaluates one component of a cubic bezier: B(u) with control points p1, p2, endpoints 0 and 1. */ +function bezierComponent(u: number, p1: number, p2: number): number { + // B(u) = 3(1-u)^2*u*p1 + 3(1-u)*u^2*p2 + u^3 + const oneMinusU = 1 - u; + return 3 * oneMinusU * oneMinusU * u * p1 + 3 * oneMinusU * u * u * p2 + u * u * u; +} + +/** First derivative of a bezier component: B'(u). */ +function bezierComponentDerivative(u: number, p1: number, p2: number): number { + // B'(u) = 3(1-u)^2*p1 + 6(1-u)*u*(p2-p1) + 3*u^2*(1-p2) + const oneMinusU = 1 - u; + return 3 * oneMinusU * oneMinusU * p1 + 6 * oneMinusU * u * (p2 - p1) + 3 * u * u * (1 - p2); +} + +//#endregion + +//#region Utility Functions + +/** + * Checks whether motion is reduced by looking for the `monaco-reduce-motion` + * class on an ancestor element. This integrates with VS Code's existing + * accessibility infrastructure in {@link AccessibilityService}. + */ +export function isMotionReduced(element: HTMLElement): boolean { + return element.closest('.monaco-reduce-motion') !== null; +} + +//#endregion diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 35f2724c1a8..faca36926fc 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -14,6 +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 './splitview.css'; export { Orientation } from '../sash/sash.js'; @@ -818,6 +819,155 @@ export class SplitView void): IDisposable { + if (index < 0 || index >= this.viewItems.length) { + throw new Error('Index out of bounds'); + } + + // Cancel any in-flight animation + this._cleanupMotion?.(); + this._cleanupMotion = undefined; + + const viewItem = this.viewItems[index]; + + // If motion is reduced or already in target state, use instant path + if (viewItem.visible === visible || isMotionReduced(this.el)) { + this.setViewVisible(index, visible); + return toDisposable(() => { }); + } + + const container = this.viewContainer.children[index] as HTMLElement; + const window = getWindow(this.el); + let disposed = false; + let rafId: number | undefined; + + // 1. Snapshot sizes BEFORE the visibility change (the animation start state) + const startSizes = this.viewItems.map(v => v.size); + + // 2. Apply the target visibility to the model instantly. + // This computes final sizes, fires events, updates sashes, etc. + this.setViewVisible(index, visible); + + // 3. Snapshot sizes AFTER the visibility change (the animation end state) + const finalSizes = this.viewItems.map(v => v.size); + + // 4. Restore start sizes so we can animate FROM them + for (let i = 0; i < this.viewItems.length; i++) { + this.viewItems[i].size = startSizes[i]; + } + + // 5. For hiding: the target container lost .visible class (→ display:none). + // Restore it so content stays visible during the animation. + if (!visible) { + container.classList.add('visible'); + } + + // 6. Clip overflow on the target container while it shrinks. + // Only apply for HIDE animations - for SHOW, we leave overflow alone + // so that box-shadow / visual effects on the child Part are not clipped + // by the parent container during the animation. + if (!visible) { + container.style.overflow = 'hidden'; + } + + // 7. Render the start state + this.layoutViews(); + + // 8. Parse easing for JS evaluation + const [x1, y1, x2, y2] = parseCubicBezier(easing); + + // Helper: snap all sizes to final state and clean up + const applyFinalState = () => { + for (let i = 0; i < this.viewItems.length; i++) { + this.viewItems[i].size = finalSizes[i]; + } + if (!visible) { + container.classList.remove('visible'); + container.style.overflow = ''; + } + this.layoutViews(); + this.saveProportions(); + }; + + const cleanup = (completed: boolean) => { + if (disposed) { + return; + } + disposed = true; + if (rafId !== undefined) { + window.cancelAnimationFrame(rafId); + rafId = undefined; + } + applyFinalState(); + this._cleanupMotion = undefined; + if (completed) { + onComplete?.(); + } + }; + this._cleanupMotion = () => cleanup(false); + + // 9. Animate via requestAnimationFrame + const startTime = performance.now(); + const totalSize = this.size; + + const animate = () => { + if (disposed) { + return; + } + + const elapsed = performance.now() - startTime; + const t = Math.min(elapsed / duration, 1); + const easedT = solveCubicBezier(x1, y1, x2, y2, t); + + // Interpolate all view sizes + let runningTotal = 0; + for (let i = 0; i < this.viewItems.length; i++) { + if (i === this.viewItems.length - 1) { + // Last item absorbs rounding errors to maintain total = this.size + this.viewItems[i].size = totalSize - runningTotal; + } else { + const size = Math.round( + startSizes[i] + (finalSizes[i] - startSizes[i]) * easedT + ); + this.viewItems[i].size = size; + runningTotal += size; + } + } + + this.layoutViews(); + + if (t < 1) { + rafId = window.requestAnimationFrame(animate); + } else { + cleanup(true); + } + }; + + rafId = window.requestAnimationFrame(animate); + + return toDisposable(() => cleanup(false)); + } + + private _cleanupMotion: (() => void) | undefined; + /** * Returns the {@link IView view}'s size previously to being hidden. * diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index fae2f7ffb45..17ab3d2f9b2 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -37,6 +37,7 @@ import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../b import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; import { QuickInputTreeController } from './tree/quickInputTreeController.js'; import { QuickTree } from './tree/quickTree.js'; +import { isMotionReduced, QUICK_INPUT_OPEN_DURATION, QUICK_INPUT_CLOSE_DURATION, EASE_OUT, EASE_IN } from '../../../base/browser/ui/motion/motion.js'; const $ = dom.$; @@ -713,6 +714,23 @@ export class QuickInputController extends Disposable { this.dndController?.layoutContainer(); ui.inputBox.setFocus(); this.quickInputTypeContext.set(controller.type); + + // Animate entrance: fade in + slide down + if (!isMotionReduced(ui.container)) { + ui.container.style.opacity = '0'; + ui.container.style.transform = 'translateY(-8px)'; + ui.container.style.transition = `opacity ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}, transform ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}`; + // Trigger reflow so the initial state is registered before applying target + void ui.container.offsetHeight; + ui.container.style.opacity = '1'; + ui.container.style.transform = 'translateY(0)'; + const onDone = () => { + ui.container.style.transition = ''; + ui.container.style.willChange = ''; + ui.container.removeEventListener('transitionend', onDone); + }; + ui.container.addEventListener('transitionend', onDone); + } } isVisible(): boolean { @@ -779,7 +797,26 @@ export class QuickInputController extends Disposable { this.controller = null; this.onHideEmitter.fire(); if (container) { - container.style.display = 'none'; + // Animate exit: fade out + slide up (faster than open) + if (!isMotionReduced(container)) { + 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 = () => { + container.style.display = 'none'; + 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); + window.setTimeout(onDone, QUICK_INPUT_CLOSE_DURATION + 50); + } else { + container.style.display = 'none'; + } } if (!focusChanged) { let currentElement = this.previousFocusElement; diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 784c9edfadc..b7bf628b882 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -47,6 +47,7 @@ import { AuxiliaryBarPart } from './parts/auxiliarybar/auxiliaryBarPart.js'; import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js'; import { IAuxiliaryWindowService } from '../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; import { CodeWindow, mainWindow } from '../../base/browser/window.js'; +import { PANEL_OPEN_DURATION, PANEL_CLOSE_DURATION, EASE_OUT, EASE_IN } from '../../base/browser/ui/motion/motion.js'; //#region Layout Implementation @@ -1864,27 +1865,33 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.stateModel.setRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN, hidden); - // Adjust CSS - if (hidden) { - this.mainContainer.classList.add(LayoutClasses.SIDEBAR_HIDDEN); - } else { + // Adjust CSS - for hiding, defer adding the class until animation + // completes so the part stays visible during the exit animation. + if (!hidden) { this.mainContainer.classList.remove(LayoutClasses.SIDEBAR_HIDDEN); } // Propagate to grid - this.workbenchGrid.setViewVisible(this.sideBarPartView, !hidden); + this.workbenchGrid.setViewVisibleAnimated( + this.sideBarPartView, + !hidden, + hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, + hidden ? EASE_IN : EASE_OUT, + hidden ? () => { + // Deferred to after close animation + this.mainContainer.classList.add(LayoutClasses.SIDEBAR_HIDDEN); + if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Sidebar); - // If sidebar becomes hidden, also hide the current active Viewlet if any - if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Sidebar); - - if (!this.isAuxiliaryBarMaximized()) { - this.focusPanelOrEditor(); // do not auto focus when auxiliary bar is maximized - } - } + if (!this.isAuxiliaryBarMaximized()) { + this.focusPanelOrEditor(); // do not auto focus when auxiliary bar is maximized + } + } + } : undefined + ); // If sidebar becomes visible, show last active Viewlet or default viewlet - else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { + if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { const viewletToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Sidebar); if (viewletToOpen) { this.openViewContainer(ViewContainerLocation.Sidebar, viewletToOpen); @@ -2008,13 +2015,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const panelOpensMaximized = this.panelOpensMaximized(); - // Adjust CSS - if (hidden) { - this.mainContainer.classList.add(LayoutClasses.PANEL_HIDDEN); - } else { - this.mainContainer.classList.remove(LayoutClasses.PANEL_HIDDEN); - } - // If maximized and in process of hiding, unmaximize FIRST before // changing visibility to prevent conflict with setEditorHidden // which would force panel visible again (fixes #281772) @@ -2022,13 +2022,31 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.toggleMaximizedPanel(); } - // Propagate layout changes to grid - this.workbenchGrid.setViewVisible(this.panelPartView, !hidden); + // Adjust CSS - for hiding, defer adding the class until animation + // completes because `.nopanel .part.panel { display: none !important }` + // would instantly hide the panel content mid-animation. + if (!hidden) { + this.mainContainer.classList.remove(LayoutClasses.PANEL_HIDDEN); + } - // If panel part becomes hidden, also hide the current active panel if any + // Propagate layout changes to grid + this.workbenchGrid.setViewVisibleAnimated( + this.panelPartView, + !hidden, + hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, + hidden ? EASE_IN : EASE_OUT, + hidden ? () => { + // Deferred to after close animation + this.mainContainer.classList.add(LayoutClasses.PANEL_HIDDEN); + if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); + } + } : undefined + ); + + // If panel part becomes hidden, focus the editor after animation starts let focusEditor = false; if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)) { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); if ( !isIOS && // do not auto focus on iOS (https://github.com/microsoft/vscode/issues/127832) !this.isAuxiliaryBarMaximized() // do not auto focus when auxiliary bar is maximized @@ -2202,24 +2220,31 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN, hidden); - // Adjust CSS - if (hidden) { - this.mainContainer.classList.add(LayoutClasses.AUXILIARYBAR_HIDDEN); - } else { + // Adjust CSS - for hiding, defer adding the class until animation + // completes because `.noauxiliarybar .part.auxiliarybar { display: none !important }` + // would instantly hide the content mid-animation. + if (!hidden) { this.mainContainer.classList.remove(LayoutClasses.AUXILIARYBAR_HIDDEN); } // Propagate to grid - this.workbenchGrid.setViewVisible(this.auxiliaryBarPartView, !hidden); - - // If auxiliary bar becomes hidden, also hide the current active pane composite if any - if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.AuxiliaryBar); - this.focusPanelOrEditor(); - } + this.workbenchGrid.setViewVisibleAnimated( + this.auxiliaryBarPartView, + !hidden, + hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, + hidden ? EASE_IN : EASE_OUT, + hidden ? () => { + // Deferred to after close animation + this.mainContainer.classList.add(LayoutClasses.AUXILIARYBAR_HIDDEN); + if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.AuxiliaryBar); + this.focusPanelOrEditor(); + } + } : undefined + ); // If auxiliary bar becomes visible, show last active pane composite or default pane composite - else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { + if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { let viewletToOpen: string | undefined = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.AuxiliaryBar); // verify that the viewlet we try to open has views before we default to it diff --git a/src/vs/workbench/browser/media/motion.css b/src/vs/workbench/browser/media/motion.css new file mode 100644 index 00000000000..5d22a5e4677 --- /dev/null +++ b/src/vs/workbench/browser/media/motion.css @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Motion custom properties -- only active when motion is enabled */ +.monaco-workbench.monaco-enable-motion { + --vscode-motion-panel-open-duration: 300ms; + --vscode-motion-panel-close-duration: 200ms; + --vscode-motion-quick-input-open-duration: 250ms; + --vscode-motion-quick-input-close-duration: 150ms; + --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); +} + +/* Disable all motion durations when reduced motion is active */ +.monaco-workbench.monaco-reduce-motion { + --vscode-motion-panel-open-duration: 0ms; + --vscode-motion-panel-close-duration: 0ms; + --vscode-motion-quick-input-open-duration: 0ms; + --vscode-motion-quick-input-close-duration: 0ms; +} diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 9250ef3f280..de344e7e46b 100644 --- a/src/vs/workbench/browser/style.ts +++ b/src/vs/workbench/browser/style.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './media/style.css'; +import './media/motion.css'; import { registerThemingParticipant } from '../../platform/theme/common/themeService.js'; import { WORKBENCH_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from '../common/theme.js'; import { isWeb, isIOS } from '../../base/common/platform.js';