x4js 2.3.16 → 2.3.18

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.
@@ -19,16 +19,21 @@
19
19
  *
20
20
  * There are two kinds of panels, which never mix:
21
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.
22
+ * tools docked in a band along a side of the contents: one above
23
+ * the other, side by side, or as tabs of the same stack;
24
+ * they can also float. Which bands a tool may be in is said
25
+ * by the view (DockKinds.sides): the left and the right
26
+ * ones, unless it says otherwise.
25
27
  * contents what is worked on (a document, as the editors of an IDE).
26
28
  * They share the room between the bands: side by side, one
27
29
  * above the other, or as tabs of the same stack.
28
30
  *
29
31
  * A layout is plain data, so that it can be saved as it is:
30
32
  *
31
- * left, right the two bands: a width, and a tree of tools.
33
+ * left, right the bands of the sides: a width, and a tree of tools.
34
+ * They take the whole height.
35
+ * top, bottom the bands above and under the contents, between those of
36
+ * the sides: a height, and a tree of tools.
32
37
  * content the tree of the contents.
33
38
  *
34
39
  * In a tree, a split puts its children side by side (row)
@@ -73,7 +78,10 @@ export interface DockStack {
73
78
 
74
79
  export type DockNode = DockSplit | DockStack;
75
80
 
76
- /** the tools docked on one side: `size` is its width in pixels, `root` is null when it has none */
81
+ /**
82
+ * The tools docked on one side: `size` is its width in pixels (its height,
83
+ * for the band of the top or of the bottom), `root` is null when it has none.
84
+ */
77
85
  export interface DockBand {
78
86
  size: number;
79
87
  root: DockNode;
@@ -91,9 +99,16 @@ export interface DockFloat extends DockRect {
91
99
  panel: string;
92
100
  }
93
101
 
102
+ /**
103
+ * `top` and `bottom` may be missing: a band that is missing is an empty
104
+ * one. A layout that does not use them does not have them.
105
+ */
106
+
94
107
  export interface DockLayout {
95
108
  left: DockBand;
96
109
  right: DockBand;
110
+ top?: DockBand;
111
+ bottom?: DockBand;
97
112
  content: DockNode;
98
113
  floats: DockFloat[];
99
114
  hidden: string[];
@@ -103,12 +118,47 @@ export interface DockLayout {
103
118
  export interface DockKinds {
104
119
  tools: string[];
105
120
  contents: string[];
121
+
122
+ /** the bands a tool may be docked in, by its id; the left and the right ones for a tool that is not there */
123
+ sides?: Record<string, DockSide[]>;
106
124
  }
107
125
 
126
+ /** the bands, in the order they are looked at */
127
+ const SIDES: DockSide[] = ["left", "right", "top", "bottom"];
128
+
129
+ /** where a tool may be docked when nothing says it */
130
+ const USUAL_SIDES: DockSide[] = ["left", "right"];
131
+
108
132
  function copy( layout: DockLayout ): DockLayout {
109
133
  return structuredClone( layout );
110
134
  }
111
135
 
136
+ /**
137
+ * true for the bands of the top and of the bottom: they lie flat. Their
138
+ * size is a height, and their tools are side by side where those of the
139
+ * left and of the right are one above the other.
140
+ */
141
+
142
+ export function isFlat( side: DockSide ): boolean {
143
+ return side === "top" || side === "bottom";
144
+ }
145
+
146
+ /**
147
+ * The bands a tool may be docked in. A list that is empty counts as none
148
+ * given: a tool has to be somewhere when its layout is lost.
149
+ */
150
+
151
+ export function dockSides( kinds: DockKinds, panel: string ): DockSide[] {
152
+ const given = kinds.sides ? kinds.sides[panel] : null;
153
+
154
+ if( !given ) {
155
+ return USUAL_SIDES;
156
+ }
157
+
158
+ const sides = given.filter( side => SIDES.includes( side ) );
159
+ return sides.length ? sides : USUAL_SIDES;
160
+ }
161
+
112
162
  /**
113
163
  * the stacks of a tree, from left to right and from top to bottom
114
164
  */
@@ -122,7 +172,7 @@ export function stacksOf( node: DockNode ): DockStack[] {
122
172
  }
123
173
 
124
174
  function allStacks( layout: DockLayout ): DockStack[] {
125
- return [...stacksOf( layout.left.root ), ...stacksOf( layout.right.root ), ...stacksOf( layout.content )];
175
+ return [...SIDES.flatMap( side => stacksOf( layout[side]?.root ) ), ...stacksOf( layout.content )];
126
176
  }
127
177
 
128
178
  function parentOf( root: DockNode, node: DockNode ): DockSplit {
@@ -179,8 +229,13 @@ function normalize( node: DockNode ): DockNode {
179
229
 
180
230
  function tidy( layout: DockLayout ): void {
181
231
  layout.content = normalize( layout.content );
182
- layout.left.root = normalize( layout.left.root );
183
- layout.right.root = normalize( layout.right.root );
232
+
233
+ for( const side of SIDES ) {
234
+ const band = layout[side];
235
+ if( band ) {
236
+ band.root = normalize( band.root );
237
+ }
238
+ }
184
239
  }
185
240
 
186
241
  /**
@@ -189,10 +244,13 @@ function tidy( layout: DockLayout ): void {
189
244
 
190
245
  function detach( layout: DockLayout, panel: string ): void {
191
246
  // 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;
247
+ // with it: its band gets narrower by its share of the row. In a band
248
+ // that lies flat it is its height, and its share of the column.
249
+ for( const side of SIDES ) {
250
+ const band = layout[side];
251
+ const root = band?.root;
252
+ const across = isFlat( side ) ? "column" : "row";
253
+ const at = root?.type === "split" && root.dir === across ? root.children.findIndex( child => child.type === "stack" && child.panels.length === 1 && child.panels[0] === panel ) : -1;
196
254
 
197
255
  if( root?.type === "split" && at >= 0 ) {
198
256
  const shares = sharesOf( root.children );
@@ -227,14 +285,8 @@ export function stackOf( layout: DockLayout, panel: string ): DockStack {
227
285
  * the band a panel is docked in, null when it is a content, floats or is unknown
228
286
  */
229
287
 
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;
288
+ export function bandOf( layout: DockLayout, panel: string ): DockSide {
289
+ return SIDES.find( side => stacksOf( layout[side]?.root ).some( stack => stack.panels.includes( panel ) ) ) ?? null;
238
290
  }
239
291
 
240
292
  export function floatOf( layout: DockLayout, panel: string ): DockFloat {
@@ -286,6 +338,10 @@ export function sharesOf( nodes: DockNode[] ): number[] {
286
338
  * takes half of its room. But a tool put on the left or on the right of
287
339
  * another one does not make it narrower: with `width`, the band gets
288
340
  * wider by that much, which is what the tool gets.
341
+ *
342
+ * In a band that lies flat (top, bottom) it is the other way round: a
343
+ * tool put above or under another one makes the band higher, by `width`,
344
+ * which is then a height.
289
345
  */
290
346
 
291
347
  export function dockMove( layout: DockLayout, panel: string, target: string, where: DockWhere, width = 0 ): DockLayout {
@@ -319,7 +375,9 @@ export function dockMove( layout: DockLayout, panel: string, target: string, whe
319
375
 
320
376
  const dir = where === "left" || where === "right" ? "row" : "column";
321
377
  const before = where === "left" || where === "top";
322
- const wider = !!band && dir === "row" && width > 0;
378
+
379
+ // across the band: the direction in which it grows
380
+ const wider = !!band && dir === ( isFlat( side ) ? "column" : "row" ) && width > 0;
323
381
 
324
382
  const half = dest.size / 2;
325
383
  const fresh: DockStack = { type: "stack", size: wider ? width : half, panels: [panel], active: panel };
@@ -357,17 +415,22 @@ export function dockMove( layout: DockLayout, panel: string, target: string, whe
357
415
  }
358
416
 
359
417
  /**
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.
418
+ * Dock a tool in a band, after the tools that are already there: under
419
+ * them on the left and on the right, on their right in a band that lies
420
+ * flat (top, bottom). `size` is the width the band gets when it was empty;
421
+ * its height, when it lies flat.
362
422
  */
363
423
 
364
- export function dockBand( layout: DockLayout, panel: string, side: "left" | "right", size = 0 ): DockLayout {
424
+ export function dockBand( layout: DockLayout, panel: string, side: DockSide, size = 0 ): DockLayout {
365
425
  const result = copy( layout );
366
- const band = result[side];
426
+ const band = result[side] ??= { size: 0, root: null };
367
427
 
368
428
  detach( result, panel );
369
429
  tidy( result );
370
430
 
431
+ // along the band: the direction its tools follow each other in
432
+ const along = isFlat( side ) ? "row" : "column";
433
+
371
434
  const fresh: DockStack = { type: "stack", size: 0, panels: [panel], active: panel };
372
435
  const root = band.root;
373
436
 
@@ -378,15 +441,15 @@ export function dockBand( layout: DockLayout, panel: string, side: "left" | "rig
378
441
  band.size = size;
379
442
  }
380
443
  }
381
- else if( root.type === "split" && root.dir === "column" ) {
382
- // as high as the others, on average
444
+ else if( root.type === "split" && root.dir === along ) {
445
+ // as big as the others, on average
383
446
  fresh.size = sharesOf( root.children ).reduce( ( a, b ) => a + b ) / root.children.length;
384
447
  root.children.push( fresh );
385
448
  }
386
449
  else {
387
- // what was there and the tool share the height
450
+ // what was there and the tool share the band
388
451
  root.size = 0;
389
- band.root = { type: "split", dir: "column", size: 0, children: [root, fresh] };
452
+ band.root = { type: "split", dir: along, size: 0, children: [root, fresh] };
390
453
  }
391
454
 
392
455
  return result;
@@ -475,7 +538,7 @@ function sizeOf( from: Record<string, unknown>, name: string ): number {
475
538
  * A layout rebuilt from something that was saved, or null when it is not a
476
539
  * layout or holds no panel. Only what is understood is kept, and only the
477
540
  * 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.
541
+ * band it may be docked in, or floating; a content among the contents.
479
542
  *
480
543
  * (a field is read only once it is known to be there: what was saved may be
481
544
  * a state of x4, which reports the reading of a missing property)
@@ -527,17 +590,21 @@ function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
527
590
  return null;
528
591
  };
529
592
 
530
- const band = ( name: string ): DockBand => {
593
+ // a band only keeps the tools that may be docked in it
594
+ const band = ( name: DockSide ): DockBand => {
531
595
  const from = name in saved ? saved[name] : null;
532
596
  if( !isObject( from ) ) {
533
597
  return { size: 0, root: null };
534
598
  }
535
599
 
536
- return { size: sizeOf( from, "size" ), root: normalize( "root" in from ? node( from.root, kinds.tools ) : null ) };
600
+ const allowed = kinds.tools.filter( id => dockSides( kinds, id ).includes( name ) );
601
+ return { size: sizeOf( from, "size" ), root: normalize( "root" in from ? node( from.root, allowed ) : null ) };
537
602
  };
538
603
 
539
604
  const left = band( "left" );
540
605
  const right = band( "right" );
606
+ const top = band( "top" );
607
+ const bottom = band( "bottom" );
541
608
  const content = normalize( "content" in saved ? node( saved.content, kinds.contents ) : null );
542
609
 
543
610
  const floats: DockFloat[] = [];
@@ -561,7 +628,23 @@ function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
561
628
  }
562
629
 
563
630
  // 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;
631
+ if( seen.size === 0 ) {
632
+ return null;
633
+ }
634
+
635
+ // the bands of the top and of the bottom are only there when they hold
636
+ // something: a layout that does not use them is as it always was
637
+ const layout: DockLayout = { left, right, content, floats, hidden };
638
+
639
+ if( top.root ) {
640
+ layout.top = top;
641
+ }
642
+
643
+ if( bottom.root ) {
644
+ layout.bottom = bottom;
645
+ }
646
+
647
+ return layout;
565
648
  }
566
649
 
567
650
  /**
@@ -570,7 +653,9 @@ function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
570
653
  *
571
654
  * `kinds` are the panels that exist today. A saved layout may be older
572
655
  * than the application: the panels it does not know are added where the
573
- * default layout has them, those that are gone are dropped.
656
+ * default layout has them, those that are gone are dropped. So is a tool
657
+ * that is in a band it may not be docked in anymore: it goes where the
658
+ * default layout has it.
574
659
  */
575
660
 
576
661
  export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLayout ): DockLayout {
@@ -582,8 +667,10 @@ export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLay
582
667
  continue;
583
668
  }
584
669
 
670
+ // where the default layout has it; otherwise on the right if it may, or in the first band it may be in
671
+ const allowed = dockSides( kinds, panel );
585
672
  const float = floatOf( usual, panel );
586
- const side = bandOf( usual, panel ) ?? "right";
673
+ const side = bandOf( usual, panel ) ?? ( allowed.includes( "right" ) ? "right" : allowed[0] );
587
674
 
588
675
  if( kinds.contents.includes( panel ) ) {
589
676
  layout = addContent( layout, panel );
@@ -592,7 +679,9 @@ export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLay
592
679
  layout = dockFloat( layout, panel, float );
593
680
  }
594
681
  else {
595
- layout = dockBand( layout, panel, side, usual[side].size );
682
+ // as wide as the default layout has that band, when it has it
683
+ const band = usual[side];
684
+ layout = dockBand( layout, panel, side, band ? band.size : 0 );
596
685
  }
597
686
 
598
687
  if( usual.hidden.includes( panel ) ) {
@@ -59,6 +59,10 @@ export class Gauge extends Component<GaugeProps> {
59
59
  constructor( props: GaugeProps ) {
60
60
  super( props );
61
61
 
62
+ // no range given: 0 to 100
63
+ this.props.min = props.min ?? 0;
64
+ this.props.max = props.max ?? 100;
65
+
62
66
  this.pos = 70;
63
67
  this.stops = [];
64
68