import React, { useState, useRef, useEffect, memo, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { splitDragState, notifyDragEnd } from "@/lib/splitDragging"; import { tabIcon } from "@/shell/tabUtils"; import type { Tab, SplitMode } from "@/types/ui-types"; // ─── useSplitSizes ──────────────────────────────────────────────────────────── type RowColSizes = number[][]; function defaultSizes(mode: SplitMode): { rowSizes: number[]; rowColSizes: RowColSizes; } { switch (mode) { case "2-way": return { rowSizes: [100], rowColSizes: [[50, 50]] }; case "3-way": return { rowSizes: [50, 50], rowColSizes: [[50, 50], [100]] }; case "3-way-horizontal": return { rowSizes: [50, 50], rowColSizes: [[50, 50], [100]] }; case "4-way": return { rowSizes: [50, 50], rowColSizes: [ [50, 50], [50, 50], ], }; case "5-way": return { rowSizes: [50, 50], rowColSizes: [ [33.3, 33.3, 33.4], [33.3, 66.7], ], }; case "6-way": return { rowSizes: [50, 50], rowColSizes: [ [33.3, 33.3, 33.4], [33.3, 33.3, 33.4], ], }; default: return { rowSizes: [100], rowColSizes: [[100]] }; } } function useSplitSizes(splitMode: SplitMode) { const init = defaultSizes(splitMode); const [rowSizes, setRowSizes] = useState(init.rowSizes); const [rowColSizes, setRowColSizes] = useState(init.rowColSizes); const [isDragging, setIsDragging] = useState(false); const containerRef = useRef(null); useEffect(() => { const d = defaultSizes(splitMode); setRowSizes(d.rowSizes); setRowColSizes(d.rowColSizes); }, [splitMode]); function reset() { const d = defaultSizes(splitMode); setRowSizes(d.rowSizes); setRowColSizes(d.rowColSizes); } function startDrag() { splitDragState.active = true; setIsDragging(true); } function endDrag() { splitDragState.active = false; setIsDragging(false); notifyDragEnd(); } function onRowDivider(e: React.MouseEvent, rowIdx: number) { e.preventDefault(); const container = containerRef.current; if (!container) return; startDrag(); const totalH = container.getBoundingClientRect().height; const startY = e.clientY; const a = rowSizes[rowIdx], b = rowSizes[rowIdx + 1]; function onMove(ev: MouseEvent) { const na = Math.max( 10, Math.min(a + b - 10, a + ((ev.clientY - startY) / totalH) * 100), ); setRowSizes((prev) => { const n = [...prev]; n[rowIdx] = na; n[rowIdx + 1] = a + b - na; return n; }); } function onUp() { endDrag(); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); } window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); } function onRowDividerTouch(e: React.TouchEvent, rowIdx: number) { const container = containerRef.current; if (!container) return; startDrag(); const totalH = container.getBoundingClientRect().height; const startY = e.touches[0].clientY; const a = rowSizes[rowIdx], b = rowSizes[rowIdx + 1]; function onMove(ev: TouchEvent) { const na = Math.max( 10, Math.min( a + b - 10, a + ((ev.touches[0].clientY - startY) / totalH) * 100, ), ); setRowSizes((prev) => { const n = [...prev]; n[rowIdx] = na; n[rowIdx + 1] = a + b - na; return n; }); } function onUp() { endDrag(); window.removeEventListener("touchmove", onMove); window.removeEventListener("touchend", onUp); } window.addEventListener("touchmove", onMove, { passive: false }); window.addEventListener("touchend", onUp); } function onColDivider(e: React.MouseEvent, rowIdx: number, colIdx: number) { e.preventDefault(); const container = containerRef.current; if (!container) return; startDrag(); const totalW = container.getBoundingClientRect().width; const startX = e.clientX; const cols = rowColSizes[rowIdx]; const a = cols[colIdx], b = cols[colIdx + 1]; function onMove(ev: MouseEvent) { const na = Math.max( 10, Math.min(a + b - 10, a + ((ev.clientX - startX) / totalW) * 100), ); setRowColSizes((prev) => { const next = prev.map((r) => [...r]); next[rowIdx][colIdx] = na; next[rowIdx][colIdx + 1] = a + b - na; return next; }); } function onUp() { endDrag(); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); } window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); } function onColDividerTouch( e: React.TouchEvent, rowIdx: number, colIdx: number, ) { const container = containerRef.current; if (!container) return; startDrag(); const totalW = container.getBoundingClientRect().width; const startX = e.touches[0].clientX; const cols = rowColSizes[rowIdx]; const a = cols[colIdx], b = cols[colIdx + 1]; function onMove(ev: TouchEvent) { const na = Math.max( 10, Math.min( a + b - 10, a + ((ev.touches[0].clientX - startX) / totalW) * 100, ), ); setRowColSizes((prev) => { const next = prev.map((r) => [...r]); next[rowIdx][colIdx] = na; next[rowIdx][colIdx + 1] = a + b - na; return next; }); } function onUp() { endDrag(); window.removeEventListener("touchmove", onMove); window.removeEventListener("touchend", onUp); } window.addEventListener("touchmove", onMove, { passive: false }); window.addEventListener("touchend", onUp); } return { rowSizes, rowColSizes, isDragging, containerRef, reset, onRowDivider, onRowDividerTouch, onColDivider, onColDividerTouch, }; } // ─── Dividers ───────────────────────────────────────────────────────────────── function ColDivider({ onMouseDown, onTouchStart, }: { onMouseDown: (e: React.MouseEvent) => void; onTouchStart: (e: React.TouchEvent) => void; }) { return (
); } function RowDivider({ onMouseDown, onTouchStart, }: { onMouseDown: (e: React.MouseEvent) => void; onTouchStart: (e: React.TouchEvent) => void; }) { return (
); } // ─── Pane ───────────────────────────────────────────────────────────────────── function PaneHeader({ tab, paneIndex, }: { tab: Tab | null; paneIndex: number; }) { const { t } = useTranslation(); return (
{tab ? ( <> {tabIcon(tab.type)} {tab.type === "dashboard" ? "Dashboard" : tab.label} ) : ( {t("splitScreen.paneEmpty", { index: paneIndex + 1 })} )}
); } function EmptyPane() { const { t } = useTranslation(); return (
{t("splitScreen.noTabAssigned")}
); } const Pane = memo(function Pane({ tab, paneIndex, isDragging, onPaneContentRef, }: { tab: Tab | null; paneIndex: number; isDragging: boolean; onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void; }) { const contentRef = useCallback( (el: HTMLDivElement | null) => { onPaneContentRef?.(paneIndex, el); }, [paneIndex, onPaneContentRef], ); return (
{tab ? (
) : ( )}
{isDragging && (
)}
); }); // ─── Row (top-level so React never sees a new component type) ───────────────── const Row = memo(function Row({ rowIdx, paneIndices, rowHeight, colWidths, paneTabIds, tabs, isDragging, onColDivider, onColDividerTouch, onPaneContentRef, }: { rowIdx: number; paneIndices: number[]; rowHeight: number; colWidths: number[]; paneTabIds: (string | null)[]; tabs: Tab[]; isDragging: boolean; onColDivider: (e: React.MouseEvent, rowIdx: number, colIdx: number) => void; onColDividerTouch: ( e: React.TouchEvent, rowIdx: number, colIdx: number, ) => void; onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void; }) { const widths = colWidths ?? []; return (
{paneIndices.map((pIdx, ci) => { const tabId = paneTabIds[pIdx]; const tab = tabId != null ? (tabs.find((t) => t.id === tabId) ?? null) : null; return (
{ci < paneIndices.length - 1 && ( onColDivider(e, rowIdx, ci)} onTouchStart={(e) => onColDividerTouch(e, rowIdx, ci)} /> )}
); })}
); }); // ─── SplitView ──────────────────────────────────────────────────────────────── export const SplitView = memo(function SplitView({ tabs, paneTabIds, splitMode, onTerminalResize, onPaneContentRef, }: { tabs: Tab[]; paneTabIds: (string | null)[]; splitMode: SplitMode; onTerminalResize?: () => void; onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void; }) { const { rowSizes, rowColSizes, isDragging, containerRef, reset, onRowDivider, onRowDividerTouch, onColDivider, onColDividerTouch, } = useSplitSizes(splitMode); useEffect(() => { if (!isDragging) { const id = requestAnimationFrame(() => onTerminalResize?.()); return () => cancelAnimationFrame(id); } }, [isDragging, onTerminalResize]); // Inline pane lookup for the non-Row layouts (3-way, 3-way-horizontal) function tab(idx: number): Tab | null { const tabId = paneTabIds[idx]; return tabId != null ? (tabs.find((t) => t.id === tabId) ?? null) : null; } return (
{splitMode === "2-way" && ( )} {splitMode === "3-way" && (
onColDivider(e, 0, 0)} onTouchStart={(e) => onColDividerTouch(e, 0, 0)} />
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)} {splitMode === "3-way-horizontal" && (
onColDivider(e, 0, 0)} onTouchStart={(e) => onColDividerTouch(e, 0, 0)} />
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)} {splitMode === "4-way" && (
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)} {splitMode === "5-way" && (
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)} {splitMode === "6-way" && (
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)}
); });