diff --git a/packages/dockview-core/src/__tests__/gridview/gridview.spec.ts b/packages/dockview-core/src/__tests__/gridview/gridview.spec.ts index 4350aa04a..c0f0f1f65 100644 --- a/packages/dockview-core/src/__tests__/gridview/gridview.spec.ts +++ b/packages/dockview-core/src/__tests__/gridview/gridview.spec.ts @@ -19,13 +19,17 @@ class MockGridview implements IGridView { >().event; element: HTMLElement = document.createElement('div'); + width: number = 0; + height: number = 0; + constructor(private id?: string) { this.element.className = 'mock-grid-view'; this.element.id = `${id ?? ''}`; } layout(width: number, height: number): void { - // + this.width = width; + this.height = height; } toJSON(): object { @@ -760,4 +764,255 @@ describe('gridview', () => { el = gridview.element.querySelectorAll('.mock-grid-view'); expect(el.length).toBe(5); }); + + test('gridview nested proportional layouts', () => { + const gridview = new Gridview( + true, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + const view1 = new MockGridview('1'); + const view2 = new MockGridview('2'); + const view3 = new MockGridview('3'); + const view4 = new MockGridview('4'); + const view5 = new MockGridview('5'); + const view6 = new MockGridview('6'); + + gridview.addView(view1, Sizing.Distribute, [0]); + gridview.addView(view2, Sizing.Distribute, [1]); + gridview.addView(view3, Sizing.Distribute, [1, 1]); + gridview.addView(view4, Sizing.Distribute, [1, 1, 0]); + gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]); + gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]); + + const views = [view1, view2, view3, view4, view5, view6]; + + const dimensions = [ + { width: 500, height: 1000 }, + { width: 500, height: 500 }, + { width: 250, height: 500 }, + { width: 250, height: 250 }, + { width: 125, height: 250 }, + { width: 125, height: 250 }, + ]; + + expect( + views.map((view) => ({ + width: view.width, + height: view.height, + })) + ).toEqual(dimensions); + + gridview.layout(2000, 1500); + + expect( + views.map((view) => ({ + width: view.width, + height: view.height, + })) + ).toEqual( + dimensions.map(({ width, height }) => ({ + width: width * 2, + height: height * 1.5, + })) + ); + + gridview.layout(200, 2000); + + expect( + views.map((view) => ({ + width: view.width, + height: view.height, + })) + ).toEqual( + dimensions.map(({ width, height }) => ({ + width: width * 0.2, + height: height * 2, + })) + ); + }); + + test('that maximizeView retains original dimensions when restored', () => { + const gridview = new Gridview( + true, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + let counter = 0; + const subscription = gridview.onDidMaxmizedNodeChange(() => { + counter++; + }); + + const view1 = new MockGridview('1'); + const view2 = new MockGridview('2'); + const view3 = new MockGridview('3'); + const view4 = new MockGridview('4'); + const view5 = new MockGridview('5'); + const view6 = new MockGridview('6'); + + gridview.addView(view1, Sizing.Distribute, [0]); + gridview.addView(view2, Sizing.Distribute, [1]); + gridview.addView(view3, Sizing.Distribute, [1, 1]); + gridview.addView(view4, Sizing.Distribute, [1, 1, 0]); + gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]); + gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]); + + /** + * _____________________________________________ + * | | | + * | | 2 | + * | | | + * | 1 |_______________________| + * | | | 4 | + * | | 3 |_____________| + * | | | 5 | 6 | + * |_____________________|_________|______|______| + */ + + const views = [view1, view2, view3, view4, view5, view6]; + + const dimensions = [ + { width: 500, height: 1000 }, + { width: 500, height: 500 }, + { width: 250, height: 500 }, + { width: 250, height: 250 }, + { width: 125, height: 250 }, + { width: 125, height: 250 }, + ]; + + function assertLayout() { + expect( + views.map((view) => ({ + width: view.width, + height: view.height, + })) + ).toEqual(dimensions); + } + + // base case assertions + assertLayout(); + expect(gridview.hasMaximizedView()).toBeFalsy(); + expect(counter).toBe(0); + + /** + * maximize each view individually and then return to the standard view + * checking on each iteration that the original layout dimensions + * are restored + */ + for (let i = 0; i < views.length; i++) { + const view = views[i]; + + gridview.maximizeView(view); + expect(counter).toBe(i * 2 + 1); + expect(gridview.hasMaximizedView()).toBeTruthy(); + gridview.exitMaximizedView(); + expect(counter).toBe(i * 2 + 2); + assertLayout(); + } + + subscription.dispose(); + }); + + test('that maximizedView is exited when a views visibility is changed', () => { + const gridview = new Gridview( + true, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + const view1 = new MockGridview('1'); + const view2 = new MockGridview('2'); + const view3 = new MockGridview('3'); + + gridview.addView(view1, Sizing.Distribute, [0]); + gridview.addView(view2, Sizing.Distribute, [1]); + gridview.addView(view3, Sizing.Distribute, [1, 1]); + + expect(gridview.hasMaximizedView()).toBeFalsy(); + gridview.maximizeView(view2); + expect(gridview.hasMaximizedView()).toBeTruthy(); + + gridview.setViewVisible([0], true); + expect(gridview.hasMaximizedView()).toBeFalsy(); + }); + + test('that maximizedView is exited when a view is moved', () => { + const gridview = new Gridview( + true, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + const view1 = new MockGridview('1'); + const view2 = new MockGridview('2'); + const view3 = new MockGridview('3'); + const view4 = new MockGridview('4'); + + gridview.addView(view1, Sizing.Distribute, [0]); + gridview.addView(view2, Sizing.Distribute, [1]); + gridview.addView(view3, Sizing.Distribute, [1, 1]); + gridview.addView(view4, Sizing.Distribute, [1, 1, 0]); + + expect(gridview.hasMaximizedView()).toBeFalsy(); + gridview.maximizeView(view2); + expect(gridview.hasMaximizedView()).toBeTruthy(); + + gridview.moveView([1, 1], 0, 1); + expect(gridview.hasMaximizedView()).toBeFalsy(); + }); + + test('that maximizedView is exited when a view is added', () => { + const gridview = new Gridview( + true, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + const view1 = new MockGridview('1'); + const view2 = new MockGridview('2'); + const view3 = new MockGridview('3'); + const view4 = new MockGridview('4'); + + gridview.addView(view1, Sizing.Distribute, [0]); + gridview.addView(view2, Sizing.Distribute, [1]); + gridview.addView(view3, Sizing.Distribute, [1, 1]); + + expect(gridview.hasMaximizedView()).toBeFalsy(); + gridview.maximizeView(view2); + expect(gridview.hasMaximizedView()).toBeTruthy(); + + gridview.addView(view4, Sizing.Distribute, [1, 1, 0]); + expect(gridview.hasMaximizedView()).toBeFalsy(); + }); + + test('that maximizedView is exited when a view is removed', () => { + const gridview = new Gridview( + true, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + const view1 = new MockGridview('1'); + const view2 = new MockGridview('2'); + const view3 = new MockGridview('3'); + + gridview.addView(view1, Sizing.Distribute, [0]); + gridview.addView(view2, Sizing.Distribute, [1]); + gridview.addView(view3, Sizing.Distribute, [1, 1]); + + expect(gridview.hasMaximizedView()).toBeFalsy(); + gridview.maximizeView(view2); + expect(gridview.hasMaximizedView()).toBeTruthy(); + + gridview.removeView([1, 1]); + expect(gridview.hasMaximizedView()).toBeFalsy(); + }); }); diff --git a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts index c277d8680..8b075d620 100644 --- a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts @@ -9,12 +9,18 @@ export interface DockviewGroupPanelApi extends GridviewPanelApi { readonly onDidRenderPositionChange: Event; readonly location: DockviewGroupLocation; moveTo(options: { group: DockviewGroupPanel; position?: Position }): void; + maximize(): void; + isMaximized(): boolean; + exitMaximized(): void; } export interface DockviewGroupPanelFloatingChangeEvent { readonly location: DockviewGroupLocation; } +// TODO find a better way to initialize and avoid needing null checks +const NOT_INITIALIZED_MESSAGE = 'DockviewGroupPanelApiImpl not initialized'; + export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { private _group: DockviewGroupPanel | undefined; @@ -25,7 +31,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { get location(): DockviewGroupLocation { if (!this._group) { - throw new Error(`DockviewGroupPanelApiImpl not initialized`); + throw new Error(NOT_INITIALIZED_MESSAGE); } return this._group.model.location; } @@ -38,7 +44,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { moveTo(options: { group: DockviewGroupPanel; position?: Position }): void { if (!this._group) { - throw new Error(`DockviewGroupPanelApiImpl not initialized`); + throw new Error(NOT_INITIALIZED_MESSAGE); } this.accessor.moveGroupOrPanel( @@ -49,6 +55,32 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { ); } + maximize(): void { + if (!this._group) { + throw new Error(NOT_INITIALIZED_MESSAGE); + } + + this.accessor.maximizeGroup(this._group); + } + + isMaximized(): boolean { + if (!this._group) { + throw new Error(NOT_INITIALIZED_MESSAGE); + } + + return this.accessor.isMaximizedGroup(this._group); + } + + exitMaximized(): void { + if (!this._group) { + throw new Error(NOT_INITIALIZED_MESSAGE); + } + + if (this.isMaximized()) { + this.accessor.exitMaximizedGroup(); + } + } + initialize(group: DockviewGroupPanel): void { this._group = group; } diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index 793430e7f..51ce82591 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -15,13 +15,10 @@ export interface RendererChangedEvent { renderer: DockviewPanelRenderer; } -/* - * omit visibility modifiers since the visibility of a single group doesn't make sense - * because it belongs to a groupview - */ export interface DockviewPanelApi extends Omit< GridviewPanelApi, + // omit properties that do not make sense here 'setVisible' | 'onDidConstraintsChange' | 'setConstraints' > { readonly group: DockviewGroupPanel; @@ -40,6 +37,8 @@ export interface DockviewPanelApi index?: number; }): void; maximize(): void; + isMaximized(): boolean; + exitMaximized(): void; } export class DockviewPanelApiImpl @@ -67,7 +66,7 @@ export class DockviewPanelApiImpl } get isGroupActive(): boolean { - return !!this.group?.isActive; + return this.group.isActive; } get renderer(): DockviewPanelRenderer { @@ -143,6 +142,14 @@ export class DockviewPanelApiImpl } maximize(): void { - this.accessor.maximizeGroup(this.panel.group); + this.group.api.maximize(); + } + + isMaximized(): boolean { + return this.group.api.isMaximized(); + } + + exitMaximized(): void { + this.group.api.exitMaximized(); } } diff --git a/packages/dockview-core/src/constants.ts b/packages/dockview-core/src/constants.ts new file mode 100644 index 000000000..b8b9fd3f6 --- /dev/null +++ b/packages/dockview-core/src/constants.ts @@ -0,0 +1,3 @@ +export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; + +export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100 }; diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index bae636de8..7ea9deb92 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -58,6 +58,10 @@ import { DockviewPanelRenderer, } from './components/greadyRenderContainer'; import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel'; +import { + DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, + DEFAULT_FLOATING_GROUP_POSITION, +} from '../constants'; function getTheme(element: HTMLElement): string | undefined { function toClassList(element: HTMLElement) { @@ -86,8 +90,6 @@ function getTheme(element: HTMLElement): string | undefined { return theme; } -const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; - export interface PanelReference { update: (event: { params: { [key: string]: any } }) => void; remove: () => void; @@ -582,9 +584,13 @@ export class DockviewComponent group.model.location = 'floating'; const overlayLeft = - typeof coord?.x === 'number' ? Math.max(coord.x, 0) : 100; + typeof coord?.x === 'number' + ? Math.max(coord.x, 0) + : DEFAULT_FLOATING_GROUP_POSITION.left; const overlayTop = - typeof coord?.y === 'number' ? Math.max(coord.y, 0) : 100; + typeof coord?.y === 'number' + ? Math.max(coord.y, 0) + : DEFAULT_FLOATING_GROUP_POSITION.top; const overlay = new Overlay({ container: this.gridview.element, diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index 7524b4120..2e4159a9b 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -65,6 +65,7 @@ export interface IBaseGrid { setVisible(panel: T, visible: boolean): void; isVisible(panel: T): boolean; maximizeGroup(panel: T): void; + isMaximizedGroup(panel: T): boolean; exitMaximizedGroup(): void; hasMaximizedGroup(): boolean; readonly onDidMaxmizedGroupChange: Event; @@ -182,6 +183,10 @@ export abstract class BaseGrid this.gridview.maximizeView(panel); } + isMaximizedGroup(panel: T): boolean { + return this.gridview.maximizedView() === panel; + } + exitMaximizedGroup(): void { this.gridview.exitMaximizedView(); } diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index 9b9dae231..1133b2502 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -273,7 +273,7 @@ export class Gridview implements IDisposable { readonly element: HTMLElement; private _root: BranchNode | undefined; - private _maximizedNode: Node | undefined = undefined; + private _maximizedNode: LeafNode | undefined = undefined; private readonly disposable: MutableDisposable = new MutableDisposable(); private readonly _onDidChange = new Emitter<{ @@ -307,6 +307,7 @@ export class Gridview implements IDisposable { get width(): number { return this.root.width; } + get height(): number { return this.root.height; } @@ -314,16 +315,23 @@ export class Gridview implements IDisposable { get minimumWidth(): number { return this.root.minimumWidth; } + get minimumHeight(): number { return this.root.minimumHeight; } + get maximumWidth(): number { return this.root.maximumHeight; } + get maximumHeight(): number { return this.root.maximumHeight; } + maximizedView(): IGridView | undefined { + return this._maximizedNode?.view; + } + hasMaximizedView(): boolean { return this._maximizedNode !== undefined; } @@ -332,6 +340,10 @@ export class Gridview implements IDisposable { const location = getGridLocation(view.element); const [_, node] = this.getNode(location); + if (!(node instanceof LeafNode)) { + return; + } + if (this._maximizedNode === node) { return; } @@ -353,7 +365,7 @@ export class Gridview implements IDisposable { } } - hideAllViewsBut(this.root, node as LeafNode); + hideAllViewsBut(this.root, node); this._maximizedNode = node; this._onDidMaxmizedNodeChange.fire(); }