regular-layout 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +194 -0
- package/README.md +55 -0
- package/dist/common/calculate_intersect.d.ts +18 -0
- package/dist/common/calculate_split.d.ts +2 -0
- package/dist/common/flatten.d.ts +13 -0
- package/dist/common/generate_grid.d.ts +32 -0
- package/dist/common/generate_overlay.d.ts +2 -0
- package/dist/common/insert_child.d.ts +15 -0
- package/dist/common/layout_config.d.ts +92 -0
- package/dist/common/redistribute_panel_sizes.d.ts +19 -0
- package/dist/common/remove_child.d.ts +13 -0
- package/dist/extensions.d.ts +15 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +7 -0
- package/dist/regular-layout-frame.d.ts +38 -0
- package/dist/regular-layout.d.ts +120 -0
- package/package.json +35 -0
- package/src/common/calculate_intersect.ts +176 -0
- package/src/common/calculate_split.ts +53 -0
- package/src/common/flatten.ts +57 -0
- package/src/common/generate_grid.ts +249 -0
- package/src/common/generate_overlay.ts +25 -0
- package/src/common/insert_child.ts +129 -0
- package/src/common/layout_config.ts +127 -0
- package/src/common/redistribute_panel_sizes.ts +100 -0
- package/src/common/remove_child.ts +102 -0
- package/src/extensions.ts +40 -0
- package/src/index.ts +72 -0
- package/src/regular-layout-frame.ts +114 -0
- package/src/regular-layout.ts +334 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { calculate_intersection } from "./calculate_intersect";
|
|
2
|
+
import { insert_child } from "./insert_child";
|
|
3
|
+
import {
|
|
4
|
+
SPLIT_EDGE_TOLERANCE,
|
|
5
|
+
type Layout,
|
|
6
|
+
type LayoutPath,
|
|
7
|
+
} from "./layout_config";
|
|
8
|
+
|
|
9
|
+
export function calculate_split(
|
|
10
|
+
col: number,
|
|
11
|
+
row: number,
|
|
12
|
+
panel: Layout,
|
|
13
|
+
slot: string,
|
|
14
|
+
config: LayoutPath,
|
|
15
|
+
): LayoutPath {
|
|
16
|
+
if (
|
|
17
|
+
config.column_offset < SPLIT_EDGE_TOLERANCE ||
|
|
18
|
+
config.column_offset > 1 - SPLIT_EDGE_TOLERANCE
|
|
19
|
+
) {
|
|
20
|
+
if (config.orientation === "vertical") {
|
|
21
|
+
const new_panel = insert_child(panel, slot, [
|
|
22
|
+
...config.path,
|
|
23
|
+
config.column_offset < SPLIT_EDGE_TOLERANCE ? 0 : 1,
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
config = calculate_intersection(col, row, new_panel, false);
|
|
27
|
+
} else {
|
|
28
|
+
const new_panel = insert_child(panel, slot, config.path);
|
|
29
|
+
config = calculate_intersection(col, row, new_panel, false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
(config.row_offset < SPLIT_EDGE_TOLERANCE &&
|
|
35
|
+
config.row_offset < config.column_offset) ||
|
|
36
|
+
(config.row_offset > 1 - SPLIT_EDGE_TOLERANCE &&
|
|
37
|
+
config.row_offset > config.column_offset)
|
|
38
|
+
) {
|
|
39
|
+
if (config.orientation === "horizontal") {
|
|
40
|
+
const new_panel = insert_child(panel, slot, [
|
|
41
|
+
...config.path,
|
|
42
|
+
config.row_offset < SPLIT_EDGE_TOLERANCE ? 0 : 1,
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
config = calculate_intersection(col, row, new_panel, false);
|
|
46
|
+
} else {
|
|
47
|
+
const new_panel = insert_child(panel, slot, config.path);
|
|
48
|
+
config = calculate_intersection(col, row, new_panel, false);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
2
|
+
// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
|
|
3
|
+
// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
|
|
4
|
+
// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
|
|
5
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
6
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
7
|
+
// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
|
|
8
|
+
// ┃ * of the Regular Layout library, distributed under the terms of the * ┃
|
|
9
|
+
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
|
+
|
|
12
|
+
import type { Layout } from "./layout_config.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Flattens the layout tree by merging parent and child split panels that have
|
|
16
|
+
* the same orientation and type.
|
|
17
|
+
*
|
|
18
|
+
* When a split panel contains a child split panel with the same orientation,
|
|
19
|
+
* they are merged into a single split panel. The sizes array is scaled
|
|
20
|
+
* correctly to maintain proportions.
|
|
21
|
+
*
|
|
22
|
+
* @param layout - The layout tree to flatten
|
|
23
|
+
* @returns A new flattened layout tree (original is not mutated).
|
|
24
|
+
*/
|
|
25
|
+
export function flatten(layout: Layout): Layout {
|
|
26
|
+
if (layout.type === "child-panel") {
|
|
27
|
+
return layout;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const flattenedChildren: Layout[] = [];
|
|
31
|
+
const flattenedSizes: number[] = [];
|
|
32
|
+
for (let i = 0; i < layout.children.length; i++) {
|
|
33
|
+
const child = layout.children[i];
|
|
34
|
+
const childSize = layout.sizes[i];
|
|
35
|
+
const flattenedChild = flatten(child);
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
flattenedChild.type === "split-panel" &&
|
|
39
|
+
flattenedChild.orientation === layout.orientation
|
|
40
|
+
) {
|
|
41
|
+
for (let j = 0; j < flattenedChild.children.length; j++) {
|
|
42
|
+
flattenedChildren.push(flattenedChild.children[j]);
|
|
43
|
+
flattenedSizes.push(flattenedChild.sizes[j] * childSize);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
flattenedChildren.push(flattenedChild);
|
|
47
|
+
flattenedSizes.push(childSize);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
type: "split-panel",
|
|
53
|
+
orientation: layout.orientation,
|
|
54
|
+
children: flattenedChildren,
|
|
55
|
+
sizes: flattenedSizes,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
2
|
+
// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
|
|
3
|
+
// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
|
|
4
|
+
// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
|
|
5
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
6
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
7
|
+
// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
|
|
8
|
+
// ┃ * of the Regular Layout library, distributed under the terms of the * ┃
|
|
9
|
+
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
|
+
|
|
12
|
+
import { GRID_TRACK_COLLAPSE_TOLERANCE, type Layout } from "./layout_config.ts";
|
|
13
|
+
import { remove_child } from "./remove_child.ts";
|
|
14
|
+
|
|
15
|
+
interface GridCell {
|
|
16
|
+
child: string;
|
|
17
|
+
colStart: number;
|
|
18
|
+
colEnd: number;
|
|
19
|
+
rowStart: number;
|
|
20
|
+
rowEnd: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function dedupePositions(positions: number[]): number[] {
|
|
24
|
+
if (positions.length === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sorted = positions.sort((a, b) => a - b);
|
|
29
|
+
const result = [sorted[0]];
|
|
30
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
31
|
+
if (
|
|
32
|
+
Math.abs(sorted[i] - result[result.length - 1]) >
|
|
33
|
+
GRID_TRACK_COLLAPSE_TOLERANCE
|
|
34
|
+
) {
|
|
35
|
+
result.push(sorted[i]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectTrackPositions(
|
|
43
|
+
panel: Layout,
|
|
44
|
+
orientation: "horizontal" | "vertical",
|
|
45
|
+
start: number,
|
|
46
|
+
end: number,
|
|
47
|
+
): number[] {
|
|
48
|
+
if (panel.type === "child-panel") {
|
|
49
|
+
return [start, end];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (panel.orientation === orientation) {
|
|
53
|
+
const positions: number[] = [start, end];
|
|
54
|
+
let current = start;
|
|
55
|
+
for (let i = 0; i < panel.children.length; i++) {
|
|
56
|
+
const size = panel.sizes[i];
|
|
57
|
+
const childPositions = collectTrackPositions(
|
|
58
|
+
panel.children[i],
|
|
59
|
+
orientation,
|
|
60
|
+
current,
|
|
61
|
+
current + size * (end - start),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
positions.push(...childPositions);
|
|
65
|
+
current = current + size * (end - start);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return dedupePositions(positions);
|
|
69
|
+
} else {
|
|
70
|
+
const allPositions: number[] = [start, end];
|
|
71
|
+
for (const child of panel.children) {
|
|
72
|
+
const childPositions = collectTrackPositions(
|
|
73
|
+
child,
|
|
74
|
+
orientation,
|
|
75
|
+
start,
|
|
76
|
+
end,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
allPositions.push(...childPositions);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return dedupePositions(allPositions);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findTrackIndex(positions: number[], value: number): number {
|
|
87
|
+
for (let i = 0; i < positions.length; i++) {
|
|
88
|
+
if (Math.abs(positions[i] - value) < 0.0001) {
|
|
89
|
+
return i;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error(`Position ${value} not found in ${positions}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildCells(
|
|
97
|
+
panel: Layout,
|
|
98
|
+
colPositions: number[],
|
|
99
|
+
rowPositions: number[],
|
|
100
|
+
colStart: number,
|
|
101
|
+
colEnd: number,
|
|
102
|
+
rowStart: number,
|
|
103
|
+
rowEnd: number,
|
|
104
|
+
): GridCell[] {
|
|
105
|
+
if (panel.type === "child-panel") {
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
child: panel.child,
|
|
109
|
+
colStart: findTrackIndex(colPositions, colStart),
|
|
110
|
+
colEnd: findTrackIndex(colPositions, colEnd),
|
|
111
|
+
rowStart: findTrackIndex(rowPositions, rowStart),
|
|
112
|
+
rowEnd: findTrackIndex(rowPositions, rowEnd),
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cells: GridCell[] = [];
|
|
118
|
+
const { children, sizes, orientation } = panel;
|
|
119
|
+
if (orientation === "horizontal") {
|
|
120
|
+
let current = colStart;
|
|
121
|
+
for (let i = 0; i < children.length; i++) {
|
|
122
|
+
const next = current + sizes[i] * (colEnd - colStart);
|
|
123
|
+
cells.push(
|
|
124
|
+
...buildCells(
|
|
125
|
+
children[i],
|
|
126
|
+
colPositions,
|
|
127
|
+
rowPositions,
|
|
128
|
+
current,
|
|
129
|
+
next,
|
|
130
|
+
rowStart,
|
|
131
|
+
rowEnd,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
current = next;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
let current = rowStart;
|
|
139
|
+
for (let i = 0; i < children.length; i++) {
|
|
140
|
+
const next = current + sizes[i] * (rowEnd - rowStart);
|
|
141
|
+
cells.push(
|
|
142
|
+
...buildCells(
|
|
143
|
+
children[i],
|
|
144
|
+
colPositions,
|
|
145
|
+
rowPositions,
|
|
146
|
+
colStart,
|
|
147
|
+
colEnd,
|
|
148
|
+
current,
|
|
149
|
+
next,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
current = next;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return cells;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const host_template = (rowTemplate: string, colTemplate: string) =>
|
|
161
|
+
`:host { display: grid; gap: 0px; grid-template-rows: ${rowTemplate}; grid-template-columns: ${colTemplate}; }`;
|
|
162
|
+
|
|
163
|
+
const child_template = (slot: string, rowPart: string, colPart: string) =>
|
|
164
|
+
`:host ::slotted([slot=${slot}]) { grid-column: ${colPart}; grid-row: ${rowPart}; }`;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generates CSS Grid styles to render a layout tree.
|
|
168
|
+
* Creates grid-template-rows, grid-template-columns, and positioning rules for
|
|
169
|
+
* all child panels.
|
|
170
|
+
*
|
|
171
|
+
* @param layout - The layout tree to convert to CSS
|
|
172
|
+
* @param round - If true, rounds percentages to whole numbers. Useful for
|
|
173
|
+
* avoiding sub-pixel rendering issues. Defaults to false.
|
|
174
|
+
* @returns CSS string containing :host and ::slotted rules implementing the
|
|
175
|
+
* layout.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* const layout = {
|
|
180
|
+
* type: "split-panel",
|
|
181
|
+
* orientation: "horizontal",
|
|
182
|
+
* children: [
|
|
183
|
+
* { type: "child-panel", child: "sidebar" },
|
|
184
|
+
* { type: "child-panel", child: "main" }
|
|
185
|
+
* ],
|
|
186
|
+
* sizes: [0.25, 0.75]
|
|
187
|
+
* };
|
|
188
|
+
*
|
|
189
|
+
* const css = create_css_grid_layout(layout);
|
|
190
|
+
* // Returns CSS like:
|
|
191
|
+
* // :host { display: grid; grid-template-columns: 25% 75%; ... }
|
|
192
|
+
* // :host ::slotted([slot=sidebar]) { grid-column: 1; grid-row: 1; }
|
|
193
|
+
* // :host ::slotted([slot=main]) { grid-column: 2; grid-row: 1; }
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export function create_css_grid_layout(
|
|
197
|
+
layout: Layout,
|
|
198
|
+
round: boolean = false,
|
|
199
|
+
overlay?: [string, string],
|
|
200
|
+
): string {
|
|
201
|
+
if (overlay) {
|
|
202
|
+
layout = remove_child(layout, overlay[0]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (layout.type === "child-panel") {
|
|
206
|
+
return `${host_template("100%", "100%")}\n${child_template(layout.child, "1", "1")}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const colPositions = collectTrackPositions(layout, "horizontal", 0, 1);
|
|
210
|
+
const colSizes: number[] = [];
|
|
211
|
+
for (let i = 0; i < colPositions.length - 1; i++) {
|
|
212
|
+
colSizes.push(colPositions[i + 1] - colPositions[i]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const colTemplate = colSizes
|
|
216
|
+
.map((s) => `${round ? Math.round(s * 100) : s * 100}%`)
|
|
217
|
+
.join(" ");
|
|
218
|
+
|
|
219
|
+
const rowPositions = collectTrackPositions(layout, "vertical", 0, 1);
|
|
220
|
+
const rowSizes: number[] = [];
|
|
221
|
+
for (let i = 0; i < rowPositions.length - 1; i++) {
|
|
222
|
+
rowSizes.push(rowPositions[i + 1] - rowPositions[i]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const rowTemplate = rowSizes
|
|
226
|
+
.map((s) => `${round ? Math.round(s * 100) : s * 100}%`)
|
|
227
|
+
.join(" ");
|
|
228
|
+
|
|
229
|
+
const cells = buildCells(layout, colPositions, rowPositions, 0, 1, 0, 1);
|
|
230
|
+
const css = [host_template(rowTemplate, colTemplate)];
|
|
231
|
+
for (const cell of cells) {
|
|
232
|
+
const colPart =
|
|
233
|
+
cell.colEnd - cell.colStart === 1
|
|
234
|
+
? `${cell.colStart + 1}`
|
|
235
|
+
: `${cell.colStart + 1} / ${cell.colEnd + 1}`;
|
|
236
|
+
const rowPart =
|
|
237
|
+
cell.rowEnd - cell.rowStart === 1
|
|
238
|
+
? `${cell.rowStart + 1}`
|
|
239
|
+
: `${cell.rowStart + 1} / ${cell.rowEnd + 1}`;
|
|
240
|
+
|
|
241
|
+
css.push(`${child_template(cell.child, rowPart, colPart)}`);
|
|
242
|
+
if (cell.child === overlay?.[1]) {
|
|
243
|
+
css.push(`${child_template(overlay[0], rowPart, colPart)}`);
|
|
244
|
+
css.push(`:host ::slotted([slot=${overlay[0]}]) { z-index: 1; }`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return css.join("\n");
|
|
249
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
2
|
+
// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
|
|
3
|
+
// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
|
|
4
|
+
// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
|
|
5
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
6
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
7
|
+
// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
|
|
8
|
+
// ┃ * of the Regular Layout library, distributed under the terms of the * ┃
|
|
9
|
+
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
|
+
|
|
12
|
+
import type { LayoutPath } from "./layout_config";
|
|
13
|
+
|
|
14
|
+
export function updateOverlaySheet({
|
|
15
|
+
view_window: { row_start, row_end, col_start, col_end },
|
|
16
|
+
box,
|
|
17
|
+
}: LayoutPath<DOMRect>) {
|
|
18
|
+
const margin = 0;
|
|
19
|
+
const top = row_start * box.height + margin / 2;
|
|
20
|
+
const left = col_start * box.width + margin / 2;
|
|
21
|
+
const height = (row_end - row_start) * box.height - margin;
|
|
22
|
+
const width = (col_end - col_start) * box.width - margin;
|
|
23
|
+
const css = `position:absolute!important;z-index:1;top:${top}px;left:${left}px;height:${height}px;width:${width}px;`;
|
|
24
|
+
return `::slotted(:not([slot])){${css}}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
2
|
+
// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
|
|
3
|
+
// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
|
|
4
|
+
// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
|
|
5
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
6
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
7
|
+
// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
|
|
8
|
+
// ┃ * of the Regular Layout library, distributed under the terms of the * ┃
|
|
9
|
+
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
|
+
|
|
12
|
+
import type { Layout } from "./layout_config.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Inserts a new child panel into the layout tree at a specified location.
|
|
16
|
+
* Creates a split panel if necessary and redistributes sizes equally among all
|
|
17
|
+
* children.
|
|
18
|
+
*
|
|
19
|
+
* @param panel - The root layout tree to insert into
|
|
20
|
+
* @param child - Unique identifier for the new child panel
|
|
21
|
+
* @param path - Array of indices defining where to insert. Empty array inserts
|
|
22
|
+
* at root level.
|
|
23
|
+
* @param orientation - Orientation for newly created split panels. Defaults to
|
|
24
|
+
* "horizontal".
|
|
25
|
+
* @returns A new layout tree with the child inserted (original is not mutated).
|
|
26
|
+
*/
|
|
27
|
+
export function insert_child(
|
|
28
|
+
panel: Layout,
|
|
29
|
+
child: string,
|
|
30
|
+
path: number[],
|
|
31
|
+
orientation: "horizontal" | "vertical" = "horizontal",
|
|
32
|
+
): Layout {
|
|
33
|
+
if (path.length === 0) {
|
|
34
|
+
// Insert at root level
|
|
35
|
+
if (panel.type === "child-panel") {
|
|
36
|
+
// Convert single child-panel to split-panel with two children
|
|
37
|
+
return {
|
|
38
|
+
type: "split-panel",
|
|
39
|
+
orientation,
|
|
40
|
+
children: [
|
|
41
|
+
panel,
|
|
42
|
+
{
|
|
43
|
+
type: "child-panel",
|
|
44
|
+
child,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
sizes: [0.5, 0.5],
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
// Append to existing split-panel
|
|
51
|
+
const newChildren = [
|
|
52
|
+
...panel.children,
|
|
53
|
+
{
|
|
54
|
+
type: "child-panel",
|
|
55
|
+
child,
|
|
56
|
+
} as Layout,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const numChildren = newChildren.length;
|
|
60
|
+
const newSizes = Array(numChildren).fill(1 / numChildren);
|
|
61
|
+
return {
|
|
62
|
+
...panel,
|
|
63
|
+
children: newChildren,
|
|
64
|
+
sizes: newSizes,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Navigate down the path
|
|
70
|
+
const [index, ...restPath] = path;
|
|
71
|
+
if (panel.type === "child-panel") {
|
|
72
|
+
// This shouldn't happen if path.length > 0, but handle it gracefully
|
|
73
|
+
// We need to split this child-panel
|
|
74
|
+
const newPanel: Layout = {
|
|
75
|
+
type: "split-panel",
|
|
76
|
+
orientation,
|
|
77
|
+
children: [panel],
|
|
78
|
+
sizes: [1],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return insert_child(newPanel, child, path, orientation);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (restPath.length === 0 || index === panel.children.length) {
|
|
85
|
+
// Insert at this level at the specified index
|
|
86
|
+
const newChildren = [...panel.children];
|
|
87
|
+
newChildren.splice(index, 0, {
|
|
88
|
+
type: "child-panel",
|
|
89
|
+
child,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const numChildren = newChildren.length;
|
|
93
|
+
const newSizes = Array(numChildren).fill(1 / numChildren);
|
|
94
|
+
return {
|
|
95
|
+
...panel,
|
|
96
|
+
children: newChildren,
|
|
97
|
+
sizes: newSizes,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const targetChild = panel.children[index];
|
|
102
|
+
if (targetChild.type === "child-panel" && restPath.length > 0) {
|
|
103
|
+
// Need to split this child-panel
|
|
104
|
+
const oppositeOrientation =
|
|
105
|
+
panel.orientation === "horizontal" ? "vertical" : "horizontal";
|
|
106
|
+
|
|
107
|
+
const newSplitPanel = insert_child(
|
|
108
|
+
targetChild,
|
|
109
|
+
child,
|
|
110
|
+
restPath,
|
|
111
|
+
oppositeOrientation,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const newChildren = [...panel.children];
|
|
115
|
+
newChildren[index] = newSplitPanel;
|
|
116
|
+
return {
|
|
117
|
+
...panel,
|
|
118
|
+
children: newChildren,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const updatedChild = insert_child(targetChild, child, restPath, orientation);
|
|
123
|
+
const newChildren = [...panel.children];
|
|
124
|
+
newChildren[index] = updatedChild;
|
|
125
|
+
return {
|
|
126
|
+
...panel,
|
|
127
|
+
children: newChildren,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
2
|
+
// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
|
|
3
|
+
// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
|
|
4
|
+
// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
|
|
5
|
+
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
6
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
7
|
+
// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
|
|
8
|
+
// ┃ * of the Regular Layout library, distributed under the terms of the * ┃
|
|
9
|
+
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The percentage of the maximum resize distance that will be clamped.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
export const MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD = 0.15;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Threshold from panel edge that is considered a split vs drop action.
|
|
20
|
+
*/
|
|
21
|
+
export const SPLIT_EDGE_TOLERANCE = 0.15;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tolerance threshold for considering two grid track positions as identical.
|
|
25
|
+
*
|
|
26
|
+
* When collecting and deduplicating track positions, any positions closer than
|
|
27
|
+
* this value are treated as the same position to avoid redundant grid tracks.
|
|
28
|
+
*/
|
|
29
|
+
export const GRID_TRACK_COLLAPSE_TOLERANCE = 0.0001;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The representation of a CSS grid, in JSON form.
|
|
33
|
+
*/
|
|
34
|
+
export type Layout = SplitLayout | TabLayout;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The orientation (of a `SplitPanel`).
|
|
38
|
+
*/
|
|
39
|
+
export type Orientation = "horizontal" | "vertical";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A logical rectange in percent-coordinates (of a (1, 1) square).
|
|
43
|
+
*/
|
|
44
|
+
export interface ViewWindow {
|
|
45
|
+
row_start: number;
|
|
46
|
+
row_end: number;
|
|
47
|
+
col_start: number;
|
|
48
|
+
col_end: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A split panel that divides space among multiple child layouts
|
|
53
|
+
* .
|
|
54
|
+
* Child panels are arranged either horizontally (side by side) or vertically
|
|
55
|
+
* (stacked), via the `orientation` property `"horizzontal"` and `"vertical"`
|
|
56
|
+
* (respectively).
|
|
57
|
+
*/
|
|
58
|
+
export interface SplitLayout {
|
|
59
|
+
type: "split-panel";
|
|
60
|
+
children: Layout[];
|
|
61
|
+
sizes: number[];
|
|
62
|
+
orientation: Orientation;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A leaf panel node that contains a single named child element.
|
|
67
|
+
*/
|
|
68
|
+
export interface TabLayout {
|
|
69
|
+
type: "child-panel";
|
|
70
|
+
child: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Represents a draggable divider between two panels in the layout.
|
|
75
|
+
*
|
|
76
|
+
* Used for hit detection.
|
|
77
|
+
*/
|
|
78
|
+
export interface LayoutDivider {
|
|
79
|
+
path: number[];
|
|
80
|
+
view_window: ViewWindow;
|
|
81
|
+
type: Orientation;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Represents a panel location result from hit detection.
|
|
86
|
+
*
|
|
87
|
+
* Contains both the panel identifier and its grid position in relative units.
|
|
88
|
+
* The generic parameter `T` allows DOM-only properties (e.g. `DOMRect`) to be
|
|
89
|
+
* shared in this cross-platform module.
|
|
90
|
+
*/
|
|
91
|
+
export interface LayoutPath<T = undefined> {
|
|
92
|
+
type: "layout-path";
|
|
93
|
+
slot: string;
|
|
94
|
+
path: number[];
|
|
95
|
+
view_window: ViewWindow;
|
|
96
|
+
column_offset: number;
|
|
97
|
+
row_offset: number;
|
|
98
|
+
orientation: Orientation;
|
|
99
|
+
box: T;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Recursively iterates over all child panel names in the layout tree, yielding
|
|
104
|
+
* panel names in depth-first order.
|
|
105
|
+
*
|
|
106
|
+
* @param panel - The layout tree to iterate over
|
|
107
|
+
* @returns Generator yielding child panel names
|
|
108
|
+
*/
|
|
109
|
+
export function* iter_panel_children(panel: Layout): Generator<string> {
|
|
110
|
+
if (panel.type === "split-panel") {
|
|
111
|
+
for (const child of panel.children) {
|
|
112
|
+
yield* iter_panel_children(child);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
yield panel.child;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* An empty `Layout` with no panels.
|
|
121
|
+
*/
|
|
122
|
+
export const EMPTY_PANEL: Layout = {
|
|
123
|
+
type: "split-panel",
|
|
124
|
+
orientation: "horizontal",
|
|
125
|
+
sizes: [],
|
|
126
|
+
children: [],
|
|
127
|
+
};
|