1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | import { ArrayExt, each } from '@lumino/algorithm';
|
11 |
|
12 | import { ElementExt } from '@lumino/domutils';
|
13 |
|
14 | import { Message, MessageLoop } from '@lumino/messaging';
|
15 |
|
16 | import { AttachedProperty } from '@lumino/properties';
|
17 |
|
18 | import { BoxEngine, BoxSizer } from './boxengine';
|
19 |
|
20 | import { LayoutItem } from './layout';
|
21 |
|
22 | import { PanelLayout } from './panellayout';
|
23 |
|
24 | import { Utils } from './utils';
|
25 |
|
26 | import { Widget } from './widget';
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | export class SplitLayout extends PanelLayout {
|
32 | |
33 |
|
34 |
|
35 |
|
36 |
|
37 | constructor(options: SplitLayout.IOptions) {
|
38 | super();
|
39 | this.renderer = options.renderer;
|
40 | if (options.orientation !== undefined) {
|
41 | this._orientation = options.orientation;
|
42 | }
|
43 | if (options.alignment !== undefined) {
|
44 | this._alignment = options.alignment;
|
45 | }
|
46 | if (options.spacing !== undefined) {
|
47 | this._spacing = Utils.clampDimension(options.spacing);
|
48 | }
|
49 | }
|
50 |
|
51 | |
52 |
|
53 |
|
54 | dispose(): void {
|
55 |
|
56 | each(this._items, item => {
|
57 | item.dispose();
|
58 | });
|
59 |
|
60 |
|
61 | this._box = null;
|
62 | this._items.length = 0;
|
63 | this._sizers.length = 0;
|
64 | this._handles.length = 0;
|
65 |
|
66 |
|
67 | super.dispose();
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
73 | readonly renderer: SplitLayout.IRenderer;
|
74 |
|
75 | |
76 |
|
77 |
|
78 | get orientation(): SplitLayout.Orientation {
|
79 | return this._orientation;
|
80 | }
|
81 |
|
82 | |
83 |
|
84 |
|
85 | set orientation(value: SplitLayout.Orientation) {
|
86 | if (this._orientation === value) {
|
87 | return;
|
88 | }
|
89 | this._orientation = value;
|
90 | if (!this.parent) {
|
91 | return;
|
92 | }
|
93 | this.parent.dataset['orientation'] = value;
|
94 | this.parent.fit();
|
95 | }
|
96 |
|
97 | |
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | get alignment(): SplitLayout.Alignment {
|
107 | return this._alignment;
|
108 | }
|
109 |
|
110 | |
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 | set alignment(value: SplitLayout.Alignment) {
|
120 | if (this._alignment === value) {
|
121 | return;
|
122 | }
|
123 | this._alignment = value;
|
124 | if (!this.parent) {
|
125 | return;
|
126 | }
|
127 | this.parent.dataset['alignment'] = value;
|
128 | this.parent.update();
|
129 | }
|
130 |
|
131 | |
132 |
|
133 |
|
134 | get spacing(): number {
|
135 | return this._spacing;
|
136 | }
|
137 |
|
138 | |
139 |
|
140 |
|
141 | set spacing(value: number) {
|
142 | value = Utils.clampDimension(value);
|
143 | if (this._spacing === value) {
|
144 | return;
|
145 | }
|
146 | this._spacing = value;
|
147 | if (!this.parent) {
|
148 | return;
|
149 | }
|
150 | this.parent.fit();
|
151 | }
|
152 |
|
153 | |
154 |
|
155 |
|
156 | get handles(): ReadonlyArray<HTMLDivElement> {
|
157 | return this._handles;
|
158 | }
|
159 |
|
160 | |
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | absoluteSizes(): number[] {
|
168 | return this._sizers.map(sizer => sizer.size);
|
169 | }
|
170 |
|
171 | |
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 | relativeSizes(): number[] {
|
183 | return Private.normalize(this._sizers.map(sizer => sizer.size));
|
184 | }
|
185 |
|
186 | |
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 | setRelativeSizes(sizes: number[], update = true): void {
|
199 |
|
200 | let n = this._sizers.length;
|
201 | let temp = sizes.slice(0, n);
|
202 | while (temp.length < n) {
|
203 | temp.push(0);
|
204 | }
|
205 |
|
206 |
|
207 | let normed = Private.normalize(temp);
|
208 |
|
209 |
|
210 | for (let i = 0; i < n; ++i) {
|
211 | let sizer = this._sizers[i];
|
212 | sizer.sizeHint = normed[i];
|
213 | sizer.size = normed[i];
|
214 | }
|
215 |
|
216 |
|
217 | this._hasNormedSizes = true;
|
218 |
|
219 |
|
220 | if (update && this.parent) {
|
221 | this.parent.update();
|
222 | }
|
223 | }
|
224 |
|
225 | |
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 | moveHandle(index: number, position: number): void {
|
239 |
|
240 | let handle = this._handles[index];
|
241 | if (!handle || handle.classList.contains('lm-mod-hidden')) {
|
242 | return;
|
243 | }
|
244 |
|
245 |
|
246 | let delta: number;
|
247 | if (this._orientation === 'horizontal') {
|
248 | delta = position - handle.offsetLeft;
|
249 | } else {
|
250 | delta = position - handle.offsetTop;
|
251 | }
|
252 |
|
253 |
|
254 | if (delta === 0) {
|
255 | return;
|
256 | }
|
257 |
|
258 |
|
259 | for (let sizer of this._sizers) {
|
260 | if (sizer.size > 0) {
|
261 | sizer.sizeHint = sizer.size;
|
262 | }
|
263 | }
|
264 |
|
265 |
|
266 | BoxEngine.adjust(this._sizers, index, delta);
|
267 |
|
268 |
|
269 | if (this.parent) {
|
270 | this.parent.update();
|
271 | }
|
272 | }
|
273 |
|
274 | |
275 |
|
276 |
|
277 | protected init(): void {
|
278 | this.parent!.dataset['orientation'] = this.orientation;
|
279 | this.parent!.dataset['alignment'] = this.alignment;
|
280 | super.init();
|
281 | }
|
282 |
|
283 | |
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 | protected attachWidget(index: number, widget: Widget): void {
|
294 |
|
295 | let item = new LayoutItem(widget);
|
296 | let handle = Private.createHandle(this.renderer);
|
297 | let average = Private.averageSize(this._sizers);
|
298 | let sizer = Private.createSizer(average);
|
299 |
|
300 |
|
301 | ArrayExt.insert(this._items, index, item);
|
302 | ArrayExt.insert(this._sizers, index, sizer);
|
303 | ArrayExt.insert(this._handles, index, handle);
|
304 |
|
305 |
|
306 | if (this.parent!.isAttached) {
|
307 | MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
|
308 | }
|
309 |
|
310 |
|
311 | this.parent!.node.appendChild(widget.node);
|
312 | this.parent!.node.appendChild(handle);
|
313 |
|
314 |
|
315 | if (this.parent!.isAttached) {
|
316 | MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
|
317 | }
|
318 |
|
319 |
|
320 | this.parent!.fit();
|
321 | }
|
322 |
|
323 | |
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 | protected moveWidget(
|
336 | fromIndex: number,
|
337 | toIndex: number,
|
338 | widget: Widget
|
339 | ): void {
|
340 |
|
341 | ArrayExt.move(this._items, fromIndex, toIndex);
|
342 | ArrayExt.move(this._sizers, fromIndex, toIndex);
|
343 | ArrayExt.move(this._handles, fromIndex, toIndex);
|
344 |
|
345 |
|
346 | this.parent!.fit();
|
347 | }
|
348 |
|
349 | |
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 | protected detachWidget(index: number, widget: Widget): void {
|
360 |
|
361 | let item = ArrayExt.removeAt(this._items, index);
|
362 | let handle = ArrayExt.removeAt(this._handles, index);
|
363 | ArrayExt.removeAt(this._sizers, index);
|
364 |
|
365 |
|
366 | if (this.parent!.isAttached) {
|
367 | MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
|
368 | }
|
369 |
|
370 |
|
371 | this.parent!.node.removeChild(widget.node);
|
372 | this.parent!.node.removeChild(handle!);
|
373 |
|
374 |
|
375 | if (this.parent!.isAttached) {
|
376 | MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
|
377 | }
|
378 |
|
379 |
|
380 | item!.dispose();
|
381 |
|
382 |
|
383 | this.parent!.fit();
|
384 | }
|
385 |
|
386 | |
387 |
|
388 |
|
389 | protected onBeforeShow(msg: Message): void {
|
390 | super.onBeforeShow(msg);
|
391 | this.parent!.update();
|
392 | }
|
393 |
|
394 | |
395 |
|
396 |
|
397 | protected onBeforeAttach(msg: Message): void {
|
398 | super.onBeforeAttach(msg);
|
399 | this.parent!.fit();
|
400 | }
|
401 |
|
402 | |
403 |
|
404 |
|
405 | protected onChildShown(msg: Widget.ChildMessage): void {
|
406 | this.parent!.fit();
|
407 | }
|
408 |
|
409 | |
410 |
|
411 |
|
412 | protected onChildHidden(msg: Widget.ChildMessage): void {
|
413 | this.parent!.fit();
|
414 | }
|
415 |
|
416 | |
417 |
|
418 |
|
419 | protected onResize(msg: Widget.ResizeMessage): void {
|
420 | if (this.parent!.isVisible) {
|
421 | this._update(msg.width, msg.height);
|
422 | }
|
423 | }
|
424 |
|
425 | |
426 |
|
427 |
|
428 | protected onUpdateRequest(msg: Message): void {
|
429 | if (this.parent!.isVisible) {
|
430 | this._update(-1, -1);
|
431 | }
|
432 | }
|
433 |
|
434 | |
435 |
|
436 |
|
437 | protected onFitRequest(msg: Message): void {
|
438 | if (this.parent!.isAttached) {
|
439 | this._fit();
|
440 | }
|
441 | }
|
442 |
|
443 | |
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 | protected updateItemPosition(
|
455 | i: number,
|
456 | isHorizontal: boolean,
|
457 | left: number,
|
458 | top: number,
|
459 | height: number,
|
460 | width: number,
|
461 | size: number
|
462 | ): void {
|
463 | const item = this._items[i];
|
464 | if (item.isHidden) {
|
465 | return;
|
466 | }
|
467 |
|
468 |
|
469 | let handleStyle = this._handles[i].style;
|
470 |
|
471 |
|
472 | if (isHorizontal) {
|
473 | left += this.widgetOffset;
|
474 | item.update(left, top, size, height);
|
475 | left += size;
|
476 | handleStyle.top = `${top}px`;
|
477 | handleStyle.left = `${left}px`;
|
478 | handleStyle.width = `${this._spacing}px`;
|
479 | handleStyle.height = `${height}px`;
|
480 | } else {
|
481 | top += this.widgetOffset;
|
482 | item.update(left, top, width, size);
|
483 | top += size;
|
484 | handleStyle.top = `${top}px`;
|
485 | handleStyle.left = `${left}px`;
|
486 | handleStyle.width = `${width}px`;
|
487 | handleStyle.height = `${this._spacing}px`;
|
488 | }
|
489 | }
|
490 |
|
491 | |
492 |
|
493 |
|
494 | private _fit(): void {
|
495 |
|
496 | let nVisible = 0;
|
497 | let lastHandleIndex = -1;
|
498 | for (let i = 0, n = this._items.length; i < n; ++i) {
|
499 | if (this._items[i].isHidden) {
|
500 | this._handles[i].classList.add('lm-mod-hidden');
|
501 |
|
502 | this._handles[i].classList.add('p-mod-hidden');
|
503 |
|
504 | } else {
|
505 | this._handles[i].classList.remove('lm-mod-hidden');
|
506 |
|
507 | this._handles[i].classList.remove('p-mod-hidden');
|
508 |
|
509 | lastHandleIndex = i;
|
510 | nVisible++;
|
511 | }
|
512 | }
|
513 |
|
514 |
|
515 | if (lastHandleIndex !== -1) {
|
516 | this._handles[lastHandleIndex].classList.add('lm-mod-hidden');
|
517 |
|
518 | this._handles[lastHandleIndex].classList.add('p-mod-hidden');
|
519 |
|
520 | }
|
521 |
|
522 |
|
523 | this._fixed =
|
524 | this._spacing * Math.max(0, nVisible - 1) +
|
525 | this.widgetOffset * this._items.length;
|
526 |
|
527 |
|
528 | let horz = this._orientation === 'horizontal';
|
529 | let minW = horz ? this._fixed : 0;
|
530 | let minH = horz ? 0 : this._fixed;
|
531 |
|
532 |
|
533 | for (let i = 0, n = this._items.length; i < n; ++i) {
|
534 |
|
535 | let item = this._items[i];
|
536 | let sizer = this._sizers[i];
|
537 |
|
538 |
|
539 | if (sizer.size > 0) {
|
540 | sizer.sizeHint = sizer.size;
|
541 | }
|
542 |
|
543 |
|
544 | if (item.isHidden) {
|
545 | sizer.minSize = 0;
|
546 | sizer.maxSize = 0;
|
547 | continue;
|
548 | }
|
549 |
|
550 |
|
551 | item.fit();
|
552 |
|
553 |
|
554 | sizer.stretch = SplitLayout.getStretch(item.widget);
|
555 |
|
556 |
|
557 | if (horz) {
|
558 | sizer.minSize = item.minWidth;
|
559 | sizer.maxSize = item.maxWidth;
|
560 | minW += item.minWidth;
|
561 | minH = Math.max(minH, item.minHeight);
|
562 | } else {
|
563 | sizer.minSize = item.minHeight;
|
564 | sizer.maxSize = item.maxHeight;
|
565 | minH += item.minHeight;
|
566 | minW = Math.max(minW, item.minWidth);
|
567 | }
|
568 | }
|
569 |
|
570 |
|
571 | let box = (this._box = ElementExt.boxSizing(this.parent!.node));
|
572 | minW += box.horizontalSum;
|
573 | minH += box.verticalSum;
|
574 |
|
575 |
|
576 | let style = this.parent!.node.style;
|
577 | style.minWidth = `${minW}px`;
|
578 | style.minHeight = `${minH}px`;
|
579 |
|
580 |
|
581 | this._dirty = true;
|
582 |
|
583 |
|
584 |
|
585 | if (this.parent!.parent) {
|
586 | MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
|
587 | }
|
588 |
|
589 |
|
590 |
|
591 | if (this._dirty) {
|
592 | MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
|
593 | }
|
594 | }
|
595 |
|
596 | |
597 |
|
598 |
|
599 |
|
600 |
|
601 | private _update(offsetWidth: number, offsetHeight: number): void {
|
602 |
|
603 | this._dirty = false;
|
604 |
|
605 |
|
606 | let nVisible = 0;
|
607 | for (let i = 0, n = this._items.length; i < n; ++i) {
|
608 | nVisible += +!this._items[i].isHidden;
|
609 | }
|
610 |
|
611 |
|
612 | if (nVisible === 0 && this.widgetOffset === 0) {
|
613 | return;
|
614 | }
|
615 |
|
616 |
|
617 | if (offsetWidth < 0) {
|
618 | offsetWidth = this.parent!.node.offsetWidth;
|
619 | }
|
620 | if (offsetHeight < 0) {
|
621 | offsetHeight = this.parent!.node.offsetHeight;
|
622 | }
|
623 |
|
624 |
|
625 | if (!this._box) {
|
626 | this._box = ElementExt.boxSizing(this.parent!.node);
|
627 | }
|
628 |
|
629 |
|
630 | let top = this._box.paddingTop;
|
631 | let left = this._box.paddingLeft;
|
632 | let width = offsetWidth - this._box.horizontalSum;
|
633 | let height = offsetHeight - this._box.verticalSum;
|
634 |
|
635 |
|
636 | let extra = 0;
|
637 | let offset = 0;
|
638 | let horz = this._orientation === 'horizontal';
|
639 |
|
640 | if (nVisible > 0) {
|
641 |
|
642 | let space: number;
|
643 | if (horz) {
|
644 |
|
645 | space = Math.max(0, width - this._fixed);
|
646 | } else {
|
647 |
|
648 | space = Math.max(0, height - this._fixed);
|
649 | }
|
650 |
|
651 |
|
652 | if (this._hasNormedSizes) {
|
653 | for (let sizer of this._sizers) {
|
654 | sizer.sizeHint *= space;
|
655 | }
|
656 | this._hasNormedSizes = false;
|
657 | }
|
658 |
|
659 |
|
660 | let delta = BoxEngine.calc(this._sizers, space);
|
661 |
|
662 |
|
663 | if (delta > 0) {
|
664 | switch (this._alignment) {
|
665 | case 'start':
|
666 | break;
|
667 | case 'center':
|
668 | extra = 0;
|
669 | offset = delta / 2;
|
670 | break;
|
671 | case 'end':
|
672 | extra = 0;
|
673 | offset = delta;
|
674 | break;
|
675 | case 'justify':
|
676 | extra = delta / nVisible;
|
677 | offset = 0;
|
678 | break;
|
679 | default:
|
680 | throw 'unreachable';
|
681 | }
|
682 | }
|
683 | }
|
684 |
|
685 |
|
686 | for (let i = 0, n = this._items.length; i < n; ++i) {
|
687 |
|
688 | const item = this._items[i];
|
689 |
|
690 |
|
691 | const size = item.isHidden ? 0 : this._sizers[i].size + extra;
|
692 |
|
693 | this.updateItemPosition(
|
694 | i,
|
695 | horz,
|
696 | horz ? left + offset : left,
|
697 | horz ? top : top + offset,
|
698 | height,
|
699 | width,
|
700 | size
|
701 | );
|
702 |
|
703 | const fullOffset =
|
704 | this.widgetOffset +
|
705 | (this._handles[i].classList.contains('lm-mod-hidden')
|
706 | ? 0
|
707 | : this._spacing);
|
708 |
|
709 | if (horz) {
|
710 | left += size + fullOffset;
|
711 | } else {
|
712 | top += size + fullOffset;
|
713 | }
|
714 | }
|
715 | }
|
716 |
|
717 | protected widgetOffset = 0;
|
718 | private _fixed = 0;
|
719 | private _spacing = 4;
|
720 | private _dirty = false;
|
721 | private _hasNormedSizes = false;
|
722 | private _sizers: BoxSizer[] = [];
|
723 | private _items: LayoutItem[] = [];
|
724 | private _handles: HTMLDivElement[] = [];
|
725 | private _box: ElementExt.IBoxSizing | null = null;
|
726 | private _alignment: SplitLayout.Alignment = 'start';
|
727 | private _orientation: SplitLayout.Orientation = 'horizontal';
|
728 | }
|
729 |
|
730 |
|
731 |
|
732 |
|
733 | export namespace SplitLayout {
|
734 | |
735 |
|
736 |
|
737 | export type Orientation = 'horizontal' | 'vertical';
|
738 |
|
739 | |
740 |
|
741 |
|
742 | export type Alignment = 'start' | 'center' | 'end' | 'justify';
|
743 |
|
744 | |
745 |
|
746 |
|
747 | export interface IOptions {
|
748 | |
749 |
|
750 |
|
751 | renderer: IRenderer;
|
752 |
|
753 | |
754 |
|
755 |
|
756 |
|
757 |
|
758 | orientation?: Orientation;
|
759 |
|
760 | |
761 |
|
762 |
|
763 |
|
764 |
|
765 | alignment?: Alignment;
|
766 |
|
767 | |
768 |
|
769 |
|
770 |
|
771 |
|
772 | spacing?: number;
|
773 | }
|
774 |
|
775 | |
776 |
|
777 |
|
778 | export interface IRenderer {
|
779 | |
780 |
|
781 |
|
782 |
|
783 |
|
784 | createHandle(): HTMLDivElement;
|
785 | }
|
786 |
|
787 | |
788 |
|
789 |
|
790 |
|
791 |
|
792 |
|
793 |
|
794 | export function getStretch(widget: Widget): number {
|
795 | return Private.stretchProperty.get(widget);
|
796 | }
|
797 |
|
798 | |
799 |
|
800 |
|
801 |
|
802 |
|
803 |
|
804 |
|
805 | export function setStretch(widget: Widget, value: number): void {
|
806 | Private.stretchProperty.set(widget, value);
|
807 | }
|
808 | }
|
809 |
|
810 |
|
811 |
|
812 |
|
813 | namespace Private {
|
814 | |
815 |
|
816 |
|
817 | export const stretchProperty = new AttachedProperty<Widget, number>({
|
818 | name: 'stretch',
|
819 | create: () => 0,
|
820 | coerce: (owner, value) => Math.max(0, Math.floor(value)),
|
821 | changed: onChildSizingChanged
|
822 | });
|
823 |
|
824 | |
825 |
|
826 |
|
827 | export function createSizer(size: number): BoxSizer {
|
828 | let sizer = new BoxSizer();
|
829 | sizer.sizeHint = Math.floor(size);
|
830 | return sizer;
|
831 | }
|
832 |
|
833 | |
834 |
|
835 |
|
836 | export function createHandle(
|
837 | renderer: SplitLayout.IRenderer
|
838 | ): HTMLDivElement {
|
839 | let handle = renderer.createHandle();
|
840 | handle.style.position = 'absolute';
|
841 | return handle;
|
842 | }
|
843 |
|
844 | |
845 |
|
846 |
|
847 | export function averageSize(sizers: BoxSizer[]): number {
|
848 | return sizers.reduce((v, s) => v + s.size, 0) / sizers.length || 0;
|
849 | }
|
850 |
|
851 | |
852 |
|
853 |
|
854 | export function normalize(values: number[]): number[] {
|
855 | let n = values.length;
|
856 | if (n === 0) {
|
857 | return [];
|
858 | }
|
859 | let sum = values.reduce((a, b) => a + Math.abs(b), 0);
|
860 | return sum === 0 ? values.map(v => 1 / n) : values.map(v => v / sum);
|
861 | }
|
862 |
|
863 | |
864 |
|
865 |
|
866 | function onChildSizingChanged(child: Widget): void {
|
867 | if (child.parent && child.parent.layout instanceof SplitLayout) {
|
868 | child.parent.fit();
|
869 | }
|
870 | }
|
871 | }
|