quilt-vanilla 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/action-icons.d.ts +18 -0
  4. package/dist/action-icons.d.ts.map +1 -0
  5. package/dist/action-icons.js +36 -0
  6. package/dist/action-icons.js.map +1 -0
  7. package/dist/chrome-icons.d.ts +3 -0
  8. package/dist/chrome-icons.d.ts.map +1 -0
  9. package/dist/chrome-icons.js +32 -0
  10. package/dist/chrome-icons.js.map +1 -0
  11. package/dist/corners.d.ts +8 -0
  12. package/dist/corners.d.ts.map +1 -0
  13. package/dist/corners.js +205 -0
  14. package/dist/corners.js.map +1 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +5 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/lifetime.d.ts +10 -0
  20. package/dist/lifetime.d.ts.map +1 -0
  21. package/dist/lifetime.js +44 -0
  22. package/dist/lifetime.js.map +1 -0
  23. package/dist/menu.d.ts +10 -0
  24. package/dist/menu.d.ts.map +1 -0
  25. package/dist/menu.js +356 -0
  26. package/dist/menu.js.map +1 -0
  27. package/dist/panes.d.ts +13 -0
  28. package/dist/panes.d.ts.map +1 -0
  29. package/dist/panes.js +58 -0
  30. package/dist/panes.js.map +1 -0
  31. package/dist/picker.d.ts +6 -0
  32. package/dist/picker.d.ts.map +1 -0
  33. package/dist/picker.js +112 -0
  34. package/dist/picker.js.map +1 -0
  35. package/dist/registry.d.ts +25 -0
  36. package/dist/registry.d.ts.map +1 -0
  37. package/dist/registry.js +39 -0
  38. package/dist/registry.js.map +1 -0
  39. package/dist/renderer.d.ts +3 -0
  40. package/dist/renderer.d.ts.map +1 -0
  41. package/dist/renderer.js +570 -0
  42. package/dist/renderer.js.map +1 -0
  43. package/dist/resize.d.ts +7 -0
  44. package/dist/resize.d.ts.map +1 -0
  45. package/dist/resize.js +56 -0
  46. package/dist/resize.js.map +1 -0
  47. package/dist/shortcuts.d.ts +6 -0
  48. package/dist/shortcuts.d.ts.map +1 -0
  49. package/dist/shortcuts.js +64 -0
  50. package/dist/shortcuts.js.map +1 -0
  51. package/dist/styles.css +842 -0
  52. package/dist/tab-bar.d.ts +6 -0
  53. package/dist/tab-bar.d.ts.map +1 -0
  54. package/dist/tab-bar.js +336 -0
  55. package/dist/tab-bar.js.map +1 -0
  56. package/dist/tab-tooltip.d.ts +4 -0
  57. package/dist/tab-tooltip.d.ts.map +1 -0
  58. package/dist/tab-tooltip.js +95 -0
  59. package/dist/tab-tooltip.js.map +1 -0
  60. package/dist/tabs.d.ts +15 -0
  61. package/dist/tabs.d.ts.map +1 -0
  62. package/dist/tabs.js +126 -0
  63. package/dist/tabs.js.map +1 -0
  64. package/dist/theme.d.ts +206 -0
  65. package/dist/theme.d.ts.map +1 -0
  66. package/dist/theme.js +166 -0
  67. package/dist/theme.js.map +1 -0
  68. package/dist/types.d.ts +66 -0
  69. package/dist/types.d.ts.map +1 -0
  70. package/dist/types.js +2 -0
  71. package/dist/types.js.map +1 -0
  72. package/dist/windows.d.ts +23 -0
  73. package/dist/windows.d.ts.map +1 -0
  74. package/dist/windows.js +247 -0
  75. package/dist/windows.js.map +1 -0
  76. package/package.json +50 -0
  77. package/src/action-icons.ts +40 -0
  78. package/src/chrome-icons.ts +30 -0
  79. package/src/corners.ts +259 -0
  80. package/src/index.ts +27 -0
  81. package/src/lifetime.ts +46 -0
  82. package/src/menu.ts +432 -0
  83. package/src/panes.ts +76 -0
  84. package/src/picker.ts +114 -0
  85. package/src/registry.ts +46 -0
  86. package/src/renderer.ts +654 -0
  87. package/src/resize.ts +65 -0
  88. package/src/shortcuts.ts +90 -0
  89. package/src/styles.css +842 -0
  90. package/src/tab-bar.ts +382 -0
  91. package/src/tab-tooltip.ts +96 -0
  92. package/src/tabs.ts +129 -0
  93. package/src/theme.ts +211 -0
  94. package/src/types.ts +67 -0
  95. package/src/windows.ts +244 -0
@@ -0,0 +1,570 @@
1
+ import { isolatedResize } from './resize.js';
2
+ import { chromeIcon } from './chrome-icons.js';
3
+ import { bindTabBar, validateTabBar } from './tab-bar.js';
4
+ import { bindCorners } from './corners.js';
5
+ import { applyTheme } from './theme.js';
6
+ import { fillTabs } from './tabs.js';
7
+ import { bindShortcuts } from './shortcuts.js';
8
+ import { allocate, bounds, DIVIDER, findNode, groups, paneIds } from 'quilt-core';
9
+ import { Scope, el, syncChildren } from './lifetime.js';
10
+ import { mountPane } from './panes.js';
11
+ import { Windows } from './windows.js';
12
+ import { createPaneMenu } from './menu.js';
13
+ let nextMount = 0;
14
+ export function mountLayout(host, options) {
15
+ validateTabBar(options.tabBar ?? {});
16
+ let tabBar = structuredClone(options.tabBar ?? {});
17
+ const prefix = `layouts-${++nextMount}`;
18
+ const doc = host.ownerDocument, win = doc.defaultView;
19
+ if (!win)
20
+ throw new Error('Layout host must belong to a live document');
21
+ const scope = new Scope(), regions = new Map(), panes = new Map();
22
+ const root = el(doc, 'div', 'layouts');
23
+ applyTheme(root, options.theme ?? {});
24
+ root.setAttribute('aria-label', 'Pane workspace');
25
+ const stage = el(doc, 'div', 'layouts-stage');
26
+ const status = el(doc, 'div', 'layouts-status');
27
+ status.setAttribute('role', 'status');
28
+ root.append(stage, status);
29
+ host.append(root);
30
+ let disposed = false, rendering = false, again = false, dragScope;
31
+ let dragId;
32
+ const error = (e) => {
33
+ status.textContent = e instanceof Error ? e.message : String(e);
34
+ try {
35
+ options.onError?.(e);
36
+ }
37
+ catch {
38
+ /* A consumer reporter must not prevent cleanup. */
39
+ }
40
+ };
41
+ const act = (fn) => {
42
+ try {
43
+ fn();
44
+ }
45
+ catch (e) {
46
+ error(e);
47
+ }
48
+ };
49
+ const windows = new Windows(doc, options, error, root);
50
+ const menu = createPaneMenu(root, options, windows, render, error);
51
+ bindShortcuts(root, options, scope, act, (group) => {
52
+ const region = regions.get(group.id);
53
+ if (region)
54
+ menu.addTab(region.header ?? region.element, group);
55
+ });
56
+ const renderOptions = { ...options, onError: error };
57
+ function button(text, title, action) {
58
+ const b = el(doc, 'button', 'layouts-button', text);
59
+ b.type = 'button';
60
+ b.title = title;
61
+ b.setAttribute('aria-label', title);
62
+ b.onclick = () => act(action);
63
+ return b;
64
+ }
65
+ function region(node) {
66
+ const existing = regions.get(node.id);
67
+ if (existing?.kind === node.kind)
68
+ return existing;
69
+ if (existing) {
70
+ existing.scope.dispose();
71
+ existing.element.remove();
72
+ regions.delete(node.id);
73
+ }
74
+ const r = {
75
+ kind: node.kind,
76
+ element: el(doc, 'section', `layouts-node layouts-${node.kind}`),
77
+ scope: new Scope(),
78
+ };
79
+ r.element.dataset.nodeId = node.id;
80
+ if (node.kind === 'group') {
81
+ r.header = el(doc, 'header', 'layouts-header');
82
+ r.body = el(doc, 'div', 'layouts-body');
83
+ r.element.append(r.header, r.body);
84
+ r.updateTabBar = bindTabBar(r.element, r.header, root, r.scope, () => {
85
+ const current = findNode(options.store.getSnapshot().root, node.id);
86
+ return {
87
+ ...tabBar,
88
+ ...(tabBar.regions?.[node.id] ?? {}),
89
+ ...(current?.kind === 'group' && current.tabDisplay
90
+ ? { display: current.tabDisplay }
91
+ : {}),
92
+ ...(current?.kind === 'group' && current.tabPlacement
93
+ ? { placement: current.tabPlacement }
94
+ : {}),
95
+ };
96
+ });
97
+ bindCorners(r.element, node.id, options, r.scope, (pane, group, direction, ratio) => menu.open(r.header, pane, group, direction, ratio), error);
98
+ r.element.setAttribute('aria-label', 'Pane region');
99
+ r.scope.listen(r.element, 'dragover', (event) => {
100
+ if (!dragId)
101
+ return;
102
+ const e = event;
103
+ const current = findNode(options.store.getSnapshot().root, node.id);
104
+ if (current?.kind !== 'group')
105
+ return;
106
+ const position = dropPosition(e, r.element);
107
+ if (!options.store.can(dragId, 'move') ||
108
+ !current.panes.every((id) => options.store.can(id, 'move') &&
109
+ (position === 'tab' || options.store.can(id, 'split'))))
110
+ return;
111
+ e.preventDefault();
112
+ e.stopPropagation();
113
+ r.element.dataset.drop = position;
114
+ });
115
+ r.scope.listen(r.element, 'dragleave', (event) => {
116
+ if (!r.element.contains(event.relatedTarget))
117
+ delete r.element.dataset.drop;
118
+ });
119
+ r.scope.listen(r.element, 'drop', (event) => {
120
+ const e = event;
121
+ if (!dragId)
122
+ return;
123
+ e.preventDefault();
124
+ e.stopPropagation();
125
+ const id = dragId;
126
+ dragId = undefined;
127
+ delete r.element.dataset.drop;
128
+ act(() => options.store.move(id, node.id, dropPosition(e, r.element), undefined, {
129
+ source: 'user',
130
+ }));
131
+ });
132
+ }
133
+ else {
134
+ r.divider = el(doc, 'div', 'layouts-divider');
135
+ r.divider.tabIndex = 0;
136
+ r.divider.setAttribute('role', 'separator');
137
+ r.divider.setAttribute('aria-label', 'Resize panes');
138
+ const start = (event) => {
139
+ const e = event;
140
+ if (e.button !== 0)
141
+ return;
142
+ const n = findNode(options.store.getSnapshot().root, node.id);
143
+ if (n?.kind !== 'split' || !resizable(n, options.store.getSnapshot()))
144
+ return;
145
+ e.preventDefault();
146
+ dragScope?.dispose();
147
+ const drag = new Scope();
148
+ dragScope = drag;
149
+ const resize = isolatedResize(geometryLayout(), node.id, (child, axis) => {
150
+ const rect = regions.get(child.id).element.getBoundingClientRect();
151
+ return axis === 'horizontal' ? rect.width : rect.height;
152
+ });
153
+ r.divider.setPointerCapture(e.pointerId);
154
+ root.classList.add('layouts-resizing');
155
+ drag.add(() => {
156
+ root.classList.remove('layouts-resizing');
157
+ try {
158
+ r.divider.releasePointerCapture(e.pointerId);
159
+ }
160
+ catch { }
161
+ });
162
+ const set = (move) => {
163
+ const current = findNode(geometryLayout().root, node.id);
164
+ if (current?.kind !== 'split')
165
+ return;
166
+ const rect = r.element.getBoundingClientRect();
167
+ const available = (current.axis === 'horizontal' ? rect.width : rect.height) - (current.gap ?? DIVIDER);
168
+ if (available > 0)
169
+ act(() => options.store.resizeMany(resize.ratios(Math.max(0.001, Math.min(0.999, (current.axis === 'horizontal'
170
+ ? move.clientX - rect.left
171
+ : move.clientY - rect.top) / available))), { source: 'user' }));
172
+ };
173
+ drag.listen(r.divider, 'pointermove', (move) => set(move));
174
+ drag.listen(r.divider, 'pointerup', () => drag.dispose());
175
+ drag.listen(r.divider, 'pointercancel', () => drag.dispose());
176
+ drag.listen(doc, 'keydown', (key) => {
177
+ if (key.key === 'Escape') {
178
+ act(() => options.store.resizeMany(resize.original));
179
+ drag.dispose();
180
+ }
181
+ });
182
+ };
183
+ r.scope.listen(r.divider, 'pointerdown', start);
184
+ r.scope.listen(r.divider, 'keydown', (event) => {
185
+ const e = event;
186
+ const n = findNode(options.store.getSnapshot().root, node.id);
187
+ if (n?.kind !== 'split' || !resizable(n, options.store.getSnapshot()))
188
+ return;
189
+ const keys = n.axis === 'horizontal' ? ['ArrowLeft', 'ArrowRight'] : ['ArrowUp', 'ArrowDown'];
190
+ if (keys.includes(e.key)) {
191
+ e.preventDefault();
192
+ const resize = isolatedResize(geometryLayout(), node.id, (child, axis) => {
193
+ const rect = regions.get(child.id).element.getBoundingClientRect();
194
+ return axis === 'horizontal' ? rect.width : rect.height;
195
+ });
196
+ act(() => options.store.resizeMany(resize.ratios(Math.max(0.001, Math.min(0.999, n.ratio + (e.key === keys[0] ? -1 : 1) * (e.shiftKey ? 0.1 : 0.02)))), { source: 'user' }));
197
+ }
198
+ });
199
+ }
200
+ regions.set(node.id, r);
201
+ return r;
202
+ }
203
+ function dropPosition(e, element) {
204
+ const rect = element.getBoundingClientRect(), x = (e.clientX - rect.left) / rect.width, y = (e.clientY - rect.top) / rect.height;
205
+ if (x < 0.22)
206
+ return 'left';
207
+ if (x > 0.78)
208
+ return 'right';
209
+ if (y < 0.22)
210
+ return 'top';
211
+ if (y > 0.78)
212
+ return 'bottom';
213
+ return 'tab';
214
+ }
215
+ function content(pane) {
216
+ const old = panes.get(pane.id), signature = JSON.stringify(pane);
217
+ if (old &&
218
+ old.renderer !==
219
+ (Object.hasOwn(options.renderers, pane.type) ? options.renderers[pane.type] : undefined)) {
220
+ old.dispose();
221
+ panes.delete(pane.id);
222
+ return content(pane);
223
+ }
224
+ if (old) {
225
+ if (old.signature !== signature) {
226
+ if (old.pane.type !== pane.type ||
227
+ JSON.stringify(old.pane.params) !== JSON.stringify(pane.params)) {
228
+ old.dispose();
229
+ panes.delete(pane.id);
230
+ }
231
+ else {
232
+ old.pane = pane;
233
+ old.signature = signature;
234
+ old.view.update?.(pane);
235
+ return old;
236
+ }
237
+ }
238
+ else
239
+ return old;
240
+ }
241
+ try {
242
+ const mounted = mountPane(doc, pane, 'main', renderOptions);
243
+ panes.set(pane.id, mounted);
244
+ return mounted;
245
+ }
246
+ catch (e) {
247
+ error(e);
248
+ const element = el(doc, 'div', 'layouts-pane layouts-placeholder', `Could not mount ${pane.title}. Reload the layout to retry.`);
249
+ element.dataset.paneId = pane.id;
250
+ const mounted = {
251
+ element,
252
+ pane,
253
+ signature,
254
+ renderer: options.renderers[pane.type],
255
+ failed: true,
256
+ view: { dispose() { } },
257
+ dispose() {
258
+ element.remove();
259
+ },
260
+ };
261
+ panes.set(pane.id, mounted);
262
+ return mounted;
263
+ }
264
+ }
265
+ function paintGroup(g, r, layout) {
266
+ const definitions = g.panes.map((id) => layout.panes[id]);
267
+ const signature = JSON.stringify([
268
+ definitions,
269
+ g.active,
270
+ layout.maximized,
271
+ layout.root.id === g.id,
272
+ definitions.map((p) => windows.pending.has(p.id)),
273
+ ]);
274
+ if (r.signature !== signature) {
275
+ r.signature = signature;
276
+ r.header.replaceChildren();
277
+ r.header.hidden = definitions.length === 1 && definitions[0].header === false;
278
+ const tabs = el(doc, 'div', 'layouts-tabs');
279
+ tabs.setAttribute('role', 'tablist');
280
+ tabs.setAttribute('aria-label', `Tabs in ${g.id}`);
281
+ fillTabs(tabs, g, definitions, {
282
+ options,
283
+ prefix,
284
+ act,
285
+ getDrag: () => dragId,
286
+ setDrag: (id) => {
287
+ dragId = id;
288
+ },
289
+ clearDrop: () => {
290
+ for (const region of regions.values())
291
+ delete region.element.dataset.drop;
292
+ for (const marker of root.querySelectorAll('[data-tab-drop]'))
293
+ delete marker.dataset.tabDrop;
294
+ },
295
+ focusActive: () => regions.get(g.id)?.header?.querySelector('[aria-selected="true"]')?.focus(),
296
+ });
297
+ r.header.append(tabs);
298
+ if (!definitions.length) {
299
+ const close = button('', 'Close empty pane', () => options.store.removeEmptyGroup(g.id));
300
+ close.append(chromeIcon(doc, 'close'));
301
+ close.disabled = layout.root.id === g.id;
302
+ const settings = button('', 'Empty pane actions', () => menu.openEmpty(settings, g));
303
+ settings.append(chromeIcon(doc, 'more'));
304
+ settings.setAttribute('aria-haspopup', 'dialog');
305
+ settings.dataset.focusId = `menu-${g.id}`;
306
+ r.header.append(close, settings);
307
+ }
308
+ const active = g.active ? layout.panes[g.active] : undefined;
309
+ if (active) {
310
+ const more = button('', `${active.title} actions`, () => menu.open(more, active, g));
311
+ more.append(chromeIcon(doc, 'more'));
312
+ more.dataset.focusId = `menu-${active.id}`;
313
+ more.setAttribute('aria-haspopup', 'dialog');
314
+ r.header.append(more);
315
+ }
316
+ }
317
+ r.updateTabBar?.();
318
+ const bodies = definitions.map((pane) => {
319
+ const p = content(pane);
320
+ p.element.hidden = pane.id !== g.active;
321
+ p.element.id = `${prefix}-panel-${pane.id}`;
322
+ p.element.setAttribute('role', 'tabpanel');
323
+ if (r.header.hidden) {
324
+ p.element.setAttribute('aria-label', pane.title);
325
+ p.element.removeAttribute('aria-labelledby');
326
+ }
327
+ else {
328
+ p.element.setAttribute('aria-labelledby', `${prefix}-tab-${pane.id}`);
329
+ p.element.removeAttribute('aria-label');
330
+ }
331
+ return p.element;
332
+ });
333
+ if (!bodies.length) {
334
+ let placeholder = r.body.querySelector('.layouts-empty');
335
+ if (!placeholder) {
336
+ placeholder = button('', 'Choose a tab', () => {
337
+ const current = options.store.getSnapshot();
338
+ const source = Object.values(current.panes).find((pane) => pane.header !== false);
339
+ const group = findNode(current.root, g.id);
340
+ if (group?.kind === 'group' && !group.panes.length)
341
+ menu.open(placeholder, source, group);
342
+ });
343
+ placeholder.className = 'layouts-empty';
344
+ placeholder.removeAttribute('title');
345
+ placeholder.disabled = !options.tabs;
346
+ placeholder.setAttribute('aria-haspopup', 'dialog');
347
+ const lines = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
348
+ lines.setAttribute('viewBox', '0 0 100 100');
349
+ lines.setAttribute('preserveAspectRatio', 'none');
350
+ lines.setAttribute('aria-hidden', 'true');
351
+ const path = doc.createElementNS(lines.namespaceURI, 'path');
352
+ path.setAttribute('d', 'M0 0 L100 100 M100 0 L0 100');
353
+ path.setAttribute('vector-effect', 'non-scaling-stroke');
354
+ lines.append(path);
355
+ placeholder.append(lines);
356
+ }
357
+ bodies.push(placeholder);
358
+ }
359
+ syncChildren(r.body, bodies);
360
+ }
361
+ function tree(node, layout, used) {
362
+ used.add(node.id);
363
+ const r = region(node);
364
+ if (node.kind === 'group')
365
+ paintGroup(node, r, layout);
366
+ else {
367
+ const a = tree(node.children[0], layout, used), b = tree(node.children[1], layout, used);
368
+ syncChildren(r.element, [a, r.divider, b]);
369
+ r.divider.setAttribute('aria-orientation', node.axis === 'horizontal' ? 'vertical' : 'horizontal');
370
+ r.divider.dataset.axis = node.axis;
371
+ r.divider.setAttribute('aria-valuenow', String(Math.round(node.ratio * 100)));
372
+ r.divider.setAttribute('aria-valuemin', '0');
373
+ r.divider.setAttribute('aria-valuemax', '100');
374
+ r.divider.setAttribute('aria-disabled', String(!resizable(node, layout)));
375
+ }
376
+ return r.element;
377
+ }
378
+ function geometry(node, x, y, width, height, layout, shared = []) {
379
+ const r = regions.get(node.id);
380
+ if (!r)
381
+ return;
382
+ if (r.element.dataset.sharedEdges !== shared.join(' ')) {
383
+ r.element.dataset.sharedEdges = shared.join(' ');
384
+ r.updateTabBar?.();
385
+ }
386
+ const b = bounds(node, layout);
387
+ width = Math.max(b.minWidth, Math.min(width, b.maxWidth));
388
+ height = Math.max(b.minHeight, Math.min(height, b.maxHeight));
389
+ Object.assign(r.element.style, {
390
+ left: `${x}px`,
391
+ top: `${y}px`,
392
+ width: `${width}px`,
393
+ height: `${height}px`,
394
+ });
395
+ if (node.kind === 'split') {
396
+ const a = bounds(node.children[0], layout), b = bounds(node.children[1], layout), horizontal = node.axis === 'horizontal', gap = node.gap ?? DIVIDER;
397
+ const [first, second] = allocate(horizontal ? width : height, node.ratio, horizontal ? a.minWidth : a.minHeight, horizontal ? a.maxWidth : a.maxHeight, horizontal ? b.minWidth : b.minHeight, horizontal ? b.maxWidth : b.maxHeight, gap);
398
+ const frozen = gap === 0 && !resizable(node, layout);
399
+ const firstEdge = horizontal ? 'right' : 'bottom';
400
+ const secondEdge = horizontal ? 'left' : 'top';
401
+ geometry(node.children[0], 0, 0, horizontal ? first : width, horizontal ? height : first, layout, [...shared.filter((edge) => edge !== firstEdge), ...(frozen ? [firstEdge] : [])]);
402
+ geometry(node.children[1], horizontal ? first + gap : 0, horizontal ? 0 : first + gap, horizontal ? second : width, horizontal ? height : second, layout, [...shared.filter((edge) => edge !== secondEdge), ...(frozen ? [secondEdge] : [])]);
403
+ r.divider.hidden = gap === 0;
404
+ // Paint a shared edge without reserving space or adding a resize target.
405
+ r.element.dataset.frozenBorder = gap === 0 && !resizable(node, layout) ? node.axis : '';
406
+ r.element.style.setProperty('--layouts-frozen-boundary', `${first}px`);
407
+ Object.assign(r.divider.style, horizontal
408
+ ? { left: `${first}px`, top: '0', width: `${gap}px`, height: `${height}px` }
409
+ : { left: '0', top: `${first}px`, width: `${width}px`, height: `${gap}px` });
410
+ }
411
+ }
412
+ function resizable(node, layout) {
413
+ if (node.kind !== 'split' || !paneIds(node).every((id) => options.store.can(id, 'resize')))
414
+ return false;
415
+ return node.children.every((child) => {
416
+ const b = bounds(child, layout);
417
+ return node.axis === 'horizontal' ? b.minWidth < b.maxWidth : b.minHeight < b.maxHeight;
418
+ });
419
+ }
420
+ // Renderer-only gaps participate in bounds/allocation without modifying Layout JSON.
421
+ function geometryLayout() {
422
+ const layout = options.store.getSnapshot();
423
+ const css = win.getComputedStyle(root);
424
+ const pixels = (property, fallback) => {
425
+ const value = parseFloat(css.getPropertyValue(property));
426
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
427
+ };
428
+ const width = pixels('--layouts-resize-handle-width', DIVIDER);
429
+ const disabledWidth = pixels('--layouts-disabled-resize-handle-width', 0);
430
+ const visit = (node) => node.kind === 'group'
431
+ ? node
432
+ : {
433
+ ...node,
434
+ gap: resizable(node, layout) ? (node.gap ?? width) : disabledWidth,
435
+ children: [visit(node.children[0]), visit(node.children[1])],
436
+ };
437
+ return { ...layout, root: visit(layout.root) };
438
+ }
439
+ function measure() {
440
+ const layout = geometryLayout();
441
+ const node = layout.maximized ? findNode(layout.root, layout.maximized) : layout.root;
442
+ if (!node)
443
+ return;
444
+ // Reparented maximized regions can temporarily overflow their old split.
445
+ // Discard those stale scrollbars before allocating the restored layout.
446
+ const overflow = stage.style.overflow;
447
+ let width, height;
448
+ try {
449
+ stage.style.overflow = 'hidden';
450
+ width = stage.clientWidth;
451
+ height = stage.clientHeight;
452
+ geometry(node, 0, 0, width, height, layout);
453
+ }
454
+ finally {
455
+ stage.style.overflow = overflow;
456
+ }
457
+ // Minimum sizes may still require real scrollbars; account for their space.
458
+ if (stage.clientWidth !== width || stage.clientHeight !== height)
459
+ geometry(node, 0, 0, stage.clientWidth, stage.clientHeight, layout);
460
+ }
461
+ function render() {
462
+ if (disposed)
463
+ return;
464
+ if (rendering) {
465
+ again = true;
466
+ return;
467
+ }
468
+ rendering = true;
469
+ try {
470
+ const focused = doc.activeElement?.dataset.focusId;
471
+ const layout = options.store.getSnapshot(), used = new Set();
472
+ const element = tree(layout.root, layout, used);
473
+ // Maximizing reparents a region; pane views remain mounted.
474
+ const visible = layout.maximized ? regions.get(layout.maximized)?.element : element;
475
+ if (visible)
476
+ syncChildren(stage, [visible]);
477
+ for (const [id, r] of regions)
478
+ if (!used.has(id)) {
479
+ r.scope.dispose();
480
+ r.element.remove();
481
+ regions.delete(id);
482
+ }
483
+ const docked = new Set(groups(layout.root).flatMap((g) => g.panes));
484
+ for (const [id, p] of panes)
485
+ if (!docked.has(id)) {
486
+ p.dispose();
487
+ panes.delete(id);
488
+ }
489
+ measure();
490
+ if (focused && doc.activeElement === doc.body)
491
+ Array.from(root.querySelectorAll('[data-focus-id]'))
492
+ .find((n) => n.dataset.focusId === focused)
493
+ ?.focus();
494
+ }
495
+ catch (e) {
496
+ error(e);
497
+ }
498
+ finally {
499
+ rendering = false;
500
+ if (again) {
501
+ again = false;
502
+ render();
503
+ }
504
+ }
505
+ }
506
+ scope.add(options.store.subscribe((event) => {
507
+ if (event.action === 'load') {
508
+ for (const [id, pane] of panes)
509
+ if (pane.failed) {
510
+ pane.dispose();
511
+ panes.delete(id);
512
+ }
513
+ }
514
+ render();
515
+ }));
516
+ const observer = new ResizeObserver(measure);
517
+ observer.observe(stage);
518
+ scope.add(() => observer.disconnect());
519
+ scope.listen(doc, 'dragend', () => {
520
+ dragId = undefined;
521
+ for (const r of regions.values())
522
+ delete r.element.dataset.drop;
523
+ });
524
+ render();
525
+ return {
526
+ setTabBar(next) {
527
+ if (disposed)
528
+ return;
529
+ validateTabBar(next);
530
+ tabBar = structuredClone(next);
531
+ for (const region of regions.values())
532
+ region.updateTabBar?.();
533
+ },
534
+ setTheme(theme) {
535
+ applyTheme(root, theme);
536
+ measure();
537
+ },
538
+ popout(id, placement) {
539
+ const opened = windows.open(id, placement);
540
+ render();
541
+ return opened;
542
+ },
543
+ returnPane(id) {
544
+ windows.returnPane(id);
545
+ },
546
+ dispose() {
547
+ if (disposed)
548
+ return;
549
+ disposed = true;
550
+ menu.dispose();
551
+ dragScope?.dispose();
552
+ scope.dispose();
553
+ windows.dispose();
554
+ for (const p of panes.values()) {
555
+ try {
556
+ p.dispose();
557
+ }
558
+ catch (e) {
559
+ error(e);
560
+ }
561
+ }
562
+ for (const r of regions.values())
563
+ r.scope.dispose();
564
+ panes.clear();
565
+ regions.clear();
566
+ root.remove();
567
+ },
568
+ };
569
+ }
570
+ //# sourceMappingURL=renderer.js.map