/** * The open documents of the tab row (#171): which tabs there are, in which order, and which one is * showing. `null` as the active one means a fixed tab (Scene, Game, Plugins, a plugin's) is showing. */ export interface DocumentRow { docs: readonly T[]; active: T | null; } /** Opens `doc`: an open one merely becomes active, a new one goes right of the active document, or last. */ export function openDocument(row: DocumentRow, doc: T): DocumentRow { if (row.docs.includes(doc)) return { docs: row.docs, active: doc }; const at = row.active === null ? -1 : row.docs.indexOf(row.active); const docs = [...row.docs]; docs.splice(at < 0 ? docs.length : at + 1, 0, doc); return { docs, active: doc }; } /** * Closes `doc`. Closing the one on screen shows its left neighbour, or the right one when it was * first; with none left, `active` is null and the caller goes back to the scene. */ export function closeDocument(row: DocumentRow, doc: T): DocumentRow { const at = row.docs.indexOf(doc); if (at < 0) return row; const docs = row.docs.filter((d) => d !== doc); if (row.active !== doc) return { docs, active: row.active }; return { docs, active: docs[Math.max(0, at - 1)] ?? null }; } /** * What shows when the document that stood at `at` in `docs` has left it (#371): the nearest one to * its left that `fits`, else the nearest to its right – as `closeDocument` picks, but skipping the * documents that cannot show in the tabs now. Null means the scene. */ export function neighbourOf(docs: readonly T[], at: number, fits: (doc: T) => boolean): T | null { for (let i = Math.min(at, docs.length) - 1; i >= 0; i--) if (fits(docs[i]!)) return docs[i]!; for (let i = Math.max(0, at); i < docs.length; i++) if (fits(docs[i]!)) return docs[i]!; return null; } /** * Puts `to` where `from` is – a tab that goes on to another document, like a behavior followed * into a call. Where `to` was open already, that tab goes: a document has one tab. */ export function replaceDocument(row: DocumentRow, from: T, to: T): DocumentRow { if (from === to || !row.docs.includes(from)) return row; const docs = row.docs.filter((d) => d !== to).map((d) => (d === from ? to : d)); return { docs, active: row.active === from || row.active === to ? to : row.active }; }