Revert "Merge pull request #294021 from microsoft/eli/workbench-motion"

This reverts commit 7e632ad633, reversing
changes made to 386be2607e.
This commit is contained in:
eli-w-king
2026-02-13 10:36:28 -08:00
parent 7392f65cf6
commit a3fd9a09a0
10 changed files with 47 additions and 530 deletions

View File

@@ -9,10 +9,9 @@ import { Event } from '../../../common/event.js';
import { Disposable } 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, IViewVisibilityAnimationOptions } from '../splitview/splitview.js';
import type { SplitView, AutoSizing as SplitViewAutoSizing } from '../splitview/splitview.js';
export type { IViewSize };
export type { IViewVisibilityAnimationOptions } from '../splitview/splitview.js';
export { LayoutPriority, Orientation, orthogonal } from './gridview.js';
export const enum Direction {
@@ -651,12 +650,10 @@ export class Grid<T extends IView = IView> extends Disposable {
* Set the visibility state of a {@link IView view}.
*
* @param view The {@link IView view}.
* @param visible Whether the view should be visible.
* @param animation Optional animation options.
*/
setViewVisible(view: T, visible: boolean, animation?: IViewVisibilityAnimationOptions): void {
setViewVisible(view: T, visible: boolean): void {
const location = this.getViewLocation(view);
this.gridview.setViewVisible(location, visible, animation);
this.gridview.setViewVisible(location, visible);
}
/**

View File

@@ -5,7 +5,7 @@
import { $ } from '../../dom.js';
import { IBoundarySashes, Orientation, Sash } from '../sash/sash.js';
import { DistributeSizing, ISplitViewStyles, IView as ISplitView, IViewVisibilityAnimationOptions, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js';
import { DistributeSizing, ISplitViewStyles, IView as ISplitView, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js';
import { equals as arrayEquals, tail } from '../../../common/arrays.js';
import { Color } from '../../../common/color.js';
import { Emitter, Event, Relay } from '../../../common/event.js';
@@ -615,7 +615,7 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
return this.splitview.isViewVisible(index);
}
setChildVisible(index: number, visible: boolean, animation?: IViewVisibilityAnimationOptions): void {
setChildVisible(index: number, visible: boolean): void {
index = validateIndex(index, this.children.length);
if (this.splitview.isViewVisible(index) === visible) {
@@ -623,7 +623,7 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
}
const wereAllChildrenHidden = this.splitview.contentSize === 0;
this.splitview.setViewVisible(index, visible, animation);
this.splitview.setViewVisible(index, visible);
const areAllChildrenHidden = this.splitview.contentSize === 0;
// If all children are hidden then the parent should hide the entire splitview
@@ -1663,7 +1663,7 @@ export class GridView implements IDisposable {
*
* @param location The {@link GridLocation location} of the view.
*/
setViewVisible(location: GridLocation, visible: boolean, animation?: IViewVisibilityAnimationOptions): void {
setViewVisible(location: GridLocation, visible: boolean): void {
if (this.hasMaximizedView()) {
this.exitMaximizedView();
return;
@@ -1676,7 +1676,7 @@ export class GridView implements IDisposable {
throw new Error('Invalid from location');
}
parent.setChildVisible(index, visible, animation);
parent.setChildVisible(index, visible);
}
/**

View File

@@ -1,9 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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;
}

View File

@@ -1,155 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 Easing Curves
/**
* A pre-parsed cubic bezier easing curve that can be evaluated directly
* without reparsing a CSS string on every frame.
*
* Given control points `(x1, y1)` and `(x2, y2)` (the CSS `cubic-bezier`
* parameters), {@link solve} finds the bezier parameter `u` such that
* `Bx(u) = t` using Newton's method, then returns `By(u)`.
*/
export class CubicBezierCurve {
constructor(
readonly x1: number,
readonly y1: number,
readonly x2: number,
readonly y2: number,
) { }
/**
* Evaluate the curve at time `t` (0-1), returning the eased value.
*/
solve(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, this.x1, this.x2);
const error = currentX - t;
if (Math.abs(error) < 1e-6) {
break;
}
const dx = bezierComponentDerivative(u, this.x1, this.x2);
if (Math.abs(dx) < 1e-6) {
break;
}
u -= error / dx;
}
u = Math.max(0, Math.min(1, u));
return bezierComponent(u, this.y1, this.y2);
}
/**
* Returns the CSS `cubic-bezier(…)` string representation, for use in
* CSS `transition` or `animation` properties.
*/
toCssString(): string {
return `cubic-bezier(${this.x1}, ${this.y1}, ${this.x2}, ${this.y2})`;
}
}
/**
* Fluent 2 ease-out curve - default for entrances and expansions.
* Starts fast and decelerates to a stop.
*/
export const EASE_OUT = new CubicBezierCurve(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 = new CubicBezierCurve(0.9, 0.1, 1, 0.2);
//#endregion
//#region Cubic Bezier Evaluation
/**
* Parses a CSS `cubic-bezier(x1, y1, x2, y2)` string into a
* {@link CubicBezierCurve}. Returns a linear curve on parse failure.
*/
export function parseCubicBezier(css: string): CubicBezierCurve {
const match = css.match(/cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/);
if (!match) {
return new CubicBezierCurve(0, 0, 1, 1);
}
return new CubicBezierCurve(parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4]));
}
/** 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 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
/**
* 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

View File

@@ -8,41 +8,15 @@ import { DomEmitter } from '../../event.js';
import { ISashEvent as IBaseSashEvent, Orientation, Sash, SashState } from '../sash/sash.js';
import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js';
import { pushToEnd, pushToStart, range } from '../../../common/arrays.js';
import { CancellationToken } from '../../../common/cancellation.js';
import { Color } from '../../../common/color.js';
import { Emitter, Event } from '../../../common/event.js';
import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } from '../../../common/lifecycle.js';
import { clamp } from '../../../common/numbers.js';
import { Scrollable, ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';
import * as types from '../../../common/types.js';
import { CubicBezierCurve, isMotionReduced, scaleDuration } from '../motion/motion.js';
import './splitview.css';
export { Orientation } from '../sash/sash.js';
/**
* Options for animating a view visibility change in a {@link SplitView}.
*/
export interface IViewVisibilityAnimationOptions {
/** Transition duration in milliseconds. */
readonly duration: number;
/** The easing curve applied to the animation. */
readonly easing: CubicBezierCurve;
/**
* Optional callback invoked when the animation finishes naturally.
* NOT called if the animation is cancelled via the {@link token}.
*/
readonly onComplete?: () => void;
/**
* A cancellation token that allows the caller to stop the animation.
* When cancellation is requested the animation snaps to its final state.
*/
readonly token: CancellationToken;
}
export interface ISplitViewStyles {
readonly separatorBorder: Color;
}
@@ -828,38 +802,14 @@ export class SplitView<TLayoutContext = undefined, TView extends IView<TLayoutCo
/**
* Set a {@link IView view}'s visibility.
*
* When {@link animation} is provided and motion is not reduced, the
* visibility change is animated. Otherwise the change is applied
* instantly. Any in-flight animation is always cancelled first.
*
* @param index The {@link IView view} index.
* @param visible Whether the {@link IView view} should be visible.
* @param animation Optional animation options. When omitted (or when
* the user prefers reduced motion) the change is instant.
*/
setViewVisible(index: number, visible: boolean, animation?: IViewVisibilityAnimationOptions): void {
setViewVisible(index: number, visible: boolean): void {
if (index < 0 || index >= this.viewItems.length) {
throw new Error('Index out of bounds');
}
// Cancel any in-flight animation before changing visibility.
// An animated visibility change interpolates ALL view sizes each
// frame, so a concurrent change on a different view would be
// overwritten on the next frame. Snapping first prevents that.
this._cleanupMotion?.();
this._cleanupMotion = undefined;
if (animation && !animation.token.isCancellationRequested && !isMotionReduced(this.el) && this.viewItems[index].visible !== visible) {
this._setViewVisibleAnimated(index, visible, animation);
} else {
this._setViewVisibleInstant(index, visible);
}
}
/**
* Apply the visibility change to the model without animation.
*/
private _setViewVisibleInstant(index: number, visible: boolean): void {
const viewItem = this.viewItems[index];
viewItem.setVisible(visible);
@@ -868,146 +818,6 @@ export class SplitView<TLayoutContext = undefined, TView extends IView<TLayoutCo
this.saveProportions();
}
/**
* Animate the visibility change using `requestAnimationFrame`.
*
* Interpolates all view sizes on each frame, which naturally cascades
* layout changes through nested splitviews in the grid hierarchy
* (e.g., the bottom panel resizing when the sidebar animates).
*
* The animation can be cancelled via {@link IViewVisibilityAnimationOptions.token}.
* {@link IViewVisibilityAnimationOptions.onComplete} is only called when the
* animation finishes naturally (not on cancellation).
*/
private _setViewVisibleAnimated(index: number, visible: boolean, animation: IViewVisibilityAnimationOptions): void {
const { duration: baseDuration, easing, onComplete, token } = animation;
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._setViewVisibleInstant(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';
}
// 6b. Set initial opacity for fade effect
container.style.opacity = visible ? '0' : '1';
// 7. Scale duration based on pixel distance for consistent perceived velocity
const pixelDistance = Math.abs(finalSizes[index] - startSizes[index]);
const duration = scaleDuration(baseDuration, pixelDistance);
// 8. Render the start state
this.layoutViews();
// 9. Easing curve is pre-parsed - ready for JS evaluation
// 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];
}
container.style.opacity = '';
if (!visible) {
container.classList.remove('visible');
container.style.overflow = '';
}
this.layoutViews();
this.saveProportions();
};
const cleanup = (completed: boolean) => {
if (disposed) {
return;
}
disposed = true;
tokenListener.dispose();
if (rafId !== undefined) {
window.cancelAnimationFrame(rafId);
rafId = undefined;
}
applyFinalState();
this._cleanupMotion = undefined;
if (completed) {
onComplete?.();
}
};
this._cleanupMotion = () => cleanup(false);
// Listen to the cancellation token so the caller can stop the animation
const tokenListener = token.onCancellationRequested(() => cleanup(false));
// 10. 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 = easing.solve(t);
// Interpolate opacity for fade effect
container.style.opacity = String(visible ? easedT : 1 - easedT);
// 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);
}
private _cleanupMotion: (() => void) | undefined;
/**
* Returns the {@link IView view}'s size previously to being hidden.
*