x4js 2.3.15 → 2.3.16
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/aicontext.md +117 -2
- package/package.json +1 -1
- package/src/components/components.ts +1 -0
- package/src/components/dockingview/dockingview.module.scss +279 -0
- package/src/components/dockingview/dockingview.ts +899 -0
- package/src/components/dockingview/docklayout.ts +659 -0
- package/src/core/component.ts +1 -1
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file docklayout.ts
|
|
9
|
+
* @author Etienne Cochard
|
|
10
|
+
*
|
|
11
|
+
* @copyright (c) 2026 R-libre ingenierie
|
|
12
|
+
*
|
|
13
|
+
* Use of this source code is governed by an MIT-style license
|
|
14
|
+
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How the panels of a DockingView are laid out: docked, floating or hidden.
|
|
19
|
+
*
|
|
20
|
+
* There are two kinds of panels, which never mix:
|
|
21
|
+
*
|
|
22
|
+
* tools docked in a band on the left or on the right: one above the
|
|
23
|
+
* other, side by side, or as tabs of the same stack; they
|
|
24
|
+
* can also float.
|
|
25
|
+
* contents what is worked on (a document, as the editors of an IDE).
|
|
26
|
+
* They share the room between the bands: side by side, one
|
|
27
|
+
* above the other, or as tabs of the same stack.
|
|
28
|
+
*
|
|
29
|
+
* A layout is plain data, so that it can be saved as it is:
|
|
30
|
+
*
|
|
31
|
+
* left, right the two bands: a width, and a tree of tools.
|
|
32
|
+
* content the tree of the contents.
|
|
33
|
+
*
|
|
34
|
+
* In a tree, a split puts its children side by side (row)
|
|
35
|
+
* or one above the other (column); a stack holds panels,
|
|
36
|
+
* one tab each.
|
|
37
|
+
*
|
|
38
|
+
* floats the tools that float above the rest, with their rectangle.
|
|
39
|
+
* hidden the panels that are closed. They keep their place, and
|
|
40
|
+
* come back there when shown again.
|
|
41
|
+
*
|
|
42
|
+
* Panels are known by their id only: what they show is the business of the
|
|
43
|
+
* view (dockingview.ts), which also says what may go where. Nothing here
|
|
44
|
+
* changes the layout it is given: every function returns a new one.
|
|
45
|
+
*
|
|
46
|
+
* (this file imports nothing, and knows nothing of the DOM)
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export type DockSide = "left" | "right" | "top" | "bottom";
|
|
50
|
+
|
|
51
|
+
/** where a panel goes, relative to another one: beside it, or with it as a tab */
|
|
52
|
+
export type DockWhere = DockSide | "tab";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `size` is the share a child has of its split: the room is divided in
|
|
56
|
+
* proportion. The sizes on screen, in pixels, are written there (see the
|
|
57
|
+
* view); 0 is a share that is not known yet.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
export interface DockSplit {
|
|
61
|
+
type: "split";
|
|
62
|
+
dir: "row" | "column";
|
|
63
|
+
size: number;
|
|
64
|
+
children: DockNode[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DockStack {
|
|
68
|
+
type: "stack";
|
|
69
|
+
size: number;
|
|
70
|
+
panels: string[];
|
|
71
|
+
active: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type DockNode = DockSplit | DockStack;
|
|
75
|
+
|
|
76
|
+
/** the tools docked on one side: `size` is its width in pixels, `root` is null when it has none */
|
|
77
|
+
export interface DockBand {
|
|
78
|
+
size: number;
|
|
79
|
+
root: DockNode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface DockRect {
|
|
83
|
+
x: number;
|
|
84
|
+
y: number;
|
|
85
|
+
w: number;
|
|
86
|
+
h: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** a floating panel; its rectangle is relative to the box that holds the layout */
|
|
90
|
+
export interface DockFloat extends DockRect {
|
|
91
|
+
panel: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DockLayout {
|
|
95
|
+
left: DockBand;
|
|
96
|
+
right: DockBand;
|
|
97
|
+
content: DockNode;
|
|
98
|
+
floats: DockFloat[];
|
|
99
|
+
hidden: string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** the panels that exist, by kind */
|
|
103
|
+
export interface DockKinds {
|
|
104
|
+
tools: string[];
|
|
105
|
+
contents: string[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function copy( layout: DockLayout ): DockLayout {
|
|
109
|
+
return structuredClone( layout );
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* the stacks of a tree, from left to right and from top to bottom
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
export function stacksOf( node: DockNode ): DockStack[] {
|
|
117
|
+
if( !node ) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return node.type === "stack" ? [node] : node.children.flatMap( child => stacksOf( child ) );
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function allStacks( layout: DockLayout ): DockStack[] {
|
|
125
|
+
return [...stacksOf( layout.left.root ), ...stacksOf( layout.right.root ), ...stacksOf( layout.content )];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parentOf( root: DockNode, node: DockNode ): DockSplit {
|
|
129
|
+
if( !root || root.type === "stack" ) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return root.children.includes( node ) ? root : root.children.map( child => parentOf( child, node ) ).find( p => !!p ) ?? null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Tidy a tree: no stack without panel, no split with a single child, no
|
|
138
|
+
* split straight inside a split of the same direction.
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
function normalize( node: DockNode ): DockNode {
|
|
142
|
+
if( !node ) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if( node.type === "stack" ) {
|
|
147
|
+
if( node.panels.length === 0 ) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if( !node.panels.includes( node.active ) ) {
|
|
152
|
+
node.active = node.panels[0];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return node;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
node.children = node.children.flatMap( child => {
|
|
159
|
+
const tidy = normalize( child );
|
|
160
|
+
if( !tidy ) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return tidy.type === "split" && tidy.dir === node.dir ? tidy.children : [tidy];
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
if( node.children.length === 0 ) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// the only child takes the place of the split
|
|
172
|
+
if( node.children.length === 1 ) {
|
|
173
|
+
node.children[0].size = node.size;
|
|
174
|
+
return node.children[0];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return node;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function tidy( layout: DockLayout ): void {
|
|
181
|
+
layout.content = normalize( layout.content );
|
|
182
|
+
layout.left.root = normalize( layout.left.root );
|
|
183
|
+
layout.right.root = normalize( layout.right.root );
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* take a panel out of where it is; the layout is not tidied
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
function detach( layout: DockLayout, panel: string ): void {
|
|
191
|
+
// A tool that was beside others, alone in its stack, takes its width
|
|
192
|
+
// with it: its band gets narrower by its share of the row.
|
|
193
|
+
for( const band of [layout.left, layout.right] ) {
|
|
194
|
+
const root = band.root;
|
|
195
|
+
const at = root?.type === "split" && root.dir === "row" ? root.children.findIndex( child => child.type === "stack" && child.panels.length === 1 && child.panels[0] === panel ) : -1;
|
|
196
|
+
|
|
197
|
+
if( root?.type === "split" && at >= 0 ) {
|
|
198
|
+
const shares = sharesOf( root.children );
|
|
199
|
+
band.size = Math.round( band.size * ( 1 - shares[at] / shares.reduce( ( a, b ) => a + b ) ) );
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for( const stack of allStacks( layout ) ) {
|
|
204
|
+
stack.panels = stack.panels.filter( id => id !== panel );
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
layout.floats = layout.floats.filter( float => float.panel !== panel );
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* every panel of a layout: docked, floating, hidden or not
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
export function dockPanels( layout: DockLayout ): string[] {
|
|
215
|
+
return [...allStacks( layout ).flatMap( stack => stack.panels ), ...layout.floats.map( float => float.panel )];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* the stack a panel is docked in, null when it floats or is unknown
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
export function stackOf( layout: DockLayout, panel: string ): DockStack {
|
|
223
|
+
return allStacks( layout ).find( stack => stack.panels.includes( panel ) ) ?? null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* the band a panel is docked in, null when it is a content, floats or is unknown
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
export function bandOf( layout: DockLayout, panel: string ): "left" | "right" {
|
|
231
|
+
const holds = ( band: DockBand ) => stacksOf( band.root ).some( stack => stack.panels.includes( panel ) );
|
|
232
|
+
|
|
233
|
+
if( holds( layout.left ) ) {
|
|
234
|
+
return "left";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return holds( layout.right ) ? "right" : null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function floatOf( layout: DockLayout, panel: string ): DockFloat {
|
|
241
|
+
return layout.floats.find( float => float.panel === panel ) ?? null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* the panels of a stack that are not hidden
|
|
246
|
+
*/
|
|
247
|
+
|
|
248
|
+
export function visiblePanels( stack: DockStack, hidden: string[] ): string[] {
|
|
249
|
+
return stack.panels.filter( id => !hidden.includes( id ) );
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* the panel a stack shows: its active one, or the first that is not hidden
|
|
254
|
+
*/
|
|
255
|
+
|
|
256
|
+
export function shownPanel( stack: DockStack, hidden: string[] ): string {
|
|
257
|
+
const visible = visiblePanels( stack, hidden );
|
|
258
|
+
return visible.includes( stack.active ) ? stack.active : visible[0] ?? null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* false for a stack whose panels are all hidden, and for a split of such stacks
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
export function isShown( node: DockNode, hidden: string[] ): boolean {
|
|
266
|
+
return stacksOf( node ).some( stack => visiblePanels( stack, hidden ).length > 0 );
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The share of each node among those that are shown together, as numbers
|
|
271
|
+
* that only mean something compared to each other. A node whose size is
|
|
272
|
+
* not known gets the average of the others.
|
|
273
|
+
*/
|
|
274
|
+
|
|
275
|
+
export function sharesOf( nodes: DockNode[] ): number[] {
|
|
276
|
+
const known = nodes.map( node => node.size ).filter( size => size > 0 );
|
|
277
|
+
const average = known.length ? known.reduce( ( a, b ) => a + b ) / known.length : 1;
|
|
278
|
+
|
|
279
|
+
return nodes.map( node => node.size > 0 ? node.size : average );
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Dock a panel beside another one, or with it as a tab.
|
|
284
|
+
*
|
|
285
|
+
* `target` is a panel of the stack that receives it. Beside it, the panel
|
|
286
|
+
* takes half of its room. But a tool put on the left or on the right of
|
|
287
|
+
* another one does not make it narrower: with `width`, the band gets
|
|
288
|
+
* wider by that much, which is what the tool gets.
|
|
289
|
+
*/
|
|
290
|
+
|
|
291
|
+
export function dockMove( layout: DockLayout, panel: string, target: string, where: DockWhere, width = 0 ): DockLayout {
|
|
292
|
+
const result = copy( layout );
|
|
293
|
+
const dest = stackOf( result, target );
|
|
294
|
+
|
|
295
|
+
if( !dest ) {
|
|
296
|
+
return layout;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if( where === "tab" ) {
|
|
300
|
+
if( !dest.panels.includes( panel ) ) {
|
|
301
|
+
detach( result, panel );
|
|
302
|
+
dest.panels.push( panel );
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
dest.active = panel;
|
|
306
|
+
tidy( result );
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// beside the stack it is alone in: nothing to do
|
|
311
|
+
if( dest.panels.length === 1 && dest.panels[0] === panel ) {
|
|
312
|
+
return layout;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const side = bandOf( result, target );
|
|
316
|
+
const band = side ? result[side] : null;
|
|
317
|
+
|
|
318
|
+
detach( result, panel );
|
|
319
|
+
|
|
320
|
+
const dir = where === "left" || where === "right" ? "row" : "column";
|
|
321
|
+
const before = where === "left" || where === "top";
|
|
322
|
+
const wider = !!band && dir === "row" && width > 0;
|
|
323
|
+
|
|
324
|
+
const half = dest.size / 2;
|
|
325
|
+
const fresh: DockStack = { type: "stack", size: wider ? width : half, panels: [panel], active: panel };
|
|
326
|
+
const parent = parentOf( band ? band.root : result.content, dest );
|
|
327
|
+
|
|
328
|
+
// the parent already cuts that way: one more child
|
|
329
|
+
if( parent && parent.dir === dir ) {
|
|
330
|
+
dest.size = wider ? dest.size : half;
|
|
331
|
+
parent.children.splice( parent.children.indexOf( dest ) + ( before ? 0 : 1 ), 0, fresh );
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// the stack is cut in two: a split takes its place, its two children share it
|
|
335
|
+
const split: DockSplit = { type: "split", dir, size: dest.size, children: before ? [fresh, dest] : [dest, fresh] };
|
|
336
|
+
|
|
337
|
+
dest.size = wider ? band.size : 0;
|
|
338
|
+
fresh.size = wider ? width : 0;
|
|
339
|
+
|
|
340
|
+
if( parent ) {
|
|
341
|
+
parent.children[parent.children.indexOf( dest )] = split;
|
|
342
|
+
}
|
|
343
|
+
else if( band ) {
|
|
344
|
+
band.root = split;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
result.content = split;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if( wider ) {
|
|
352
|
+
band.size += width;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
tidy( result );
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Dock a tool in a band, under the tools that are already there.
|
|
361
|
+
* `size` is the width the band gets when it was empty.
|
|
362
|
+
*/
|
|
363
|
+
|
|
364
|
+
export function dockBand( layout: DockLayout, panel: string, side: "left" | "right", size = 0 ): DockLayout {
|
|
365
|
+
const result = copy( layout );
|
|
366
|
+
const band = result[side];
|
|
367
|
+
|
|
368
|
+
detach( result, panel );
|
|
369
|
+
tidy( result );
|
|
370
|
+
|
|
371
|
+
const fresh: DockStack = { type: "stack", size: 0, panels: [panel], active: panel };
|
|
372
|
+
const root = band.root;
|
|
373
|
+
|
|
374
|
+
if( !root ) {
|
|
375
|
+
band.root = fresh;
|
|
376
|
+
|
|
377
|
+
if( size > 0 ) {
|
|
378
|
+
band.size = size;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else if( root.type === "split" && root.dir === "column" ) {
|
|
382
|
+
// as high as the others, on average
|
|
383
|
+
fresh.size = sharesOf( root.children ).reduce( ( a, b ) => a + b ) / root.children.length;
|
|
384
|
+
root.children.push( fresh );
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// what was there and the tool share the height
|
|
388
|
+
root.size = 0;
|
|
389
|
+
band.root = { type: "split", dir: "column", size: 0, children: [root, fresh] };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* A content that is nowhere yet: a tab of the first stack of contents.
|
|
397
|
+
*/
|
|
398
|
+
|
|
399
|
+
function addContent( layout: DockLayout, panel: string ): DockLayout {
|
|
400
|
+
const first = stacksOf( layout.content )[0];
|
|
401
|
+
if( first ) {
|
|
402
|
+
return dockMove( layout, panel, first.panels[0], "tab" );
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const result = copy( layout );
|
|
406
|
+
result.content = { type: "stack", size: 0, panels: [panel], active: panel };
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Make a panel float, or move the one that already does. It comes above
|
|
412
|
+
* the other floating panels.
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
export function dockFloat( layout: DockLayout, panel: string, rect: DockRect ): DockLayout {
|
|
416
|
+
const result = copy( layout );
|
|
417
|
+
|
|
418
|
+
detach( result, panel );
|
|
419
|
+
tidy( result );
|
|
420
|
+
result.floats.push( { panel, x: rect.x, y: rect.y, w: rect.w, h: rect.h } );
|
|
421
|
+
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Hide a panel or show it again, where it was.
|
|
427
|
+
*/
|
|
428
|
+
|
|
429
|
+
export function dockShow( layout: DockLayout, panel: string, show = true ): DockLayout {
|
|
430
|
+
const result = copy( layout );
|
|
431
|
+
|
|
432
|
+
result.hidden = result.hidden.filter( id => id !== panel );
|
|
433
|
+
if( !show ) {
|
|
434
|
+
result.hidden.push( panel );
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// a panel that comes back is the one its stack shows
|
|
438
|
+
const stack = stackOf( result, panel );
|
|
439
|
+
if( show && stack ) {
|
|
440
|
+
stack.active = panel;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* the tab of a panel comes in front of the others of its stack
|
|
448
|
+
*/
|
|
449
|
+
|
|
450
|
+
export function dockActivate( layout: DockLayout, panel: string ): DockLayout {
|
|
451
|
+
const result = copy( layout );
|
|
452
|
+
const stack = stackOf( result, panel );
|
|
453
|
+
|
|
454
|
+
if( stack ) {
|
|
455
|
+
stack.active = panel;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
// :: RESTORE :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
463
|
+
|
|
464
|
+
function isObject( value: unknown ): value is Record<string, unknown> {
|
|
465
|
+
return typeof value === "object" && value !== null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** a number of a saved layout: 0 when it is missing or not a size */
|
|
469
|
+
function sizeOf( from: Record<string, unknown>, name: string ): number {
|
|
470
|
+
const value = name in from ? from[name] : 0;
|
|
471
|
+
return typeof value === "number" && Number.isFinite( value ) && value > 0 ? value : 0;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* A layout rebuilt from something that was saved, or null when it is not a
|
|
476
|
+
* layout or holds no panel. Only what is understood is kept, and only the
|
|
477
|
+
* panels that are known, each once and where its kind may be: a tool in a
|
|
478
|
+
* band or floating, a content among the contents.
|
|
479
|
+
*
|
|
480
|
+
* (a field is read only once it is known to be there: what was saved may be
|
|
481
|
+
* a state of x4, which reports the reading of a missing property)
|
|
482
|
+
*/
|
|
483
|
+
|
|
484
|
+
function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
|
|
485
|
+
if( !isObject( saved ) ) {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const seen = new Set<string>( );
|
|
490
|
+
|
|
491
|
+
const known = ( among: string[] ) => ( id: unknown ): id is string => {
|
|
492
|
+
if( typeof id !== "string" || !among.includes( id ) || seen.has( id ) ) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
seen.add( id );
|
|
497
|
+
return true;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const stack = ( from: unknown, among: string[] ): DockStack => {
|
|
501
|
+
if( !isObject( from ) || !( "panels" in from ) || !Array.isArray( from.panels ) ) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const ids = [...from.panels].filter( known( among ) );
|
|
506
|
+
const active = "active" in from && ids.includes( from.active as string ) ? from.active as string : ids[0];
|
|
507
|
+
|
|
508
|
+
return ids.length ? { type: "stack", size: sizeOf( from, "size" ), panels: ids, active } : null;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const node = ( from: unknown, among: string[] ): DockNode => {
|
|
512
|
+
if( !isObject( from ) || !( "type" in from ) ) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if( from.type === "stack" ) {
|
|
517
|
+
return stack( from, among );
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if( from.type === "split" && "children" in from && Array.isArray( from.children ) ) {
|
|
521
|
+
const children = [...from.children].map( child => node( child, among ) ).filter( child => !!child );
|
|
522
|
+
const dir = "dir" in from && from.dir === "column" ? "column" : "row";
|
|
523
|
+
|
|
524
|
+
return { type: "split", dir, size: sizeOf( from, "size" ), children };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return null;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const band = ( name: string ): DockBand => {
|
|
531
|
+
const from = name in saved ? saved[name] : null;
|
|
532
|
+
if( !isObject( from ) ) {
|
|
533
|
+
return { size: 0, root: null };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return { size: sizeOf( from, "size" ), root: normalize( "root" in from ? node( from.root, kinds.tools ) : null ) };
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const left = band( "left" );
|
|
540
|
+
const right = band( "right" );
|
|
541
|
+
const content = normalize( "content" in saved ? node( saved.content, kinds.contents ) : null );
|
|
542
|
+
|
|
543
|
+
const floats: DockFloat[] = [];
|
|
544
|
+
const hidden: string[] = [];
|
|
545
|
+
const floating = known( kinds.tools );
|
|
546
|
+
|
|
547
|
+
if( "floats" in saved && Array.isArray( saved.floats ) ) {
|
|
548
|
+
for( const float of [...saved.floats] ) {
|
|
549
|
+
if( isObject( float ) && "panel" in float && floating( float.panel ) ) {
|
|
550
|
+
floats.push( { panel: float.panel, x: sizeOf( float, "x" ), y: sizeOf( float, "y" ), w: sizeOf( float, "w" ), h: sizeOf( float, "h" ) } );
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if( "hidden" in saved && Array.isArray( saved.hidden ) ) {
|
|
556
|
+
for( const id of [...saved.hidden] ) {
|
|
557
|
+
if( typeof id === "string" && seen.has( id ) && !hidden.includes( id ) ) {
|
|
558
|
+
hidden.push( id );
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// nothing in it that is known: not worth more than no layout at all
|
|
564
|
+
return seen.size > 0 ? { left, right, content, floats, hidden } : null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The layout to start with: the one that was saved if it can be used,
|
|
569
|
+
* otherwise the default one.
|
|
570
|
+
*
|
|
571
|
+
* `kinds` are the panels that exist today. A saved layout may be older
|
|
572
|
+
* than the application: the panels it does not know are added where the
|
|
573
|
+
* default layout has them, those that are gone are dropped.
|
|
574
|
+
*/
|
|
575
|
+
|
|
576
|
+
export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLayout ): DockLayout {
|
|
577
|
+
const usual = sanitize( defaults, kinds ) ?? { left: { size: 0, root: null }, right: { size: 0, root: null }, content: null, floats: [], hidden: [] };
|
|
578
|
+
let layout = sanitize( saved, kinds ) ?? usual;
|
|
579
|
+
|
|
580
|
+
for( const panel of [...kinds.contents, ...kinds.tools] ) {
|
|
581
|
+
if( dockPanels( layout ).includes( panel ) ) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const float = floatOf( usual, panel );
|
|
586
|
+
const side = bandOf( usual, panel ) ?? "right";
|
|
587
|
+
|
|
588
|
+
if( kinds.contents.includes( panel ) ) {
|
|
589
|
+
layout = addContent( layout, panel );
|
|
590
|
+
}
|
|
591
|
+
else if( float ) {
|
|
592
|
+
layout = dockFloat( layout, panel, float );
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
layout = dockBand( layout, panel, side, usual[side].size );
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if( usual.hidden.includes( panel ) ) {
|
|
599
|
+
layout = dockShow( layout, panel, false );
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return layout;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
// :: GEOMETRY ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Where a point falls in a rectangle: near one of its sides, in a band of
|
|
611
|
+
* `band` pixels (a third of the rectangle at most), or in its "center".
|
|
612
|
+
* null when it is outside.
|
|
613
|
+
*/
|
|
614
|
+
|
|
615
|
+
export function dropZone( rect: DockRect, x: number, y: number, band: number ): DockSide | "center" {
|
|
616
|
+
if( x < rect.x || y < rect.y || x >= rect.x + rect.w || y >= rect.y + rect.h ) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const bandX = Math.min( band, rect.w / 3 );
|
|
621
|
+
const bandY = Math.min( band, rect.h / 3 );
|
|
622
|
+
|
|
623
|
+
// how deep the point is in each band: 0 on the side, 1 where the band ends
|
|
624
|
+
const depths: [DockSide, number][] = [
|
|
625
|
+
["left", ( x - rect.x ) / bandX],
|
|
626
|
+
["right", ( rect.x + rect.w - x ) / bandX],
|
|
627
|
+
["top", ( y - rect.y ) / bandY],
|
|
628
|
+
["bottom", ( rect.y + rect.h - y ) / bandY],
|
|
629
|
+
];
|
|
630
|
+
|
|
631
|
+
const [side, depth] = depths.reduce( ( a, b ) => b[1] < a[1] ? b : a );
|
|
632
|
+
return depth < 1 ? side : "center";
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* the part of a rectangle a panel takes when it is docked on one of its sides
|
|
637
|
+
*/
|
|
638
|
+
|
|
639
|
+
export function sideRect( rect: DockRect, side: DockSide, size: number ): DockRect {
|
|
640
|
+
switch( side ) {
|
|
641
|
+
case "left": return { x: rect.x, y: rect.y, w: size, h: rect.h };
|
|
642
|
+
case "right": return { x: rect.x + rect.w - size, y: rect.y, w: size, h: rect.h };
|
|
643
|
+
case "top": return { x: rect.x, y: rect.y, w: rect.w, h: size };
|
|
644
|
+
case "bottom": return { x: rect.x, y: rect.y + rect.h - size, w: rect.w, h: size };
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* A rectangle brought back inside another one, as far as needed for its
|
|
650
|
+
* top-left part to stay there: `keep` pixels of it, so that a floating
|
|
651
|
+
* panel can always be caught by its title.
|
|
652
|
+
*/
|
|
653
|
+
|
|
654
|
+
export function keepInside( rect: DockRect, within: DockRect, keep: number ): DockRect {
|
|
655
|
+
const x = Math.max( within.x, Math.min( rect.x, within.x + within.w - keep ) );
|
|
656
|
+
const y = Math.max( within.y, Math.min( rect.y, within.y + within.h - keep ) );
|
|
657
|
+
|
|
658
|
+
return { x, y, w: rect.w, h: rect.h };
|
|
659
|
+
}
|
package/src/core/component.ts
CHANGED
|
@@ -1197,7 +1197,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
1197
1197
|
|
|
1198
1198
|
// straight on the element, not with addDOMEvent: the handlers of the
|
|
1199
1199
|
// children are called exactly as before
|
|
1200
|
-
this.dom.addEventListener( "keydown", ( ev
|
|
1200
|
+
this.dom.addEventListener( "keydown", ( ev ) => shortcuts.handle( ev as KeyboardEvent ) );
|
|
1201
1201
|
this.dom.addEventListener( "focusin", ( ) => Application.instance( ).setActiveShortcuts( this, shortcuts ) );
|
|
1202
1202
|
}
|
|
1203
1203
|
|