mirror of
https://github.com/mathuo/dockview
synced 2025-10-25 17:28:08 +00:00
Merge branch 'master' of https://github.com/mathuo/dockview into fix/github-issue-991-group-activation
This commit is contained in:
commit
e9df48e294
@ -45,6 +45,12 @@ export function exhaustMicrotaskQueue(): Promise<void> {
|
|||||||
return new Promise<void>((resolve) => resolve());
|
return new Promise<void>((resolve) => resolve());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function exhaustAnimationFrame(): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
requestAnimationFrame(() => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const mockGetBoundingClientRect = ({
|
export const mockGetBoundingClientRect = ({
|
||||||
left,
|
left,
|
||||||
top,
|
top,
|
||||||
|
|||||||
@ -180,10 +180,13 @@ describe('droptarget', () => {
|
|||||||
height: string;
|
height: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
// Check positioning (back to top/left with GPU layer maintained)
|
||||||
expect(element.style.top).toBe(box.top);
|
expect(element.style.top).toBe(box.top);
|
||||||
expect(element.style.left).toBe(box.left);
|
expect(element.style.left).toBe(box.left);
|
||||||
expect(element.style.width).toBe(box.width);
|
expect(element.style.width).toBe(box.width);
|
||||||
expect(element.style.height).toBe(box.height);
|
expect(element.style.height).toBe(box.height);
|
||||||
|
// Ensure GPU layer is maintained
|
||||||
|
expect(element.style.transform).toBe('translate3d(0, 0, 0)');
|
||||||
}
|
}
|
||||||
|
|
||||||
viewQuery = element.querySelectorAll(
|
viewQuery = element.querySelectorAll(
|
||||||
@ -273,13 +276,14 @@ describe('droptarget', () => {
|
|||||||
createOffsetDragOverEvent({ clientX: 100, clientY: 50 })
|
createOffsetDragOverEvent({ clientX: 100, clientY: 50 })
|
||||||
);
|
);
|
||||||
expect(droptarget.state).toBe('center');
|
expect(droptarget.state).toBe('center');
|
||||||
|
// With GPU optimizations, elements always have a base transform layer
|
||||||
expect(
|
expect(
|
||||||
(
|
(
|
||||||
element
|
element
|
||||||
.getElementsByClassName('dv-drop-target-selection')
|
.getElementsByClassName('dv-drop-target-selection')
|
||||||
.item(0) as HTMLDivElement
|
.item(0) as HTMLDivElement
|
||||||
).style.transform
|
).style.transform
|
||||||
).toBe('');
|
).toBe('translate3d(0, 0, 0)');
|
||||||
|
|
||||||
fireEvent.dragLeave(target);
|
fireEvent.dragLeave(target);
|
||||||
expect(droptarget.state).toBe('center');
|
expect(droptarget.state).toBe('center');
|
||||||
|
|||||||
@ -5826,9 +5826,9 @@ describe('dockviewComponent', () => {
|
|||||||
dockview.fromJSON(state);
|
dockview.fromJSON(state);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* exhaust task queue since popout group completion is async but not awaited in `fromJSON(...)`
|
* Wait for delayed popout group creation to complete
|
||||||
*/
|
*/
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await dockview.popoutRestorationPromise;
|
||||||
|
|
||||||
expect(dockview.panels.length).toBe(4);
|
expect(dockview.panels.length).toBe(4);
|
||||||
|
|
||||||
@ -6129,6 +6129,7 @@ describe('dockviewComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('persistance with custom url', async () => {
|
test('persistance with custom url', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
|
|
||||||
window.open = () => setupMockWindow();
|
window.open = () => setupMockWindow();
|
||||||
@ -6212,7 +6213,12 @@ describe('dockviewComponent', () => {
|
|||||||
expect(dockview.groups.length).toBe(0);
|
expect(dockview.groups.length).toBe(0);
|
||||||
|
|
||||||
dockview.fromJSON(state);
|
dockview.fromJSON(state);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0)); // popout views are completed as a promise so must complete microtask-queue
|
|
||||||
|
// Advance timers to trigger delayed popout creation (0ms, 100ms delays)
|
||||||
|
jest.advanceTimersByTime(200);
|
||||||
|
|
||||||
|
// Wait for the popout restoration to complete
|
||||||
|
await dockview.popoutRestorationPromise;
|
||||||
|
|
||||||
expect(dockview.toJSON().popoutGroups).toEqual([
|
expect(dockview.toJSON().popoutGroups).toEqual([
|
||||||
{
|
{
|
||||||
@ -6246,6 +6252,8 @@ describe('dockviewComponent', () => {
|
|||||||
url: '/custom.html',
|
url: '/custom.html',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when browsers block popups', () => {
|
describe('when browsers block popups', () => {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
OverlayRenderContainer,
|
OverlayRenderContainer,
|
||||||
} from '../../overlay/overlayRenderContainer';
|
} from '../../overlay/overlayRenderContainer';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { Writable, exhaustMicrotaskQueue } from '../__test_utils__/utils';
|
import { Writable, exhaustMicrotaskQueue, exhaustAnimationFrame } from '../__test_utils__/utils';
|
||||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||||
|
|
||||||
@ -160,6 +160,7 @@ describe('overlayRenderContainer', () => {
|
|||||||
const container = cut.attach({ panel, referenceContainer });
|
const container = cut.attach({ panel, referenceContainer });
|
||||||
|
|
||||||
await exhaustMicrotaskQueue();
|
await exhaustMicrotaskQueue();
|
||||||
|
await exhaustAnimationFrame();
|
||||||
|
|
||||||
expect(panelContentEl.parentElement).toBe(container);
|
expect(panelContentEl.parentElement).toBe(container);
|
||||||
expect(container.parentElement).toBe(parentContainer);
|
expect(container.parentElement).toBe(parentContainer);
|
||||||
@ -175,6 +176,7 @@ describe('overlayRenderContainer', () => {
|
|||||||
).toHaveBeenCalledTimes(1);
|
).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
onDidDimensionsChange.fire({});
|
onDidDimensionsChange.fire({});
|
||||||
|
await exhaustAnimationFrame();
|
||||||
expect(container.style.display).toBe('');
|
expect(container.style.display).toBe('');
|
||||||
|
|
||||||
expect(container.style.left).toBe('49px');
|
expect(container.style.left).toBe('49px');
|
||||||
@ -196,13 +198,13 @@ describe('overlayRenderContainer', () => {
|
|||||||
onDidVisibilityChange.fire({});
|
onDidVisibilityChange.fire({});
|
||||||
expect(container.style.display).toBe('');
|
expect(container.style.display).toBe('');
|
||||||
|
|
||||||
expect(container.style.left).toBe('50px');
|
expect(container.style.left).toBe('49px');
|
||||||
expect(container.style.top).toBe('100px');
|
expect(container.style.top).toBe('99px');
|
||||||
expect(container.style.width).toBe('100px');
|
expect(container.style.width).toBe('101px');
|
||||||
expect(container.style.height).toBe('200px');
|
expect(container.style.height).toBe('201px');
|
||||||
expect(
|
expect(
|
||||||
referenceContainer.element.getBoundingClientRect
|
referenceContainer.element.getBoundingClientRect
|
||||||
).toHaveBeenCalledTimes(3);
|
).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('related z-index from `aria-level` set on floating panels', async () => {
|
test('related z-index from `aria-level` set on floating panels', async () => {
|
||||||
@ -262,4 +264,87 @@ describe('overlayRenderContainer', () => {
|
|||||||
'calc(var(--dv-overlay-z-index, 999) + 5)'
|
'calc(var(--dv-overlay-z-index, 999) + 5)'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('that frequent resize calls are batched to prevent shaking (issue #988)', async () => {
|
||||||
|
const cut = new OverlayRenderContainer(
|
||||||
|
parentContainer,
|
||||||
|
fromPartial<DockviewComponent>({})
|
||||||
|
);
|
||||||
|
|
||||||
|
const panelContentEl = document.createElement('div');
|
||||||
|
const onDidVisibilityChange = new Emitter<any>();
|
||||||
|
const onDidDimensionsChange = new Emitter<any>();
|
||||||
|
const onDidLocationChange = new Emitter<any>();
|
||||||
|
|
||||||
|
const panel = fromPartial<IDockviewPanel>({
|
||||||
|
api: {
|
||||||
|
id: 'test_panel_id',
|
||||||
|
onDidVisibilityChange: onDidVisibilityChange.event,
|
||||||
|
onDidDimensionsChange: onDidDimensionsChange.event,
|
||||||
|
onDidLocationChange: onDidLocationChange.event,
|
||||||
|
isVisible: true,
|
||||||
|
location: { type: 'grid' },
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
content: {
|
||||||
|
element: panelContentEl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
api: {
|
||||||
|
location: {
|
||||||
|
type: 'grid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(referenceContainer.element, 'getBoundingClientRect')
|
||||||
|
.mockReturnValue(
|
||||||
|
fromPartial<DOMRect>({
|
||||||
|
left: 100,
|
||||||
|
top: 200,
|
||||||
|
width: 150,
|
||||||
|
height: 250,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(parentContainer, 'getBoundingClientRect').mockReturnValue(
|
||||||
|
fromPartial<DOMRect>({
|
||||||
|
left: 50,
|
||||||
|
top: 100,
|
||||||
|
width: 200,
|
||||||
|
height: 300,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const container = cut.attach({ panel, referenceContainer });
|
||||||
|
|
||||||
|
// Wait for initial positioning
|
||||||
|
await exhaustMicrotaskQueue();
|
||||||
|
await exhaustAnimationFrame();
|
||||||
|
|
||||||
|
expect(container.style.left).toBe('50px');
|
||||||
|
expect(container.style.top).toBe('100px');
|
||||||
|
|
||||||
|
// Simulate rapid resize events that could cause shaking
|
||||||
|
onDidDimensionsChange.fire({});
|
||||||
|
onDidDimensionsChange.fire({});
|
||||||
|
onDidDimensionsChange.fire({});
|
||||||
|
onDidDimensionsChange.fire({});
|
||||||
|
onDidDimensionsChange.fire({});
|
||||||
|
|
||||||
|
// Even with multiple rapid events, only one RAF should be scheduled
|
||||||
|
await exhaustAnimationFrame();
|
||||||
|
|
||||||
|
expect(container.style.left).toBe('50px');
|
||||||
|
expect(container.style.top).toBe('100px');
|
||||||
|
expect(container.style.width).toBe('150px');
|
||||||
|
expect(container.style.height).toBe('250px');
|
||||||
|
|
||||||
|
// Verify that DOM measurements are cached within the same frame
|
||||||
|
// Should be called initially and possibly one more time for visibility change
|
||||||
|
expect(referenceContainer.element.getBoundingClientRect).toHaveBeenCalledTimes(2);
|
||||||
|
expect(parentContainer.getBoundingClientRect).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
|
export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
|
||||||
|
|
||||||
export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100, width: 300, height: 300 };
|
export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100, width: 300, height: 300 };
|
||||||
|
|
||||||
|
export const DESERIALIZATION_POPOUT_DELAY_MS = 100
|
||||||
|
|||||||
@ -12,12 +12,16 @@
|
|||||||
.dv-drop-target-anchor {
|
.dv-drop-target-anchor {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: var(--dv-drag-over-border);
|
border: var(--dv-drag-over-border);
|
||||||
transition: opacity var(--dv-transition-duration) ease-in,
|
|
||||||
top var(--dv-transition-duration) ease-out,
|
|
||||||
left var(--dv-transition-duration) ease-out,
|
|
||||||
width var(--dv-transition-duration) ease-out,
|
|
||||||
height var(--dv-transition-duration) ease-out;
|
|
||||||
background-color: var(--dv-drag-over-background-color);
|
background-color: var(--dv-drag-over-background-color);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
|
/* GPU optimizations */
|
||||||
|
will-change: transform, opacity;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
contain: layout paint;
|
||||||
|
|
||||||
|
transition: opacity var(--dv-transition-duration) ease-in,
|
||||||
|
transform var(--dv-transition-duration) ease-out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,68 @@ import { DragAndDropObserver } from './dnd';
|
|||||||
import { clamp } from '../math';
|
import { clamp } from '../math';
|
||||||
import { Direction } from '../gridview/baseComponentGridview';
|
import { Direction } from '../gridview/baseComponentGridview';
|
||||||
|
|
||||||
|
interface DropTargetRect {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGPUOptimizedBounds(element: HTMLElement, bounds: DropTargetRect): void {
|
||||||
|
const { top, left, width, height } = bounds;
|
||||||
|
const topPx = `${Math.round(top)}px`;
|
||||||
|
const leftPx = `${Math.round(left)}px`;
|
||||||
|
const widthPx = `${Math.round(width)}px`;
|
||||||
|
const heightPx = `${Math.round(height)}px`;
|
||||||
|
|
||||||
|
// Use traditional positioning but maintain GPU layer
|
||||||
|
element.style.top = topPx;
|
||||||
|
element.style.left = leftPx;
|
||||||
|
element.style.width = widthPx;
|
||||||
|
element.style.height = heightPx;
|
||||||
|
element.style.visibility = 'visible';
|
||||||
|
|
||||||
|
// Ensure GPU layer is maintained
|
||||||
|
if (!element.style.transform || element.style.transform === '') {
|
||||||
|
element.style.transform = 'translate3d(0, 0, 0)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGPUOptimizedBoundsFromStrings(element: HTMLElement, bounds: {
|
||||||
|
top: string;
|
||||||
|
left: string;
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
}): void {
|
||||||
|
const { top, left, width, height } = bounds;
|
||||||
|
|
||||||
|
// Use traditional positioning but maintain GPU layer
|
||||||
|
element.style.top = top;
|
||||||
|
element.style.left = left;
|
||||||
|
element.style.width = width;
|
||||||
|
element.style.height = height;
|
||||||
|
element.style.visibility = 'visible';
|
||||||
|
|
||||||
|
// Ensure GPU layer is maintained
|
||||||
|
if (!element.style.transform || element.style.transform === '') {
|
||||||
|
element.style.transform = 'translate3d(0, 0, 0)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkBoundsChanged(element: HTMLElement, bounds: DropTargetRect): boolean {
|
||||||
|
const { top, left, width, height } = bounds;
|
||||||
|
const topPx = `${Math.round(top)}px`;
|
||||||
|
const leftPx = `${Math.round(left)}px`;
|
||||||
|
const widthPx = `${Math.round(width)}px`;
|
||||||
|
const heightPx = `${Math.round(height)}px`;
|
||||||
|
|
||||||
|
// Check if position or size changed (back to traditional method)
|
||||||
|
return element.style.top !== topPx ||
|
||||||
|
element.style.left !== leftPx ||
|
||||||
|
element.style.width !== widthPx ||
|
||||||
|
element.style.height !== heightPx;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DroptargetEvent {
|
export interface DroptargetEvent {
|
||||||
readonly position: Position;
|
readonly position: Position;
|
||||||
readonly nativeEvent: DragEvent;
|
readonly nativeEvent: DragEvent;
|
||||||
@ -422,25 +484,12 @@ export class Droptarget extends CompositeDisposable {
|
|||||||
box.width = 4;
|
box.width = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
const topPx = `${Math.round(box.top)}px`;
|
// Use GPU-optimized bounds checking and setting
|
||||||
const leftPx = `${Math.round(box.left)}px`;
|
if (!checkBoundsChanged(overlay, box)) {
|
||||||
const widthPx = `${Math.round(box.width)}px`;
|
|
||||||
const heightPx = `${Math.round(box.height)}px`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
overlay.style.top === topPx &&
|
|
||||||
overlay.style.left === leftPx &&
|
|
||||||
overlay.style.width === widthPx &&
|
|
||||||
overlay.style.height === heightPx
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay.style.top = topPx;
|
setGPUOptimizedBounds(overlay, box);
|
||||||
overlay.style.left = leftPx;
|
|
||||||
overlay.style.width = widthPx;
|
|
||||||
overlay.style.height = heightPx;
|
|
||||||
overlay.style.visibility = 'visible';
|
|
||||||
|
|
||||||
overlay.className = `dv-drop-target-anchor${
|
overlay.className = `dv-drop-target-anchor${
|
||||||
this.options.className ? ` ${this.options.className}` : ''
|
this.options.className ? ` ${this.options.className}` : ''
|
||||||
@ -511,10 +560,7 @@ export class Droptarget extends CompositeDisposable {
|
|||||||
box.height = `${100 * size}%`;
|
box.height = `${100 * size}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.overlayElement.style.top = box.top;
|
setGPUOptimizedBoundsFromStrings(this.overlayElement, box);
|
||||||
this.overlayElement.style.left = box.left;
|
|
||||||
this.overlayElement.style.width = box.width;
|
|
||||||
this.overlayElement.style.height = box.height;
|
|
||||||
|
|
||||||
toggleClass(
|
toggleClass(
|
||||||
this.overlayElement,
|
this.overlayElement,
|
||||||
|
|||||||
@ -4,6 +4,10 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-width: thin; // firefox
|
scrollbar-width: thin; // firefox
|
||||||
|
|
||||||
|
/* GPU optimizations for smooth scrolling */
|
||||||
|
will-change: scroll-position;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
|
||||||
&.dv-horizontal {
|
&.dv-horizontal {
|
||||||
.dv-tab {
|
.dv-tab {
|
||||||
&:not(:first-child)::before {
|
&:not(:first-child)::before {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
.dv-dockview {
|
.dv-dockview {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--dv-group-view-background-color);
|
background-color: var(--dv-group-view-background-color);
|
||||||
|
contain: layout;
|
||||||
|
|
||||||
.dv-watermark-container {
|
.dv-watermark-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@ -70,6 +70,7 @@ import { AnchoredBox, AnchorPosition, Box } from '../types';
|
|||||||
import {
|
import {
|
||||||
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
|
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
|
||||||
DEFAULT_FLOATING_GROUP_POSITION,
|
DEFAULT_FLOATING_GROUP_POSITION,
|
||||||
|
DESERIALIZATION_POPOUT_DELAY_MS,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import {
|
import {
|
||||||
DockviewPanelRenderer,
|
DockviewPanelRenderer,
|
||||||
@ -351,6 +352,7 @@ export class DockviewComponent
|
|||||||
disposable: { dispose: () => DockviewGroupPanel | undefined };
|
disposable: { dispose: () => DockviewGroupPanel | undefined };
|
||||||
}[] = [];
|
}[] = [];
|
||||||
private readonly _rootDropTarget: Droptarget;
|
private readonly _rootDropTarget: Droptarget;
|
||||||
|
private _popoutRestorationPromise: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
private readonly _onDidRemoveGroup = new Emitter<DockviewGroupPanel>();
|
private readonly _onDidRemoveGroup = new Emitter<DockviewGroupPanel>();
|
||||||
readonly onDidRemoveGroup: Event<DockviewGroupPanel> =
|
readonly onDidRemoveGroup: Event<DockviewGroupPanel> =
|
||||||
@ -407,6 +409,14 @@ export class DockviewComponent
|
|||||||
return this._floatingGroups;
|
return this._floatingGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise that resolves when all popout groups from the last fromJSON call are restored.
|
||||||
|
* Useful for tests that need to wait for delayed popout creation.
|
||||||
|
*/
|
||||||
|
get popoutRestorationPromise(): Promise<void> {
|
||||||
|
return this._popoutRestorationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(container: HTMLElement, options: DockviewComponentOptions) {
|
constructor(container: HTMLElement, options: DockviewComponentOptions) {
|
||||||
super(container, {
|
super(container, {
|
||||||
proportionalLayout: true,
|
proportionalLayout: true,
|
||||||
@ -1524,12 +1534,19 @@ export class DockviewComponent
|
|||||||
|
|
||||||
const serializedPopoutGroups = data.popoutGroups ?? [];
|
const serializedPopoutGroups = data.popoutGroups ?? [];
|
||||||
|
|
||||||
for (const serializedPopoutGroup of serializedPopoutGroups) {
|
// Create a promise that resolves when all popout groups are created
|
||||||
|
const popoutPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
// Queue popup group creation with delays to avoid browser blocking
|
||||||
|
serializedPopoutGroups.forEach((serializedPopoutGroup, index) => {
|
||||||
const { data, position, gridReferenceGroup, url } =
|
const { data, position, gridReferenceGroup, url } =
|
||||||
serializedPopoutGroup;
|
serializedPopoutGroup;
|
||||||
|
|
||||||
const group = createGroupFromSerializedState(data);
|
const group = createGroupFromSerializedState(data);
|
||||||
|
|
||||||
|
// Add a small delay for each popup after the first to avoid browser popup blocking
|
||||||
|
const popoutPromise = new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
this.addPopoutGroup(group, {
|
this.addPopoutGroup(group, {
|
||||||
position: position ?? undefined,
|
position: position ?? undefined,
|
||||||
overridePopoutGroup: gridReferenceGroup ? group : undefined,
|
overridePopoutGroup: gridReferenceGroup ? group : undefined,
|
||||||
@ -1538,7 +1555,15 @@ export class DockviewComponent
|
|||||||
: undefined,
|
: undefined,
|
||||||
popoutUrl: url,
|
popoutUrl: url,
|
||||||
});
|
});
|
||||||
}
|
resolve();
|
||||||
|
}, index * DESERIALIZATION_POPOUT_DELAY_MS); // 100ms delay between each popup
|
||||||
|
});
|
||||||
|
|
||||||
|
popoutPromises.push(popoutPromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the promise for tests to wait on
|
||||||
|
this._popoutRestorationPromise = Promise.all(popoutPromises).then(() => void 0);
|
||||||
|
|
||||||
for (const floatingGroup of this._floatingGroups) {
|
for (const floatingGroup of this._floatingGroups) {
|
||||||
floatingGroup.overlay.setBounds();
|
floatingGroup.overlay.setBounds();
|
||||||
|
|||||||
@ -34,12 +34,19 @@
|
|||||||
border: 1px solid var(--dv-tab-divider-color);
|
border: 1px solid var(--dv-tab-divider-color);
|
||||||
box-shadow: var(--dv-floating-box-shadow);
|
box-shadow: var(--dv-floating-box-shadow);
|
||||||
|
|
||||||
|
/* GPU optimizations for floating group movement */
|
||||||
|
will-change: transform, opacity;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
|
||||||
&.dv-hidden {
|
&.dv-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dv-resize-container-dragging {
|
&.dv-resize-container-dragging {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
/* Enhanced GPU acceleration during drag */
|
||||||
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dv-resize-handle-top {
|
.dv-resize-handle-top {
|
||||||
|
|||||||
@ -3,7 +3,15 @@
|
|||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
contain: layout paint;
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
/* GPU optimizations */
|
||||||
|
will-change: transform;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
|
||||||
&.dv-render-overlay-float {
|
&.dv-render-overlay-float {
|
||||||
z-index: calc(var(--dv-overlay-z-index) - 1);
|
z-index: calc(var(--dv-overlay-z-index) - 1);
|
||||||
|
|||||||
@ -10,6 +10,36 @@ import {
|
|||||||
import { IDockviewPanel } from '../dockview/dockviewPanel';
|
import { IDockviewPanel } from '../dockview/dockviewPanel';
|
||||||
import { DockviewComponent } from '../dockview/dockviewComponent';
|
import { DockviewComponent } from '../dockview/dockviewComponent';
|
||||||
|
|
||||||
|
class PositionCache {
|
||||||
|
private cache = new Map<Element, { rect: { left: number; top: number; width: number; height: number }; frameId: number }>();
|
||||||
|
private currentFrameId = 0;
|
||||||
|
private rafId: number | null = null;
|
||||||
|
|
||||||
|
getPosition(element: Element): { left: number; top: number; width: number; height: number } {
|
||||||
|
const cached = this.cache.get(element);
|
||||||
|
if (cached && cached.frameId === this.currentFrameId) {
|
||||||
|
return cached.rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleFrameUpdate();
|
||||||
|
const rect = getDomNodePagePosition(element);
|
||||||
|
this.cache.set(element, { rect, frameId: this.currentFrameId });
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate(): void {
|
||||||
|
this.currentFrameId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleFrameUpdate() {
|
||||||
|
if (this.rafId) return;
|
||||||
|
this.rafId = requestAnimationFrame(() => {
|
||||||
|
this.currentFrameId++;
|
||||||
|
this.rafId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type DockviewPanelRenderer = 'onlyWhenVisible' | 'always';
|
export type DockviewPanelRenderer = 'onlyWhenVisible' | 'always';
|
||||||
|
|
||||||
export interface IRenderable {
|
export interface IRenderable {
|
||||||
@ -35,6 +65,8 @@ export class OverlayRenderContainer extends CompositeDisposable {
|
|||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
private _disposed = false;
|
private _disposed = false;
|
||||||
|
private readonly positionCache = new PositionCache();
|
||||||
|
private readonly pendingUpdates = new Set<string>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly element: HTMLElement,
|
readonly element: HTMLElement,
|
||||||
@ -94,23 +126,46 @@ export class OverlayRenderContainer extends CompositeDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
// TODO propagate position to avoid getDomNodePagePosition calls, possible performance bottleneck?
|
const panelId = panel.api.id;
|
||||||
const box = getDomNodePagePosition(referenceContainer.element);
|
|
||||||
const box2 = getDomNodePagePosition(this.element);
|
if (this.pendingUpdates.has(panelId)) {
|
||||||
focusContainer.style.left = `${box.left - box2.left}px`;
|
return; // Update already scheduled
|
||||||
focusContainer.style.top = `${box.top - box2.top}px`;
|
}
|
||||||
focusContainer.style.width = `${box.width}px`;
|
|
||||||
focusContainer.style.height = `${box.height}px`;
|
this.pendingUpdates.add(panelId);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.pendingUpdates.delete(panelId);
|
||||||
|
|
||||||
|
if (this.isDisposed || !this.map[panelId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = this.positionCache.getPosition(referenceContainer.element);
|
||||||
|
const box2 = this.positionCache.getPosition(this.element);
|
||||||
|
|
||||||
|
// Use traditional positioning for overlay containers
|
||||||
|
const left = box.left - box2.left;
|
||||||
|
const top = box.top - box2.top;
|
||||||
|
const width = box.width;
|
||||||
|
const height = box.height;
|
||||||
|
|
||||||
|
focusContainer.style.left = `${left}px`;
|
||||||
|
focusContainer.style.top = `${top}px`;
|
||||||
|
focusContainer.style.width = `${width}px`;
|
||||||
|
focusContainer.style.height = `${height}px`;
|
||||||
|
|
||||||
toggleClass(
|
toggleClass(
|
||||||
focusContainer,
|
focusContainer,
|
||||||
'dv-render-overlay-float',
|
'dv-render-overlay-float',
|
||||||
panel.group.api.location.type === 'floating'
|
panel.group.api.location.type === 'floating'
|
||||||
);
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const visibilityChanged = () => {
|
const visibilityChanged = () => {
|
||||||
if (panel.api.isVisible) {
|
if (panel.api.isVisible) {
|
||||||
|
this.positionCache.invalidate();
|
||||||
resize();
|
resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,12 @@
|
|||||||
|
|
||||||
&.dv-animated {
|
&.dv-animated {
|
||||||
.dv-view {
|
.dv-view {
|
||||||
transition-duration: 0.15s;
|
/* GPU optimizations for smooth pane animations */
|
||||||
transition-timing-function: ease-out;
|
will-change: transform;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
|
||||||
|
transition: transform 0.15s ease-out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dv-view {
|
.dv-view {
|
||||||
|
|||||||
@ -9,6 +9,12 @@
|
|||||||
height: 4px;
|
height: 4px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
|
/* GPU optimizations */
|
||||||
|
will-change: background-color, transform;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
|
||||||
transition-property: background-color;
|
transition-property: background-color;
|
||||||
transition-timing-function: ease-in-out;
|
transition-timing-function: ease-in-out;
|
||||||
transition-duration: 1s;
|
transition-duration: 1s;
|
||||||
|
|||||||
@ -34,8 +34,12 @@
|
|||||||
&.dv-animation {
|
&.dv-animation {
|
||||||
.dv-view,
|
.dv-view,
|
||||||
.dv-sash {
|
.dv-sash {
|
||||||
transition-duration: 0.15s;
|
/* GPU optimizations for smooth animations */
|
||||||
transition-timing-function: ease-out;
|
will-change: transform;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
|
||||||
|
transition: transform 0.15s ease-out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user