/** * Pure utility functions for immutable pane manipulation. * All functions return new objects (no mutation). */ import type { Pane, PaneLayout } from '@renderer/types/panes';import type { Tab } from '@renderer/types/tabs'; /** * Find a pane by its ID. */export function findPane(layout: PaneLayout, paneId: string): Pane | undefined { return layout.panes.find((p) => p.id === paneId);} /** * Find which pane contains a given tab. */export function findPaneByTabId(layout: PaneLayout, tabId: string): Pane | undefined { return layout.panes.find((p) => p.tabs.some((t) => t.id === tabId));} /** * Replace a pane immutably in the layout. */export function updatePane(layout: PaneLayout, updatedPane: Pane): PaneLayout { return { ...layout, panes: layout.panes.map((p) => (p.id === updatedPane.id ? updatedPane : p)), };} /** * Remove a pane and redistribute its width to a neighbor. * If removing the focused pane, focus shifts to the nearest neighbor. */export function removePane(layout: PaneLayout, paneId: string): PaneLayout { const index = layout.panes.findIndex((p) => p.id === paneId); if (index === -1 || layout.panes.length <= 1) return layout; const removedPane = layout.panes[index]; const newPanes = layout.panes.filter((p) => p.id !== paneId); // Redistribute width to the nearest neighbor const neighborIndex = index > 0 ? index - 1 : 0; const redistributed = newPanes.map((p, i) => i === neighborIndex ? { ...p, widthFraction: p.widthFraction + removedPane.widthFraction } : p ); // Equalize to avoid floating point drift const equalized = redistributeWidths(redistributed); // Update focus if the removed pane was focused let newFocusedId = layout.focusedPaneId; if (layout.focusedPaneId === paneId) { const focusTarget = equalized[Math.min(index, equalized.length - 1)]; newFocusedId = focusTarget.id; } return { panes: equalized, focusedPaneId: newFocusedId, };} /** * Insert a new pane adjacent to an existing pane. */export function insertPane( layout: PaneLayout, adjacentPaneId: string, newPane: Pane, direction: 'left' | 'right'): PaneLayout { const index = layout.panes.findIndex((p) => p.id === adjacentPaneId); if (index === -1) return layout; const insertAt = direction === 'right' ? index + 1 : index; const newPanes = [...layout.panes]; newPanes.splice(insertAt, 0, newPane); return { ...layout, panes: redistributeWidths(newPanes), };} /** * Equalize widths across all panes so they sum to 1. */function redistributeWidths(panes: Pane[]): Pane[] { if (panes.length === 0) return panes; const fraction = 1 / panes.length; return panes.map((p) => ({ ...p, widthFraction: fraction }));} /** * Extract the focused pane's tab state for root-level sync. */export function syncFocusedPaneState(layout: PaneLayout): { openTabs: Tab[]; activeTabId: string | null; selectedTabIds: string[];} { const focused = findPane(layout, layout.focusedPaneId); if (!focused) { return { openTabs: [], activeTabId: null, selectedTabIds: [] }; } return { openTabs: focused.tabs, activeTabId: focused.activeTabId, selectedTabIds: focused.selectedTabIds, };} /** * Get all tabs across all panes (flat list). */export function getAllTabs(layout: PaneLayout): Tab[] { return layout.panes.flatMap((p) => p.tabs);} /** * Create a new empty pane with a unique ID. */export function createEmptyPane(id: string): Pane { return { id, tabs: [], activeTabId: null, selectedTabIds: [], widthFraction: 0, };}
/**
* Pure utility functions for immutable pane manipulation.
* All functions return new objects (no mutation).
*/
import type { Pane, PaneLayout } from '@renderer/types/panes';
import type { Tab } from '@renderer/types/tabs';
/**
* Find a pane by its ID.
*/
export function findPane(layout: PaneLayout, paneId: string): Pane | undefined {
return layout.panes.find((p) => p.id === paneId);
}
/**
* Find which pane contains a given tab.
*/
export function findPaneByTabId(layout: PaneLayout, tabId: string): Pane | undefined {
return layout.panes.find((p) => p.tabs.some((t) => t.id === tabId));
}
/**
* Replace a pane immutably in the layout.
*/
export function updatePane(layout: PaneLayout, updatedPane: Pane): PaneLayout {
return {
...layout,
panes: layout.panes.map((p) => (p.id === updatedPane.id ? updatedPane : p)),
};
}
/**
* Remove a pane and redistribute its width to a neighbor.
* If removing the focused pane, focus shifts to the nearest neighbor.
*/
export function removePane(layout: PaneLayout, paneId: string): PaneLayout {
const index = layout.panes.findIndex((p) => p.id === paneId);
if (index === -1 || layout.panes.length <= 1) return layout;
const removedPane = layout.panes[index];
const newPanes = layout.panes.filter((p) => p.id !== paneId);
// Redistribute width to the nearest neighbor
const neighborIndex = index > 0 ? index - 1 : 0;
const redistributed = newPanes.map((p, i) =>
i === neighborIndex ? { ...p, widthFraction: p.widthFraction + removedPane.widthFraction } : p
);
// Equalize to avoid floating point drift
const equalized = redistributeWidths(redistributed);
// Update focus if the removed pane was focused
let newFocusedId = layout.focusedPaneId;
if (layout.focusedPaneId === paneId) {
const focusTarget = equalized[Math.min(index, equalized.length - 1)];
newFocusedId = focusTarget.id;
}
return {
panes: equalized,
focusedPaneId: newFocusedId,
};
}
/**
* Insert a new pane adjacent to an existing pane.
*/
export function insertPane(
layout: PaneLayout,
adjacentPaneId: string,
newPane: Pane,
direction: 'left' | 'right'
): PaneLayout {
const index = layout.panes.findIndex((p) => p.id === adjacentPaneId);
if (index === -1) return layout;
const insertAt = direction === 'right' ? index + 1 : index;
const newPanes = [...layout.panes];
newPanes.splice(insertAt, 0, newPane);
return {
...layout,
panes: redistributeWidths(newPanes),
};
}
/**
* Equalize widths across all panes so they sum to 1.
*/
function redistributeWidths(panes: Pane[]): Pane[] {
if (panes.length === 0) return panes;
const fraction = 1 / panes.length;
return panes.map((p) => ({ ...p, widthFraction: fraction }));
}
/**
* Extract the focused pane's tab state for root-level sync.
*/
export function syncFocusedPaneState(layout: PaneLayout): {
openTabs: Tab[];
activeTabId: string | null;
selectedTabIds: string[];
} {
const focused = findPane(layout, layout.focusedPaneId);
if (!focused) {
return { openTabs: [], activeTabId: null, selectedTabIds: [] };
}
return {
openTabs: focused.tabs,
activeTabId: focused.activeTabId,
selectedTabIds: focused.selectedTabIds,
};
}
/**
* Get all tabs across all panes (flat list).
*/
export function getAllTabs(layout: PaneLayout): Tab[] {
return layout.panes.flatMap((p) => p.tabs);
}
/**
* Create a new empty pane with a unique ID.
*/
export function createEmptyPane(id: string): Pane {
return {
id,
tabs: [],
activeTabId: null,
selectedTabIds: [],
widthFraction: 0,
};
}