react-headless-dock-layout 0.1.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/.vscode/settings.json +7 -0
- package/biome.json +34 -0
- package/index.html +12 -0
- package/package.json +43 -0
- package/src/App.tsx +119 -0
- package/src/global.css +5 -0
- package/src/index.ts +3 -0
- package/src/index.tsx +8 -0
- package/src/internal/EventEmitter.ts +17 -0
- package/src/internal/LayoutManager/LayoutManager.test.ts +948 -0
- package/src/internal/LayoutManager/LayoutManager.ts +515 -0
- package/src/internal/LayoutManager/LayoutTree.test.ts +341 -0
- package/src/internal/LayoutManager/LayoutTree.ts +82 -0
- package/src/internal/LayoutManager/calculateLayoutRects.test.ts +211 -0
- package/src/internal/LayoutManager/calculateLayoutRects.ts +88 -0
- package/src/internal/LayoutManager/calculateMinSize.test.ts +77 -0
- package/src/internal/LayoutManager/calculateMinSize.ts +40 -0
- package/src/internal/LayoutManager/findClosestDirection.test.ts +95 -0
- package/src/internal/LayoutManager/findClosestDirection.ts +15 -0
- package/src/internal/LayoutManager/types.ts +20 -0
- package/src/internal/assertNever.ts +3 -0
- package/src/internal/clamp.tsx +3 -0
- package/src/internal/findParentNode.ts +30 -0
- package/src/internal/invariant.tsx +6 -0
- package/src/strategies.ts +76 -0
- package/src/types.ts +31 -0
- package/src/useDockLayout.ts +249 -0
- package/tsconfig.json +7 -0
- package/tsup.config.ts +9 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { LayoutNode, PanelNode, SplitNode } from "../../types";
|
|
3
|
+
import { LayoutTree } from "./LayoutTree";
|
|
4
|
+
|
|
5
|
+
describe("LayoutTree", () => {
|
|
6
|
+
describe("findNode", () => {
|
|
7
|
+
it("should return null when the root is null", () => {
|
|
8
|
+
const root = null;
|
|
9
|
+
const tree = new LayoutTree(root);
|
|
10
|
+
expect(tree.findNode("root")).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should return null when the node is not found", () => {
|
|
14
|
+
const root: LayoutNode = {
|
|
15
|
+
id: "root",
|
|
16
|
+
type: "panel",
|
|
17
|
+
};
|
|
18
|
+
const tree = new LayoutTree(root);
|
|
19
|
+
expect(tree.findNode("non-existent-id")).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should return node when the node is root panel", () => {
|
|
23
|
+
const root: LayoutNode = {
|
|
24
|
+
id: "root",
|
|
25
|
+
type: "panel",
|
|
26
|
+
};
|
|
27
|
+
const tree = new LayoutTree(root);
|
|
28
|
+
expect(tree.findNode("root")).toBe(root);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should return node when the node is root split", () => {
|
|
32
|
+
const root: LayoutNode = {
|
|
33
|
+
id: "root",
|
|
34
|
+
type: "split",
|
|
35
|
+
orientation: "horizontal",
|
|
36
|
+
ratio: 0.5,
|
|
37
|
+
left: {
|
|
38
|
+
id: "left",
|
|
39
|
+
type: "panel",
|
|
40
|
+
},
|
|
41
|
+
right: {
|
|
42
|
+
id: "right",
|
|
43
|
+
type: "panel",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const tree = new LayoutTree(root);
|
|
47
|
+
expect(tree.findNode("root")).toEqual(root);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should return node when the node is a child of the root", () => {
|
|
51
|
+
const left: PanelNode = {
|
|
52
|
+
id: "left",
|
|
53
|
+
type: "panel",
|
|
54
|
+
};
|
|
55
|
+
const root: LayoutNode = {
|
|
56
|
+
id: "root",
|
|
57
|
+
type: "split",
|
|
58
|
+
orientation: "horizontal",
|
|
59
|
+
ratio: 0.5,
|
|
60
|
+
left,
|
|
61
|
+
right: {
|
|
62
|
+
id: "right",
|
|
63
|
+
type: "panel",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const tree = new LayoutTree(root);
|
|
67
|
+
expect(tree.findNode("left")).toEqual(left);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should return node when the node is a grand child of the root", () => {
|
|
71
|
+
const leftLeft: PanelNode = {
|
|
72
|
+
id: "left-left",
|
|
73
|
+
type: "panel",
|
|
74
|
+
};
|
|
75
|
+
const root: LayoutNode = {
|
|
76
|
+
id: "root",
|
|
77
|
+
type: "split",
|
|
78
|
+
orientation: "horizontal",
|
|
79
|
+
ratio: 0.5,
|
|
80
|
+
left: {
|
|
81
|
+
id: "left",
|
|
82
|
+
type: "split",
|
|
83
|
+
orientation: "horizontal",
|
|
84
|
+
ratio: 0.5,
|
|
85
|
+
left: leftLeft,
|
|
86
|
+
right: {
|
|
87
|
+
id: "left-right",
|
|
88
|
+
type: "panel",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
right: {
|
|
92
|
+
id: "right",
|
|
93
|
+
type: "panel",
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const tree = new LayoutTree(root);
|
|
97
|
+
expect(tree.findNode("left-left")).toBe(leftLeft);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("findParentNode", () => {
|
|
102
|
+
it("should return null when the root is null", () => {
|
|
103
|
+
const root = null;
|
|
104
|
+
const tree = new LayoutTree(root);
|
|
105
|
+
expect(tree.findParentNode("root")).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should return null when the child node is root panel", () => {
|
|
109
|
+
const root: PanelNode = {
|
|
110
|
+
id: "root",
|
|
111
|
+
type: "panel",
|
|
112
|
+
};
|
|
113
|
+
const tree = new LayoutTree(root);
|
|
114
|
+
expect(tree.findParentNode("root")).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should return null when the child node is root split", () => {
|
|
118
|
+
const root: LayoutNode = {
|
|
119
|
+
id: "root",
|
|
120
|
+
type: "split",
|
|
121
|
+
orientation: "horizontal",
|
|
122
|
+
ratio: 0.5,
|
|
123
|
+
left: {
|
|
124
|
+
id: "left",
|
|
125
|
+
type: "panel",
|
|
126
|
+
},
|
|
127
|
+
right: {
|
|
128
|
+
id: "right",
|
|
129
|
+
type: "panel",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const tree = new LayoutTree(root);
|
|
133
|
+
expect(tree.findParentNode("root")).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should return null when the child node is not found", () => {
|
|
137
|
+
const root: LayoutNode = {
|
|
138
|
+
id: "root",
|
|
139
|
+
type: "split",
|
|
140
|
+
orientation: "horizontal",
|
|
141
|
+
ratio: 0.5,
|
|
142
|
+
left: {
|
|
143
|
+
id: "left",
|
|
144
|
+
type: "panel",
|
|
145
|
+
},
|
|
146
|
+
right: {
|
|
147
|
+
id: "right",
|
|
148
|
+
type: "panel",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const tree = new LayoutTree(root);
|
|
152
|
+
expect(tree.findParentNode("non-existent-child-id")).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should return node when the child node is a child of the root", () => {
|
|
156
|
+
const root: LayoutNode = {
|
|
157
|
+
id: "root",
|
|
158
|
+
type: "split",
|
|
159
|
+
orientation: "horizontal",
|
|
160
|
+
ratio: 0.5,
|
|
161
|
+
left: {
|
|
162
|
+
id: "left",
|
|
163
|
+
type: "panel",
|
|
164
|
+
},
|
|
165
|
+
right: {
|
|
166
|
+
id: "right",
|
|
167
|
+
type: "panel",
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
const tree = new LayoutTree(root);
|
|
171
|
+
expect(tree.findParentNode("left")).toBe(root);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should return node when the child node is a grand child of the root", () => {
|
|
175
|
+
const left: SplitNode = {
|
|
176
|
+
id: "left",
|
|
177
|
+
type: "split",
|
|
178
|
+
orientation: "horizontal",
|
|
179
|
+
ratio: 0.5,
|
|
180
|
+
left: {
|
|
181
|
+
id: "left-left",
|
|
182
|
+
type: "panel",
|
|
183
|
+
},
|
|
184
|
+
right: {
|
|
185
|
+
id: "left-right",
|
|
186
|
+
type: "panel",
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
const root: LayoutNode = {
|
|
190
|
+
id: "root",
|
|
191
|
+
type: "split",
|
|
192
|
+
orientation: "horizontal",
|
|
193
|
+
ratio: 0.5,
|
|
194
|
+
left,
|
|
195
|
+
right: {
|
|
196
|
+
id: "right",
|
|
197
|
+
type: "panel",
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
const tree = new LayoutTree(root);
|
|
201
|
+
expect(tree.findParentNode("left-left")).toBe(left);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("replaceChildNode", () => {
|
|
206
|
+
it("should throw an error if the parent node is not found", () => {
|
|
207
|
+
const tree = new LayoutTree(null);
|
|
208
|
+
expect(() =>
|
|
209
|
+
tree.replaceChildNode({
|
|
210
|
+
parentId: "non-existent-parent-id",
|
|
211
|
+
oldChildId: "child",
|
|
212
|
+
newChild: { id: "new-child", type: "panel" },
|
|
213
|
+
}),
|
|
214
|
+
).toThrowError("Parent node with id non-existent-parent-id not found");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should throw an error if the parent node is not a split node", () => {
|
|
218
|
+
const root: PanelNode = {
|
|
219
|
+
id: "root",
|
|
220
|
+
type: "panel",
|
|
221
|
+
};
|
|
222
|
+
const tree = new LayoutTree(root);
|
|
223
|
+
expect(() =>
|
|
224
|
+
tree.replaceChildNode({
|
|
225
|
+
parentId: "root",
|
|
226
|
+
oldChildId: "child",
|
|
227
|
+
newChild: { id: "new-child", type: "panel" },
|
|
228
|
+
}),
|
|
229
|
+
).toThrowError("Parent node with id root is not a split node");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should throw an error if the child node is not found", () => {
|
|
233
|
+
const root: LayoutNode = {
|
|
234
|
+
id: "root",
|
|
235
|
+
type: "split",
|
|
236
|
+
orientation: "horizontal",
|
|
237
|
+
ratio: 0.5,
|
|
238
|
+
left: {
|
|
239
|
+
id: "left",
|
|
240
|
+
type: "panel",
|
|
241
|
+
},
|
|
242
|
+
right: {
|
|
243
|
+
id: "right",
|
|
244
|
+
type: "panel",
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
const tree = new LayoutTree(root);
|
|
248
|
+
expect(() =>
|
|
249
|
+
tree.replaceChildNode({
|
|
250
|
+
parentId: "root",
|
|
251
|
+
oldChildId: "non-existent-child-id",
|
|
252
|
+
newChild: { id: "new-child", type: "panel" },
|
|
253
|
+
}),
|
|
254
|
+
).toThrowError("Child node with id non-existent-child-id not found");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should throw an error if the child node is not a child of the parent node", () => {
|
|
258
|
+
const root: LayoutNode = {
|
|
259
|
+
id: "root",
|
|
260
|
+
type: "split",
|
|
261
|
+
orientation: "horizontal",
|
|
262
|
+
ratio: 0.5,
|
|
263
|
+
left: { id: "left", type: "panel" },
|
|
264
|
+
right: {
|
|
265
|
+
id: "right",
|
|
266
|
+
type: "split",
|
|
267
|
+
orientation: "horizontal",
|
|
268
|
+
ratio: 0.5,
|
|
269
|
+
left: { id: "right-left", type: "panel" },
|
|
270
|
+
right: { id: "right-right", type: "panel" },
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const tree = new LayoutTree(root);
|
|
274
|
+
expect(() =>
|
|
275
|
+
tree.replaceChildNode({
|
|
276
|
+
parentId: "root",
|
|
277
|
+
oldChildId: "right-left",
|
|
278
|
+
newChild: { id: "new-child", type: "panel" },
|
|
279
|
+
}),
|
|
280
|
+
).toThrow(
|
|
281
|
+
"Child node with id right-left is not a child of the parent node with id root",
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should replace the child node when the child node is the left child of the parent node", () => {
|
|
286
|
+
const root: LayoutNode = {
|
|
287
|
+
id: "root",
|
|
288
|
+
type: "split",
|
|
289
|
+
orientation: "horizontal",
|
|
290
|
+
ratio: 0.5,
|
|
291
|
+
left: {
|
|
292
|
+
id: "left",
|
|
293
|
+
type: "panel",
|
|
294
|
+
},
|
|
295
|
+
right: {
|
|
296
|
+
id: "right",
|
|
297
|
+
type: "panel",
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
const tree = new LayoutTree(root);
|
|
301
|
+
const newChild: PanelNode = {
|
|
302
|
+
id: "new-child",
|
|
303
|
+
type: "panel",
|
|
304
|
+
};
|
|
305
|
+
tree.replaceChildNode({
|
|
306
|
+
parentId: "root",
|
|
307
|
+
oldChildId: "left",
|
|
308
|
+
newChild: newChild,
|
|
309
|
+
});
|
|
310
|
+
expect(root.left).toBe(newChild);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should replace the child node when the child node is the right child of the parent node", () => {
|
|
314
|
+
const root: LayoutNode = {
|
|
315
|
+
id: "root",
|
|
316
|
+
type: "split",
|
|
317
|
+
orientation: "horizontal",
|
|
318
|
+
ratio: 0.5,
|
|
319
|
+
left: {
|
|
320
|
+
id: "left",
|
|
321
|
+
type: "panel",
|
|
322
|
+
},
|
|
323
|
+
right: {
|
|
324
|
+
id: "right",
|
|
325
|
+
type: "panel",
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
const tree = new LayoutTree(root);
|
|
329
|
+
const newChild: PanelNode = {
|
|
330
|
+
id: "new-child",
|
|
331
|
+
type: "panel",
|
|
332
|
+
};
|
|
333
|
+
tree.replaceChildNode({
|
|
334
|
+
parentId: "root",
|
|
335
|
+
oldChildId: "right",
|
|
336
|
+
newChild: newChild,
|
|
337
|
+
});
|
|
338
|
+
expect(root.right).toBe(newChild);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { LayoutNode } from "../../types";
|
|
2
|
+
import { assertNever } from "../assertNever";
|
|
3
|
+
import { findParentNode } from "../findParentNode";
|
|
4
|
+
|
|
5
|
+
export class LayoutTree {
|
|
6
|
+
private _root: LayoutNode | null = null;
|
|
7
|
+
|
|
8
|
+
constructor(root: LayoutNode | null) {
|
|
9
|
+
this._root = root;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get root() {
|
|
13
|
+
return this._root;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
set root(root: LayoutNode | null) {
|
|
17
|
+
this._root = root;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
findNode(id: string) {
|
|
21
|
+
if (this._root === null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return this.findNodeInSubTree(id, this._root);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private findNodeInSubTree(id: string, node: LayoutNode): LayoutNode | null {
|
|
29
|
+
if (id === node.id) {
|
|
30
|
+
return node;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (node.type === "panel") {
|
|
34
|
+
return null;
|
|
35
|
+
} else if (node.type === "split") {
|
|
36
|
+
return (
|
|
37
|
+
this.findNodeInSubTree(id, node.left) ??
|
|
38
|
+
this.findNodeInSubTree(id, node.right)
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
assertNever(node);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
findParentNode(id: string) {
|
|
46
|
+
return findParentNode(this._root, id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
replaceChildNode({
|
|
50
|
+
parentId,
|
|
51
|
+
oldChildId,
|
|
52
|
+
newChild,
|
|
53
|
+
}: {
|
|
54
|
+
parentId: string;
|
|
55
|
+
oldChildId: string;
|
|
56
|
+
newChild: LayoutNode;
|
|
57
|
+
}) {
|
|
58
|
+
const parentNode = this.findNode(parentId);
|
|
59
|
+
if (parentNode === null) {
|
|
60
|
+
throw new Error(`Parent node with id ${parentId} not found`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (parentNode.type !== "split") {
|
|
64
|
+
throw new Error(`Parent node with id ${parentId} is not a split node`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const oldChildNode = this.findNode(oldChildId);
|
|
68
|
+
if (oldChildNode === null) {
|
|
69
|
+
throw new Error(`Child node with id ${oldChildId} not found`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (parentNode.left.id === oldChildId) {
|
|
73
|
+
parentNode.left = newChild;
|
|
74
|
+
} else if (parentNode.right.id === oldChildId) {
|
|
75
|
+
parentNode.right = newChild;
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Child node with id ${oldChildId} is not a child of the parent node with id ${parentId}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { LayoutRect, PanelNode, SplitNode } from "../../types";
|
|
3
|
+
import { calculateLayoutRects } from "./calculateLayoutRects";
|
|
4
|
+
|
|
5
|
+
describe("calculateLayoutRects", () => {
|
|
6
|
+
it("should return an empty array when the root is null", () => {
|
|
7
|
+
const root = null;
|
|
8
|
+
const options = {
|
|
9
|
+
gap: 10,
|
|
10
|
+
size: { width: 100, height: 100 },
|
|
11
|
+
};
|
|
12
|
+
const result = calculateLayoutRects(root, options);
|
|
13
|
+
expect(result).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should return correct layout rects when the root is panel node", () => {
|
|
17
|
+
const root: PanelNode = {
|
|
18
|
+
id: "root",
|
|
19
|
+
type: "panel",
|
|
20
|
+
};
|
|
21
|
+
const options = {
|
|
22
|
+
gap: 10,
|
|
23
|
+
size: { width: 100, height: 100 },
|
|
24
|
+
};
|
|
25
|
+
const result = calculateLayoutRects(root, options);
|
|
26
|
+
expect(result).toEqual([
|
|
27
|
+
{
|
|
28
|
+
id: "root",
|
|
29
|
+
type: "panel",
|
|
30
|
+
x: 0,
|
|
31
|
+
y: 0,
|
|
32
|
+
width: 100,
|
|
33
|
+
height: 100,
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return correct layout rects when the root is split node with horizontal orientation", () => {
|
|
39
|
+
const root: SplitNode = {
|
|
40
|
+
id: "root",
|
|
41
|
+
type: "split",
|
|
42
|
+
orientation: "horizontal",
|
|
43
|
+
ratio: 0.5,
|
|
44
|
+
left: {
|
|
45
|
+
id: "left",
|
|
46
|
+
type: "panel",
|
|
47
|
+
},
|
|
48
|
+
right: {
|
|
49
|
+
id: "right",
|
|
50
|
+
type: "panel",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const options = {
|
|
54
|
+
gap: 10,
|
|
55
|
+
size: { width: 100, height: 100 },
|
|
56
|
+
};
|
|
57
|
+
const result = calculateLayoutRects(root, options);
|
|
58
|
+
expect(result).toEqual<LayoutRect[]>([
|
|
59
|
+
{
|
|
60
|
+
id: "root",
|
|
61
|
+
type: "split",
|
|
62
|
+
orientation: "horizontal",
|
|
63
|
+
x: 45,
|
|
64
|
+
y: 0,
|
|
65
|
+
width: 10,
|
|
66
|
+
height: 100,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "left",
|
|
70
|
+
type: "panel",
|
|
71
|
+
x: 0,
|
|
72
|
+
y: 0,
|
|
73
|
+
width: 45,
|
|
74
|
+
height: 100,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "right",
|
|
78
|
+
type: "panel",
|
|
79
|
+
x: 55,
|
|
80
|
+
y: 0,
|
|
81
|
+
width: 45,
|
|
82
|
+
height: 100,
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should return correct layout rects when the root is split node with vertical orientation", () => {
|
|
88
|
+
const root: SplitNode = {
|
|
89
|
+
id: "root",
|
|
90
|
+
type: "split",
|
|
91
|
+
orientation: "vertical",
|
|
92
|
+
ratio: 0.5,
|
|
93
|
+
left: {
|
|
94
|
+
id: "left",
|
|
95
|
+
type: "panel",
|
|
96
|
+
},
|
|
97
|
+
right: {
|
|
98
|
+
id: "right",
|
|
99
|
+
type: "panel",
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const options = {
|
|
103
|
+
gap: 10,
|
|
104
|
+
size: { width: 100, height: 100 },
|
|
105
|
+
};
|
|
106
|
+
const result = calculateLayoutRects(root, options);
|
|
107
|
+
expect(result).toEqual<LayoutRect[]>([
|
|
108
|
+
{
|
|
109
|
+
id: "root",
|
|
110
|
+
type: "split",
|
|
111
|
+
orientation: "vertical",
|
|
112
|
+
x: 0,
|
|
113
|
+
y: 45,
|
|
114
|
+
width: 100,
|
|
115
|
+
height: 10,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "left",
|
|
119
|
+
type: "panel",
|
|
120
|
+
x: 0,
|
|
121
|
+
y: 0,
|
|
122
|
+
width: 100,
|
|
123
|
+
height: 45,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "right",
|
|
127
|
+
type: "panel",
|
|
128
|
+
x: 0,
|
|
129
|
+
y: 55,
|
|
130
|
+
width: 100,
|
|
131
|
+
height: 45,
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should return correct layout rects when the root is nested split node", () => {
|
|
137
|
+
const root: SplitNode = {
|
|
138
|
+
id: "root",
|
|
139
|
+
type: "split",
|
|
140
|
+
orientation: "horizontal",
|
|
141
|
+
ratio: 0.5,
|
|
142
|
+
left: {
|
|
143
|
+
id: "left",
|
|
144
|
+
type: "split",
|
|
145
|
+
orientation: "vertical",
|
|
146
|
+
ratio: 0.5,
|
|
147
|
+
left: {
|
|
148
|
+
id: "left-left",
|
|
149
|
+
type: "panel",
|
|
150
|
+
},
|
|
151
|
+
right: {
|
|
152
|
+
id: "left-right",
|
|
153
|
+
type: "panel",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
right: {
|
|
157
|
+
id: "right",
|
|
158
|
+
type: "panel",
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const options = {
|
|
162
|
+
gap: 10,
|
|
163
|
+
size: { width: 100, height: 100 },
|
|
164
|
+
};
|
|
165
|
+
const result = calculateLayoutRects(root, options);
|
|
166
|
+
expect(result).toEqual<LayoutRect[]>([
|
|
167
|
+
{
|
|
168
|
+
id: "root",
|
|
169
|
+
type: "split",
|
|
170
|
+
orientation: "horizontal",
|
|
171
|
+
x: 45,
|
|
172
|
+
y: 0,
|
|
173
|
+
width: 10,
|
|
174
|
+
height: 100,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "left",
|
|
178
|
+
type: "split",
|
|
179
|
+
orientation: "vertical",
|
|
180
|
+
x: 0,
|
|
181
|
+
y: 45,
|
|
182
|
+
width: 45,
|
|
183
|
+
height: 10,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "left-left",
|
|
187
|
+
type: "panel",
|
|
188
|
+
x: 0,
|
|
189
|
+
y: 0,
|
|
190
|
+
width: 45,
|
|
191
|
+
height: 45,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "left-right",
|
|
195
|
+
type: "panel",
|
|
196
|
+
x: 0,
|
|
197
|
+
y: 55,
|
|
198
|
+
width: 45,
|
|
199
|
+
height: 45,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "right",
|
|
203
|
+
type: "panel",
|
|
204
|
+
x: 55,
|
|
205
|
+
y: 0,
|
|
206
|
+
width: 45,
|
|
207
|
+
height: 100,
|
|
208
|
+
},
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { LayoutManagerOptions, LayoutNode, LayoutRect } from "../../types";
|
|
2
|
+
import { assertNever } from "../assertNever";
|
|
3
|
+
import type { Rect } from "./types";
|
|
4
|
+
|
|
5
|
+
export function calculateLayoutRects(
|
|
6
|
+
root: LayoutNode | null,
|
|
7
|
+
options: Required<LayoutManagerOptions>,
|
|
8
|
+
): LayoutRect[] {
|
|
9
|
+
if (root === null) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const rects: LayoutRect[] = [];
|
|
14
|
+
|
|
15
|
+
const traverse = (node: LayoutNode, rect: Rect) => {
|
|
16
|
+
if (node.type === "split") {
|
|
17
|
+
if (node.orientation === "horizontal") {
|
|
18
|
+
rects.push({
|
|
19
|
+
id: node.id,
|
|
20
|
+
type: "split",
|
|
21
|
+
orientation: node.orientation,
|
|
22
|
+
x: Math.round(rect.x + rect.width * node.ratio - options.gap / 2),
|
|
23
|
+
y: Math.round(rect.y),
|
|
24
|
+
width: Math.round(options.gap),
|
|
25
|
+
height: Math.round(rect.height),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
traverse(node.left, {
|
|
29
|
+
x: rect.x,
|
|
30
|
+
y: rect.y,
|
|
31
|
+
width: rect.width * node.ratio - options.gap / 2,
|
|
32
|
+
height: rect.height,
|
|
33
|
+
});
|
|
34
|
+
traverse(node.right, {
|
|
35
|
+
x: rect.x + rect.width * node.ratio + options.gap / 2,
|
|
36
|
+
y: rect.y,
|
|
37
|
+
width: rect.width * (1 - node.ratio) - options.gap / 2,
|
|
38
|
+
height: rect.height,
|
|
39
|
+
});
|
|
40
|
+
} else if (node.orientation === "vertical") {
|
|
41
|
+
rects.push({
|
|
42
|
+
id: node.id,
|
|
43
|
+
type: "split",
|
|
44
|
+
orientation: node.orientation,
|
|
45
|
+
x: Math.round(rect.x),
|
|
46
|
+
y: Math.round(rect.y + rect.height * node.ratio - options.gap / 2),
|
|
47
|
+
width: Math.round(rect.width),
|
|
48
|
+
height: Math.round(options.gap),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
traverse(node.left, {
|
|
52
|
+
x: rect.x,
|
|
53
|
+
y: rect.y,
|
|
54
|
+
width: rect.width,
|
|
55
|
+
height: rect.height * node.ratio - options.gap / 2,
|
|
56
|
+
});
|
|
57
|
+
traverse(node.right, {
|
|
58
|
+
x: rect.x,
|
|
59
|
+
y: rect.y + rect.height * node.ratio + options.gap / 2,
|
|
60
|
+
width: rect.width,
|
|
61
|
+
height: rect.height * (1 - node.ratio) - options.gap / 2,
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
assertNever(node.orientation);
|
|
65
|
+
}
|
|
66
|
+
} else if (node.type === "panel") {
|
|
67
|
+
rects.push({
|
|
68
|
+
id: node.id,
|
|
69
|
+
type: "panel",
|
|
70
|
+
x: Math.round(rect.x),
|
|
71
|
+
y: Math.round(rect.y),
|
|
72
|
+
width: Math.round(rect.width),
|
|
73
|
+
height: Math.round(rect.height),
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
assertNever(node);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
traverse(root, {
|
|
81
|
+
x: 0,
|
|
82
|
+
y: 0,
|
|
83
|
+
width: options.size.width,
|
|
84
|
+
height: options.size.height,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return rects;
|
|
88
|
+
}
|