regular-layout 0.2.2 → 0.4.0
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/README.md +110 -22
- package/dist/{layout → core}/types.d.ts +14 -4
- package/dist/extensions.d.ts +6 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +4 -4
- package/dist/layout/calculate_edge.d.ts +4 -4
- package/dist/layout/calculate_intersect.d.ts +1 -1
- package/dist/layout/calculate_path.d.ts +12 -0
- package/dist/layout/calculate_presize_paths.d.ts +10 -0
- package/dist/layout/flatten.d.ts +1 -1
- package/dist/layout/generate_grid.d.ts +5 -5
- package/dist/layout/generate_overlay.d.ts +18 -2
- package/dist/layout/insert_child.d.ts +2 -2
- package/dist/layout/redistribute_panel_sizes.d.ts +2 -2
- package/dist/layout/remove_child.d.ts +1 -1
- package/dist/model/overlay_controller.d.ts +34 -0
- package/dist/model/presize_queue.d.ts +17 -0
- package/dist/regular-layout-tab.d.ts +1 -1
- package/dist/regular-layout.d.ts +44 -9
- package/package.json +5 -4
- package/src/{layout → core}/constants.ts +2 -2
- package/src/{layout → core}/types.ts +17 -5
- package/src/extensions.ts +13 -1
- package/src/index.ts +3 -1
- package/src/layout/calculate_edge.ts +17 -8
- package/src/layout/calculate_intersect.ts +13 -4
- package/src/layout/calculate_path.ts +53 -0
- package/src/layout/calculate_presize_paths.ts +93 -0
- package/src/layout/flatten.ts +4 -4
- package/src/layout/generate_grid.ts +8 -8
- package/src/layout/generate_overlay.ts +48 -16
- package/src/layout/insert_child.ts +16 -16
- package/src/layout/redistribute_panel_sizes.ts +5 -5
- package/src/layout/remove_child.ts +6 -6
- package/src/model/overlay_controller.ts +162 -0
- package/src/model/presize_queue.ts +79 -0
- package/src/regular-layout-frame.ts +3 -3
- package/src/regular-layout-tab.ts +1 -1
- package/src/regular-layout.ts +180 -133
- package/themes/borland.css +103 -0
- package/themes/chicago.css +55 -49
- package/themes/fluxbox.css +64 -60
- package/themes/gibson.css +174 -164
- package/themes/hotdog.css +53 -47
- package/themes/lorax.css +82 -75
- /package/dist/{layout → core}/constants.d.ts +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
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 {
|
|
13
|
+
Layout,
|
|
14
|
+
LayoutPath,
|
|
15
|
+
Orientation,
|
|
16
|
+
ViewWindow,
|
|
17
|
+
} from "../core/types.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Walks a layout tree and returns a {@link LayoutPath} for every visible
|
|
21
|
+
* panel, keyed by panel name. Each path's `column`/`row` are set to the
|
|
22
|
+
* center of the panel's view window, with offsets of 0.5.
|
|
23
|
+
*
|
|
24
|
+
* @param layout - The layout tree to walk.
|
|
25
|
+
* @returns A record mapping panel names to their layout paths.
|
|
26
|
+
*/
|
|
27
|
+
export function calculate_presize_paths(
|
|
28
|
+
layout: Layout,
|
|
29
|
+
): Record<string, LayoutPath> {
|
|
30
|
+
const result: Record<string, LayoutPath> = {};
|
|
31
|
+
walk_layout(layout, result, [], null, {
|
|
32
|
+
row_start: 0,
|
|
33
|
+
row_end: 1,
|
|
34
|
+
col_start: 0,
|
|
35
|
+
col_end: 1,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function walk_layout(
|
|
42
|
+
layout: Layout,
|
|
43
|
+
result: Record<string, LayoutPath>,
|
|
44
|
+
path: number[],
|
|
45
|
+
parentOrientation: Orientation | null,
|
|
46
|
+
viewWindow: ViewWindow,
|
|
47
|
+
): void {
|
|
48
|
+
if (layout.type === "tab-layout") {
|
|
49
|
+
const selected = layout.selected ?? 0;
|
|
50
|
+
const slot = layout.tabs[selected];
|
|
51
|
+
const col = (viewWindow.col_start + viewWindow.col_end) / 2;
|
|
52
|
+
const row = (viewWindow.row_start + viewWindow.row_end) / 2;
|
|
53
|
+
result[slot] = {
|
|
54
|
+
type: "layout-path",
|
|
55
|
+
layout,
|
|
56
|
+
slot,
|
|
57
|
+
path,
|
|
58
|
+
view_window: viewWindow,
|
|
59
|
+
is_edge: false,
|
|
60
|
+
column: col,
|
|
61
|
+
row: row,
|
|
62
|
+
column_offset: 0.5,
|
|
63
|
+
row_offset: 0.5,
|
|
64
|
+
orientation: parentOrientation || "horizontal",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isVertical = layout.orientation === "vertical";
|
|
71
|
+
const startKey = isVertical ? "row_start" : "col_start";
|
|
72
|
+
const endKey = isVertical ? "row_end" : "col_end";
|
|
73
|
+
let currentPos = viewWindow[startKey];
|
|
74
|
+
const totalSize = viewWindow[endKey] - viewWindow[startKey];
|
|
75
|
+
for (let i = 0; i < layout.children.length; i++) {
|
|
76
|
+
const nextPos = currentPos + totalSize * layout.sizes[i];
|
|
77
|
+
const childWindow: ViewWindow = {
|
|
78
|
+
...viewWindow,
|
|
79
|
+
[startKey]: currentPos,
|
|
80
|
+
[endKey]: nextPos,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
walk_layout(
|
|
84
|
+
layout.children[i],
|
|
85
|
+
result,
|
|
86
|
+
[...path, i],
|
|
87
|
+
layout.orientation,
|
|
88
|
+
childWindow,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
currentPos = nextPos;
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/layout/flatten.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import type { Layout } from "
|
|
12
|
+
import type { Layout } from "../core/types.ts";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Flattens the layout tree by merging parent and child split panels that have
|
|
@@ -23,7 +23,7 @@ import type { Layout } from "./types.ts";
|
|
|
23
23
|
* @returns A new flattened layout tree (original is not mutated).
|
|
24
24
|
*/
|
|
25
25
|
export function flatten(layout: Layout): Layout {
|
|
26
|
-
if (layout.type === "
|
|
26
|
+
if (layout.type === "tab-layout") {
|
|
27
27
|
layout.selected = layout.selected || 0;
|
|
28
28
|
return layout;
|
|
29
29
|
}
|
|
@@ -36,7 +36,7 @@ export function flatten(layout: Layout): Layout {
|
|
|
36
36
|
const flattenedChild = flatten(child);
|
|
37
37
|
|
|
38
38
|
if (
|
|
39
|
-
flattenedChild.type === "split-
|
|
39
|
+
flattenedChild.type === "split-layout" &&
|
|
40
40
|
flattenedChild.orientation === layout.orientation
|
|
41
41
|
) {
|
|
42
42
|
for (let j = 0; j < flattenedChild.children.length; j++) {
|
|
@@ -54,7 +54,7 @@ export function flatten(layout: Layout): Layout {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
return {
|
|
57
|
-
type: "split-
|
|
57
|
+
type: "split-layout",
|
|
58
58
|
orientation: layout.orientation,
|
|
59
59
|
children: flattenedChildren,
|
|
60
60
|
sizes: flattenedSizes,
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import { DEFAULT_PHYSICS, type Physics } from "
|
|
13
|
-
import type { Layout } from "
|
|
12
|
+
import { DEFAULT_PHYSICS, type Physics } from "../core/constants.ts";
|
|
13
|
+
import type { Layout } from "../core/types.ts";
|
|
14
14
|
|
|
15
15
|
interface GridCell {
|
|
16
16
|
child: string;
|
|
@@ -44,7 +44,7 @@ function collect_track_positions(
|
|
|
44
44
|
end: number,
|
|
45
45
|
physics: Physics,
|
|
46
46
|
): number[] {
|
|
47
|
-
if (panel.type === "
|
|
47
|
+
if (panel.type === "tab-layout") {
|
|
48
48
|
return [start, end];
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -100,7 +100,7 @@ function build_cells(
|
|
|
100
100
|
rowEnd: number,
|
|
101
101
|
physics: Physics,
|
|
102
102
|
): GridCell[] {
|
|
103
|
-
if (panel.type === "
|
|
103
|
+
if (panel.type === "tab-layout") {
|
|
104
104
|
const selected = panel.selected ?? 0;
|
|
105
105
|
return [
|
|
106
106
|
{
|
|
@@ -179,11 +179,11 @@ const child_template = (
|
|
|
179
179
|
* @example
|
|
180
180
|
* ```typescript
|
|
181
181
|
* const layout = {
|
|
182
|
-
* type: "split-
|
|
182
|
+
* type: "split-layout",
|
|
183
183
|
* orientation: "horizontal",
|
|
184
184
|
* children: [
|
|
185
|
-
* { type: "
|
|
186
|
-
* { type: "
|
|
185
|
+
* { type: "tab-layout", tabs: "sidebar" },
|
|
186
|
+
* { type: "tab-layout", tabs: "main" }
|
|
187
187
|
* ],
|
|
188
188
|
* sizes: [0.25, 0.75]
|
|
189
189
|
* };
|
|
@@ -200,7 +200,7 @@ export function create_css_grid_layout(
|
|
|
200
200
|
overlay?: [string, string],
|
|
201
201
|
physics: Physics = DEFAULT_PHYSICS,
|
|
202
202
|
): string {
|
|
203
|
-
if (layout.type === "
|
|
203
|
+
if (layout.type === "tab-layout") {
|
|
204
204
|
const selected = layout.selected ?? 0;
|
|
205
205
|
return [
|
|
206
206
|
host_template("100%", "100%"),
|
|
@@ -9,8 +9,46 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import { DEFAULT_PHYSICS } from "
|
|
13
|
-
import type { LayoutPath } from "
|
|
12
|
+
import { DEFAULT_PHYSICS } from "../core/constants";
|
|
13
|
+
import type { LayoutPath, ViewWindow } from "../core/types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a {@link ViewWindow} to element-relative pixel coordinates,
|
|
17
|
+
* accounting for padding and optionally CSS `gap` and child `margin`.
|
|
18
|
+
*
|
|
19
|
+
* @param window - The view window in normalized 0–1 coordinates.
|
|
20
|
+
* @param box - The element's bounding client rect.
|
|
21
|
+
* @param style - The element's computed style (for padding).
|
|
22
|
+
* @param margin - Optional child element's computed style (for margin inset).
|
|
23
|
+
* @returns Pixel coordinates relative to the element's border-box origin.
|
|
24
|
+
*/
|
|
25
|
+
export function viewWindowToLocalRect(
|
|
26
|
+
window: ViewWindow,
|
|
27
|
+
box: DOMRect,
|
|
28
|
+
style: CSSStyleDeclaration,
|
|
29
|
+
margin?: CSSStyleDeclaration,
|
|
30
|
+
): { x: number; y: number; width: number; height: number } {
|
|
31
|
+
const paddingLeft = parseFloat(style.paddingLeft);
|
|
32
|
+
const paddingTop = parseFloat(style.paddingTop);
|
|
33
|
+
const contentWidth = box.width - paddingLeft - parseFloat(style.paddingRight);
|
|
34
|
+
const contentHeight =
|
|
35
|
+
box.height - paddingTop - parseFloat(style.paddingBottom);
|
|
36
|
+
|
|
37
|
+
const x = paddingLeft + window.col_start * contentWidth;
|
|
38
|
+
const y = paddingTop + window.row_start * contentHeight;
|
|
39
|
+
let width = (window.col_end - window.col_start) * contentWidth;
|
|
40
|
+
let height = (window.row_end - window.row_start) * contentHeight;
|
|
41
|
+
if (margin) {
|
|
42
|
+
const marginTop = parseFloat(margin.marginTop);
|
|
43
|
+
const marginRight = parseFloat(margin.marginRight);
|
|
44
|
+
const marginBottom = parseFloat(margin.marginBottom);
|
|
45
|
+
const marginLeft = parseFloat(margin.marginLeft);
|
|
46
|
+
width -= marginLeft + marginRight;
|
|
47
|
+
height -= marginTop + marginBottom;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { x, y, width, height };
|
|
51
|
+
}
|
|
14
52
|
|
|
15
53
|
export function updateOverlaySheet(
|
|
16
54
|
slot: string,
|
|
@@ -18,25 +56,19 @@ export function updateOverlaySheet(
|
|
|
18
56
|
style: CSSStyleDeclaration,
|
|
19
57
|
drag_target: LayoutPath | null,
|
|
20
58
|
physics = DEFAULT_PHYSICS,
|
|
59
|
+
margin?: CSSStyleDeclaration,
|
|
21
60
|
) {
|
|
22
61
|
if (!drag_target) {
|
|
23
62
|
return `:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){display:none;}`;
|
|
24
63
|
}
|
|
25
64
|
|
|
26
|
-
const
|
|
27
|
-
view_window
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const box_width =
|
|
34
|
-
box.width - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
|
65
|
+
const local = viewWindowToLocalRect(
|
|
66
|
+
drag_target.view_window,
|
|
67
|
+
box,
|
|
68
|
+
style,
|
|
69
|
+
margin,
|
|
70
|
+
);
|
|
35
71
|
|
|
36
|
-
const
|
|
37
|
-
const left = col_start * box_width + parseFloat(style.paddingLeft);
|
|
38
|
-
const height = (row_end - row_start) * box_height;
|
|
39
|
-
const width = (col_end - col_start) * box_width;
|
|
40
|
-
const css = `display:flex;position:absolute!important;z-index:1;top:${top}px;left:${left}px;height:${height}px;width:${width}px;`;
|
|
72
|
+
const css = `display:flex;position:absolute!important;z-index:1;top:${local.y}px;left:${local.x}px;height:${local.height}px;width:${local.width}px;`;
|
|
41
73
|
return `::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){${css}}`;
|
|
42
74
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import type { Layout } from "
|
|
12
|
+
import type { Layout, LayoutPathTraversal } from "../core/types.ts";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Inserts a new child panel into the layout tree at a specified location.
|
|
@@ -28,32 +28,32 @@ import type { Layout } from "./types.ts";
|
|
|
28
28
|
export function insert_child(
|
|
29
29
|
panel: Layout,
|
|
30
30
|
child: string,
|
|
31
|
-
path:
|
|
31
|
+
path: LayoutPathTraversal,
|
|
32
32
|
orientation?: "horizontal" | "vertical",
|
|
33
33
|
): Layout {
|
|
34
34
|
const createChildPanel = (childId: string): Layout => ({
|
|
35
|
-
type: "
|
|
35
|
+
type: "tab-layout",
|
|
36
36
|
tabs: [childId],
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
if (path.length === 0) {
|
|
40
40
|
// Insert at root level
|
|
41
|
-
if (panel.type === "
|
|
42
|
-
// Add to existing
|
|
41
|
+
if (panel.type === "tab-layout") {
|
|
42
|
+
// Add to existing tab-layout as a tab
|
|
43
43
|
return {
|
|
44
|
-
type: "
|
|
44
|
+
type: "tab-layout",
|
|
45
45
|
tabs: [child, ...panel.tabs],
|
|
46
46
|
};
|
|
47
47
|
} else if (orientation) {
|
|
48
48
|
// When inserting at edge of root, wrap the entire panel in a new split
|
|
49
49
|
return {
|
|
50
|
-
type: "split-
|
|
50
|
+
type: "split-layout",
|
|
51
51
|
orientation: orientation,
|
|
52
52
|
children: [createChildPanel(child), panel],
|
|
53
53
|
sizes: [0.5, 0.5],
|
|
54
54
|
};
|
|
55
55
|
} else {
|
|
56
|
-
// Append to existing split-
|
|
56
|
+
// Append to existing split-layout
|
|
57
57
|
const newChildren = [...panel.children, createChildPanel(child)];
|
|
58
58
|
const newSizes = [...panel.sizes, 1 / (newChildren.length - 1)];
|
|
59
59
|
return {
|
|
@@ -69,8 +69,8 @@ export function insert_child(
|
|
|
69
69
|
|
|
70
70
|
// Special case: when orientation is provided and restPath is empty, handle edge insertion
|
|
71
71
|
if (orientation && restPath.length === 0) {
|
|
72
|
-
// If panel is a split-
|
|
73
|
-
if (panel.type === "split-
|
|
72
|
+
// If panel is a split-layout with the same orientation, insert into its children
|
|
73
|
+
if (panel.type === "split-layout" && panel.orientation === orientation) {
|
|
74
74
|
const newChildren = [...panel.children];
|
|
75
75
|
newChildren.splice(index, 0, createChildPanel(child));
|
|
76
76
|
const newSizes = [...panel.sizes];
|
|
@@ -89,14 +89,14 @@ export function insert_child(
|
|
|
89
89
|
: [panel, createChildPanel(child)];
|
|
90
90
|
|
|
91
91
|
return {
|
|
92
|
-
type: "split-
|
|
92
|
+
type: "split-layout",
|
|
93
93
|
orientation: orientation,
|
|
94
94
|
children,
|
|
95
95
|
sizes: [0.5, 0.5],
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
if (panel.type === "
|
|
99
|
+
if (panel.type === "tab-layout") {
|
|
100
100
|
// Stack into child array only when ALL of these conditions are met:
|
|
101
101
|
// 1. Path has exactly one element (restPath is empty)
|
|
102
102
|
// 2. Orientation was NOT explicitly provided (orientation is undefined)
|
|
@@ -117,7 +117,7 @@ export function insert_child(
|
|
|
117
117
|
|
|
118
118
|
// Otherwise, wrap in a split panel and recurse
|
|
119
119
|
const newPanel: Layout = {
|
|
120
|
-
type: "split-
|
|
120
|
+
type: "split-layout",
|
|
121
121
|
orientation: orientation || "horizontal",
|
|
122
122
|
children: [panel],
|
|
123
123
|
sizes: [1],
|
|
@@ -130,7 +130,7 @@ export function insert_child(
|
|
|
130
130
|
if (orientation && panel.children[index]) {
|
|
131
131
|
// When inserting at an edge, create a split panel with the new child and existing child
|
|
132
132
|
const newSplitPanel: Layout = {
|
|
133
|
-
type: "split-
|
|
133
|
+
type: "split-layout",
|
|
134
134
|
orientation: orientation,
|
|
135
135
|
children: [createChildPanel(child), panel.children[index]],
|
|
136
136
|
sizes: [0.5, 0.5],
|
|
@@ -159,9 +159,9 @@ export function insert_child(
|
|
|
159
159
|
|
|
160
160
|
const targetChild = panel.children[index];
|
|
161
161
|
|
|
162
|
-
// Determine the orientation to pass down when navigating into a
|
|
162
|
+
// Determine the orientation to pass down when navigating into a tab-layout
|
|
163
163
|
const childOrientation =
|
|
164
|
-
targetChild.type === "
|
|
164
|
+
targetChild.type === "tab-layout" &&
|
|
165
165
|
restPath.length > 0 &&
|
|
166
166
|
orientation !== undefined
|
|
167
167
|
? panel.orientation === "horizontal"
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import { DEFAULT_PHYSICS } from "
|
|
13
|
-
import type { Layout } from "
|
|
12
|
+
import { DEFAULT_PHYSICS } from "../core/constants.ts";
|
|
13
|
+
import type { Layout, LayoutPathTraversal } from "../core/types.ts";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Adjusts panel sizes during a drag operation on a divider.
|
|
@@ -31,7 +31,7 @@ import type { Layout } from "./types.ts";
|
|
|
31
31
|
*/
|
|
32
32
|
export function redistribute_panel_sizes(
|
|
33
33
|
panel: Layout,
|
|
34
|
-
path:
|
|
34
|
+
path: LayoutPathTraversal,
|
|
35
35
|
delta: number | undefined,
|
|
36
36
|
physics = DEFAULT_PHYSICS,
|
|
37
37
|
): Layout {
|
|
@@ -43,14 +43,14 @@ export function redistribute_panel_sizes(
|
|
|
43
43
|
let current: Layout = result;
|
|
44
44
|
const deltas = { horizontal: delta || 0, vertical: delta || 0 };
|
|
45
45
|
for (let i = 0; i < path.length - 1; i++) {
|
|
46
|
-
if (current.type === "split-
|
|
46
|
+
if (current.type === "split-layout") {
|
|
47
47
|
deltas[current.orientation] /= current.sizes[path[i]];
|
|
48
48
|
current = current.children[path[i]];
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Apply the redistribution at the final path index
|
|
53
|
-
if (current.type === "split-
|
|
53
|
+
if (current.type === "split-layout") {
|
|
54
54
|
if (delta === undefined) {
|
|
55
55
|
current.sizes = current.sizes.map((_) => 1 / current.sizes.length);
|
|
56
56
|
} else {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import type { Layout, TabLayout } from "
|
|
13
|
-
import { EMPTY_PANEL } from "
|
|
12
|
+
import type { Layout, TabLayout } from "../core/types.ts";
|
|
13
|
+
import { EMPTY_PANEL } from "../core/types.ts";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Removes a child panel from the layout tree by its name.
|
|
@@ -25,14 +25,14 @@ import { EMPTY_PANEL } from "./types.ts";
|
|
|
25
25
|
*/
|
|
26
26
|
export function remove_child(panel: Layout, child: string): Layout {
|
|
27
27
|
// If this is a child panel, handle tab removal
|
|
28
|
-
if (panel.type === "
|
|
28
|
+
if (panel.type === "tab-layout") {
|
|
29
29
|
if (panel.tabs.includes(child)) {
|
|
30
30
|
const newChild = panel.tabs.filter((c) => c !== child);
|
|
31
31
|
if (newChild.length === 0) {
|
|
32
32
|
return structuredClone(EMPTY_PANEL);
|
|
33
33
|
}
|
|
34
34
|
return {
|
|
35
|
-
type: "
|
|
35
|
+
type: "tab-layout",
|
|
36
36
|
tabs: newChild,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
@@ -45,7 +45,7 @@ export function remove_child(panel: Layout, child: string): Layout {
|
|
|
45
45
|
|
|
46
46
|
// Try to remove the child from this split panel's children
|
|
47
47
|
const index = result.children.findIndex((p) => {
|
|
48
|
-
if (p.type === "
|
|
48
|
+
if (p.type === "tab-layout") {
|
|
49
49
|
return p.tabs.includes(child);
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -82,7 +82,7 @@ export function remove_child(panel: Layout, child: string): Layout {
|
|
|
82
82
|
// Child not found at this level - recursively search children
|
|
83
83
|
let modified = false;
|
|
84
84
|
const newChildren = result.children.map((p) => {
|
|
85
|
-
if (p.type === "split-
|
|
85
|
+
if (p.type === "split-layout") {
|
|
86
86
|
const updated = remove_child(p, child);
|
|
87
87
|
if (updated !== p) {
|
|
88
88
|
modified = true;
|
|
@@ -0,0 +1,162 @@
|
|
|
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, LayoutPath, OverlayMode } from "../core/types.ts";
|
|
13
|
+
import type { Physics } from "../core/constants.ts";
|
|
14
|
+
import type { PresizeQueue } from "./presize_queue.ts";
|
|
15
|
+
import { calculate_intersection } from "../layout/calculate_intersect.ts";
|
|
16
|
+
import { calculate_edge } from "../layout/calculate_edge.ts";
|
|
17
|
+
import { create_css_grid_layout } from "../layout/generate_grid.ts";
|
|
18
|
+
import { updateOverlaySheet } from "../layout/generate_overlay.ts";
|
|
19
|
+
import { remove_child } from "../layout/remove_child.ts";
|
|
20
|
+
import { insert_child } from "../layout/insert_child.ts";
|
|
21
|
+
|
|
22
|
+
export interface OverlayHost {
|
|
23
|
+
readonly panel: Layout;
|
|
24
|
+
readonly physics: Physics;
|
|
25
|
+
readonly stylesheet: CSSStyleSheet;
|
|
26
|
+
readonly presizeQueue: PresizeQueue;
|
|
27
|
+
relativeCoordinates(
|
|
28
|
+
event: { clientX: number; clientY: number },
|
|
29
|
+
recalculate: boolean,
|
|
30
|
+
): [number, number, DOMRect, CSSStyleDeclaration];
|
|
31
|
+
restore(layout: Layout, isFlattened?: boolean): Promise<void>;
|
|
32
|
+
querySelector(selectors: string): Element | null;
|
|
33
|
+
dispatchEvent(event: Event): boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Manages overlay state during drag-and-drop panel rearrangement.
|
|
38
|
+
*
|
|
39
|
+
* Handles rendering a preview of where a dragged panel would land,
|
|
40
|
+
* and committing or cancelling the placement when the drag ends.
|
|
41
|
+
*/
|
|
42
|
+
export class OverlayController {
|
|
43
|
+
constructor(private _host: OverlayHost) {}
|
|
44
|
+
|
|
45
|
+
async set(
|
|
46
|
+
event: { clientX: number; clientY: number },
|
|
47
|
+
{ slot }: LayoutPath,
|
|
48
|
+
className: string = this._host.physics.OVERLAY_CLASSNAME,
|
|
49
|
+
mode: OverlayMode = this._host.physics.OVERLAY_DEFAULT,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const host = this._host;
|
|
52
|
+
const panel = remove_child(host.panel, slot);
|
|
53
|
+
const query = `:scope > [${host.physics.CHILD_ATTRIBUTE_NAME}="${slot}"]`;
|
|
54
|
+
let drag_element = host.querySelector(query);
|
|
55
|
+
if (drag_element) {
|
|
56
|
+
drag_element.classList.add(className);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [col, row, box, style] = host.relativeCoordinates(event, true);
|
|
60
|
+
let drop_target = calculate_intersection(col, row, panel);
|
|
61
|
+
console.log(row, col, drop_target);
|
|
62
|
+
if (drop_target) {
|
|
63
|
+
drop_target = calculate_edge(
|
|
64
|
+
col,
|
|
65
|
+
row,
|
|
66
|
+
panel,
|
|
67
|
+
slot,
|
|
68
|
+
drop_target,
|
|
69
|
+
box,
|
|
70
|
+
host.physics,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await host.presizeQueue.run(panel, () => {
|
|
75
|
+
if (mode === "grid" && drop_target) {
|
|
76
|
+
const path: [string, string] = [slot, drop_target?.slot];
|
|
77
|
+
const css = create_css_grid_layout(panel, path, host.physics);
|
|
78
|
+
host.stylesheet.replaceSync(css);
|
|
79
|
+
} else if (mode === "absolute") {
|
|
80
|
+
const grid_css = create_css_grid_layout(panel, undefined, host.physics);
|
|
81
|
+
|
|
82
|
+
while (drag_element?.tagName === "SLOT" && drag_element) {
|
|
83
|
+
drag_element = (
|
|
84
|
+
drag_element as HTMLSlotElement
|
|
85
|
+
).assignedElements()[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const margin = drag_element
|
|
89
|
+
? getComputedStyle(drag_element)
|
|
90
|
+
: undefined;
|
|
91
|
+
|
|
92
|
+
const overlay_css = updateOverlaySheet(
|
|
93
|
+
slot,
|
|
94
|
+
box,
|
|
95
|
+
style,
|
|
96
|
+
drop_target,
|
|
97
|
+
host.physics,
|
|
98
|
+
margin,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
host.stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const event_name = `${host.physics.CUSTOM_EVENT_NAME_PREFIX}-before-update`;
|
|
106
|
+
const custom_event = new CustomEvent<Layout>(event_name, {
|
|
107
|
+
detail: panel,
|
|
108
|
+
});
|
|
109
|
+
host.dispatchEvent(custom_event);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async clear(
|
|
113
|
+
event: { clientX: number; clientY: number } | null,
|
|
114
|
+
{ slot, layout }: LayoutPath,
|
|
115
|
+
className: string = this._host.physics.OVERLAY_CLASSNAME,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
const host = this._host;
|
|
118
|
+
const panel = remove_child(host.panel, slot);
|
|
119
|
+
const query = `:scope > [${host.physics.CHILD_ATTRIBUTE_NAME}="${slot}"]`;
|
|
120
|
+
const drag_element = host.querySelector(query);
|
|
121
|
+
|
|
122
|
+
if (event === null) {
|
|
123
|
+
await host.restore(layout);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const [col, row, box] = host.relativeCoordinates(event, false);
|
|
128
|
+
let drop_target = calculate_intersection(col, row, panel);
|
|
129
|
+
if (drop_target) {
|
|
130
|
+
drop_target = calculate_edge(
|
|
131
|
+
col,
|
|
132
|
+
row,
|
|
133
|
+
panel,
|
|
134
|
+
slot,
|
|
135
|
+
drop_target,
|
|
136
|
+
box,
|
|
137
|
+
host.physics,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (drop_target) {
|
|
142
|
+
const orientation = drop_target?.is_edge
|
|
143
|
+
? drop_target.orientation
|
|
144
|
+
: undefined;
|
|
145
|
+
|
|
146
|
+
const new_layout = insert_child(
|
|
147
|
+
panel,
|
|
148
|
+
slot,
|
|
149
|
+
drop_target.path,
|
|
150
|
+
orientation,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await host.restore(new_layout);
|
|
154
|
+
} else {
|
|
155
|
+
await host.restore(layout);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (drag_element) {
|
|
159
|
+
drag_element.classList.remove(className);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|