UNPKG

62.1 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3/*-----------------------------------------------------------------------------
4| Copyright (c) 2014-2017, PhosphorJS Contributors
5|
6| Distributed under the terms of the BSD 3-Clause License.
7|
8| The full license is in the file LICENSE, distributed with this software.
9|----------------------------------------------------------------------------*/
10import {
11 ArrayExt,
12 chain,
13 ChainIterator,
14 each,
15 empty,
16 IIterator,
17 map,
18 once,
19 reduce
20} from '@lumino/algorithm';
21
22import { ElementExt } from '@lumino/domutils';
23
24import { Message, MessageLoop } from '@lumino/messaging';
25
26import { BoxEngine, BoxSizer } from './boxengine';
27
28import { Layout, LayoutItem } from './layout';
29
30import { TabBar } from './tabbar';
31
32import Utils from './utils';
33
34import { Widget } from './widget';
35
36/**
37 * A layout which provides a flexible docking arrangement.
38 *
39 * #### Notes
40 * The consumer of this layout is responsible for handling all signals
41 * from the generated tab bars and managing the visibility of widgets
42 * and tab bars as needed.
43 */
44export class DockLayout extends Layout {
45 /**
46 * Construct a new dock layout.
47 *
48 * @param options - The options for initializing the layout.
49 */
50 constructor(options: DockLayout.IOptions) {
51 super();
52 this.renderer = options.renderer;
53 if (options.spacing !== undefined) {
54 this._spacing = Utils.clampDimension(options.spacing);
55 }
56 this._document = options.document || document;
57 this._hiddenMode =
58 options.hiddenMode !== undefined
59 ? options.hiddenMode
60 : Widget.HiddenMode.Display;
61 }
62
63 /**
64 * Dispose of the resources held by the layout.
65 *
66 * #### Notes
67 * This will clear and dispose all widgets in the layout.
68 */
69 dispose(): void {
70 // Get an iterator over the widgets in the layout.
71 let widgets = this.iter();
72
73 // Dispose of the layout items.
74 this._items.forEach(item => {
75 item.dispose();
76 });
77
78 // Clear the layout state before disposing the widgets.
79 this._box = null;
80 this._root = null;
81 this._items.clear();
82
83 // Dispose of the widgets contained in the old layout root.
84 each(widgets, widget => {
85 widget.dispose();
86 });
87
88 // Dispose of the base class.
89 super.dispose();
90 }
91
92 /**
93 * The renderer used by the dock layout.
94 */
95 readonly renderer: DockLayout.IRenderer;
96
97 /**
98 * The method for hiding child widgets.
99 *
100 * #### Notes
101 * If there is only one child widget, `Display` hiding mode will be used
102 * regardless of this setting.
103 */
104 get hiddenMode(): Widget.HiddenMode {
105 return this._hiddenMode;
106 }
107 set hiddenMode(v: Widget.HiddenMode) {
108 if (this._hiddenMode === v) {
109 return;
110 }
111 this._hiddenMode = v;
112 each(this.tabBars(), bar => {
113 if (bar.titles.length > 1) {
114 bar.titles.forEach(title => {
115 title.owner.hiddenMode = this._hiddenMode;
116 });
117 }
118 });
119 }
120
121 /**
122 * Get the inter-element spacing for the dock layout.
123 */
124 get spacing(): number {
125 return this._spacing;
126 }
127
128 /**
129 * Set the inter-element spacing for the dock layout.
130 */
131 set spacing(value: number) {
132 value = Utils.clampDimension(value);
133 if (this._spacing === value) {
134 return;
135 }
136 this._spacing = value;
137 if (!this.parent) {
138 return;
139 }
140 this.parent.fit();
141 }
142
143 /**
144 * Whether the dock layout is empty.
145 */
146 get isEmpty(): boolean {
147 return this._root === null;
148 }
149
150 /**
151 * Create an iterator over all widgets in the layout.
152 *
153 * @returns A new iterator over the widgets in the layout.
154 *
155 * #### Notes
156 * This iterator includes the generated tab bars.
157 */
158 iter(): IIterator<Widget> {
159 return this._root ? this._root.iterAllWidgets() : empty<Widget>();
160 }
161
162 /**
163 * Create an iterator over the user widgets in the layout.
164 *
165 * @returns A new iterator over the user widgets in the layout.
166 *
167 * #### Notes
168 * This iterator does not include the generated tab bars.
169 */
170 widgets(): IIterator<Widget> {
171 return this._root ? this._root.iterUserWidgets() : empty<Widget>();
172 }
173
174 /**
175 * Create an iterator over the selected widgets in the layout.
176 *
177 * @returns A new iterator over the selected user widgets.
178 *
179 * #### Notes
180 * This iterator yields the widgets corresponding to the current tab
181 * of each tab bar in the layout.
182 */
183 selectedWidgets(): IIterator<Widget> {
184 return this._root ? this._root.iterSelectedWidgets() : empty<Widget>();
185 }
186
187 /**
188 * Create an iterator over the tab bars in the layout.
189 *
190 * @returns A new iterator over the tab bars in the layout.
191 *
192 * #### Notes
193 * This iterator does not include the user widgets.
194 */
195 tabBars(): IIterator<TabBar<Widget>> {
196 return this._root ? this._root.iterTabBars() : empty<TabBar<Widget>>();
197 }
198
199 /**
200 * Create an iterator over the handles in the layout.
201 *
202 * @returns A new iterator over the handles in the layout.
203 */
204 handles(): IIterator<HTMLDivElement> {
205 return this._root ? this._root.iterHandles() : empty<HTMLDivElement>();
206 }
207
208 /**
209 * Move a handle to the given offset position.
210 *
211 * @param handle - The handle to move.
212 *
213 * @param offsetX - The desired offset X position of the handle.
214 *
215 * @param offsetY - The desired offset Y position of the handle.
216 *
217 * #### Notes
218 * If the given handle is not contained in the layout, this is no-op.
219 *
220 * The handle will be moved as close as possible to the desired
221 * position without violating any of the layout constraints.
222 *
223 * Only one of the coordinates is used depending on the orientation
224 * of the handle. This method accepts both coordinates to make it
225 * easy to invoke from a mouse move event without needing to know
226 * the handle orientation.
227 */
228 moveHandle(handle: HTMLDivElement, offsetX: number, offsetY: number): void {
229 // Bail early if there is no root or if the handle is hidden.
230 let hidden = handle.classList.contains('lm-mod-hidden');
231 /* <DEPRECATED> */
232 hidden = hidden || handle.classList.contains('p-mod-hidden');
233 /* </DEPRECATED> */
234 if (!this._root || hidden) {
235 return;
236 }
237
238 // Lookup the split node for the handle.
239 let data = this._root.findSplitNode(handle);
240 if (!data) {
241 return;
242 }
243
244 // Compute the desired delta movement for the handle.
245 let delta: number;
246 if (data.node.orientation === 'horizontal') {
247 delta = offsetX - handle.offsetLeft;
248 } else {
249 delta = offsetY - handle.offsetTop;
250 }
251
252 // Bail if there is no handle movement.
253 if (delta === 0) {
254 return;
255 }
256
257 // Prevent sibling resizing unless needed.
258 data.node.holdSizes();
259
260 // Adjust the sizers to reflect the handle movement.
261 BoxEngine.adjust(data.node.sizers, data.index, delta);
262
263 // Update the layout of the widgets.
264 if (this.parent) {
265 this.parent.update();
266 }
267 }
268
269 /**
270 * Save the current configuration of the dock layout.
271 *
272 * @returns A new config object for the current layout state.
273 *
274 * #### Notes
275 * The return value can be provided to the `restoreLayout` method
276 * in order to restore the layout to its current configuration.
277 */
278 saveLayout(): DockLayout.ILayoutConfig {
279 // Bail early if there is no root.
280 if (!this._root) {
281 return { main: null };
282 }
283
284 // Hold the current sizes in the layout tree.
285 this._root.holdAllSizes();
286
287 // Return the layout config.
288 return { main: this._root.createConfig() };
289 }
290
291 /**
292 * Restore the layout to a previously saved configuration.
293 *
294 * @param config - The layout configuration to restore.
295 *
296 * #### Notes
297 * Widgets which currently belong to the layout but which are not
298 * contained in the config will be unparented.
299 */
300 restoreLayout(config: DockLayout.ILayoutConfig): void {
301 // Create the widget set for validating the config.
302 let widgetSet = new Set<Widget>();
303
304 // Normalize the main area config and collect the widgets.
305 let mainConfig: DockLayout.AreaConfig | null;
306 if (config.main) {
307 mainConfig = Private.normalizeAreaConfig(config.main, widgetSet);
308 } else {
309 mainConfig = null;
310 }
311
312 // Create iterators over the old content.
313 let oldWidgets = this.widgets();
314 let oldTabBars = this.tabBars();
315 let oldHandles = this.handles();
316
317 // Clear the root before removing the old content.
318 this._root = null;
319
320 // Unparent the old widgets which are not in the new config.
321 each(oldWidgets, widget => {
322 if (!widgetSet.has(widget)) {
323 widget.parent = null;
324 }
325 });
326
327 // Dispose of the old tab bars.
328 each(oldTabBars, tabBar => {
329 tabBar.dispose();
330 });
331
332 // Remove the old handles.
333 each(oldHandles, handle => {
334 if (handle.parentNode) {
335 handle.parentNode.removeChild(handle);
336 }
337 });
338
339 // Reparent the new widgets to the current parent.
340 widgetSet.forEach(widget => {
341 widget.parent = this.parent;
342 });
343
344 // Create the root node for the new config.
345 if (mainConfig) {
346 this._root = Private.realizeAreaConfig(
347 mainConfig,
348 {
349 // Ignoring optional `document` argument as we must reuse `this._document`
350 createTabBar: (document?: Document | ShadowRoot) =>
351 this._createTabBar(),
352 createHandle: () => this._createHandle()
353 },
354 this._document
355 );
356 } else {
357 this._root = null;
358 }
359
360 // If there is no parent, there is nothing more to do.
361 if (!this.parent) {
362 return;
363 }
364
365 // Attach the new widgets to the parent.
366 widgetSet.forEach(widget => {
367 this.attachWidget(widget);
368 });
369
370 // Post a fit request to the parent.
371 this.parent.fit();
372 }
373
374 /**
375 * Add a widget to the dock layout.
376 *
377 * @param widget - The widget to add to the dock layout.
378 *
379 * @param options - The additional options for adding the widget.
380 *
381 * #### Notes
382 * The widget will be moved if it is already contained in the layout.
383 *
384 * An error will be thrown if the reference widget is invalid.
385 */
386 addWidget(widget: Widget, options: DockLayout.IAddOptions = {}): void {
387 // Parse the options.
388 let ref = options.ref || null;
389 let mode = options.mode || 'tab-after';
390
391 // Find the tab node which holds the reference widget.
392 let refNode: Private.TabLayoutNode | null = null;
393 if (this._root && ref) {
394 refNode = this._root.findTabNode(ref);
395 }
396
397 // Throw an error if the reference widget is invalid.
398 if (ref && !refNode) {
399 throw new Error('Reference widget is not in the layout.');
400 }
401
402 // Reparent the widget to the current layout parent.
403 widget.parent = this.parent;
404
405 // Insert the widget according to the insert mode.
406 switch (mode) {
407 case 'tab-after':
408 this._insertTab(widget, ref, refNode, true);
409 break;
410 case 'tab-before':
411 this._insertTab(widget, ref, refNode, false);
412 break;
413 case 'split-top':
414 this._insertSplit(widget, ref, refNode, 'vertical', false);
415 break;
416 case 'split-left':
417 this._insertSplit(widget, ref, refNode, 'horizontal', false);
418 break;
419 case 'split-right':
420 this._insertSplit(widget, ref, refNode, 'horizontal', true);
421 break;
422 case 'split-bottom':
423 this._insertSplit(widget, ref, refNode, 'vertical', true);
424 break;
425 }
426
427 // Do nothing else if there is no parent widget.
428 if (!this.parent) {
429 return;
430 }
431
432 // Ensure the widget is attached to the parent widget.
433 this.attachWidget(widget);
434
435 // Post a fit request for the parent widget.
436 this.parent.fit();
437 }
438
439 /**
440 * Remove a widget from the layout.
441 *
442 * @param widget - The widget to remove from the layout.
443 *
444 * #### Notes
445 * A widget is automatically removed from the layout when its `parent`
446 * is set to `null`. This method should only be invoked directly when
447 * removing a widget from a layout which has yet to be installed on a
448 * parent widget.
449 *
450 * This method does *not* modify the widget's `parent`.
451 */
452 removeWidget(widget: Widget): void {
453 // Remove the widget from its current layout location.
454 this._removeWidget(widget);
455
456 // Do nothing else if there is no parent widget.
457 if (!this.parent) {
458 return;
459 }
460
461 // Detach the widget from the parent widget.
462 this.detachWidget(widget);
463
464 // Post a fit request for the parent widget.
465 this.parent.fit();
466 }
467
468 /**
469 * Find the tab area which contains the given client position.
470 *
471 * @param clientX - The client X position of interest.
472 *
473 * @param clientY - The client Y position of interest.
474 *
475 * @returns The geometry of the tab area at the given position, or
476 * `null` if there is no tab area at the given position.
477 */
478 hitTestTabAreas(
479 clientX: number,
480 clientY: number
481 ): DockLayout.ITabAreaGeometry | null {
482 // Bail early if hit testing cannot produce valid results.
483 if (!this._root || !this.parent || !this.parent.isVisible) {
484 return null;
485 }
486
487 // Ensure the parent box sizing data is computed.
488 if (!this._box) {
489 this._box = ElementExt.boxSizing(this.parent.node);
490 }
491
492 // Convert from client to local coordinates.
493 let rect = this.parent.node.getBoundingClientRect();
494 let x = clientX - rect.left - this._box.borderLeft;
495 let y = clientY - rect.top - this._box.borderTop;
496
497 // Find the tab layout node at the local position.
498 let tabNode = this._root.hitTestTabNodes(x, y);
499
500 // Bail if a tab layout node was not found.
501 if (!tabNode) {
502 return null;
503 }
504
505 // Extract the data from the tab node.
506 let { tabBar, top, left, width, height } = tabNode;
507
508 // Compute the right and bottom edges of the tab area.
509 let borderWidth = this._box.borderLeft + this._box.borderRight;
510 let borderHeight = this._box.borderTop + this._box.borderBottom;
511 let right = rect.width - borderWidth - (left + width);
512 let bottom = rect.height - borderHeight - (top + height);
513
514 // Return the hit test results.
515 return { tabBar, x, y, top, left, right, bottom, width, height };
516 }
517
518 /**
519 * Perform layout initialization which requires the parent widget.
520 */
521 protected init(): void {
522 // Perform superclass initialization.
523 super.init();
524
525 // Attach each widget to the parent.
526 each(this, widget => {
527 this.attachWidget(widget);
528 });
529
530 // Attach each handle to the parent.
531 each(this.handles(), handle => {
532 this.parent!.node.appendChild(handle);
533 });
534
535 // Post a fit request for the parent widget.
536 this.parent!.fit();
537 }
538
539 /**
540 * Attach the widget to the layout parent widget.
541 *
542 * @param widget - The widget to attach to the parent.
543 *
544 * #### Notes
545 * This is a no-op if the widget is already attached.
546 */
547 protected attachWidget(widget: Widget): void {
548 // Do nothing if the widget is already attached.
549 if (this.parent!.node === widget.node.parentNode) {
550 return;
551 }
552
553 // Create the layout item for the widget.
554 this._items.set(widget, new LayoutItem(widget));
555
556 // Send a `'before-attach'` message if the parent is attached.
557 if (this.parent!.isAttached) {
558 MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
559 }
560
561 // Add the widget's node to the parent.
562 this.parent!.node.appendChild(widget.node);
563
564 // Send an `'after-attach'` message if the parent is attached.
565 if (this.parent!.isAttached) {
566 MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
567 }
568 }
569
570 /**
571 * Detach the widget from the layout parent widget.
572 *
573 * @param widget - The widget to detach from the parent.
574 *
575 * #### Notes
576 * This is a no-op if the widget is not attached.
577 */
578 protected detachWidget(widget: Widget): void {
579 // Do nothing if the widget is not attached.
580 if (this.parent!.node !== widget.node.parentNode) {
581 return;
582 }
583
584 // Send a `'before-detach'` message if the parent is attached.
585 if (this.parent!.isAttached) {
586 MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
587 }
588
589 // Remove the widget's node from the parent.
590 this.parent!.node.removeChild(widget.node);
591
592 // Send an `'after-detach'` message if the parent is attached.
593 if (this.parent!.isAttached) {
594 MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
595 }
596
597 // Delete the layout item for the widget.
598 let item = this._items.get(widget);
599 if (item) {
600 this._items.delete(widget);
601 item.dispose();
602 }
603 }
604
605 /**
606 * A message handler invoked on a `'before-show'` message.
607 */
608 protected onBeforeShow(msg: Message): void {
609 super.onBeforeShow(msg);
610 this.parent!.update();
611 }
612
613 /**
614 * A message handler invoked on a `'before-attach'` message.
615 */
616 protected onBeforeAttach(msg: Message): void {
617 super.onBeforeAttach(msg);
618 this.parent!.fit();
619 }
620
621 /**
622 * A message handler invoked on a `'child-shown'` message.
623 */
624 protected onChildShown(msg: Widget.ChildMessage): void {
625 this.parent!.fit();
626 }
627
628 /**
629 * A message handler invoked on a `'child-hidden'` message.
630 */
631 protected onChildHidden(msg: Widget.ChildMessage): void {
632 this.parent!.fit();
633 }
634
635 /**
636 * A message handler invoked on a `'resize'` message.
637 */
638 protected onResize(msg: Widget.ResizeMessage): void {
639 if (this.parent!.isVisible) {
640 this._update(msg.width, msg.height);
641 }
642 }
643
644 /**
645 * A message handler invoked on an `'update-request'` message.
646 */
647 protected onUpdateRequest(msg: Message): void {
648 if (this.parent!.isVisible) {
649 this._update(-1, -1);
650 }
651 }
652
653 /**
654 * A message handler invoked on a `'fit-request'` message.
655 */
656 protected onFitRequest(msg: Message): void {
657 if (this.parent!.isAttached) {
658 this._fit();
659 }
660 }
661
662 /**
663 * Remove the specified widget from the layout structure.
664 *
665 * #### Notes
666 * This is a no-op if the widget is not in the layout tree.
667 *
668 * This does not detach the widget from the parent node.
669 */
670 private _removeWidget(widget: Widget): void {
671 // Bail early if there is no layout root.
672 if (!this._root) {
673 return;
674 }
675
676 // Find the tab node which contains the given widget.
677 let tabNode = this._root.findTabNode(widget);
678
679 // Bail early if the tab node is not found.
680 if (!tabNode) {
681 return;
682 }
683
684 Private.removeAria(widget);
685
686 // If there are multiple tabs, just remove the widget's tab.
687 if (tabNode.tabBar.titles.length > 1) {
688 tabNode.tabBar.removeTab(widget.title);
689 if (
690 this._hiddenMode === Widget.HiddenMode.Scale &&
691 tabNode.tabBar.titles.length == 1
692 ) {
693 const existingWidget = tabNode.tabBar.titles[0].owner;
694 existingWidget.hiddenMode = Widget.HiddenMode.Display;
695 }
696 return;
697 }
698
699 // Otherwise, the tab node needs to be removed...
700
701 // Dispose the tab bar.
702 tabNode.tabBar.dispose();
703
704 // Handle the case where the tab node is the root.
705 if (this._root === tabNode) {
706 this._root = null;
707 return;
708 }
709
710 // Otherwise, remove the tab node from its parent...
711
712 // Prevent widget resizing unless needed.
713 this._root.holdAllSizes();
714
715 // Clear the parent reference on the tab node.
716 let splitNode = tabNode.parent!;
717 tabNode.parent = null;
718
719 // Remove the tab node from its parent split node.
720 let i = ArrayExt.removeFirstOf(splitNode.children, tabNode);
721 let handle = ArrayExt.removeAt(splitNode.handles, i)!;
722 ArrayExt.removeAt(splitNode.sizers, i);
723
724 // Remove the handle from its parent DOM node.
725 if (handle.parentNode) {
726 handle.parentNode.removeChild(handle);
727 }
728
729 // If there are multiple children, just update the handles.
730 if (splitNode.children.length > 1) {
731 splitNode.syncHandles();
732 return;
733 }
734
735 // Otherwise, the split node also needs to be removed...
736
737 // Clear the parent reference on the split node.
738 let maybeParent = splitNode.parent;
739 splitNode.parent = null;
740
741 // Lookup the remaining child node and handle.
742 let childNode = splitNode.children[0];
743 let childHandle = splitNode.handles[0];
744
745 // Clear the split node data.
746 splitNode.children.length = 0;
747 splitNode.handles.length = 0;
748 splitNode.sizers.length = 0;
749
750 // Remove the child handle from its parent node.
751 if (childHandle.parentNode) {
752 childHandle.parentNode.removeChild(childHandle);
753 }
754
755 // Handle the case where the split node is the root.
756 if (this._root === splitNode) {
757 childNode.parent = null;
758 this._root = childNode;
759 return;
760 }
761
762 // Otherwise, move the child node to the parent node...
763 let parentNode = maybeParent!;
764
765 // Lookup the index of the split node.
766 let j = parentNode.children.indexOf(splitNode);
767
768 // Handle the case where the child node is a tab node.
769 if (childNode instanceof Private.TabLayoutNode) {
770 childNode.parent = parentNode;
771 parentNode.children[j] = childNode;
772 return;
773 }
774
775 // Remove the split data from the parent.
776 let splitHandle = ArrayExt.removeAt(parentNode.handles, j)!;
777 ArrayExt.removeAt(parentNode.children, j);
778 ArrayExt.removeAt(parentNode.sizers, j);
779
780 // Remove the handle from its parent node.
781 if (splitHandle.parentNode) {
782 splitHandle.parentNode.removeChild(splitHandle);
783 }
784
785 // The child node and the split parent node will have the same
786 // orientation. Merge the grand-children with the parent node.
787 for (let i = 0, n = childNode.children.length; i < n; ++i) {
788 let gChild = childNode.children[i];
789 let gHandle = childNode.handles[i];
790 let gSizer = childNode.sizers[i];
791 ArrayExt.insert(parentNode.children, j + i, gChild);
792 ArrayExt.insert(parentNode.handles, j + i, gHandle);
793 ArrayExt.insert(parentNode.sizers, j + i, gSizer);
794 gChild.parent = parentNode;
795 }
796
797 // Clear the child node.
798 childNode.children.length = 0;
799 childNode.handles.length = 0;
800 childNode.sizers.length = 0;
801 childNode.parent = null;
802
803 // Sync the handles on the parent node.
804 parentNode.syncHandles();
805 }
806
807 /**
808 * Insert a widget next to an existing tab.
809 *
810 * #### Notes
811 * This does not attach the widget to the parent widget.
812 */
813 private _insertTab(
814 widget: Widget,
815 ref: Widget | null,
816 refNode: Private.TabLayoutNode | null,
817 after: boolean
818 ): void {
819 // Do nothing if the tab is inserted next to itself.
820 if (widget === ref) {
821 return;
822 }
823
824 // Create the root if it does not exist.
825 if (!this._root) {
826 let tabNode = new Private.TabLayoutNode(this._createTabBar());
827 tabNode.tabBar.addTab(widget.title);
828 this._root = tabNode;
829 Private.addAria(widget, tabNode.tabBar);
830 return;
831 }
832
833 // Use the first tab node as the ref node if needed.
834 if (!refNode) {
835 refNode = this._root.findFirstTabNode()!;
836 }
837
838 // If the widget is not contained in the ref node, ensure it is
839 // removed from the layout and hidden before being added again.
840 if (refNode.tabBar.titles.indexOf(widget.title) === -1) {
841 this._removeWidget(widget);
842 widget.hide();
843 }
844
845 // Lookup the target index for inserting the tab.
846 let index: number;
847 if (ref) {
848 index = refNode.tabBar.titles.indexOf(ref.title);
849 } else {
850 index = refNode.tabBar.currentIndex;
851 }
852
853 // Using transform create an additional layer in the pixel pipeline
854 // to limit the number of layer, it is set only if there is more than one widget.
855 if (
856 this._hiddenMode === Widget.HiddenMode.Scale &&
857 refNode.tabBar.titles.length > 0
858 ) {
859 if (refNode.tabBar.titles.length == 1) {
860 const existingWidget = refNode.tabBar.titles[0].owner;
861 existingWidget.hiddenMode = Widget.HiddenMode.Scale;
862 }
863
864 widget.hiddenMode = Widget.HiddenMode.Scale;
865 } else {
866 widget.hiddenMode = Widget.HiddenMode.Display;
867 }
868
869 // Insert the widget's tab relative to the target index.
870 refNode.tabBar.insertTab(index + (after ? 1 : 0), widget.title);
871 Private.addAria(widget, refNode.tabBar);
872 }
873
874 /**
875 * Insert a widget as a new split area.
876 *
877 * #### Notes
878 * This does not attach the widget to the parent widget.
879 */
880 private _insertSplit(
881 widget: Widget,
882 ref: Widget | null,
883 refNode: Private.TabLayoutNode | null,
884 orientation: Private.Orientation,
885 after: boolean
886 ): void {
887 // Do nothing if there is no effective split.
888 if (widget === ref && refNode && refNode.tabBar.titles.length === 1) {
889 return;
890 }
891
892 // Ensure the widget is removed from the current layout.
893 this._removeWidget(widget);
894
895 // Create the tab layout node to hold the widget.
896 let tabNode = new Private.TabLayoutNode(this._createTabBar());
897 tabNode.tabBar.addTab(widget.title);
898 Private.addAria(widget, tabNode.tabBar);
899
900 // Set the root if it does not exist.
901 if (!this._root) {
902 this._root = tabNode;
903 return;
904 }
905
906 // If the ref node parent is null, split the root.
907 if (!refNode || !refNode.parent) {
908 // Ensure the root is split with the correct orientation.
909 let root = this._splitRoot(orientation);
910
911 // Determine the insert index for the new tab node.
912 let i = after ? root.children.length : 0;
913
914 // Normalize the split node.
915 root.normalizeSizes();
916
917 // Create the sizer for new tab node.
918 let sizer = Private.createSizer(refNode ? 1 : Private.GOLDEN_RATIO);
919
920 // Insert the tab node sized to the golden ratio.
921 ArrayExt.insert(root.children, i, tabNode);
922 ArrayExt.insert(root.sizers, i, sizer);
923 ArrayExt.insert(root.handles, i, this._createHandle());
924 tabNode.parent = root;
925
926 // Re-normalize the split node to maintain the ratios.
927 root.normalizeSizes();
928
929 // Finally, synchronize the visibility of the handles.
930 root.syncHandles();
931 return;
932 }
933
934 // Lookup the split node for the ref widget.
935 let splitNode = refNode.parent;
936
937 // If the split node already had the correct orientation,
938 // the widget can be inserted into the split node directly.
939 if (splitNode.orientation === orientation) {
940 // Find the index of the ref node.
941 let i = splitNode.children.indexOf(refNode);
942
943 // Normalize the split node.
944 splitNode.normalizeSizes();
945
946 // Consume half the space for the insert location.
947 let s = (splitNode.sizers[i].sizeHint /= 2);
948
949 // Insert the tab node sized to the other half.
950 let j = i + (after ? 1 : 0);
951 ArrayExt.insert(splitNode.children, j, tabNode);
952 ArrayExt.insert(splitNode.sizers, j, Private.createSizer(s));
953 ArrayExt.insert(splitNode.handles, j, this._createHandle());
954 tabNode.parent = splitNode;
955
956 // Finally, synchronize the visibility of the handles.
957 splitNode.syncHandles();
958 return;
959 }
960
961 // Remove the ref node from the split node.
962 let i = ArrayExt.removeFirstOf(splitNode.children, refNode);
963
964 // Create a new normalized split node for the children.
965 let childNode = new Private.SplitLayoutNode(orientation);
966 childNode.normalized = true;
967
968 // Add the ref node sized to half the space.
969 childNode.children.push(refNode);
970 childNode.sizers.push(Private.createSizer(0.5));
971 childNode.handles.push(this._createHandle());
972 refNode.parent = childNode;
973
974 // Add the tab node sized to the other half.
975 let j = after ? 1 : 0;
976 ArrayExt.insert(childNode.children, j, tabNode);
977 ArrayExt.insert(childNode.sizers, j, Private.createSizer(0.5));
978 ArrayExt.insert(childNode.handles, j, this._createHandle());
979 tabNode.parent = childNode;
980
981 // Synchronize the visibility of the handles.
982 childNode.syncHandles();
983
984 // Finally, add the new child node to the original split node.
985 ArrayExt.insert(splitNode.children, i, childNode);
986 childNode.parent = splitNode;
987 }
988
989 /**
990 * Ensure the root is a split node with the given orientation.
991 */
992 private _splitRoot(
993 orientation: Private.Orientation
994 ): Private.SplitLayoutNode {
995 // Bail early if the root already meets the requirements.
996 let oldRoot = this._root;
997 if (oldRoot instanceof Private.SplitLayoutNode) {
998 if (oldRoot.orientation === orientation) {
999 return oldRoot;
1000 }
1001 }
1002
1003 // Create a new root node with the specified orientation.
1004 let newRoot = (this._root = new Private.SplitLayoutNode(orientation));
1005
1006 // Add the old root to the new root.
1007 if (oldRoot) {
1008 newRoot.children.push(oldRoot);
1009 newRoot.sizers.push(Private.createSizer(0));
1010 newRoot.handles.push(this._createHandle());
1011 oldRoot.parent = newRoot;
1012 }
1013
1014 // Return the new root as a convenience.
1015 return newRoot;
1016 }
1017
1018 /**
1019 * Fit the layout to the total size required by the widgets.
1020 */
1021 private _fit(): void {
1022 // Set up the computed minimum size.
1023 let minW = 0;
1024 let minH = 0;
1025
1026 // Update the size limits for the layout tree.
1027 if (this._root) {
1028 let limits = this._root.fit(this._spacing, this._items);
1029 minW = limits.minWidth;
1030 minH = limits.minHeight;
1031 }
1032
1033 // Update the box sizing and add it to the computed min size.
1034 let box = (this._box = ElementExt.boxSizing(this.parent!.node));
1035 minW += box.horizontalSum;
1036 minH += box.verticalSum;
1037
1038 // Update the parent's min size constraints.
1039 let style = this.parent!.node.style;
1040 style.minWidth = `${minW}px`;
1041 style.minHeight = `${minH}px`;
1042
1043 // Set the dirty flag to ensure only a single update occurs.
1044 this._dirty = true;
1045
1046 // Notify the ancestor that it should fit immediately. This may
1047 // cause a resize of the parent, fulfilling the required update.
1048 if (this.parent!.parent) {
1049 MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
1050 }
1051
1052 // If the dirty flag is still set, the parent was not resized.
1053 // Trigger the required update on the parent widget immediately.
1054 if (this._dirty) {
1055 MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
1056 }
1057 }
1058
1059 /**
1060 * Update the layout position and size of the widgets.
1061 *
1062 * The parent offset dimensions should be `-1` if unknown.
1063 */
1064 private _update(offsetWidth: number, offsetHeight: number): void {
1065 // Clear the dirty flag to indicate the update occurred.
1066 this._dirty = false;
1067
1068 // Bail early if there is no root layout node.
1069 if (!this._root) {
1070 return;
1071 }
1072
1073 // Measure the parent if the offset dimensions are unknown.
1074 if (offsetWidth < 0) {
1075 offsetWidth = this.parent!.node.offsetWidth;
1076 }
1077 if (offsetHeight < 0) {
1078 offsetHeight = this.parent!.node.offsetHeight;
1079 }
1080
1081 // Ensure the parent box sizing data is computed.
1082 if (!this._box) {
1083 this._box = ElementExt.boxSizing(this.parent!.node);
1084 }
1085
1086 // Compute the actual layout bounds adjusted for border and padding.
1087 let x = this._box.paddingTop;
1088 let y = this._box.paddingLeft;
1089 let width = offsetWidth - this._box.horizontalSum;
1090 let height = offsetHeight - this._box.verticalSum;
1091
1092 // Update the geometry of the layout tree.
1093 this._root.update(x, y, width, height, this._spacing, this._items);
1094 }
1095
1096 /**
1097 * Create a new tab bar for use by the dock layout.
1098 *
1099 * #### Notes
1100 * The tab bar will be attached to the parent if it exists.
1101 */
1102 private _createTabBar(): TabBar<Widget> {
1103 // Create the tab bar using the renderer.
1104 let tabBar = this.renderer.createTabBar(this._document);
1105
1106 // Enforce necessary tab bar behavior.
1107 tabBar.orientation = 'horizontal';
1108
1109 // Reparent and attach the tab bar to the parent if possible.
1110 if (this.parent) {
1111 tabBar.parent = this.parent;
1112 this.attachWidget(tabBar);
1113 }
1114
1115 // Return the initialized tab bar.
1116 return tabBar;
1117 }
1118
1119 /**
1120 * Create a new handle for the dock layout.
1121 *
1122 * #### Notes
1123 * The handle will be attached to the parent if it exists.
1124 */
1125 private _createHandle(): HTMLDivElement {
1126 // Create the handle using the renderer.
1127 let handle = this.renderer.createHandle();
1128
1129 // Initialize the handle layout behavior.
1130 let style = handle.style;
1131 style.position = 'absolute';
1132 style.top = '0';
1133 style.left = '0';
1134 style.width = '0';
1135 style.height = '0';
1136
1137 // Attach the handle to the parent if it exists.
1138 if (this.parent) {
1139 this.parent.node.appendChild(handle);
1140 }
1141
1142 // Return the initialized handle.
1143 return handle;
1144 }
1145
1146 private _spacing = 4;
1147 private _dirty = false;
1148 private _root: Private.LayoutNode | null = null;
1149 private _box: ElementExt.IBoxSizing | null = null;
1150 private _document: Document | ShadowRoot;
1151 private _hiddenMode: Widget.HiddenMode;
1152 private _items: Private.ItemMap = new Map<Widget, LayoutItem>();
1153}
1154
1155/**
1156 * The namespace for the `DockLayout` class statics.
1157 */
1158export namespace DockLayout {
1159 /**
1160 * An options object for creating a dock layout.
1161 */
1162 export interface IOptions {
1163 /**
1164 * The document to use with the dock panel.
1165 *
1166 * The default is the global `document` instance.
1167 */
1168 document?: Document | ShadowRoot;
1169
1170 /**
1171 * The method for hiding widgets.
1172 *
1173 * The default is `Widget.HiddenMode.Display`.
1174 */
1175 hiddenMode?: Widget.HiddenMode;
1176
1177 /**
1178 * The renderer to use for the dock layout.
1179 */
1180 renderer: IRenderer;
1181
1182 /**
1183 * The spacing between items in the layout.
1184 *
1185 * The default is `4`.
1186 */
1187 spacing?: number;
1188 }
1189
1190 /**
1191 * A renderer for use with a dock layout.
1192 */
1193 export interface IRenderer {
1194 /**
1195 * Create a new tab bar for use with a dock layout.
1196 *
1197 * @returns A new tab bar for a dock layout.
1198 */
1199 createTabBar(document?: Document | ShadowRoot): TabBar<Widget>;
1200
1201 /**
1202 * Create a new handle node for use with a dock layout.
1203 *
1204 * @returns A new handle node for a dock layout.
1205 */
1206 createHandle(): HTMLDivElement;
1207 }
1208
1209 /**
1210 * A type alias for the supported insertion modes.
1211 *
1212 * An insert mode is used to specify how a widget should be added
1213 * to the dock layout relative to a reference widget.
1214 */
1215 export type InsertMode =
1216 | /**
1217 * The area to the top of the reference widget.
1218 *
1219 * The widget will be inserted just above the reference widget.
1220 *
1221 * If the reference widget is null or invalid, the widget will be
1222 * inserted at the top edge of the dock layout.
1223 */
1224 'split-top'
1225
1226 /**
1227 * The area to the left of the reference widget.
1228 *
1229 * The widget will be inserted just left of the reference widget.
1230 *
1231 * If the reference widget is null or invalid, the widget will be
1232 * inserted at the left edge of the dock layout.
1233 */
1234 | 'split-left'
1235
1236 /**
1237 * The area to the right of the reference widget.
1238 *
1239 * The widget will be inserted just right of the reference widget.
1240 *
1241 * If the reference widget is null or invalid, the widget will be
1242 * inserted at the right edge of the dock layout.
1243 */
1244 | 'split-right'
1245
1246 /**
1247 * The area to the bottom of the reference widget.
1248 *
1249 * The widget will be inserted just below the reference widget.
1250 *
1251 * If the reference widget is null or invalid, the widget will be
1252 * inserted at the bottom edge of the dock layout.
1253 */
1254 | 'split-bottom'
1255
1256 /**
1257 * The tab position before the reference widget.
1258 *
1259 * The widget will be added as a tab before the reference widget.
1260 *
1261 * If the reference widget is null or invalid, a sensible default
1262 * will be used.
1263 */
1264 | 'tab-before'
1265
1266 /**
1267 * The tab position after the reference widget.
1268 *
1269 * The widget will be added as a tab after the reference widget.
1270 *
1271 * If the reference widget is null or invalid, a sensible default
1272 * will be used.
1273 */
1274 | 'tab-after';
1275
1276 /**
1277 * An options object for adding a widget to the dock layout.
1278 */
1279 export interface IAddOptions {
1280 /**
1281 * The insertion mode for adding the widget.
1282 *
1283 * The default is `'tab-after'`.
1284 */
1285 mode?: InsertMode;
1286
1287 /**
1288 * The reference widget for the insert location.
1289 *
1290 * The default is `null`.
1291 */
1292 ref?: Widget | null;
1293 }
1294
1295 /**
1296 * A layout config object for a tab area.
1297 */
1298 export interface ITabAreaConfig {
1299 /**
1300 * The discriminated type of the config object.
1301 */
1302 type: 'tab-area';
1303
1304 /**
1305 * The widgets contained in the tab area.
1306 */
1307 widgets: Widget[];
1308
1309 /**
1310 * The index of the selected tab.
1311 */
1312 currentIndex: number;
1313 }
1314
1315 /**
1316 * A layout config object for a split area.
1317 */
1318 export interface ISplitAreaConfig {
1319 /**
1320 * The discriminated type of the config object.
1321 */
1322 type: 'split-area';
1323
1324 /**
1325 * The orientation of the split area.
1326 */
1327 orientation: 'horizontal' | 'vertical';
1328
1329 /**
1330 * The children in the split area.
1331 */
1332 children: AreaConfig[];
1333
1334 /**
1335 * The relative sizes of the children.
1336 */
1337 sizes: number[];
1338 }
1339
1340 /**
1341 * A type alias for a general area config.
1342 */
1343 export type AreaConfig = ITabAreaConfig | ISplitAreaConfig;
1344
1345 /**
1346 * A dock layout configuration object.
1347 */
1348 export interface ILayoutConfig {
1349 /**
1350 * The layout config for the main dock area.
1351 */
1352 main: AreaConfig | null;
1353 }
1354
1355 /**
1356 * An object which represents the geometry of a tab area.
1357 */
1358 export interface ITabAreaGeometry {
1359 /**
1360 * The tab bar for the tab area.
1361 */
1362 tabBar: TabBar<Widget>;
1363
1364 /**
1365 * The local X position of the hit test in the dock area.
1366 *
1367 * #### Notes
1368 * This is the distance from the left edge of the layout parent
1369 * widget, to the local X coordinate of the hit test query.
1370 */
1371 x: number;
1372
1373 /**
1374 * The local Y position of the hit test in the dock area.
1375 *
1376 * #### Notes
1377 * This is the distance from the top edge of the layout parent
1378 * widget, to the local Y coordinate of the hit test query.
1379 */
1380 y: number;
1381
1382 /**
1383 * The local coordinate of the top edge of the tab area.
1384 *
1385 * #### Notes
1386 * This is the distance from the top edge of the layout parent
1387 * widget, to the top edge of the tab area.
1388 */
1389 top: number;
1390
1391 /**
1392 * The local coordinate of the left edge of the tab area.
1393 *
1394 * #### Notes
1395 * This is the distance from the left edge of the layout parent
1396 * widget, to the left edge of the tab area.
1397 */
1398 left: number;
1399
1400 /**
1401 * The local coordinate of the right edge of the tab area.
1402 *
1403 * #### Notes
1404 * This is the distance from the right edge of the layout parent
1405 * widget, to the right edge of the tab area.
1406 */
1407 right: number;
1408
1409 /**
1410 * The local coordinate of the bottom edge of the tab area.
1411 *
1412 * #### Notes
1413 * This is the distance from the bottom edge of the layout parent
1414 * widget, to the bottom edge of the tab area.
1415 */
1416 bottom: number;
1417
1418 /**
1419 * The width of the tab area.
1420 *
1421 * #### Notes
1422 * This is total width allocated for the tab area.
1423 */
1424 width: number;
1425
1426 /**
1427 * The height of the tab area.
1428 *
1429 * #### Notes
1430 * This is total height allocated for the tab area.
1431 */
1432 height: number;
1433 }
1434}
1435
1436/**
1437 * The namespace for the module implementation details.
1438 */
1439namespace Private {
1440 /**
1441 * A fraction used for sizing root panels; ~= `1 / golden_ratio`.
1442 */
1443 export const GOLDEN_RATIO = 0.618;
1444
1445 /**
1446 * A type alias for a dock layout node.
1447 */
1448 export type LayoutNode = TabLayoutNode | SplitLayoutNode;
1449
1450 /**
1451 * A type alias for the orientation of a split layout node.
1452 */
1453 export type Orientation = 'horizontal' | 'vertical';
1454
1455 /**
1456 * A type alias for a layout item map.
1457 */
1458 export type ItemMap = Map<Widget, LayoutItem>;
1459
1460 /**
1461 * Create a box sizer with an initial size hint.
1462 */
1463 export function createSizer(hint: number): BoxSizer {
1464 let sizer = new BoxSizer();
1465 sizer.sizeHint = hint;
1466 sizer.size = hint;
1467 return sizer;
1468 }
1469
1470 /**
1471 * Normalize an area config object and collect the visited widgets.
1472 */
1473 export function normalizeAreaConfig(
1474 config: DockLayout.AreaConfig,
1475 widgetSet: Set<Widget>
1476 ): DockLayout.AreaConfig | null {
1477 let result: DockLayout.AreaConfig | null;
1478 if (config.type === 'tab-area') {
1479 result = normalizeTabAreaConfig(config, widgetSet);
1480 } else {
1481 result = normalizeSplitAreaConfig(config, widgetSet);
1482 }
1483 return result;
1484 }
1485
1486 /**
1487 * Convert a normalized area config into a layout tree.
1488 */
1489 export function realizeAreaConfig(
1490 config: DockLayout.AreaConfig,
1491 renderer: DockLayout.IRenderer,
1492 document: Document | ShadowRoot
1493 ): LayoutNode {
1494 let node: LayoutNode;
1495 if (config.type === 'tab-area') {
1496 node = realizeTabAreaConfig(config, renderer, document);
1497 } else {
1498 node = realizeSplitAreaConfig(config, renderer, document);
1499 }
1500 return node;
1501 }
1502
1503 /**
1504 * A layout node which holds the data for a tabbed area.
1505 */
1506 export class TabLayoutNode {
1507 /**
1508 * Construct a new tab layout node.
1509 *
1510 * @param tabBar - The tab bar to use for the layout node.
1511 */
1512 constructor(tabBar: TabBar<Widget>) {
1513 let tabSizer = new BoxSizer();
1514 let widgetSizer = new BoxSizer();
1515 tabSizer.stretch = 0;
1516 widgetSizer.stretch = 1;
1517 this.tabBar = tabBar;
1518 this.sizers = [tabSizer, widgetSizer];
1519 }
1520
1521 /**
1522 * The parent of the layout node.
1523 */
1524 parent: SplitLayoutNode | null = null;
1525
1526 /**
1527 * The tab bar for the layout node.
1528 */
1529 readonly tabBar: TabBar<Widget>;
1530
1531 /**
1532 * The sizers for the layout node.
1533 */
1534 readonly sizers: [BoxSizer, BoxSizer];
1535
1536 /**
1537 * The most recent value for the `top` edge of the layout box.
1538 */
1539 get top(): number {
1540 return this._top;
1541 }
1542
1543 /**
1544 * The most recent value for the `left` edge of the layout box.
1545 */
1546 get left(): number {
1547 return this._left;
1548 }
1549
1550 /**
1551 * The most recent value for the `width` of the layout box.
1552 */
1553 get width(): number {
1554 return this._width;
1555 }
1556
1557 /**
1558 * The most recent value for the `height` of the layout box.
1559 */
1560 get height(): number {
1561 return this._height;
1562 }
1563
1564 /**
1565 * Create an iterator for all widgets in the layout tree.
1566 */
1567 iterAllWidgets(): IIterator<Widget> {
1568 return chain(once(this.tabBar), this.iterUserWidgets());
1569 }
1570
1571 /**
1572 * Create an iterator for the user widgets in the layout tree.
1573 */
1574 iterUserWidgets(): IIterator<Widget> {
1575 return map(this.tabBar.titles, title => title.owner);
1576 }
1577
1578 /**
1579 * Create an iterator for the selected widgets in the layout tree.
1580 */
1581 iterSelectedWidgets(): IIterator<Widget> {
1582 let title = this.tabBar.currentTitle;
1583 return title ? once(title.owner) : empty<Widget>();
1584 }
1585
1586 /**
1587 * Create an iterator for the tab bars in the layout tree.
1588 */
1589 iterTabBars(): IIterator<TabBar<Widget>> {
1590 return once(this.tabBar);
1591 }
1592
1593 /**
1594 * Create an iterator for the handles in the layout tree.
1595 */
1596 iterHandles(): IIterator<HTMLDivElement> {
1597 return empty<HTMLDivElement>();
1598 }
1599
1600 /**
1601 * Find the tab layout node which contains the given widget.
1602 */
1603 findTabNode(widget: Widget): TabLayoutNode | null {
1604 return this.tabBar.titles.indexOf(widget.title) !== -1 ? this : null;
1605 }
1606
1607 /**
1608 * Find the split layout node which contains the given handle.
1609 */
1610 findSplitNode(
1611 handle: HTMLDivElement
1612 ): { index: number; node: SplitLayoutNode } | null {
1613 return null;
1614 }
1615
1616 /**
1617 * Find the first tab layout node in a layout tree.
1618 */
1619 findFirstTabNode(): TabLayoutNode | null {
1620 return this;
1621 }
1622
1623 /**
1624 * Find the tab layout node which contains the local point.
1625 */
1626 hitTestTabNodes(x: number, y: number): TabLayoutNode | null {
1627 if (x < this._left || x >= this._left + this._width) {
1628 return null;
1629 }
1630 if (y < this._top || y >= this._top + this._height) {
1631 return null;
1632 }
1633 return this;
1634 }
1635
1636 /**
1637 * Create a configuration object for the layout tree.
1638 */
1639 createConfig(): DockLayout.ITabAreaConfig {
1640 let widgets = this.tabBar.titles.map(title => title.owner);
1641 let currentIndex = this.tabBar.currentIndex;
1642 return { type: 'tab-area', widgets, currentIndex };
1643 }
1644
1645 /**
1646 * Recursively hold all of the sizes in the layout tree.
1647 *
1648 * This ignores the sizers of tab layout nodes.
1649 */
1650 holdAllSizes(): void {
1651 return;
1652 }
1653
1654 /**
1655 * Fit the layout tree.
1656 */
1657 fit(spacing: number, items: ItemMap): ElementExt.ISizeLimits {
1658 // Set up the limit variables.
1659 let minWidth = 0;
1660 let minHeight = 0;
1661 let maxWidth = Infinity;
1662 let maxHeight = Infinity;
1663
1664 // Lookup the tab bar layout item.
1665 let tabBarItem = items.get(this.tabBar);
1666
1667 // Lookup the widget layout item.
1668 let current = this.tabBar.currentTitle;
1669 let widgetItem = current ? items.get(current.owner) : undefined;
1670
1671 // Lookup the tab bar and widget sizers.
1672 let [tabBarSizer, widgetSizer] = this.sizers;
1673
1674 // Update the tab bar limits.
1675 if (tabBarItem) {
1676 tabBarItem.fit();
1677 }
1678
1679 // Update the widget limits.
1680 if (widgetItem) {
1681 widgetItem.fit();
1682 }
1683
1684 // Update the results and sizer for the tab bar.
1685 if (tabBarItem && !tabBarItem.isHidden) {
1686 minWidth = Math.max(minWidth, tabBarItem.minWidth);
1687 minHeight += tabBarItem.minHeight;
1688 tabBarSizer.minSize = tabBarItem.minHeight;
1689 tabBarSizer.maxSize = tabBarItem.maxHeight;
1690 } else {
1691 tabBarSizer.minSize = 0;
1692 tabBarSizer.maxSize = 0;
1693 }
1694
1695 // Update the results and sizer for the current widget.
1696 if (widgetItem && !widgetItem.isHidden) {
1697 minWidth = Math.max(minWidth, widgetItem.minWidth);
1698 minHeight += widgetItem.minHeight;
1699 widgetSizer.minSize = widgetItem.minHeight;
1700 widgetSizer.maxSize = Infinity;
1701 } else {
1702 widgetSizer.minSize = 0;
1703 widgetSizer.maxSize = Infinity;
1704 }
1705
1706 // Return the computed size limits for the layout node.
1707 return { minWidth, minHeight, maxWidth, maxHeight };
1708 }
1709
1710 /**
1711 * Update the layout tree.
1712 */
1713 update(
1714 left: number,
1715 top: number,
1716 width: number,
1717 height: number,
1718 spacing: number,
1719 items: ItemMap
1720 ): void {
1721 // Update the layout box values.
1722 this._top = top;
1723 this._left = left;
1724 this._width = width;
1725 this._height = height;
1726
1727 // Lookup the tab bar layout item.
1728 let tabBarItem = items.get(this.tabBar);
1729
1730 // Lookup the widget layout item.
1731 let current = this.tabBar.currentTitle;
1732 let widgetItem = current ? items.get(current.owner) : undefined;
1733
1734 // Distribute the layout space to the sizers.
1735 BoxEngine.calc(this.sizers, height);
1736
1737 // Update the tab bar item using the computed size.
1738 if (tabBarItem && !tabBarItem.isHidden) {
1739 let size = this.sizers[0].size;
1740 tabBarItem.update(left, top, width, size);
1741 top += size;
1742 }
1743
1744 // Layout the widget using the computed size.
1745 if (widgetItem && !widgetItem.isHidden) {
1746 let size = this.sizers[1].size;
1747 widgetItem.update(left, top, width, size);
1748 }
1749 }
1750
1751 private _top = 0;
1752 private _left = 0;
1753 private _width = 0;
1754 private _height = 0;
1755 }
1756
1757 /**
1758 * A layout node which holds the data for a split area.
1759 */
1760 export class SplitLayoutNode {
1761 /**
1762 * Construct a new split layout node.
1763 *
1764 * @param orientation - The orientation of the node.
1765 */
1766 constructor(orientation: Orientation) {
1767 this.orientation = orientation;
1768 }
1769
1770 /**
1771 * The parent of the layout node.
1772 */
1773 parent: SplitLayoutNode | null = null;
1774
1775 /**
1776 * Whether the sizers have been normalized.
1777 */
1778 normalized = false;
1779
1780 /**
1781 * The orientation of the node.
1782 */
1783 readonly orientation: Orientation;
1784
1785 /**
1786 * The child nodes for the split node.
1787 */
1788 readonly children: LayoutNode[] = [];
1789
1790 /**
1791 * The box sizers for the layout children.
1792 */
1793 readonly sizers: BoxSizer[] = [];
1794
1795 /**
1796 * The handles for the layout children.
1797 */
1798 readonly handles: HTMLDivElement[] = [];
1799
1800 /**
1801 * Create an iterator for all widgets in the layout tree.
1802 */
1803 iterAllWidgets(): IIterator<Widget> {
1804 let children = map(this.children, child => child.iterAllWidgets());
1805 return new ChainIterator<Widget>(children);
1806 }
1807
1808 /**
1809 * Create an iterator for the user widgets in the layout tree.
1810 */
1811 iterUserWidgets(): IIterator<Widget> {
1812 let children = map(this.children, child => child.iterUserWidgets());
1813 return new ChainIterator<Widget>(children);
1814 }
1815
1816 /**
1817 * Create an iterator for the selected widgets in the layout tree.
1818 */
1819 iterSelectedWidgets(): IIterator<Widget> {
1820 let children = map(this.children, child => child.iterSelectedWidgets());
1821 return new ChainIterator<Widget>(children);
1822 }
1823
1824 /**
1825 * Create an iterator for the tab bars in the layout tree.
1826 */
1827 iterTabBars(): IIterator<TabBar<Widget>> {
1828 let children = map(this.children, child => child.iterTabBars());
1829 return new ChainIterator<TabBar<Widget>>(children);
1830 }
1831
1832 /**
1833 * Create an iterator for the handles in the layout tree.
1834 */
1835 iterHandles(): IIterator<HTMLDivElement> {
1836 let children = map(this.children, child => child.iterHandles());
1837 return chain(this.handles, new ChainIterator<HTMLDivElement>(children));
1838 }
1839
1840 /**
1841 * Find the tab layout node which contains the given widget.
1842 */
1843 findTabNode(widget: Widget): TabLayoutNode | null {
1844 for (let i = 0, n = this.children.length; i < n; ++i) {
1845 let result = this.children[i].findTabNode(widget);
1846 if (result) {
1847 return result;
1848 }
1849 }
1850 return null;
1851 }
1852
1853 /**
1854 * Find the split layout node which contains the given handle.
1855 */
1856 findSplitNode(
1857 handle: HTMLDivElement
1858 ): { index: number; node: SplitLayoutNode } | null {
1859 let index = this.handles.indexOf(handle);
1860 if (index !== -1) {
1861 return { index, node: this };
1862 }
1863 for (let i = 0, n = this.children.length; i < n; ++i) {
1864 let result = this.children[i].findSplitNode(handle);
1865 if (result) {
1866 return result;
1867 }
1868 }
1869 return null;
1870 }
1871
1872 /**
1873 * Find the first tab layout node in a layout tree.
1874 */
1875 findFirstTabNode(): TabLayoutNode | null {
1876 if (this.children.length === 0) {
1877 return null;
1878 }
1879 return this.children[0].findFirstTabNode();
1880 }
1881
1882 /**
1883 * Find the tab layout node which contains the local point.
1884 */
1885 hitTestTabNodes(x: number, y: number): TabLayoutNode | null {
1886 for (let i = 0, n = this.children.length; i < n; ++i) {
1887 let result = this.children[i].hitTestTabNodes(x, y);
1888 if (result) {
1889 return result;
1890 }
1891 }
1892 return null;
1893 }
1894
1895 /**
1896 * Create a configuration object for the layout tree.
1897 */
1898 createConfig(): DockLayout.ISplitAreaConfig {
1899 let orientation = this.orientation;
1900 let sizes = this.createNormalizedSizes();
1901 let children = this.children.map(child => child.createConfig());
1902 return { type: 'split-area', orientation, children, sizes };
1903 }
1904
1905 /**
1906 * Sync the visibility and orientation of the handles.
1907 */
1908 syncHandles(): void {
1909 each(this.handles, (handle, i) => {
1910 handle.setAttribute('data-orientation', this.orientation);
1911 if (i === this.handles.length - 1) {
1912 handle.classList.add('lm-mod-hidden');
1913 /* <DEPRECATED> */
1914 handle.classList.add('p-mod-hidden');
1915 /* </DEPRECATED> */
1916 } else {
1917 handle.classList.remove('lm-mod-hidden');
1918 /* <DEPRECATED> */
1919 handle.classList.remove('p-mod-hidden');
1920 /* </DEPRECATED> */
1921 }
1922 });
1923 }
1924
1925 /**
1926 * Hold the current sizes of the box sizers.
1927 *
1928 * This sets the size hint of each sizer to its current size.
1929 */
1930 holdSizes(): void {
1931 each(this.sizers, sizer => {
1932 sizer.sizeHint = sizer.size;
1933 });
1934 }
1935
1936 /**
1937 * Recursively hold all of the sizes in the layout tree.
1938 *
1939 * This ignores the sizers of tab layout nodes.
1940 */
1941 holdAllSizes(): void {
1942 each(this.children, child => child.holdAllSizes());
1943 this.holdSizes();
1944 }
1945
1946 /**
1947 * Normalize the sizes of the split layout node.
1948 */
1949 normalizeSizes(): void {
1950 // Bail early if the sizers are empty.
1951 let n = this.sizers.length;
1952 if (n === 0) {
1953 return;
1954 }
1955
1956 // Hold the current sizes of the sizers.
1957 this.holdSizes();
1958
1959 // Compute the sum of the sizes.
1960 let sum = reduce(this.sizers, (v, sizer) => v + sizer.sizeHint, 0);
1961
1962 // Normalize the sizes based on the sum.
1963 if (sum === 0) {
1964 each(this.sizers, sizer => {
1965 sizer.size = sizer.sizeHint = 1 / n;
1966 });
1967 } else {
1968 each(this.sizers, sizer => {
1969 sizer.size = sizer.sizeHint /= sum;
1970 });
1971 }
1972
1973 // Mark the sizes as normalized.
1974 this.normalized = true;
1975 }
1976
1977 /**
1978 * Snap the normalized sizes of the split layout node.
1979 */
1980 createNormalizedSizes(): number[] {
1981 // Bail early if the sizers are empty.
1982 let n = this.sizers.length;
1983 if (n === 0) {
1984 return [];
1985 }
1986
1987 // Grab the current sizes of the sizers.
1988 let sizes = this.sizers.map(sizer => sizer.size);
1989
1990 // Compute the sum of the sizes.
1991 let sum = reduce(sizes, (v, size) => v + size, 0);
1992
1993 // Normalize the sizes based on the sum.
1994 if (sum === 0) {
1995 each(sizes, (size, i) => {
1996 sizes[i] = 1 / n;
1997 });
1998 } else {
1999 each(sizes, (size, i) => {
2000 sizes[i] = size / sum;
2001 });
2002 }
2003
2004 // Return the normalized sizes.
2005 return sizes;
2006 }
2007
2008 /**
2009 * Fit the layout tree.
2010 */
2011 fit(spacing: number, items: ItemMap): ElementExt.ISizeLimits {
2012 // Compute the required fixed space.
2013 let horizontal = this.orientation === 'horizontal';
2014 let fixed = Math.max(0, this.children.length - 1) * spacing;
2015
2016 // Set up the limit variables.
2017 let minWidth = horizontal ? fixed : 0;
2018 let minHeight = horizontal ? 0 : fixed;
2019 let maxWidth = Infinity;
2020 let maxHeight = Infinity;
2021
2022 // Fit the children and update the limits.
2023 for (let i = 0, n = this.children.length; i < n; ++i) {
2024 let limits = this.children[i].fit(spacing, items);
2025 if (horizontal) {
2026 minHeight = Math.max(minHeight, limits.minHeight);
2027 minWidth += limits.minWidth;
2028 this.sizers[i].minSize = limits.minWidth;
2029 } else {
2030 minWidth = Math.max(minWidth, limits.minWidth);
2031 minHeight += limits.minHeight;
2032 this.sizers[i].minSize = limits.minHeight;
2033 }
2034 }
2035
2036 // Return the computed limits for the layout node.
2037 return { minWidth, minHeight, maxWidth, maxHeight };
2038 }
2039
2040 /**
2041 * Update the layout tree.
2042 */
2043 update(
2044 left: number,
2045 top: number,
2046 width: number,
2047 height: number,
2048 spacing: number,
2049 items: ItemMap
2050 ): void {
2051 // Compute the available layout space.
2052 let horizontal = this.orientation === 'horizontal';
2053 let fixed = Math.max(0, this.children.length - 1) * spacing;
2054 let space = Math.max(0, (horizontal ? width : height) - fixed);
2055
2056 // De-normalize the sizes if needed.
2057 if (this.normalized) {
2058 each(this.sizers, sizer => {
2059 sizer.sizeHint *= space;
2060 });
2061 this.normalized = false;
2062 }
2063
2064 // Distribute the layout space to the sizers.
2065 BoxEngine.calc(this.sizers, space);
2066
2067 // Update the geometry of the child nodes and handles.
2068 for (let i = 0, n = this.children.length; i < n; ++i) {
2069 let child = this.children[i];
2070 let size = this.sizers[i].size;
2071 let handleStyle = this.handles[i].style;
2072 if (horizontal) {
2073 child.update(left, top, size, height, spacing, items);
2074 left += size;
2075 handleStyle.top = `${top}px`;
2076 handleStyle.left = `${left}px`;
2077 handleStyle.width = `${spacing}px`;
2078 handleStyle.height = `${height}px`;
2079 left += spacing;
2080 } else {
2081 child.update(left, top, width, size, spacing, items);
2082 top += size;
2083 handleStyle.top = `${top}px`;
2084 handleStyle.left = `${left}px`;
2085 handleStyle.width = `${width}px`;
2086 handleStyle.height = `${spacing}px`;
2087 top += spacing;
2088 }
2089 }
2090 }
2091 }
2092
2093 export function addAria(widget: Widget, tabBar: TabBar<Widget>) {
2094 widget.node.setAttribute('role', 'tabpanel');
2095 let renderer = tabBar.renderer;
2096 if (renderer instanceof TabBar.Renderer) {
2097 let tabId = renderer.createTabKey({
2098 title: widget.title,
2099 current: false,
2100 zIndex: 0
2101 });
2102 widget.node.setAttribute('aria-labelledby', tabId);
2103 }
2104 }
2105
2106 export function removeAria(widget: Widget) {
2107 widget.node.removeAttribute('role');
2108 widget.node.removeAttribute('aria-labelledby');
2109 }
2110
2111 /**
2112 * Normalize a tab area config and collect the visited widgets.
2113 */
2114 function normalizeTabAreaConfig(
2115 config: DockLayout.ITabAreaConfig,
2116 widgetSet: Set<Widget>
2117 ): DockLayout.ITabAreaConfig | null {
2118 // Bail early if there is no content.
2119 if (config.widgets.length === 0) {
2120 return null;
2121 }
2122
2123 // Setup the filtered widgets array.
2124 let widgets: Widget[] = [];
2125
2126 // Filter the config for unique widgets.
2127 each(config.widgets, widget => {
2128 if (!widgetSet.has(widget)) {
2129 widgetSet.add(widget);
2130 widgets.push(widget);
2131 }
2132 });
2133
2134 // Bail if there are no effective widgets.
2135 if (widgets.length === 0) {
2136 return null;
2137 }
2138
2139 // Normalize the current index.
2140 let index = config.currentIndex;
2141 if (index !== -1 && (index < 0 || index >= widgets.length)) {
2142 index = 0;
2143 }
2144
2145 // Return a normalized config object.
2146 return { type: 'tab-area', widgets, currentIndex: index };
2147 }
2148
2149 /**
2150 * Normalize a split area config and collect the visited widgets.
2151 */
2152 function normalizeSplitAreaConfig(
2153 config: DockLayout.ISplitAreaConfig,
2154 widgetSet: Set<Widget>
2155 ): DockLayout.AreaConfig | null {
2156 // Set up the result variables.
2157 let orientation = config.orientation;
2158 let children: DockLayout.AreaConfig[] = [];
2159 let sizes: number[] = [];
2160
2161 // Normalize the config children.
2162 for (let i = 0, n = config.children.length; i < n; ++i) {
2163 // Normalize the child config.
2164 let child = normalizeAreaConfig(config.children[i], widgetSet);
2165
2166 // Ignore an empty child.
2167 if (!child) {
2168 continue;
2169 }
2170
2171 // Add the child or hoist its content as appropriate.
2172 if (child.type === 'tab-area' || child.orientation !== orientation) {
2173 children.push(child);
2174 sizes.push(Math.abs(config.sizes[i] || 0));
2175 } else {
2176 children.push(...child.children);
2177 sizes.push(...child.sizes);
2178 }
2179 }
2180
2181 // Bail if there are no effective children.
2182 if (children.length === 0) {
2183 return null;
2184 }
2185
2186 // If there is only one effective child, return that child.
2187 if (children.length === 1) {
2188 return children[0];
2189 }
2190
2191 // Return a normalized config object.
2192 return { type: 'split-area', orientation, children, sizes };
2193 }
2194
2195 /**
2196 * Convert a normalized tab area config into a layout tree.
2197 */
2198 function realizeTabAreaConfig(
2199 config: DockLayout.ITabAreaConfig,
2200 renderer: DockLayout.IRenderer,
2201 document: Document | ShadowRoot
2202 ): TabLayoutNode {
2203 // Create the tab bar for the layout node.
2204 let tabBar = renderer.createTabBar(document);
2205
2206 // Hide each widget and add it to the tab bar.
2207 each(config.widgets, widget => {
2208 widget.hide();
2209 tabBar.addTab(widget.title);
2210 Private.addAria(widget, tabBar);
2211 });
2212
2213 // Set the current index of the tab bar.
2214 tabBar.currentIndex = config.currentIndex;
2215
2216 // Return the new tab layout node.
2217 return new TabLayoutNode(tabBar);
2218 }
2219
2220 /**
2221 * Convert a normalized split area config into a layout tree.
2222 */
2223 function realizeSplitAreaConfig(
2224 config: DockLayout.ISplitAreaConfig,
2225 renderer: DockLayout.IRenderer,
2226 document: Document | ShadowRoot
2227 ): SplitLayoutNode {
2228 // Create the split layout node.
2229 let node = new SplitLayoutNode(config.orientation);
2230
2231 // Add each child to the layout node.
2232 each(config.children, (child, i) => {
2233 // Create the child data for the layout node.
2234 let childNode = realizeAreaConfig(child, renderer, document);
2235 let sizer = createSizer(config.sizes[i]);
2236 let handle = renderer.createHandle();
2237
2238 // Add the child data to the layout node.
2239 node.children.push(childNode);
2240 node.handles.push(handle);
2241 node.sizers.push(sizer);
2242
2243 // Update the parent for the child node.
2244 childNode.parent = node;
2245 });
2246
2247 // Synchronize the handle state for the layout node.
2248 node.syncHandles();
2249
2250 // Normalize the sizes for the layout node.
2251 node.normalizeSizes();
2252
2253 // Return the new layout node.
2254 return node;
2255 }
2256}