diff --git a/README.md b/README.md index 3d0e6bfc..b17a35e2 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,12 @@ Maximum size of this pane. Overrides `maxSize` set on parent component. Minimum size of this pane. Overrides `minSize` set on parent component. +### priority + +The priority of the pane when the layout algorithm runs. Panes with higher priority will be resized first. + +Only used when `proportionalLayout` is false. + ### preferredSize Preferred size of this pane. Allotment will attempt to use this size when adding this pane (including on initial mount) as well as when a user double clicks a sash, or the `reset` method is called on the Allotment instance. diff --git a/src/allotment.tsx b/src/allotment.tsx index 0c31ab88..45a745fe 100644 --- a/src/allotment.tsx +++ b/src/allotment.tsx @@ -18,7 +18,12 @@ import { isIOS } from "./helpers/platform"; import { LayoutService } from "./layout-service"; import { PaneView } from "./pane-view"; import { Orientation, setGlobalSashSize } from "./sash"; -import { Sizing, SplitView, SplitViewOptions } from "./split-view"; +import { + LayoutPriority, + Sizing, + SplitView, + SplitViewOptions, +} from "./split-view"; function isPane(item: React.ReactNode): item is typeof Pane { return (item as any).type.displayName === "Allotment.Pane"; @@ -29,6 +34,7 @@ function isPaneProps(props: AllotmentProps | PaneProps): props is PaneProps { (props as PaneProps).minSize !== undefined || (props as PaneProps).maxSize !== undefined || (props as PaneProps).preferredSize !== undefined || + (props as PaneProps).priority !== undefined || (props as PaneProps).visible !== undefined ); } @@ -47,6 +53,7 @@ export interface CommonProps { export type PaneProps = { children: React.ReactNode; preferredSize?: number | string; + priority?: LayoutPriority; visible?: boolean; } & CommonProps; @@ -194,6 +201,7 @@ const Allotment = forwardRef( element: document.createElement("div"), minimumSize: props?.minSize ?? minSize, maximumSize: props?.maxSize ?? maxSize, + priority: props?.priority ?? LayoutPriority.Normal, ...(props?.preferredSize && { preferredSize: props?.preferredSize, }), @@ -288,6 +296,7 @@ const Allotment = forwardRef( element: document.createElement("div"), minimumSize: props?.minSize ?? minSize, maximumSize: props?.maxSize ?? maxSize, + priority: props?.priority ?? LayoutPriority.Normal, ...(props?.preferredSize && { preferredSize: props?.preferredSize, }), diff --git a/src/helpers/array.ts b/src/helpers/array.ts index 5423497a..18245eef 100644 --- a/src/helpers/array.ts +++ b/src/helpers/array.ts @@ -1,3 +1,15 @@ +/** + * Pushes an element to the start of the array, if found. + */ +export function pushToStart(arr: T[], value: T): void { + const index = arr.indexOf(value); + + if (index > -1) { + arr.splice(index, 1); + arr.unshift(value); + } +} + /** * Pushes an element to the end of the array, if found. */ diff --git a/src/pane-view/pane-view.ts b/src/pane-view/pane-view.ts index 4ce579f9..65457105 100644 --- a/src/pane-view/pane-view.ts +++ b/src/pane-view/pane-view.ts @@ -1,6 +1,6 @@ import { endsWith } from "../helpers/string"; import { LayoutService } from "../layout-service"; -import { View } from "../split-view"; +import { LayoutPriority, View } from "../split-view"; export interface Layout { getPreferredSize: () => number | undefined; @@ -42,6 +42,7 @@ export interface PaneViewOptions { element: HTMLElement; minimumSize?: number; maximumSize?: number; + priority?: LayoutPriority; preferredSize?: number | string; snap?: boolean; } @@ -51,6 +52,7 @@ export class PaneView implements View { public maximumSize: number = Number.POSITIVE_INFINITY; readonly element: HTMLElement; + readonly priority?: LayoutPriority | undefined; readonly snap: boolean; private layoutService: LayoutService; @@ -128,6 +130,8 @@ export class PaneView implements View { this.layoutStrategy = new NullLayout(); } + this.priority = options.priority ?? LayoutPriority.Normal; + this.snap = typeof options.snap === "boolean" ? options.snap : false; } diff --git a/src/split-view/split-view.ts b/src/split-view/split-view.ts index 2ddbdc25..c52e3074 100644 --- a/src/split-view/split-view.ts +++ b/src/split-view/split-view.ts @@ -2,9 +2,8 @@ import EventEmitter from "eventemitter3"; import clamp from "lodash.clamp"; import styles from "../allotment.module.css"; -import { pushToEnd, range } from "../helpers/array"; +import { pushToEnd, pushToStart, range } from "../helpers/array"; import { Disposable } from "../helpers/disposable"; -import { endsWith } from "../helpers/string"; import { Orientation, Sash, @@ -101,6 +100,12 @@ export interface SplitViewOptions { readonly getSashOrthogonalSize?: () => number; } +export const enum LayoutPriority { + Normal = "NORMAL", + Low = "LOW", + High = "HIGH", +} + /** * The interface to implement for views within a {@link SplitView}. */ @@ -122,6 +127,14 @@ export interface View { */ readonly maximumSize: number; + /** + * The priority of the view when the {@link SplitView.resize layout} algorithm + * runs. Views with higher priority will be resized first. + * + * @remarks Only used when `proportionalLayout` is false. + */ + readonly priority?: LayoutPriority; + /** * Whether the view will snap whenever the user reaches its minimum size or * attempts to grow it beyond the minimum size. @@ -174,9 +187,7 @@ export class PaneView implements View { this.snap = typeof options.snap === "boolean" ? options.snap : false; } - layout(size: number): void { - //console.log(size); - } + layout(_size: number): void {} } type ViewItemSize = number | { cachedVisibleSize: number }; @@ -211,6 +222,10 @@ abstract class ViewItem { return this._size; } + get priority(): LayoutPriority | undefined { + return this.view.priority; + } + get snap(): boolean { return !!this.view.snap; } @@ -607,7 +622,23 @@ export class SplitView extends EventEmitter implements Disposable { this.size = size; if (!this.proportions) { - this.resize(this.viewItems.length - 1, size - previousSize, undefined); + const indexes = range(0, this.viewItems.length); + + const lowPriorityIndexes = indexes.filter( + (i) => this.viewItems[i].priority === LayoutPriority.Low + ); + + const highPriorityIndexes = indexes.filter( + (i) => this.viewItems[i].priority === LayoutPriority.High + ); + + this.resize( + this.viewItems.length - 1, + size - previousSize, + undefined, + lowPriorityIndexes, + highPriorityIndexes + ); } else { for (let i = 0; i < this.viewItems.length; i++) { const item = this.viewItems[i]; @@ -635,14 +666,25 @@ export class SplitView extends EventEmitter implements Disposable { return; } - const lowPriorityIndexes = [index]; + const indexes = range(0, this.viewItems.length).filter((i) => i !== index); + + const lowPriorityIndexes = [ + ...indexes.filter( + (i) => this.viewItems[i].priority === LayoutPriority.Low + ), + index, + ]; + + const highPriorityIndexes = indexes.filter( + (i) => this.viewItems[i].priority === LayoutPriority.High + ); const item = this.viewItems[index]; size = Math.round(size); size = clamp(size, item.minimumSize, Math.min(item.maximumSize, this.size)); item.size = size; - this.relayout(lowPriorityIndexes); + this.relayout(lowPriorityIndexes, highPriorityIndexes); } public resizeViews(sizes: number[]): void { @@ -730,7 +772,17 @@ export class SplitView extends EventEmitter implements Disposable { item.size = clamp(size, item.minimumSize, item.maximumSize); } - this.relayout(); + const indexes = range(0, this.viewItems.length); + + const lowPriorityIndexes = indexes.filter( + (i) => this.viewItems[i].priority === LayoutPriority.Low + ); + + const highPriorityIndexes = indexes.filter( + (i) => this.viewItems[i].priority === LayoutPriority.High + ); + + this.relayout(lowPriorityIndexes, highPriorityIndexes); } public dispose(): void { @@ -740,14 +792,18 @@ export class SplitView extends EventEmitter implements Disposable { this.sashContainer.remove(); } - private relayout(lowPriorityIndexes?: number[]): void { + private relayout( + lowPriorityIndexes?: number[], + highPriorityIndexes?: number[] + ): void { const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this.resize( this.viewItems.length - 1, this.size - contentSize, undefined, - lowPriorityIndexes + lowPriorityIndexes, + highPriorityIndexes ); this.distributeEmptySpace(); @@ -851,11 +907,13 @@ export class SplitView extends EventEmitter implements Disposable { const delta = current - start; + // TODO: Should this be conditional on alt? this.resize( index, delta, sizes, undefined, + undefined, minDelta, maxDelta, snapBefore, @@ -894,6 +952,7 @@ export class SplitView extends EventEmitter implements Disposable { delta: number, sizes = this.viewItems.map((i) => i.size), lowPriorityIndexes?: number[], + highPriorityIndexes?: number[], overloadMinDelta: number = Number.NEGATIVE_INFINITY, overloadMaxDelta: number = Number.POSITIVE_INFINITY, snapBefore?: SashDragSnapState, @@ -906,6 +965,13 @@ export class SplitView extends EventEmitter implements Disposable { const upIndexes = range(index, -1, -1); const downIndexes = range(index + 1, this.viewItems.length); + if (highPriorityIndexes) { + for (const index of highPriorityIndexes) { + pushToStart(upIndexes, index); + pushToStart(downIndexes, index); + } + } + if (lowPriorityIndexes) { for (const index of lowPriorityIndexes) { pushToEnd(upIndexes, index); @@ -969,7 +1035,8 @@ export class SplitView extends EventEmitter implements Disposable { index, delta, sizes, - undefined, + lowPriorityIndexes, + highPriorityIndexes, overloadMinDelta, overloadMaxDelta ); diff --git a/stories/advanced.stories.tsx b/stories/advanced.stories.tsx index 0cefe37b..5b2d7c87 100644 --- a/stories/advanced.stories.tsx +++ b/stories/advanced.stories.tsx @@ -4,6 +4,7 @@ import { Meta, Story } from "@storybook/react"; import { useState } from "react"; import { Allotment } from "../src"; +import { LayoutPriority } from "../src/split-view"; import styles from "./advanced.stories.module.css"; import { ActivityBar } from "./components/activity-bar"; import { AuxiliaryBar } from "./components/auxiliary-bar"; @@ -62,6 +63,7 @@ export const VisualStudioCode: Story = ({ - + {primarySideBarPosition === "left" ? sidebar : auxiliarySidebar} - +
The size can either be a number or a string. If it is a number it will be interpreted as a number of pixels. If it is a string it should end in either "px" or "%". If it ends in "px" it will be interpreted as a number of pixels, e.g. "120px". If it ends in "%" it will be interpreted as a percentage of the size of the Allotment component, e.g. "50%". | -| `snap` | `boolean` | `false` | Enable snap to zero for this pane. Overrides `snap` set on parent component. | -| `visible` | `boolean` | `true` | Whether this pane should be visible. | +| Name | Type | Default | Description | +| --------------- | --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `maxSize` | `number` | | Maximum size of this pane. Overrides `maxSize` set on parent component. | +| `minSize` | `number` | | Minimum size of this pane. Overrides `minSize` set on parent component. | +| `priority` | `HIGH` \| `NORMAL` \| `LOW` | `NORMAL` | The priority of the pane when the layout algorithm runs. Panes with higher priority will be resized first.

Only used when `proportionalLayout` is false. | +| `preferredSize` | `number` \| `string` | | Preferred size of this pane. Allotment will attempt to use this size when adding this pane (including on initial mount) as well as when a user double clicks a sash, or the `reset` method is called on the Allotment instance.

The size can either be a number or a string. If it is a number it will be interpreted as a number of pixels. If it is a string it should end in either "px" or "%". If it ends in "px" it will be interpreted as a number of pixels, e.g. "120px". If it ends in "%" it will be interpreted as a percentage of the size of the Allotment component, e.g. "50%". | +| `snap` | `boolean` | `false` | Enable snap to zero for this pane. Overrides `snap` set on parent component. | +| `visible` | `boolean` | `true` | Whether this pane should be visible. |