Skip to content

Commit 2d3a3d8

Browse files
authored
Merge pull request #4 from texodus/bug-fixes
Fix various tab bugs and add tests.
2 parents 5835536 + af91706 commit 2d3a3d8

31 files changed

+1197
-444
lines changed

examples/index.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ regular-layout-frame::part(active-tab) {
5050
}
5151

5252
/* Frame in Overlay Mode */
53-
regular-layout-frame:not([slot]) {
53+
regular-layout-frame.overlay {
5454
background-color: rgba(0, 0, 0, 0.2) !important;
5555
border: 1px dashed rgb(0, 0, 0);
5656
border-radius: 6px;
@@ -67,7 +67,7 @@ regular-layout-frame::part(container) {
6767
display: none;
6868
}
6969

70-
regular-layout-frame[slot]::part(container) {
70+
regular-layout-frame:not(.overlay)::part(container) {
7171
display: revert;
7272
}
7373

Lines changed: 52 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@
1010
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1111

1212
import { calculate_intersection } from "./calculate_intersect";
13+
import { SPLIT_EDGE_TOLERANCE } from "./constants";
1314
import { insert_child } from "./insert_child";
14-
import {
15-
SPLIT_EDGE_TOLERANCE,
16-
type Layout,
17-
type LayoutPath,
18-
} from "./layout_config";
15+
import type { Layout, LayoutPath, Orientation } from "./layout_config";
1916

2017
/**
2118
* Calculates an insertion point (which may involve splitting a single
@@ -30,146 +27,78 @@ import {
3027
* @returns A new `LayoutPath` reflecting the updated (maybe) `"split-panel"`,
3128
* which is enough to draw the overlay.
3229
*/
33-
function handle_matching_orientation(
30+
export function calculate_edge(
3431
col: number,
3532
row: number,
3633
panel: Layout,
3734
slot: string,
3835
drop_target: LayoutPath,
39-
is_before: boolean,
4036
): LayoutPath {
41-
if (drop_target.path.length === 0) {
42-
const insert_index = is_before ? 0 : 1;
43-
const new_panel = insert_child(panel, slot, [insert_index]);
44-
if (is_before) {
45-
return calculate_intersection(col, row, new_panel, false);
46-
} else {
47-
const new_drop_target = calculate_intersection(
48-
col,
49-
row,
50-
new_panel,
51-
false,
52-
);
53-
return {
54-
...new_drop_target,
55-
path: [0],
56-
};
57-
}
58-
} else {
59-
const path_without_last = drop_target.path.slice(0, -1);
60-
const last_index = drop_target.path[drop_target.path.length - 1];
61-
const insert_index = is_before ? last_index : last_index + 1;
62-
const new_panel = insert_child(panel, slot, [
63-
...path_without_last,
64-
insert_index,
65-
]);
37+
const is_column_edge =
38+
drop_target.column_offset < SPLIT_EDGE_TOLERANCE ||
39+
drop_target.column_offset > 1 - SPLIT_EDGE_TOLERANCE;
6640

67-
if (is_before) {
68-
return calculate_intersection(col, row, new_panel, false);
69-
} else {
70-
const new_drop_target = calculate_intersection(
71-
col,
72-
row,
73-
new_panel,
74-
false,
75-
);
76-
return {
77-
...new_drop_target,
78-
path: [...path_without_last, last_index],
79-
};
80-
}
41+
const is_row_edge =
42+
drop_target.row_offset < SPLIT_EDGE_TOLERANCE ||
43+
drop_target.row_offset > 1 - SPLIT_EDGE_TOLERANCE;
44+
45+
if (is_column_edge) {
46+
return handle_axis(
47+
col,
48+
row,
49+
panel,
50+
slot,
51+
drop_target,
52+
drop_target.column_offset,
53+
"horizontal",
54+
);
55+
} else if (is_row_edge) {
56+
return handle_axis(
57+
col,
58+
row,
59+
panel,
60+
slot,
61+
drop_target,
62+
drop_target.row_offset,
63+
"vertical",
64+
);
8165
}
82-
}
8366

84-
function handle_cross_orientation(
85-
col: number,
86-
row: number,
87-
panel: Layout,
88-
slot: string,
89-
drop_target: LayoutPath,
90-
insert_index: number,
91-
new_orientation: "horizontal" | "vertical",
92-
): LayoutPath {
93-
const original_path = drop_target.path;
94-
const new_panel = insert_child(
95-
panel,
96-
slot,
97-
[...original_path, insert_index],
98-
new_orientation,
99-
);
100-
const new_drop_target = calculate_intersection(col, row, new_panel, false);
101-
return {
102-
...new_drop_target,
103-
slot,
104-
path: [...original_path, insert_index],
105-
};
67+
return drop_target;
10668
}
10769

108-
export function calculate_split(
70+
function handle_axis(
10971
col: number,
11072
row: number,
11173
panel: Layout,
11274
slot: string,
11375
drop_target: LayoutPath,
76+
axis_offset: number,
77+
axis_orientation: Orientation,
11478
): LayoutPath {
115-
const is_column_edge =
116-
drop_target.column_offset < SPLIT_EDGE_TOLERANCE ||
117-
drop_target.column_offset > 1 - SPLIT_EDGE_TOLERANCE;
118-
const is_row_edge =
119-
drop_target.row_offset < SPLIT_EDGE_TOLERANCE ||
120-
drop_target.row_offset > 1 - SPLIT_EDGE_TOLERANCE;
121-
122-
if (is_column_edge) {
123-
const is_before = drop_target.column_offset < SPLIT_EDGE_TOLERANCE;
124-
if (drop_target.orientation === "horizontal") {
125-
drop_target = handle_matching_orientation(
126-
col,
127-
row,
128-
panel,
129-
slot,
130-
drop_target,
131-
is_before,
132-
);
133-
} else {
79+
const is_before = axis_offset < SPLIT_EDGE_TOLERANCE;
80+
if (drop_target.orientation === axis_orientation) {
81+
if (drop_target.path.length === 0) {
13482
const insert_index = is_before ? 0 : 1;
135-
drop_target = handle_cross_orientation(
136-
col,
137-
row,
138-
panel,
139-
slot,
140-
drop_target,
141-
insert_index,
142-
"horizontal",
143-
);
144-
}
145-
146-
drop_target.is_edge = true;
147-
} else if (is_row_edge) {
148-
const is_before = drop_target.row_offset < SPLIT_EDGE_TOLERANCE;
149-
if (drop_target.orientation === "vertical") {
150-
drop_target = handle_matching_orientation(
151-
col,
152-
row,
153-
panel,
154-
slot,
155-
drop_target,
156-
is_before,
157-
);
83+
const new_panel = insert_child(panel, slot, [insert_index]);
84+
drop_target = calculate_intersection(col, row, new_panel, false);
15885
} else {
159-
const insert_index = is_before ? 0 : 1;
160-
drop_target = handle_cross_orientation(
161-
col,
162-
row,
163-
panel,
164-
slot,
165-
drop_target,
86+
const path_without_last = drop_target.path.slice(0, -1);
87+
const last_index = drop_target.path[drop_target.path.length - 1];
88+
const insert_index = is_before ? last_index : last_index + 1;
89+
const new_panel = insert_child(panel, slot, [
90+
...path_without_last,
16691
insert_index,
167-
"vertical",
168-
);
169-
}
92+
]);
17093

171-
drop_target.is_edge = true;
94+
drop_target = calculate_intersection(col, row, new_panel, false);
95+
}
96+
} else {
97+
const path = [...drop_target.path, is_before ? 0 : 1];
98+
const new_panel = insert_child(panel, slot, path, axis_orientation);
99+
drop_target = calculate_intersection(col, row, new_panel, false);
172100
}
173101

102+
drop_target.is_edge = true;
174103
return drop_target;
175104
}

src/common/calculate_intersect.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ function calculate_intersection_recursive(
8686
const column_offset =
8787
(column - view_window.col_start) /
8888
(view_window.col_end - view_window.col_start);
89+
8990
const row_offset =
9091
(row - view_window.row_start) /
9192
(view_window.row_end - view_window.row_start);
@@ -98,6 +99,8 @@ function calculate_intersection_recursive(
9899
path: path,
99100
view_window: view_window,
100101
is_edge: false,
102+
column,
103+
row,
101104
column_offset,
102105
row_offset,
103106
orientation: parent_orientation || "horizontal",

src/common/constants.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 { OverlayMode } from "./layout_config";
13+
14+
/**
15+
* The minimum number of pixels the mouse must move to be considered a drag.
16+
*/
17+
export const MIN_DRAG_DISTANCE = 10;
18+
19+
/**
20+
* Class name to use for child elements in overlay position (dragging).
21+
*/
22+
export const OVERLAY_CLASSNAME = "overlay";
23+
24+
/**
25+
* The percentage of the maximum resize distance that will be clamped.
26+
*
27+
*/
28+
export const MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD = 0.15;
29+
30+
/**
31+
* Threshold from panel edge that is considered a split vs drop action.
32+
*/
33+
export const SPLIT_EDGE_TOLERANCE = 0.25;
34+
35+
/**
36+
* Tolerance threshold for considering two grid track positions as identical.
37+
*
38+
* When collecting and deduplicating track positions, any positions closer than
39+
* this value are treated as the same position to avoid redundant grid tracks.
40+
*/
41+
export const GRID_TRACK_COLLAPSE_TOLERANCE = 0.001;
42+
43+
/**
44+
* The overlay default behavior.
45+
*/
46+
export const OVERLAY_DEFAULT: OverlayMode = "absolute";

src/common/flatten.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { Layout } from "./layout_config.ts";
2424
*/
2525
export function flatten(layout: Layout): Layout {
2626
if (layout.type === "child-panel") {
27+
layout.selected = layout.selected || 0;
2728
return layout;
2829
}
2930

src/common/generate_grid.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
1010
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1111

12-
import { GRID_TRACK_COLLAPSE_TOLERANCE, type Layout } from "./layout_config.ts";
12+
import { GRID_TRACK_COLLAPSE_TOLERANCE } from "./constants.ts";
13+
import type { Layout } from "./layout_config.ts";
1314
import { remove_child } from "./remove_child.ts";
1415

1516
interface GridCell {

src/common/generate_overlay.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@
1111

1212
import type { LayoutPath } from "./layout_config";
1313

14-
export function updateOverlaySheet({
15-
view_window: { row_start, row_end, col_start, col_end },
16-
box,
17-
}: LayoutPath<DOMRect>) {
14+
export function updateOverlaySheet(
15+
slot: string,
16+
{
17+
view_window: { row_start, row_end, col_start, col_end },
18+
box,
19+
}: LayoutPath<DOMRect>,
20+
) {
1821
const margin = 0;
1922
const top = row_start * box.height + margin / 2;
2023
const left = col_start * box.width + margin / 2;
2124
const height = (row_end - row_start) * box.height - margin;
2225
const width = (col_end - col_start) * box.width - margin;
2326
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}}`;
27+
return `::slotted([slot="${slot}"]){${css}}`;
2528
}

src/common/insert_child.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export function insert_child(
7777
if (restPath.length === 0 || index === panel.children.length) {
7878
if (is_edge && panel.children[index]?.type === "child-panel") {
7979
panel.children[index].child.unshift(child);
80+
panel.children[index].selected = 0;
8081
return panel;
8182
}
8283

src/common/layout_config.ts

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,10 @@
99
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
1010
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1111

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.25;
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.001;
30-
31-
/**
32-
* The overlay default behavior.
33-
*/
34-
export const OVERLAY_DEFAULT: OverlayMode = "absolute";
35-
3612
/**
3713
* The overlay behavior type.
3814
*/
39-
export type OverlayMode = "grid" | "absolute" | "interactive";
15+
export type OverlayMode = "grid" | "absolute";
4016

4117
/**
4218
* The representation of a CSS grid, in JSON form.
@@ -105,6 +81,8 @@ export interface LayoutPath<T = undefined> {
10581
panel: TabLayout;
10682
path: number[];
10783
view_window: ViewWindow;
84+
column: number;
85+
row: number;
10886
column_offset: number;
10987
row_offset: number;
11088
orientation: Orientation;
@@ -125,7 +103,7 @@ export function* iter_panel_children(panel: Layout): Generator<string> {
125103
yield* iter_panel_children(child);
126104
}
127105
} else {
128-
yield* panel.child;
106+
yield panel.child[panel.selected || 0];
129107
}
130108
}
131109

0 commit comments

Comments
 (0)