664 lines
20 KiB
TypeScript
664 lines
20 KiB
TypeScript
import { Fragment, useCallback, useMemo, useState, type Dispatch, type ReactNode, type SetStateAction } from "react";
|
|
import {
|
|
DndContext,
|
|
KeyboardSensor,
|
|
PointerSensor,
|
|
closestCenter,
|
|
pointerWithin,
|
|
useSensor,
|
|
useSensors,
|
|
type CollisionDetection,
|
|
type DragEndEvent,
|
|
type DragStartEvent,
|
|
} from "@dnd-kit/core";
|
|
import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
|
import { View } from "react-native";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
import { ResizeHandle } from "@/components/resize-handle";
|
|
import { SplitDropZone, type SplitDropZoneHover } from "@/components/split-drop-zone";
|
|
import { WorkspacePaneContent } from "@/screens/workspace/workspace-pane-content";
|
|
import { WorkspaceDesktopTabsRow } from "@/screens/workspace/workspace-desktop-tabs-row";
|
|
import { deriveWorkspaceTabModel } from "@/screens/workspace/workspace-tab-model";
|
|
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
|
import type { SplitNode, SplitPane, WorkspaceLayout } from "@/stores/workspace-layout-store";
|
|
import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
|
|
|
interface SplitContainerProps {
|
|
layout: WorkspaceLayout;
|
|
workspaceKey: string;
|
|
normalizedServerId: string;
|
|
normalizedWorkspaceId: string;
|
|
uiTabs: WorkspaceTab[];
|
|
hoveredCloseTabKey: string | null;
|
|
setHoveredTabKey: Dispatch<SetStateAction<string | null>>;
|
|
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
|
isArchivingAgent: (input: { serverId: string; agentId: string }) => boolean;
|
|
killTerminalPending: boolean;
|
|
killTerminalId: string | null;
|
|
onNavigateTab: (tabId: string) => void;
|
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
|
onCloseTabsToLeft: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
|
|
onCloseTabsToRight: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
|
|
onCloseOtherTabs: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
|
|
onSelectNewTabOption: (optionId: "__new_tab_agent__") => void;
|
|
newTabAgentOptionId?: "__new_tab_agent__";
|
|
onOpenPaneTab: (input: { paneId: string; target: WorkspaceTabTarget }) => void;
|
|
onRetargetTab: (tabId: string, target: WorkspaceTabTarget) => void;
|
|
onOpenWorkspaceFile: (input: { paneId: string; filePath: string }) => void;
|
|
onFocusPane: (paneId: string) => void;
|
|
onSplitPane: (input: {
|
|
tabId: string;
|
|
targetPaneId: string;
|
|
position: "left" | "right" | "top" | "bottom";
|
|
}) => void;
|
|
onMoveTabToPane: (tabId: string, toPaneId: string) => void;
|
|
onResizeSplit: (groupId: string, sizes: number[]) => void;
|
|
onReorderTabsInPane: (paneId: string, tabIds: string[]) => void;
|
|
renderPaneEmptyState?: () => ReactNode;
|
|
}
|
|
|
|
interface WorkspaceTabDragData {
|
|
kind: "workspace-tab";
|
|
paneId: string;
|
|
tabId: string;
|
|
}
|
|
|
|
interface SplitPaneDropData {
|
|
kind: "split-pane-drop";
|
|
paneId: string;
|
|
}
|
|
|
|
interface SplitNodeViewProps
|
|
extends Omit<SplitContainerProps, "layout" | "workspaceKey" | "uiTabs"> {
|
|
node: SplitNode;
|
|
tabsById: Map<string, WorkspaceTab>;
|
|
focusedPaneId: string;
|
|
activeDragTabId: string | null;
|
|
showDropZones: boolean;
|
|
dropPreview: SplitDropZoneHover | null;
|
|
onDropPreviewChange: (hover: SplitDropZoneHover | null) => void;
|
|
}
|
|
|
|
interface SplitPaneViewProps
|
|
extends Omit<
|
|
SplitNodeViewProps,
|
|
| "node"
|
|
| "tabsById"
|
|
| "focusedPaneId"
|
|
| "activeDragTabId"
|
|
| "showDropZones"
|
|
| "dropPreview"
|
|
| "onDropPreviewChange"
|
|
| "onSplitPane"
|
|
| "onMoveTabToPane"
|
|
| "onResizeSplit"
|
|
> {
|
|
pane: SplitPane;
|
|
tabsById: Map<string, WorkspaceTab>;
|
|
focused: boolean;
|
|
activeDragTabId: string | null;
|
|
showDropZones: boolean;
|
|
dropPreview: SplitDropZoneHover | null;
|
|
onDropPreviewChange: (hover: SplitDropZoneHover | null) => void;
|
|
}
|
|
|
|
const dropCollisionDetection: CollisionDetection = (args) => {
|
|
const pointerHits = pointerWithin(args);
|
|
const tabHits = pointerHits.filter(
|
|
(entry) => entry.data?.droppableContainer.data.current?.kind === "workspace-tab"
|
|
);
|
|
if (tabHits.length > 0) {
|
|
return tabHits;
|
|
}
|
|
|
|
const paneHits = pointerHits.filter(
|
|
(entry) => entry.data?.droppableContainer.data.current?.kind === "split-pane-drop"
|
|
);
|
|
if (paneHits.length > 0) {
|
|
return paneHits;
|
|
}
|
|
|
|
return closestCenter(args);
|
|
};
|
|
|
|
export function SplitContainer({
|
|
layout,
|
|
normalizedServerId,
|
|
normalizedWorkspaceId,
|
|
uiTabs,
|
|
hoveredCloseTabKey,
|
|
setHoveredTabKey,
|
|
setHoveredCloseTabKey,
|
|
isArchivingAgent,
|
|
killTerminalPending,
|
|
killTerminalId,
|
|
onNavigateTab,
|
|
onCloseTab,
|
|
onCopyResumeCommand,
|
|
onCopyAgentId,
|
|
onCloseTabsToLeft,
|
|
onCloseTabsToRight,
|
|
onCloseOtherTabs,
|
|
onSelectNewTabOption,
|
|
newTabAgentOptionId = "__new_tab_agent__",
|
|
onOpenPaneTab,
|
|
onRetargetTab,
|
|
onOpenWorkspaceFile,
|
|
onFocusPane,
|
|
onSplitPane,
|
|
onMoveTabToPane,
|
|
onResizeSplit,
|
|
onReorderTabsInPane,
|
|
renderPaneEmptyState = () => null,
|
|
}: SplitContainerProps) {
|
|
const [activeDragTabId, setActiveDragTabId] = useState<string | null>(null);
|
|
const [dropPreview, setDropPreview] = useState<SplitDropZoneHover | null>(null);
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 8,
|
|
},
|
|
}),
|
|
useSensor(KeyboardSensor, {
|
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
})
|
|
);
|
|
|
|
const tabsById = useMemo(() => {
|
|
const next = new Map<string, WorkspaceTab>();
|
|
for (const tab of uiTabs) {
|
|
next.set(tab.tabId, tab);
|
|
}
|
|
return next;
|
|
}, [uiTabs]);
|
|
|
|
const panesById = useMemo(() => collectPanesById(layout.root), [layout.root]);
|
|
|
|
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
const data = event.active.data.current as WorkspaceTabDragData | undefined;
|
|
if (data?.kind !== "workspace-tab") {
|
|
setActiveDragTabId(null);
|
|
setDropPreview(null);
|
|
return;
|
|
}
|
|
setActiveDragTabId(data.tabId);
|
|
}, []);
|
|
|
|
const handleDragCancel = useCallback(() => {
|
|
setActiveDragTabId(null);
|
|
setDropPreview(null);
|
|
}, []);
|
|
|
|
const handleDragEnd = useCallback(
|
|
(event: DragEndEvent) => {
|
|
const activeData = event.active.data.current as WorkspaceTabDragData | undefined;
|
|
const overData = event.over?.data.current as
|
|
| WorkspaceTabDragData
|
|
| SplitPaneDropData
|
|
| undefined;
|
|
|
|
setActiveDragTabId(null);
|
|
|
|
if (activeData?.kind !== "workspace-tab" || !event.over) {
|
|
setDropPreview(null);
|
|
return;
|
|
}
|
|
|
|
if (overData?.kind === "workspace-tab") {
|
|
const sourcePane = panesById.get(activeData.paneId) ?? null;
|
|
const targetPane = panesById.get(overData.paneId) ?? null;
|
|
if (!sourcePane || !targetPane) {
|
|
setDropPreview(null);
|
|
return;
|
|
}
|
|
|
|
const sourceTabs = getPaneTabDescriptors(sourcePane, tabsById);
|
|
const targetTabs = getPaneTabDescriptors(targetPane, tabsById);
|
|
const sourceIndex = sourceTabs.findIndex((tab) => tab.tabId === activeData.tabId);
|
|
const targetIndex = targetTabs.findIndex((tab) => tab.tabId === overData.tabId);
|
|
if (sourceIndex < 0 || targetIndex < 0) {
|
|
setDropPreview(null);
|
|
return;
|
|
}
|
|
|
|
if (activeData.paneId === overData.paneId) {
|
|
if (sourceIndex !== targetIndex) {
|
|
const nextTabs = arrayMove(sourceTabs, sourceIndex, targetIndex);
|
|
onReorderTabsInPane(activeData.paneId, nextTabs.map((tab) => tab.tabId));
|
|
}
|
|
setDropPreview(null);
|
|
return;
|
|
}
|
|
|
|
const nextTargetTabIds = targetTabs.map((tab) => tab.tabId);
|
|
nextTargetTabIds.splice(targetIndex, 0, activeData.tabId);
|
|
onMoveTabToPane(activeData.tabId, overData.paneId);
|
|
onReorderTabsInPane(overData.paneId, nextTargetTabIds);
|
|
setDropPreview(null);
|
|
return;
|
|
}
|
|
|
|
if (overData?.kind === "split-pane-drop" && dropPreview?.paneId === overData.paneId) {
|
|
if (dropPreview.position === "center") {
|
|
if (activeData.paneId !== overData.paneId) {
|
|
onMoveTabToPane(activeData.tabId, overData.paneId);
|
|
}
|
|
setDropPreview(null);
|
|
return;
|
|
}
|
|
|
|
onSplitPane({
|
|
tabId: activeData.tabId,
|
|
targetPaneId: overData.paneId,
|
|
position: dropPreview.position,
|
|
});
|
|
}
|
|
|
|
setDropPreview(null);
|
|
},
|
|
[dropPreview, onMoveTabToPane, onReorderTabsInPane, onSplitPane, panesById, tabsById]
|
|
);
|
|
|
|
return (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={dropCollisionDetection}
|
|
onDragStart={handleDragStart}
|
|
onDragCancel={handleDragCancel}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<SplitNodeView
|
|
node={layout.root}
|
|
tabsById={tabsById}
|
|
focusedPaneId={layout.focusedPaneId}
|
|
normalizedServerId={normalizedServerId}
|
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
hoveredCloseTabKey={hoveredCloseTabKey}
|
|
setHoveredTabKey={setHoveredTabKey}
|
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
|
isArchivingAgent={isArchivingAgent}
|
|
killTerminalPending={killTerminalPending}
|
|
killTerminalId={killTerminalId}
|
|
onNavigateTab={onNavigateTab}
|
|
onCloseTab={onCloseTab}
|
|
onCopyResumeCommand={onCopyResumeCommand}
|
|
onCopyAgentId={onCopyAgentId}
|
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
|
onCloseTabsToRight={onCloseTabsToRight}
|
|
onCloseOtherTabs={onCloseOtherTabs}
|
|
onSelectNewTabOption={onSelectNewTabOption}
|
|
newTabAgentOptionId={newTabAgentOptionId}
|
|
onOpenPaneTab={onOpenPaneTab}
|
|
onRetargetTab={onRetargetTab}
|
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
|
onFocusPane={onFocusPane}
|
|
onSplitPane={onSplitPane}
|
|
onMoveTabToPane={onMoveTabToPane}
|
|
onResizeSplit={onResizeSplit}
|
|
onReorderTabsInPane={onReorderTabsInPane}
|
|
renderPaneEmptyState={renderPaneEmptyState}
|
|
activeDragTabId={activeDragTabId}
|
|
showDropZones={activeDragTabId !== null}
|
|
dropPreview={dropPreview}
|
|
onDropPreviewChange={setDropPreview}
|
|
/>
|
|
</DndContext>
|
|
);
|
|
}
|
|
|
|
function SplitNodeView({
|
|
node,
|
|
tabsById,
|
|
focusedPaneId,
|
|
normalizedServerId,
|
|
normalizedWorkspaceId,
|
|
hoveredCloseTabKey,
|
|
setHoveredTabKey,
|
|
setHoveredCloseTabKey,
|
|
isArchivingAgent,
|
|
killTerminalPending,
|
|
killTerminalId,
|
|
onNavigateTab,
|
|
onCloseTab,
|
|
onCopyResumeCommand,
|
|
onCopyAgentId,
|
|
onCloseTabsToLeft,
|
|
onCloseTabsToRight,
|
|
onCloseOtherTabs,
|
|
onSelectNewTabOption,
|
|
newTabAgentOptionId,
|
|
onOpenPaneTab,
|
|
onRetargetTab,
|
|
onOpenWorkspaceFile,
|
|
onFocusPane,
|
|
onSplitPane,
|
|
onMoveTabToPane,
|
|
onResizeSplit,
|
|
onReorderTabsInPane,
|
|
renderPaneEmptyState,
|
|
activeDragTabId,
|
|
showDropZones,
|
|
dropPreview,
|
|
onDropPreviewChange,
|
|
}: SplitNodeViewProps) {
|
|
if (node.kind === "pane") {
|
|
return (
|
|
<SplitPaneView
|
|
pane={node.pane}
|
|
tabsById={tabsById}
|
|
focused={node.pane.id === focusedPaneId}
|
|
normalizedServerId={normalizedServerId}
|
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
hoveredCloseTabKey={hoveredCloseTabKey}
|
|
setHoveredTabKey={setHoveredTabKey}
|
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
|
isArchivingAgent={isArchivingAgent}
|
|
killTerminalPending={killTerminalPending}
|
|
killTerminalId={killTerminalId}
|
|
onNavigateTab={onNavigateTab}
|
|
onCloseTab={onCloseTab}
|
|
onCopyResumeCommand={onCopyResumeCommand}
|
|
onCopyAgentId={onCopyAgentId}
|
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
|
onCloseTabsToRight={onCloseTabsToRight}
|
|
onCloseOtherTabs={onCloseOtherTabs}
|
|
onSelectNewTabOption={onSelectNewTabOption}
|
|
newTabAgentOptionId={newTabAgentOptionId}
|
|
onOpenPaneTab={onOpenPaneTab}
|
|
onRetargetTab={onRetargetTab}
|
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
|
onFocusPane={onFocusPane}
|
|
onReorderTabsInPane={onReorderTabsInPane}
|
|
renderPaneEmptyState={renderPaneEmptyState}
|
|
activeDragTabId={activeDragTabId}
|
|
showDropZones={showDropZones}
|
|
dropPreview={dropPreview}
|
|
onDropPreviewChange={onDropPreviewChange}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.group,
|
|
node.group.direction === "horizontal" ? styles.groupHorizontal : styles.groupVertical,
|
|
]}
|
|
>
|
|
{node.group.children.map((child, index) => (
|
|
<Fragment key={getNodeKey(child)}>
|
|
<View style={[styles.groupChild, { flex: node.group.sizes[index] ?? 1 }]}>
|
|
<SplitNodeView
|
|
node={child}
|
|
tabsById={tabsById}
|
|
focusedPaneId={focusedPaneId}
|
|
normalizedServerId={normalizedServerId}
|
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
hoveredCloseTabKey={hoveredCloseTabKey}
|
|
setHoveredTabKey={setHoveredTabKey}
|
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
|
isArchivingAgent={isArchivingAgent}
|
|
killTerminalPending={killTerminalPending}
|
|
killTerminalId={killTerminalId}
|
|
onNavigateTab={onNavigateTab}
|
|
onCloseTab={onCloseTab}
|
|
onCopyResumeCommand={onCopyResumeCommand}
|
|
onCopyAgentId={onCopyAgentId}
|
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
|
onCloseTabsToRight={onCloseTabsToRight}
|
|
onCloseOtherTabs={onCloseOtherTabs}
|
|
onSelectNewTabOption={onSelectNewTabOption}
|
|
newTabAgentOptionId={newTabAgentOptionId}
|
|
onOpenPaneTab={onOpenPaneTab}
|
|
onRetargetTab={onRetargetTab}
|
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
|
onFocusPane={onFocusPane}
|
|
onSplitPane={onSplitPane}
|
|
onMoveTabToPane={onMoveTabToPane}
|
|
onResizeSplit={onResizeSplit}
|
|
onReorderTabsInPane={onReorderTabsInPane}
|
|
renderPaneEmptyState={renderPaneEmptyState}
|
|
activeDragTabId={activeDragTabId}
|
|
showDropZones={showDropZones}
|
|
dropPreview={dropPreview}
|
|
onDropPreviewChange={onDropPreviewChange}
|
|
/>
|
|
</View>
|
|
{index < node.group.children.length - 1 ? (
|
|
<ResizeHandle
|
|
direction={node.group.direction}
|
|
groupId={node.group.id}
|
|
index={index}
|
|
sizes={node.group.sizes}
|
|
onResizeSplit={onResizeSplit}
|
|
/>
|
|
) : null}
|
|
</Fragment>
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function SplitPaneView({
|
|
pane,
|
|
tabsById,
|
|
focused,
|
|
normalizedServerId,
|
|
normalizedWorkspaceId,
|
|
hoveredCloseTabKey,
|
|
setHoveredTabKey,
|
|
setHoveredCloseTabKey,
|
|
isArchivingAgent,
|
|
killTerminalPending,
|
|
killTerminalId,
|
|
onNavigateTab,
|
|
onCloseTab,
|
|
onCopyResumeCommand,
|
|
onCopyAgentId,
|
|
onCloseTabsToLeft,
|
|
onCloseTabsToRight,
|
|
onCloseOtherTabs,
|
|
onSelectNewTabOption,
|
|
newTabAgentOptionId,
|
|
onOpenPaneTab,
|
|
onRetargetTab,
|
|
onOpenWorkspaceFile,
|
|
onFocusPane,
|
|
onReorderTabsInPane,
|
|
renderPaneEmptyState,
|
|
activeDragTabId,
|
|
showDropZones,
|
|
dropPreview,
|
|
onDropPreviewChange,
|
|
}: SplitPaneViewProps) {
|
|
const { theme } = useUnistyles();
|
|
const paneTabs = useMemo(() => getPaneTabDescriptors(pane, tabsById), [pane, tabsById]);
|
|
const paneModel = useMemo(
|
|
() =>
|
|
deriveWorkspaceTabModel({
|
|
tabs: paneTabs.map((tab) => ({
|
|
tabId: tab.tabId,
|
|
target: tab.target,
|
|
createdAt: 0,
|
|
})),
|
|
focusedTabId: pane.focusedTabId,
|
|
}),
|
|
[pane.focusedTabId, paneTabs]
|
|
);
|
|
const activeTabDescriptor = paneModel.activeTab
|
|
? paneTabs.find((tab) => tab.tabId === paneModel.activeTab?.descriptor.tabId) ?? null
|
|
: null;
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.pane,
|
|
{
|
|
borderColor: focused ? theme.colors.borderAccent : theme.colors.border,
|
|
},
|
|
]}
|
|
onPointerDownCapture={() => {
|
|
onFocusPane(pane.id);
|
|
}}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.paneTabs,
|
|
focused
|
|
? {
|
|
borderTopColor: theme.colors.accent,
|
|
}
|
|
: null,
|
|
]}
|
|
>
|
|
<WorkspaceDesktopTabsRow
|
|
paneId={pane.id}
|
|
tabs={paneTabs}
|
|
activeTabKey={activeTabDescriptor?.key ?? ""}
|
|
normalizedServerId={normalizedServerId}
|
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
hoveredCloseTabKey={hoveredCloseTabKey}
|
|
setHoveredTabKey={setHoveredTabKey}
|
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
|
isArchivingAgent={isArchivingAgent}
|
|
killTerminalPending={killTerminalPending}
|
|
killTerminalId={killTerminalId}
|
|
onNavigateTab={onNavigateTab}
|
|
onCloseTab={onCloseTab}
|
|
onCopyResumeCommand={onCopyResumeCommand}
|
|
onCopyAgentId={onCopyAgentId}
|
|
onCloseTabsToLeft={(tabId) => onCloseTabsToLeft(tabId, paneTabs)}
|
|
onCloseTabsToRight={(tabId) => onCloseTabsToRight(tabId, paneTabs)}
|
|
onCloseOtherTabs={(tabId) => onCloseOtherTabs(tabId, paneTabs)}
|
|
onSelectNewTabOption={onSelectNewTabOption}
|
|
newTabAgentOptionId={newTabAgentOptionId ?? "__new_tab_agent__"}
|
|
onReorderTabs={(nextTabs) => {
|
|
onReorderTabsInPane(pane.id, nextTabs.map((tab) => tab.tabId));
|
|
}}
|
|
externalDndContext
|
|
activeDragTabId={activeDragTabId}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.paneContent}>
|
|
{activeTabDescriptor ? (
|
|
<WorkspacePaneContent
|
|
tab={activeTabDescriptor}
|
|
normalizedServerId={normalizedServerId}
|
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
onOpenTab={(target) => {
|
|
onOpenPaneTab({
|
|
paneId: pane.id,
|
|
target,
|
|
});
|
|
}}
|
|
onCloseCurrentTab={() => {
|
|
void onCloseTab(activeTabDescriptor.tabId);
|
|
}}
|
|
onRetargetCurrentTab={(target) => {
|
|
onRetargetTab(activeTabDescriptor.tabId, target);
|
|
}}
|
|
onOpenWorkspaceFile={(filePath) => {
|
|
onOpenWorkspaceFile({
|
|
paneId: pane.id,
|
|
filePath,
|
|
});
|
|
}}
|
|
/>
|
|
) : (
|
|
renderPaneEmptyState?.() ?? null
|
|
)}
|
|
</View>
|
|
|
|
<SplitDropZone
|
|
paneId={pane.id}
|
|
active={showDropZones}
|
|
preview={dropPreview}
|
|
onHoverChange={onDropPreviewChange}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function collectPanesById(node: SplitNode): Map<string, SplitPane> {
|
|
const next = new Map<string, SplitPane>();
|
|
function visit(current: SplitNode) {
|
|
if (current.kind === "pane") {
|
|
next.set(current.pane.id, current.pane);
|
|
return;
|
|
}
|
|
for (const child of current.group.children) {
|
|
visit(child);
|
|
}
|
|
}
|
|
visit(node);
|
|
return next;
|
|
}
|
|
|
|
function getPaneTabDescriptors(
|
|
pane: SplitPane,
|
|
tabsById: Map<string, WorkspaceTab>
|
|
): WorkspaceTabDescriptor[] {
|
|
const next: WorkspaceTabDescriptor[] = [];
|
|
for (const tabId of pane.tabIds) {
|
|
const tab = tabsById.get(tabId);
|
|
if (!tab) {
|
|
continue;
|
|
}
|
|
next.push({
|
|
key: tab.tabId,
|
|
tabId: tab.tabId,
|
|
kind: tab.target.kind,
|
|
target: tab.target,
|
|
});
|
|
}
|
|
return next;
|
|
}
|
|
|
|
function getNodeKey(node: SplitNode): string {
|
|
if (node.kind === "pane") {
|
|
return node.pane.id;
|
|
}
|
|
return node.group.id;
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
group: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
minHeight: 0,
|
|
},
|
|
groupHorizontal: {
|
|
flexDirection: "row",
|
|
},
|
|
groupVertical: {
|
|
flexDirection: "column",
|
|
},
|
|
groupChild: {
|
|
flexBasis: 0,
|
|
minWidth: 0,
|
|
minHeight: 0,
|
|
},
|
|
pane: {
|
|
position: "relative",
|
|
flex: 1,
|
|
minWidth: 0,
|
|
minHeight: 0,
|
|
backgroundColor: theme.colors.surface0,
|
|
borderWidth: 1,
|
|
overflow: "hidden",
|
|
},
|
|
paneTabs: {
|
|
borderTopWidth: 2,
|
|
borderTopColor: "transparent",
|
|
},
|
|
paneContent: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
minHeight: 0,
|
|
},
|
|
}));
|