diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 59bb957c..e30ccb65 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -449,19 +449,46 @@ overflow-y: auto; } +/* Divider: the element is a wide transparent hit area; the visible line is + drawn by ::after so that setting `background` on the element never hides it. */ .pane-divider.is-vertical, .pane-divider.is-horizontal { - background: var(--orca-terminal-divider-color, transparent) !important; + --divider-thickness: 4px; + background: transparent !important; } -.pane-divider.is-vertical:hover, -.pane-divider.is-vertical.is-dragging, -.pane-divider.is-horizontal:hover, -.pane-divider.is-horizontal.is-dragging { +.pane-divider::after { + content: ''; + position: absolute; + border-radius: 1px; + background: var(--orca-terminal-divider-color, transparent); + transition: background 100ms ease; +} + +.pane-divider.is-vertical::after { + top: 0; + bottom: 0; + left: 50%; + width: var(--divider-thickness); + transform: translateX(-50%); +} + +.pane-divider.is-horizontal::after { + left: 0; + right: 0; + top: 50%; + height: var(--divider-thickness); + transform: translateY(-50%); +} + +.pane-divider.is-vertical:hover::after, +.pane-divider.is-vertical.is-dragging::after, +.pane-divider.is-horizontal:hover::after, +.pane-divider.is-horizontal.is-dragging::after { background: var( --orca-terminal-divider-color-strong, var(--orca-terminal-divider-color, transparent) - ) !important; + ); } .number-input-clean { diff --git a/src/renderer/src/components/TerminalPane.tsx b/src/renderer/src/components/TerminalPane.tsx index 61bf6679..13a9a18e 100644 --- a/src/renderer/src/components/TerminalPane.tsx +++ b/src/renderer/src/components/TerminalPane.tsx @@ -295,11 +295,28 @@ function serializePaneTree(node: HTMLElement | null): TerminalPaneLayoutNode | n const secondNode = serializePaneTree(second ?? null) if (!firstNode || !secondNode) return null + // Capture the flex ratio so resized panes survive serialization round-trips. + // We read the computed flex-grow values to derive the first-child proportion. + let ratio: number | undefined + if (first && second) { + const firstGrow = parseFloat(first.style.flex) || 1 + const secondGrow = parseFloat(second.style.flex) || 1 + const total = firstGrow + secondGrow + if (total > 0) { + const r = firstGrow / total + // Only store if meaningfully different from 0.5 (default equal split) + if (Math.abs(r - 0.5) > 0.005) { + ratio = Math.round(r * 1000) / 1000 + } + } + } + return { type: 'split', direction: node.classList.contains('is-horizontal') ? 'horizontal' : 'vertical', first: firstNode, - second: secondNode + second: secondNode, + ...(ratio !== undefined && { ratio }) } } @@ -337,7 +354,9 @@ function replayTerminalLayout( return } - const createdPane = manager.splitPane(paneId, node.direction as TerminalPaneSplitDirection) + const createdPane = manager.splitPane(paneId, node.direction as TerminalPaneSplitDirection, { + ratio: node.ratio + }) if (!createdPane) { collectLeafIds(node, paneByLeafId, paneId) return diff --git a/src/renderer/src/lib/pane-manager.ts b/src/renderer/src/lib/pane-manager.ts index c34df180..f9ae07c8 100644 --- a/src/renderer/src/lib/pane-manager.ts +++ b/src/renderer/src/lib/pane-manager.ts @@ -95,7 +95,11 @@ export class PaneManager { return this.toPublic(pane) } - splitPane(paneId: number, direction: 'vertical' | 'horizontal'): ManagedPane | null { + splitPane( + paneId: number, + direction: 'vertical' | 'horizontal', + opts?: { ratio?: number } + ): ManagedPane | null { const existing = this.panes.get(paneId) if (!existing) return null @@ -140,6 +144,15 @@ export class PaneManager { // Apply flex styles to new pane container this.applyPaneFlexStyle(newPane.container) + // Apply custom ratio if provided (e.g. restoring a saved layout) + const ratio = opts?.ratio + if (ratio !== undefined && ratio > 0 && ratio < 1) { + const firstGrow = ratio + const secondGrow = 1 - ratio + existing.container.style.flex = `${firstGrow} 1 0%` + newPane.container.style.flex = `${secondGrow} 1 0%` + } + // Replace existing pane with split in the DOM parent.replaceChild(split, existing.container) @@ -516,28 +529,36 @@ export class PaneManager { const divider = document.createElement('div') divider.className = `pane-divider ${isVertical ? 'is-vertical' : 'is-horizontal'}` - const thickness = this.styleOptions.dividerThicknessPx ?? 4 + // Ghostty-style: the element itself is a wide transparent hit area for easy + // grabbing. The visible line is drawn by a CSS ::after pseudo-element + // (see main.css), so `background` on the element stays transparent. + const hitSize = this.getDividerHitSize() if (isVertical) { - divider.style.width = `${thickness}px` + divider.style.width = `${hitSize}px` divider.style.cursor = 'col-resize' } else { - divider.style.height = `${thickness}px` + divider.style.height = `${hitSize}px` divider.style.cursor = 'row-resize' } divider.style.flex = 'none' - - if (this.styleOptions.splitBackground) { - divider.style.background = this.styleOptions.splitBackground - } + divider.style.position = 'relative' this.attachDividerDrag(divider, isVertical) return divider } + /** Total hit area size = visible thickness + invisible padding on each side */ + private getDividerHitSize(): number { + const thickness = this.styleOptions.dividerThicknessPx ?? 4 + const HIT_PADDING = 3 + return thickness + HIT_PADDING * 2 + } + private attachDividerDrag(divider: HTMLElement, isVertical: boolean): void { const MIN_PANE_SIZE = 50 let dragging = false + let didMove = false let startPos = 0 let prevFlex = 0 let nextFlex = 0 @@ -550,6 +571,7 @@ export class PaneManager { divider.setPointerCapture(e.pointerId) divider.classList.add('is-dragging') dragging = true + didMove = false startPos = isVertical ? e.clientX : e.clientY @@ -572,6 +594,7 @@ export class PaneManager { const onPointerMove = (e: PointerEvent): void => { if (!dragging || !prevEl || !nextEl) return + didMove = true const currentPos = isVertical ? e.clientX : e.clientY const delta = currentPos - startPos @@ -605,11 +628,31 @@ export class PaneManager { divider.classList.remove('is-dragging') prevEl = null nextEl = null + + // Persist updated ratios after a real drag + if (didMove) { + this.options.onLayoutChanged?.() + } + } + + // Ghostty-style: double-click divider to equalize sibling panes + const onDoubleClick = (): void => { + const prev = divider.previousElementSibling as HTMLElement | null + const next = divider.nextElementSibling as HTMLElement | null + if (!prev || !next) return + + prev.style.flex = '1 1 0%' + next.style.flex = '1 1 0%' + + this.refitPanesUnder(prev) + this.refitPanesUnder(next) + this.options.onLayoutChanged?.() } divider.addEventListener('pointerdown', onPointerDown) divider.addEventListener('pointermove', onPointerMove) divider.addEventListener('pointerup', onPointerUp) + divider.addEventListener('dblclick', onDoubleClick) } private refitPanesUnder(el: HTMLElement): void { @@ -650,18 +693,19 @@ export class PaneManager { private applyDividerStyles(): void { const thickness = this.styleOptions.dividerThicknessPx ?? 4 - const bg = this.styleOptions.splitBackground ?? '' + const hitSize = this.getDividerHitSize() const dividers = this.root.querySelectorAll('.pane-divider') for (const div of dividers) { const el = div as HTMLElement const isVertical = el.classList.contains('is-vertical') if (isVertical) { - el.style.width = `${thickness}px` + el.style.width = `${hitSize}px` } else { - el.style.height = `${thickness}px` + el.style.height = `${hitSize}px` } - el.style.background = bg + // Store the visual thickness for the CSS ::after pseudo-element + el.style.setProperty('--divider-thickness', `${thickness}px`) } } diff --git a/src/shared/types.ts b/src/shared/types.ts index 51ea67e5..718a6ec9 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -66,6 +66,8 @@ export type TerminalPaneLayoutNode = direction: TerminalPaneSplitDirection first: TerminalPaneLayoutNode second: TerminalPaneLayoutNode + /** Flex ratio of the first child (0–1). Defaults to 0.5 if absent. */ + ratio?: number } export interface TerminalLayoutSnapshot {