diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 2067824e4..d4a8aae02 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -806,6 +806,22 @@ export class DockviewApi implements CommonApi { this.component.moveToPrevious(options); } + maximizeGroup(panel: IDockviewPanel): void { + this.component.maximizeGroup(panel.group); + } + + hasMaximizedGroup(): boolean { + return this.component.hasMaximizedGroup(); + } + + exitMaxmizedGroup(): void { + this.component.exitMaximizedGroup(); + } + + get onDidMaxmizedGroupChange(): Event { + return this.component.onDidMaxmizedGroupChange; + } + /** * Add a popout group in a new Window */ diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index b6eb47706..38544b684 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -39,6 +39,7 @@ export interface DockviewPanelApi position?: Position; index?: number; }): void; + maximize(): void; } export class DockviewPanelApiImpl @@ -140,4 +141,8 @@ export class DockviewPanelApiImpl close(): void { this.group.model.closePanel(this.panel); } + + maximize(): void { + this.accessor.maximizeGroup(this.panel.group); + } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts index 6823cbd4d..9388ced17 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts @@ -68,8 +68,8 @@ export class DockviewGroupPanel id, 'groupview_default', { - minimumHeight: 100, - minimumWidth: 100, + minimumHeight: 0, + minimumWidth: 0, }, new DockviewGroupPanelApiImpl(id, accessor) ); diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index 2e9ff31c6..7524b4120 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -64,6 +64,10 @@ export interface IBaseGrid { layout(width: number, height: number, force?: boolean): void; setVisible(panel: T, visible: boolean): void; isVisible(panel: T): boolean; + maximizeGroup(panel: T): void; + exitMaximizedGroup(): void; + hasMaximizedGroup(): boolean; + readonly onDidMaxmizedGroupChange: Event; } export abstract class BaseGrid @@ -174,6 +178,22 @@ export abstract class BaseGrid return this.gridview.isViewVisible(getGridLocation(panel.element)); } + maximizeGroup(panel: T): void { + this.gridview.maximizeView(panel); + } + + exitMaximizedGroup(): void { + this.gridview.exitMaximizedView(); + } + + hasMaximizedGroup(): boolean { + return this.gridview.hasMaximizedView(); + } + + get onDidMaxmizedGroupChange(): Event { + return this.gridview.onDidMaxmizedNodeChange; + } + protected doAddGroup( group: T, location: number[] = [0], diff --git a/packages/dockview-core/src/gridview/branchNode.ts b/packages/dockview-core/src/gridview/branchNode.ts index f354a8a52..8ba96ef05 100644 --- a/packages/dockview-core/src/gridview/branchNode.ts +++ b/packages/dockview-core/src/gridview/branchNode.ts @@ -33,6 +33,10 @@ export class BranchNode extends CompositeDisposable implements IView { readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> = this._onDidChange.event; + private readonly _onDidVisibilityChange = new Emitter(); + readonly onDidVisibilityChange: Event = + this._onDidVisibilityChange.event; + get width(): number { return this.orientation === Orientation.HORIZONTAL ? this.size @@ -48,11 +52,23 @@ export class BranchNode extends CompositeDisposable implements IView { get minimumSize(): number { return this.children.length === 0 ? 0 - : Math.max(...this.children.map((c) => c.minimumOrthogonalSize)); + : Math.max( + ...this.children.map((c, index) => + this.splitview.isViewVisible(index) + ? c.minimumOrthogonalSize + : 0 + ) + ); } get maximumSize(): number { - return Math.min(...this.children.map((c) => c.maximumOrthogonalSize)); + return Math.min( + ...this.children.map((c, index) => + this.splitview.isViewVisible(index) + ? c.maximumOrthogonalSize + : Number.POSITIVE_INFINITY + ) + ); } get minimumOrthogonalSize(): number { @@ -163,6 +179,7 @@ export class BranchNode extends CompositeDisposable implements IView { this.addDisposables( this._onDidChange, + this._onDidVisibilityChange, this.splitview.onDidSashEnd(() => { this._onDidChange.fire({}); }) @@ -185,7 +202,7 @@ export class BranchNode extends CompositeDisposable implements IView { return this.splitview.isViewVisible(index); } - setChildVisible(index: number, visible: boolean): void { + setChildVisible(index: number, visible: boolean): void { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } @@ -194,7 +211,18 @@ export class BranchNode extends CompositeDisposable implements IView { return; } + const wereAllChildrenHidden = this.splitview.contentSize === 0; this.splitview.setViewVisible(index, visible); + const areAllChildrenHidden = this.splitview.contentSize === 0; + + // If all children are hidden then the parent should hide the entire splitview + // If the entire splitview is hidden then the parent should show the splitview when a child is shown + if ( + (visible && wereAllChildrenHidden) || + (!visible && areAllChildrenHidden) + ) { + this._onDidVisibilityChange.fire(visible); + } } moveChild(from: number, to: number): void { @@ -285,15 +313,23 @@ export class BranchNode extends CompositeDisposable implements IView { private setupChildrenEvents(): void { this._childrenDisposable.dispose(); - this._childrenDisposable = Event.any( - ...this.children.map((c) => c.onDidChange) - )((e) => { - /** - * indicate a change has occured to allows any re-rendering but don't bubble - * event because that was specific to this branch - */ - this._onDidChange.fire({ size: e.orthogonalSize }); - }); + this._childrenDisposable = new CompositeDisposable( + Event.any(...this.children.map((c) => c.onDidChange))((e) => { + /** + * indicate a change has occured to allows any re-rendering but don't bubble + * event because that was specific to this branch + */ + this._onDidChange.fire({ size: e.orthogonalSize }); + }), + ...this.children.map((c, i) => { + if (c instanceof BranchNode) { + return c.onDidVisibilityChange((visible) => { + this.setChildVisible(i, visible); + }); + } + return Disposable.NONE; + }) + ); } public dispose(): void { diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index 477286f20..9b9dae231 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -270,9 +270,11 @@ export interface SerializedGridview { } export class Gridview implements IDisposable { + readonly element: HTMLElement; + private _root: BranchNode | undefined; - public readonly element: HTMLElement; - private disposable: MutableDisposable = new MutableDisposable(); + private _maximizedNode: Node | undefined = undefined; + private readonly disposable: MutableDisposable = new MutableDisposable(); private readonly _onDidChange = new Emitter<{ size?: number; @@ -281,6 +283,9 @@ export class Gridview implements IDisposable { readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> = this._onDidChange.event; + private readonly _onDidMaxmizedNodeChange = new Emitter(); + readonly onDidMaxmizedNodeChange = this._onDidMaxmizedNodeChange.event; + public get length(): number { return this._root ? this._root.children.length : 0; } @@ -319,6 +324,62 @@ export class Gridview implements IDisposable { return this.root.maximumHeight; } + hasMaximizedView(): boolean { + return this._maximizedNode !== undefined; + } + + maximizeView(view: IGridView): void { + const location = getGridLocation(view.element); + const [_, node] = this.getNode(location); + + if (this._maximizedNode === node) { + return; + } + + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + + function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void { + for (let i = 0; i < parent.children.length; i++) { + const child = parent.children[i]; + if (child instanceof LeafNode) { + if (child !== exclude) { + parent.setChildVisible(i, false); + } + } else { + hideAllViewsBut(child, exclude); + } + } + } + + hideAllViewsBut(this.root, node as LeafNode); + this._maximizedNode = node; + this._onDidMaxmizedNodeChange.fire(); + } + + exitMaximizedView(): void { + if (!this._maximizedNode) { + return; + } + + function showViewsInReverseOrder(parent: BranchNode): void { + for (let index = parent.children.length - 1; index >= 0; index--) { + const child = parent.children[index]; + if (child instanceof LeafNode) { + parent.setChildVisible(index, true); + } else { + showViewsInReverseOrder(child); + } + } + } + + showViewsInReverseOrder(this.root); + + this._maximizedNode = undefined; + this._onDidMaxmizedNodeChange.fire(); + } + public serialize(): SerializedGridview { const root = serializeBranchNode(this.getView(), this.orientation); @@ -333,6 +394,7 @@ export class Gridview implements IDisposable { public dispose(): void { this.disposable.dispose(); this._onDidChange.dispose(); + this._onDidMaxmizedNodeChange.dispose(); this.root.dispose(); this.element.remove(); @@ -584,6 +646,10 @@ export class Gridview implements IDisposable { } setViewVisible(location: number[], visible: boolean): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + const [rest, index] = tail(location); const [, parent] = this.getNode(rest); @@ -595,6 +661,10 @@ export class Gridview implements IDisposable { } public moveView(parentLocation: number[], from: number, to: number): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + const [, parent] = this.getNode(parentLocation); if (!(parent instanceof BranchNode)) { @@ -609,6 +679,10 @@ export class Gridview implements IDisposable { size: number | Sizing, location: number[] ): void { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); @@ -670,6 +744,10 @@ export class Gridview implements IDisposable { } removeView(location: number[], sizing?: Sizing): IGridView { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + } + const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); diff --git a/packages/dockview-core/src/gridview/leafNode.ts b/packages/dockview-core/src/gridview/leafNode.ts index 3abdb57ea..017d4e06b 100644 --- a/packages/dockview-core/src/gridview/leafNode.ts +++ b/packages/dockview-core/src/gridview/leafNode.ts @@ -121,7 +121,6 @@ export class LeafNode implements IView { public setVisible(visible: boolean): void { if (this.view.setVisible) { this.view.setVisible(visible); - this._onDidChange.fire({}); } } diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 0d56cce24..c8b3fdf66 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -104,8 +104,8 @@ export class Splitview { private _orientation: Orientation; private _size = 0; private _orthogonalSize = 0; - private contentSize = 0; - private _proportions: number[] | undefined = undefined; + private _contentSize = 0; + private _proportions: (number | undefined)[] | undefined = undefined; private proportionalLayout: boolean; private _startSnappingEnabled = true; private _endSnappingEnabled = true; @@ -117,6 +117,10 @@ export class Splitview { private readonly _onDidRemoveView = new Emitter(); readonly onDidRemoveView = this._onDidRemoveView.event; + get contentSize(): number { + return this._contentSize; + } + get size(): number { return this._size; } @@ -137,7 +141,7 @@ export class Splitview { return this.viewItems.length; } - public get proportions(): number[] | undefined { + public get proportions(): (number | undefined)[] | undefined { return this._proportions ? [...this._proportions] : undefined; } @@ -242,7 +246,7 @@ export class Splitview { }); // Initialize content size and proportions for first layout - this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); + this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this.saveProportions(); } } @@ -654,7 +658,7 @@ export class Splitview { } public layout(size: number, orthogonalSize: number): void { - const previousSize = Math.max(this.size, this.contentSize); + const previousSize = Math.max(this.size, this._contentSize); this.size = size; this.orthogonalSize = orthogonalSize; @@ -675,14 +679,30 @@ export class Splitview { highPriorityIndexes ); } else { + let total = 0; + for (let i = 0; i < this.viewItems.length; i++) { const item = this.viewItems[i]; + const proportion = this.proportions[i]; - item.size = clamp( - Math.round(this.proportions[i] * size), - item.minimumSize, - item.maximumSize - ); + if (typeof proportion === 'number') { + total += proportion; + } else { + size -= item.size; + } + } + + for (let i = 0; i < this.viewItems.length; i++) { + const item = this.viewItems[i]; + const proportion = this.proportions[i]; + + if (typeof proportion === 'number' && total > 0) { + item.size = clamp( + Math.round((proportion * size) / total), + item.minimumSize, + item.maximumSize + ); + } } } @@ -747,15 +767,15 @@ export class Splitview { } private saveProportions(): void { - if (this.proportionalLayout && this.contentSize > 0) { - this._proportions = this.viewItems.map( - (i) => i.size / this.contentSize + if (this.proportionalLayout && this._contentSize > 0) { + this._proportions = this.viewItems.map((i) => + i.visible ? i.size / this._contentSize : undefined ); } } private layoutViews(): void { - this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); + this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); let sum = 0; const x: number[] = []; @@ -880,7 +900,7 @@ export class Splitview { } else if ( snappedAfter && collapsesDown[index] && - (position < this.contentSize || this.endSnappingEnabled) + (position < this._contentSize || this.endSnappingEnabled) ) { this.updateSash(sash, SashState.MAXIMUM); } else { diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index dc87e6a0d..bdb44f01a 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -396,6 +396,13 @@ props.containerApi.addPopoutGroup(props.api.group); /> +## Maximized Groups + +To maximize a group you can all + +```tsx +api.maxmimizeGroup(group); +``` ## Panels diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 447a18843..a837aaee7 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -109,29 +109,6 @@ const Icon = (props: { ); }; -const Button = () => { - const [position, setPosition] = React.useState< - { x: number; y: number } | undefined - >(undefined); - - const close = () => setPosition(undefined); - - const onClick = (event: React.MouseEvent) => { - setPosition({ x: event.pageX, y: event.pageY }); - }; - - return ( - <> - - {position && ( - -
hello
-
- )} - - ); -}; - const groupControlsComponents = { panel_1: () => { return ; @@ -147,6 +124,34 @@ const RightControls = (props: IDockviewHeaderActionsProps) => { return groupControlsComponents[props.activePanel.id]; }, [props.isGroupActive, props.activePanel]); + const [icon, setIcon] = React.useState( + props.containerApi.hasMaximizedGroup() + ? 'collapse_content' + : 'expand_content' + ); + + React.useEffect(() => { + const disposable = props.containerApi.onDidMaxmizedGroupChange(() => { + setIcon( + props.containerApi.hasMaximizedGroup() + ? 'collapse_content' + : 'expand_content' + ); + }); + + return () => { + disposable.dispose(); + }; + }, [props.containerApi]); + + const onClick = () => { + if (props.containerApi.hasMaximizedGroup()) { + props.containerApi.exitMaxmizedGroup(); + } else { + props.activePanel?.api.maximize(); + } + }; + return (
{ > {props.isGroupActive && } {Component && } -
); }; diff --git a/packages/docs/src/generated/api.output.json b/packages/docs/src/generated/api.output.json index d5d9a667f..4c3fdca77 100644 --- a/packages/docs/src/generated/api.output.json +++ b/packages/docs/src/generated/api.output.json @@ -359,11 +359,6 @@ "signature": "(options: AddPanelOptions): IDockviewPanel", "type": "method" }, - { - "name": "addPopoutGroup", - "signature": "(item: IDockviewPanel | DockviewGroupPanel, options?: { position: Box, skipRemoveGroup: boolean }): void", - "type": "method" - }, { "name": "clear", "comment": { @@ -1530,21 +1525,11 @@ "signature": "Event", "type": "property" }, - { - "name": "onDidRendererChange", - "signature": "Event", - "type": "property" - }, { "name": "onDidVisibilityChange", "signature": "Event", "type": "property" }, - { - "name": "renderer", - "signature": "DockviewPanelRenderer", - "type": "property" - }, { "name": "title", "signature": "string | undefined", @@ -1578,11 +1563,6 @@ "signature": "(): void", "type": "method" }, - { - "name": "setRenderer", - "signature": "(renderer: DockviewPanelRenderer): void", - "type": "method" - }, { "name": "setSize", "signature": "(event: SizeEvent): void", @@ -2025,16 +2005,6 @@ "signature": "PanelCollection>", "type": "property" }, - { - "name": "debug", - "signature": "boolean", - "type": "property" - }, - { - "name": "defaultRenderer", - "signature": "DockviewPanelRenderer", - "type": "property" - }, { "name": "defaultTabComponent", "signature": "FunctionComponent>",