diff --git a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts index ed02acf4c..631fa0407 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts @@ -772,4 +772,26 @@ describe('splitview', () => { view1.fireChangeEvent({ size: 300 }); expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); }); + + test('that margins are applied to view sizing', () => { + const splitview = new Splitview(container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: false, + margin: 24, + }); + splitview.layout(924, 500); + + const view1 = new Testview(0, 1000); + const view2 = new Testview(0, 1000, LayoutPriority.High); + const view3 = new Testview(0, 1000); + + splitview.addView(view1); + expect([view1.size]).toEqual([924]); + + splitview.addView(view2); + expect([view1.size, view2.size]).toEqual([450, 450]); // 450 + 24 + 450 = 924 + + splitview.addView(view3); + expect([view1.size, view2.size, view3.size]).toEqual([292, 292, 292]); // 292 + 24 + 292 + 24 + 292 = 924 + }); }); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 6d76f1225..713724ab1 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -876,4 +876,8 @@ export class DockviewApi implements CommonApi { ): Promise { return this.component.addPopoutGroup(item, options); } + + setGap(gap: number | undefined): void { + this.component.updateOptions({ gap }); + } } diff --git a/packages/dockview-core/src/dockview/dockviewComponent.scss b/packages/dockview-core/src/dockview/dockviewComponent.scss index f47d90bab..54a0e290d 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.scss +++ b/packages/dockview-core/src/dockview/dockviewComponent.scss @@ -14,41 +14,6 @@ .dv-overlay-render-container { position: relative; } - - .split-view-container { - &.horizontal { - > .view-container > .view { - &:not(:last-child) { - .groupview { - border-right: var(--dv-group-gap-size) solid transparent; - } - } - - &:not(:first-child) { - .groupview { - border-left: var(--dv-group-gap-size) solid transparent; - } - } - } - } - - &.vertical { - > .view-container > .view { - &:not(:last-child) { - .groupview { - border-bottom: var(--dv-group-gap-size) solid - transparent; - } - } - - &:not(:first-child) { - .groupview { - border-top: var(--dv-group-gap-size) solid transparent; - } - } - } - } - } } .groupview { diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 5374e626d..e64965f89 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -359,6 +359,7 @@ export class DockviewComponent parentElement: options.parentElement, disableAutoResizing: options.disableAutoResizing, locked: options.locked, + margin: options.gap, }); const gready = document.createElement('div'); @@ -1032,6 +1033,14 @@ export class DockviewComponent this._rootDropTarget.setOverlayModel(options.rootOverlayModel!); } + if (this.gridview.margin !== 0 && options.gap === undefined) { + this.gridview.margin = 0; + } + + if (typeof options.gap === 'number') { + this.gridview.margin = options.gap; + } + this.layout(this.gridview.width, this.gridview.height, true); } diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index e39bffe4d..e72862c56 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -49,6 +49,7 @@ export interface DockviewOptions { rootOverlayModel?: DroptargetOverlayModel; locked?: boolean; disableDnd?: boolean; + gap?: number; } export interface DockviewDndOverlayEvent { @@ -99,6 +100,7 @@ export const PROPERTY_KEYS: (keyof DockviewOptions)[] = (() => { rootOverlayModel: undefined, locked: undefined, disableDnd: undefined, + gap: undefined, }; return Object.keys(properties) as (keyof DockviewOptions)[]; diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index c1d04bf68..e3f3b327f 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -35,6 +35,7 @@ export interface BaseGridOptions { readonly parentElement: HTMLElement; readonly disableAutoResizing?: boolean; readonly locked?: boolean; + readonly margin?: number; } export interface IGridPanelView extends IGridView, IPanel { @@ -152,7 +153,9 @@ export abstract class BaseGrid this.gridview = new Gridview( !!options.proportionalLayout, options.styles, - options.orientation + options.orientation, + options.locked, + options.margin ); this.gridview.locked = !!options.locked; diff --git a/packages/dockview-core/src/gridview/branchNode.ts b/packages/dockview-core/src/gridview/branchNode.ts index ad6add4f7..76688d3eb 100644 --- a/packages/dockview-core/src/gridview/branchNode.ts +++ b/packages/dockview-core/src/gridview/branchNode.ts @@ -142,6 +142,20 @@ export class BranchNode extends CompositeDisposable implements IView { this.splitview.disabled = value; } + get margin(): number { + return this.splitview.margin; + } + + set margin(value: number) { + this.splitview.margin = value; + + this.children.forEach((child) => { + if (child instanceof BranchNode) { + child.margin = value; + } + }); + } + constructor( readonly orientation: Orientation, readonly proportionalLayout: boolean, @@ -149,6 +163,7 @@ export class BranchNode extends CompositeDisposable implements IView { size: number, orthogonalSize: number, disabled: boolean, + margin: number | undefined, childDescriptors?: INodeDescriptor[] ) { super(); @@ -163,6 +178,7 @@ export class BranchNode extends CompositeDisposable implements IView { orientation: this.orientation, proportionalLayout, styles, + margin, }); this.splitview.layout(this.size, this.orthogonalSize); } else { @@ -187,6 +203,7 @@ export class BranchNode extends CompositeDisposable implements IView { descriptor, proportionalLayout, styles, + margin, }); } diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index 8f0e10191..f2b4806cf 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -42,7 +42,8 @@ function flipNode( node.styles, size, orthogonalSize, - node.disabled + node.disabled, + node.margin ); let totalSize = 0; @@ -276,6 +277,7 @@ export class Gridview implements IDisposable { private _root: BranchNode | undefined; private _locked = false; + private _margin = 0; private _maximizedNode: | { leaf: LeafNode; hiddenOnMaximize: LeafNode[] } | undefined = undefined; @@ -360,6 +362,15 @@ export class Gridview implements IDisposable { } } + get margin(): number { + return this._margin; + } + + set margin(value: number) { + this._margin = value; + this.root.margin = value; + } + maximizedView(): IGridView | undefined { return this._maximizedNode?.leaf.view; } @@ -472,7 +483,8 @@ export class Gridview implements IDisposable { this.styles, this.root.size, this.root.orthogonalSize, - this._locked + this.locked, + this.margin ); } @@ -533,7 +545,8 @@ export class Gridview implements IDisposable { this.styles, node.size, // <- orthogonal size - flips at each depth orthogonalSize, // <- size - flips at each depth, - this._locked, + this.locked, + this.margin, children ); } else { @@ -586,7 +599,8 @@ export class Gridview implements IDisposable { this.styles, this.root.orthogonalSize, this.root.size, - this._locked + this.locked, + this.margin ); if (oldRoot.children.length === 0) { @@ -692,17 +706,24 @@ export class Gridview implements IDisposable { constructor( readonly proportionalLayout: boolean, readonly styles: ISplitviewStyles | undefined, - orientation: Orientation + orientation: Orientation, + locked?: boolean, + margin?: number ) { this.element = document.createElement('div'); this.element.className = 'grid-view'; + + this._locked = locked ?? false; + this._margin = margin ?? 0; + this.root = new BranchNode( orientation, proportionalLayout, styles, 0, 0, - this._locked + this.locked, + this.margin ); } @@ -789,7 +810,8 @@ export class Gridview implements IDisposable { this.styles, parent.size, parent.orthogonalSize, - this._locked + this.locked, + this.margin ); grandParent.addChild(newParent, parent.size, parentIndex); diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index c5e007cb6..6fd1c2f28 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -36,6 +36,7 @@ export interface SplitViewOptions { readonly descriptor?: ISplitViewDescriptor; readonly proportionalLayout?: boolean; readonly styles?: ISplitviewStyles; + readonly margin?: number; } export enum LayoutPriority { @@ -110,6 +111,7 @@ export class Splitview { private _startSnappingEnabled = true; private _endSnappingEnabled = true; private _disabled = false; + private _margin = 0; private readonly _onDidSashEnd = new Emitter(); readonly onDidSashEnd = this._onDidSashEnd.event; @@ -211,6 +213,14 @@ export class Splitview { toggleClass(this.element, 'dv-splitview-disabled', value); } + get margin(): number { + return this._margin; + } + + set margin(value: number) { + this._margin = value; + } + constructor( private readonly container: HTMLElement, options: SplitViewOptions @@ -218,6 +228,8 @@ export class Splitview { this._orientation = options.orientation; this.element = this.createContainer(); + this.margin = options.margin ?? 0; + this.proportionalLayout = options.proportionalLayout === undefined ? true @@ -781,18 +793,39 @@ export class Splitview { } } + /** + * Margin explain: + * + * For `n` views in a splitview there will be `n-1` margins `m`. + * + * To fit the margins each view must reduce in size by `(m * (n - 1)) / n`. + * + * For each view `i` the offet must be adjusted by `m * i/(n - 1)`. + */ private layoutViews(): void { this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); - let sum = 0; - const x: number[] = []; this.updateSashEnablement(); - for (let i = 0; i < this.viewItems.length - 1; i++) { - sum += this.viewItems[i].size; - x.push(sum); + if (this.viewItems.length === 0) { + return; + } - const offset = Math.min(Math.max(0, sum - 2), this.size - 4); + const sashCount = this.viewItems.length - 1; + const marginReducedSize = + (this.margin * sashCount) / this.viewItems.length; + + let totalLeftOffset = 0; + const viewLeftOffsets: number[] = []; + + for (let i = 0; i < this.viewItems.length - 1; i++) { + totalLeftOffset += this.viewItems[i].size; + viewLeftOffsets.push(totalLeftOffset); + + const offset = Math.min( + Math.max(0, totalLeftOffset - 2), + this.size - this.margin + ); if (this._orientation === Orientation.HORIZONTAL) { this.sashes[i].container.style.left = `${offset}px`; @@ -804,20 +837,30 @@ export class Splitview { } } this.viewItems.forEach((view, i) => { + const size = view.size - marginReducedSize; + const offset = + i === 0 + ? 0 + : viewLeftOffsets[i - 1] + + (i / sashCount) * marginReducedSize; + if (this._orientation === Orientation.HORIZONTAL) { - view.container.style.width = `${view.size}px`; - view.container.style.left = i == 0 ? '0px' : `${x[i - 1]}px`; + view.container.style.width = `${size}px`; + view.container.style.left = `${offset}px`; view.container.style.top = ''; view.container.style.height = ''; } if (this._orientation === Orientation.VERTICAL) { - view.container.style.height = `${view.size}px`; - view.container.style.top = i == 0 ? '0px' : `${x[i - 1]}px`; + view.container.style.height = `${size}px`; + view.container.style.top = `${offset}px`; view.container.style.width = ''; view.container.style.left = ''; } - view.view.layout(view.size, this._orthogonalSize); + view.view.layout( + view.size - marginReducedSize, + this._orthogonalSize + ); }); } diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index 007fc426e..47cfab4ce 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -229,8 +229,6 @@ } @mixin dockview-design-replit-mixin { - --dv-group-gap-size: 3px; - .dv-resize-container:has(> .groupview) { border-radius: 8px; } diff --git a/packages/dockview/src/__tests__/gridview/gridview.spec.tsx b/packages/dockview/src/__tests__/gridview/gridview.spec.tsx index bd8e3944c..c6037c743 100644 --- a/packages/dockview/src/__tests__/gridview/gridview.spec.tsx +++ b/packages/dockview/src/__tests__/gridview/gridview.spec.tsx @@ -9,10 +9,10 @@ import { import { setMockRefElement } from '../__test_utils__/utils'; describe('gridview react', () => { - let components: Record< + let components: Record< string, React.FunctionComponent - > + >; beforeEach(() => { components = { diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx index 993176577..29c6ae8d6 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx @@ -52,6 +52,12 @@ export const GridActions = (props: { props.api?.addGroup(); }; + const [gap, setGap] = React.useState(0); + + React.useEffect(() => { + props.api?.setGap(gap); + }, [gap, props.api]); + return (
+ +
+ Gap + setGap(Number(event.target.value))} + /> +
); };