mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-12 08:27:27 +02:00
243 lines
5.9 KiB
TypeScript
243 lines
5.9 KiB
TypeScript
import type { MosaicNode } from "react-mosaic-component";
|
|
|
|
/**
|
|
* A layout preset that can be applied to arrange windows
|
|
*/
|
|
export interface LayoutPreset {
|
|
/** Unique identifier for the preset */
|
|
id: string;
|
|
/** Display name for the preset */
|
|
name: string;
|
|
/** Description of the layout arrangement */
|
|
description: string;
|
|
/** Minimum number of windows required */
|
|
minSlots: number;
|
|
/** Maximum number of windows (undefined = no limit) */
|
|
maxSlots?: number;
|
|
/** Function to generate layout for given window IDs */
|
|
generate: (windowIds: string[]) => MosaicNode<string>;
|
|
}
|
|
|
|
/**
|
|
* Builds a horizontal row of windows with equal splits
|
|
*/
|
|
function buildHorizontalRow(windowIds: string[]): MosaicNode<string> {
|
|
if (windowIds.length === 0) {
|
|
throw new Error("Cannot build row with zero windows");
|
|
}
|
|
if (windowIds.length === 1) {
|
|
return windowIds[0];
|
|
}
|
|
|
|
// Calculate percentage for first window to make equal splits
|
|
const splitPercent = 100 / windowIds.length;
|
|
|
|
return {
|
|
direction: "row",
|
|
first: windowIds[0],
|
|
second: buildHorizontalRow(windowIds.slice(1)),
|
|
splitPercentage: splitPercent,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Builds a vertical stack of windows with equal splits
|
|
*/
|
|
function buildVerticalStack(windowIds: string[]): MosaicNode<string> {
|
|
if (windowIds.length === 0) {
|
|
throw new Error("Cannot build stack with zero windows");
|
|
}
|
|
if (windowIds.length === 1) {
|
|
return windowIds[0];
|
|
}
|
|
|
|
// Calculate percentage for first window to make equal splits
|
|
const splitPercent = 100 / windowIds.length;
|
|
|
|
return {
|
|
direction: "column",
|
|
first: windowIds[0],
|
|
second: buildVerticalStack(windowIds.slice(1)),
|
|
splitPercentage: splitPercent,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Builds a vertical stack of MosaicNodes with equal splits
|
|
*/
|
|
function buildVerticalStackOfNodes(
|
|
nodes: MosaicNode<string>[],
|
|
): MosaicNode<string> {
|
|
if (nodes.length === 0) {
|
|
throw new Error("Cannot build stack with zero nodes");
|
|
}
|
|
if (nodes.length === 1) {
|
|
return nodes[0];
|
|
}
|
|
|
|
// Calculate percentage for first node to make equal splits
|
|
const splitPercent = 100 / nodes.length;
|
|
|
|
return {
|
|
direction: "column",
|
|
first: nodes[0],
|
|
second: buildVerticalStackOfNodes(nodes.slice(1)),
|
|
splitPercentage: splitPercent,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculates best grid dimensions for N windows
|
|
* Prefers square-ish grids, slightly favoring more columns than rows
|
|
*/
|
|
function calculateGridDimensions(windowCount: number): {
|
|
rows: number;
|
|
cols: number;
|
|
} {
|
|
const sqrt = Math.sqrt(windowCount);
|
|
const rows = Math.floor(sqrt);
|
|
const cols = Math.ceil(windowCount / rows);
|
|
return { rows, cols };
|
|
}
|
|
|
|
/**
|
|
* Chunks an array into groups of specified size
|
|
*/
|
|
function chunkArray<T>(array: T[], size: number): T[][] {
|
|
const chunks: T[][] = [];
|
|
for (let i = 0; i < array.length; i += size) {
|
|
chunks.push(array.slice(i, i + size));
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
/**
|
|
* Builds a grid layout from window IDs
|
|
*/
|
|
function buildGridLayout(windowIds: string[]): MosaicNode<string> {
|
|
if (windowIds.length === 0) {
|
|
throw new Error("Cannot build grid with zero windows");
|
|
}
|
|
if (windowIds.length === 1) {
|
|
return windowIds[0];
|
|
}
|
|
|
|
const { cols } = calculateGridDimensions(windowIds.length);
|
|
|
|
// Split windows into rows
|
|
const rowChunks = chunkArray(windowIds, cols);
|
|
|
|
// Build each row as a horizontal split
|
|
const rowNodes = rowChunks.map((chunk) => buildHorizontalRow(chunk));
|
|
|
|
// Stack rows vertically
|
|
return buildVerticalStackOfNodes(rowNodes);
|
|
}
|
|
|
|
/**
|
|
* Built-in layout presets
|
|
*/
|
|
export const BUILT_IN_PRESETS: Record<string, LayoutPreset> = {
|
|
"side-by-side": {
|
|
id: "side-by-side",
|
|
name: "Side by Side",
|
|
description: "All windows in a single row",
|
|
minSlots: 2,
|
|
generate: (windowIds: string[]) => {
|
|
return buildHorizontalRow(windowIds);
|
|
},
|
|
},
|
|
|
|
"main-sidebar": {
|
|
id: "main-sidebar",
|
|
name: "Main + Sidebar",
|
|
description: "Large main window with sidebar windows stacked",
|
|
minSlots: 2,
|
|
generate: (windowIds: string[]) => {
|
|
const [main, ...sidebars] = windowIds;
|
|
|
|
if (sidebars.length === 0) {
|
|
return main;
|
|
}
|
|
|
|
return {
|
|
direction: "row",
|
|
first: main,
|
|
second: buildVerticalStack(sidebars),
|
|
splitPercentage: 70,
|
|
};
|
|
},
|
|
},
|
|
|
|
grid: {
|
|
id: "grid",
|
|
name: "Grid",
|
|
description: "All windows in an adaptive grid layout",
|
|
minSlots: 2,
|
|
generate: (windowIds: string[]) => {
|
|
return buildGridLayout(windowIds);
|
|
},
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Collects window IDs from a layout tree in depth-first order
|
|
*/
|
|
export function collectWindowIds(layout: MosaicNode<string> | null): string[] {
|
|
if (layout === null) {
|
|
return [];
|
|
}
|
|
|
|
if (typeof layout === "string") {
|
|
return [layout];
|
|
}
|
|
|
|
return [
|
|
...collectWindowIds(layout.first),
|
|
...collectWindowIds(layout.second),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Applies a preset layout to existing windows
|
|
* Uses ALL windows in the adaptive layout
|
|
*/
|
|
export function applyPresetToLayout(
|
|
currentLayout: MosaicNode<string> | null,
|
|
preset: LayoutPreset,
|
|
): MosaicNode<string> {
|
|
// Collect all window IDs from current layout
|
|
const windowIds = collectWindowIds(currentLayout);
|
|
|
|
// Check minimum requirement
|
|
if (windowIds.length < preset.minSlots) {
|
|
throw new Error(
|
|
`Preset "${preset.name}" requires at least ${preset.minSlots} windows but only ${windowIds.length} available`,
|
|
);
|
|
}
|
|
|
|
// Check maximum limit if defined
|
|
if (preset.maxSlots && windowIds.length > preset.maxSlots) {
|
|
throw new Error(
|
|
`Preset "${preset.name}" supports maximum ${preset.maxSlots} windows but ${windowIds.length} available`,
|
|
);
|
|
}
|
|
|
|
// Generate layout using all windows
|
|
return preset.generate(windowIds);
|
|
}
|
|
|
|
/**
|
|
* Get a preset by ID
|
|
*/
|
|
export function getPreset(presetId: string): LayoutPreset | undefined {
|
|
return BUILT_IN_PRESETS[presetId];
|
|
}
|
|
|
|
/**
|
|
* Get all available presets
|
|
*/
|
|
export function getAllPresets(): LayoutPreset[] {
|
|
return Object.values(BUILT_IN_PRESETS);
|
|
}
|