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.
Files changed (47) hide show
  1. package/README.md +110 -22
  2. package/dist/{layout → core}/types.d.ts +14 -4
  3. package/dist/extensions.d.ts +6 -1
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +7 -7
  6. package/dist/index.js.map +4 -4
  7. package/dist/layout/calculate_edge.d.ts +4 -4
  8. package/dist/layout/calculate_intersect.d.ts +1 -1
  9. package/dist/layout/calculate_path.d.ts +12 -0
  10. package/dist/layout/calculate_presize_paths.d.ts +10 -0
  11. package/dist/layout/flatten.d.ts +1 -1
  12. package/dist/layout/generate_grid.d.ts +5 -5
  13. package/dist/layout/generate_overlay.d.ts +18 -2
  14. package/dist/layout/insert_child.d.ts +2 -2
  15. package/dist/layout/redistribute_panel_sizes.d.ts +2 -2
  16. package/dist/layout/remove_child.d.ts +1 -1
  17. package/dist/model/overlay_controller.d.ts +34 -0
  18. package/dist/model/presize_queue.d.ts +17 -0
  19. package/dist/regular-layout-tab.d.ts +1 -1
  20. package/dist/regular-layout.d.ts +44 -9
  21. package/package.json +5 -4
  22. package/src/{layout → core}/constants.ts +2 -2
  23. package/src/{layout → core}/types.ts +17 -5
  24. package/src/extensions.ts +13 -1
  25. package/src/index.ts +3 -1
  26. package/src/layout/calculate_edge.ts +17 -8
  27. package/src/layout/calculate_intersect.ts +13 -4
  28. package/src/layout/calculate_path.ts +53 -0
  29. package/src/layout/calculate_presize_paths.ts +93 -0
  30. package/src/layout/flatten.ts +4 -4
  31. package/src/layout/generate_grid.ts +8 -8
  32. package/src/layout/generate_overlay.ts +48 -16
  33. package/src/layout/insert_child.ts +16 -16
  34. package/src/layout/redistribute_panel_sizes.ts +5 -5
  35. package/src/layout/remove_child.ts +6 -6
  36. package/src/model/overlay_controller.ts +162 -0
  37. package/src/model/presize_queue.ts +79 -0
  38. package/src/regular-layout-frame.ts +3 -3
  39. package/src/regular-layout-tab.ts +1 -1
  40. package/src/regular-layout.ts +180 -133
  41. package/themes/borland.css +103 -0
  42. package/themes/chicago.css +55 -49
  43. package/themes/fluxbox.css +64 -60
  44. package/themes/gibson.css +174 -164
  45. package/themes/hotdog.css +53 -47
  46. package/themes/lorax.css +82 -75
  47. /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
+ }
@@ -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 "./types.ts";
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 === "child-panel") {
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-panel" &&
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-panel",
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 "./constants.ts";
13
- import type { Layout } from "./types.ts";
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 === "child-panel") {
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 === "child-panel") {
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-panel",
182
+ * type: "split-layout",
183
183
  * orientation: "horizontal",
184
184
  * children: [
185
- * { type: "child-panel", tabs: "sidebar" },
186
- * { type: "child-panel", tabs: "main" }
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 === "child-panel") {
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 "./constants";
13
- import type { LayoutPath } from "./types";
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: { row_start, row_end, col_start, col_end },
28
- } = drag_target;
29
-
30
- const box_height =
31
- box.height - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
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 top = row_start * box_height + parseFloat(style.paddingTop);
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 "./types.ts";
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: number[],
31
+ path: LayoutPathTraversal,
32
32
  orientation?: "horizontal" | "vertical",
33
33
  ): Layout {
34
34
  const createChildPanel = (childId: string): Layout => ({
35
- type: "child-panel",
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 === "child-panel") {
42
- // Add to existing child-panel as a tab
41
+ if (panel.type === "tab-layout") {
42
+ // Add to existing tab-layout as a tab
43
43
  return {
44
- type: "child-panel",
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-panel",
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-panel
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-panel with the same orientation, insert into its children
73
- if (panel.type === "split-panel" && panel.orientation === orientation) {
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-panel",
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 === "child-panel") {
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-panel",
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-panel",
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 child-panel
162
+ // Determine the orientation to pass down when navigating into a tab-layout
163
163
  const childOrientation =
164
- targetChild.type === "child-panel" &&
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 "./constants.ts";
13
- import type { Layout } from "./types.ts";
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: number[],
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-panel") {
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-panel") {
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 "./types.ts";
13
- import { EMPTY_PANEL } from "./types.ts";
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 === "child-panel") {
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: "child-panel",
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 === "child-panel") {
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-panel") {
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
+ }