Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ export interface SplitPaneGutterOptions {
color?: RGBA | string;
}

export interface GutterOptions extends Pick<BoxOptions, "id" | "width" | "height"> {
export interface GutterOptions
extends Pick<BoxOptions, "id" | "width" | "height">, SplitPaneGutterOptions {
direction: SplitDirection;
onGrab: (event: MouseEvent) => void;
visible?: boolean;
glyphs?: GutterGlyphs;
color?: RGBA | string;
}

export interface SplitPaneOptions extends BoxOptions {
Expand Down
21 changes: 14 additions & 7 deletions src/pane-dragdrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import {
} from "@opentui/core";
import type { PaneDragDrop, PaneDragDropOptions } from "./models.js";
import {
containsPoint,
findPaneTarget,
type PaneTarget,
paneTargetAtPoint,
reorderPane,
swapPanes,
isVisibleInTree,
} from "./pane-target.js";

// Reorder panes by dragging within a split or across splits to swap places.
Expand Down Expand Up @@ -53,9 +55,6 @@ export function createPaneDragDrop(
return index;
};

const containsPoint = (pane: BoxRenderable, x: number, y: number): boolean =>
x >= pane.x && x < pane.x + pane.width && y >= pane.y && y < pane.y + pane.height;

const onDown = (event: MouseEvent): void => {
active = null;
if (event.button !== MouseButton.LEFT || !modifierHeld(event)) return;
Expand All @@ -67,6 +66,7 @@ export function createPaneDragDrop(
if (!active) return;
const target = active;
active = null;
if (!isVisibleInTree(target.pane) || target.pane.parent !== target.split) return;

const drop = paneTargetAtPoint(root, event.x, event.y, isPane);
if (drop) {
Expand All @@ -79,17 +79,24 @@ export function createPaneDragDrop(
}
};

root.onMouse = (event) => {
if (event.type === "down") onDown(event);
if (event.type === "up" || event.type === "drag-end") onDrop(event);
let disposed = false;
const processMouseEvent = root.processMouseEvent;
const listener = function (this: Renderable, event: MouseEvent): void {
if (!disposed) {
if (event.type === "down") onDown(event);
if (event.type === "up" || event.type === "drag-end") onDrop(event);
}
processMouseEvent.call(this, event);
};
root.processMouseEvent = listener;

return {
get dragging() {
return active !== null;
},
dispose: () => {
root.onMouse = undefined;
disposed = true;
if (root.processMouseEvent === listener) root.processMouseEvent = processMouseEvent;
active = null;
},
};
Expand Down
4 changes: 3 additions & 1 deletion src/pane-navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type Renderable,
} from "@opentui/core";
import type { PaneNavigator, PaneNavigatorOptions } from "./models.js";
import { isVisible, isVisibleInTree } from "./pane-target.js";

function defaultKeymap(event: KeyEvent): "next" | "previous" | undefined {
if (event.name !== "tab" || event.ctrl || event.meta || event.option) return;
Expand All @@ -17,9 +18,10 @@ function collectPanes(
isPane: (renderable: Renderable) => boolean,
): Renderable[] {
const panes: Renderable[] = [];
if (!isVisibleInTree(root)) return panes;
const visit = (node: BaseRenderable): void => {
for (const child of node.getChildren()) {
if (isRenderable(child) && isPane(child)) panes.push(child);
if (isVisible(child) && isRenderable(child) && isPane(child)) panes.push(child);
visit(child);
}
};
Expand Down
3 changes: 1 addition & 2 deletions src/pane-reorder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { CliRenderer, KeyEvent } from "@opentui/core";
import type { BoxRenderable } from "@opentui/core";
import type { BoxRenderable, CliRenderer, KeyEvent } from "@opentui/core";
import type {
PaneMoveDirection,
PaneReorder,
Expand Down
21 changes: 21 additions & 0 deletions src/pane-sizing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function scaleSizes(sizes: number[], total: number): number[] {
if (sizes.length === 0) return [];
const sum = sizes.reduce((value, size) => value + size, 0);
const exact = sizes.map((size) => (sum > 0 ? (size * total) / sum : total / sizes.length));
const scaled = exact.map(Math.floor);
const remainder = total - scaled.reduce((value, size) => value + size, 0);
const order = exact
.map((size, index) => ({ index, fraction: size - scaled[index]! }))
.sort((first, second) => second.fraction - first.fraction);
for (let index = 0; index < remainder; index++) scaled[order[index]!.index]!++;
return scaled;
}

export function fitSizes(sizes: number[], total: number): number[] {
if (sizes.length === 0) return [];
const fitted = sizes.map(Math.round);
const fixedTotal = fitted.slice(0, -1).reduce((value, size) => value + size, 0);
if (fixedTotal > total) return scaleSizes(fitted, total);
fitted[fitted.length - 1] = total - fixedTotal;
return fitted;
}
26 changes: 22 additions & 4 deletions src/pane-target.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BoxRenderable, isRenderable, type Renderable } from "@opentui/core";
import { type BaseRenderable, BoxRenderable, isRenderable, type Renderable } from "@opentui/core";
import type {
PaneMoveDirection,
PanePredicate,
Expand All @@ -12,11 +12,24 @@ export interface PaneTarget {
pane: BoxRenderable;
}

export function isVisible(node: BaseRenderable): boolean {
return node.visible && !(isRenderable(node) && node.isDestroyed);
}

export function isVisibleInTree(node: BaseRenderable | null): boolean {
if (!node) return false;
for (let current: BaseRenderable | null = node; current; current = current.parent) {
if (!isVisible(current)) return false;
}
return true;
}

export function findPaneTarget(
start: Renderable | null,
root: Renderable,
isPane: PanePredicate,
): PaneTarget | null {
if (!isVisibleInTree(start)) return null;
let target: PaneTarget | null = null;

for (let node = start; node; node = node.parent) {
Expand Down Expand Up @@ -47,9 +60,12 @@ export function isLeafPane(pane: BoxRenderable): boolean {

export function collectLeafPanes(root: Renderable, isPane: PanePredicate): BoxRenderable[] {
const panes: BoxRenderable[] = [];
if (!isVisibleInTree(root)) return panes;
const visit = (node: Renderable): void => {
if (!isVisible(node)) return;
if (node instanceof SplitPaneRenderable) {
for (const pane of node.paneList) {
if (!isVisible(pane)) continue;
if (isLeafPane(pane)) {
if (isPane(pane)) panes.push(pane);
} else {
Expand All @@ -64,7 +80,7 @@ export function collectLeafPanes(root: Renderable, isPane: PanePredicate): BoxRe
return panes;
}

function containsPoint(pane: BoxRenderable, x: number, y: number): boolean {
export function containsPoint(pane: BoxRenderable, x: number, y: number): boolean {
return x >= pane.x && x < pane.x + pane.width && y >= pane.y && y < pane.y + pane.height;
}

Expand All @@ -74,9 +90,11 @@ export function paneTargetAtPoint(
y: number,
isPane: PanePredicate,
): PaneTarget | null {
if (!isVisibleInTree(root)) return null;
const visitChildren = (node: Renderable): PaneTarget | null => {
for (const child of node.getChildren()) {
if (!(child instanceof BoxRenderable) || !containsPoint(child, x, y)) continue;
if (!(child instanceof BoxRenderable) || !isVisible(child) || !containsPoint(child, x, y))
continue;
const target = visit(child);
if (target) return target;
}
Expand All @@ -85,7 +103,7 @@ export function paneTargetAtPoint(
const visit = (node: Renderable): PaneTarget | null => {
if (node instanceof SplitPaneRenderable) {
for (const pane of node.paneList) {
if (!containsPoint(pane, x, y)) continue;
if (!isVisible(pane) || !containsPoint(pane, x, y)) continue;
if (pane instanceof SplitPaneRenderable) return visit(pane);
return (
visitChildren(pane) ?? (isPane(pane) && isLeafPane(pane) ? { split: node, pane } : null)
Expand Down
21 changes: 16 additions & 5 deletions src/renderables/gutter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import type { GutterOptions, SplitDirection } from "../models.js";
export class GutterRenderable extends BoxRenderable {
private grabbed = false;
private hovered = false;
private readonly direction: SplitDirection;
private readonly glyph: string;
private readonly color: RGBA;
private direction: SplitDirection;
private glyph: string;
private color: RGBA;
private gutterVisible: boolean;

constructor(ctx: RenderContext, options: GutterOptions) {
Expand Down Expand Up @@ -59,16 +59,27 @@ export class GutterRenderable extends BoxRenderable {
};
}

get showHairline(): boolean {
get gutterVisibility(): boolean {
return this.gutterVisible;
}

set showHairline(value: boolean) {
set gutterVisibility(value: boolean) {
if (value === this.gutterVisible) return;
this.gutterVisible = value;
this.requestRender();
}

updateAppearance(
options: Pick<GutterOptions, "direction" | "visible" | "glyphs" | "color">,
): void {
const color = options.color === undefined ? DefaultGutterColor : parseColor(options.color);
this.direction = options.direction;
this.glyph = options.glyphs?.[options.direction] ?? DefaultGutterGlyphs[options.direction];
this.color = color;
this.gutterVisible = options.visible ?? true;
this.requestRender();
}

protected renderSelf(buffer: OptimizedBuffer): void {
if (!this.gutterVisible) return;

Expand Down
Loading
Loading