virtual-tree-canvas 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +2 -2
- package/src/core/dynamic-state.js +47 -0
- package/src/core/icon-registry.js +44 -0
- package/src/core/patch-batcher.js +4 -3
- package/src/core/theme-manager.js +8 -1
- package/src/core/tree-model.js +2 -1
- package/src/core/types.js +3 -1
- package/src/input/tree-view-input-controller.js +27 -4
- package/src/inspector/cell-editor-manager.js +27 -8
- package/src/renderers/tree-row-renderer.js +169 -47
- package/src/tree-view-controller.js +34 -0
package/README.md
CHANGED
|
@@ -131,7 +131,8 @@ tree.setModel(model, meta, {
|
|
|
131
131
|
presentation: 'pane',
|
|
132
132
|
flatRoot: true, // render root properties directly
|
|
133
133
|
enforceMeta: true, // fields without metadata are readonly/disabled
|
|
134
|
-
filter: true
|
|
134
|
+
filter: true, // use the header as a filter input
|
|
135
|
+
markUpdated: false // do not show update dots for local user edits
|
|
135
136
|
});
|
|
136
137
|
```
|
|
137
138
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "virtual-tree-canvas",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "High-performance Canvas2D virtual tree/table widget for very large hierarchical datasets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"demo": "python3 -m http.server 4173 --bind localhost",
|
|
32
|
-
"test": "node --test test/tree-view-controller.test.js test/theme-icons.test.js test/tree-columns.test.js test/benchmark.test.js test/tree-worker-operations.test.js test/model-inspector.test.js"
|
|
32
|
+
"test": "node --test test/tree-view-controller.test.js test/theme-icons.test.js test/tree-columns.test.js test/benchmark.test.js test/tree-worker-operations.test.js test/model-inspector.test.js test/dynamic-state.test.js"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"tree",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
2
|
+
|
|
3
|
+
const knownDynamicStateKeys = new Set([
|
|
4
|
+
'value',
|
|
5
|
+
'status',
|
|
6
|
+
'progress',
|
|
7
|
+
'pulse',
|
|
8
|
+
'color',
|
|
9
|
+
'selected',
|
|
10
|
+
'highlighted',
|
|
11
|
+
'visible',
|
|
12
|
+
'updated',
|
|
13
|
+
'updatedAt',
|
|
14
|
+
'icon',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {import('./types.js').NodeDynamicState} target
|
|
19
|
+
* @param {import('./types.js').NodeDynamicState} state
|
|
20
|
+
*/
|
|
21
|
+
export function mergeDynamicState(target, state) {
|
|
22
|
+
if (!state) return target;
|
|
23
|
+
|
|
24
|
+
if (hasOwn.call(state, 'value') && target.value !== state.value) target.value = state.value;
|
|
25
|
+
if (hasOwn.call(state, 'status') && target.status !== state.status) target.status = state.status;
|
|
26
|
+
if (hasOwn.call(state, 'progress') && target.progress !== state.progress) target.progress = state.progress;
|
|
27
|
+
if (hasOwn.call(state, 'pulse') && target.pulse !== state.pulse) target.pulse = state.pulse;
|
|
28
|
+
if (hasOwn.call(state, 'color') && target.color !== state.color) target.color = state.color;
|
|
29
|
+
if (hasOwn.call(state, 'selected') && target.selected !== state.selected) target.selected = state.selected;
|
|
30
|
+
if (hasOwn.call(state, 'highlighted') && target.highlighted !== state.highlighted) target.highlighted = state.highlighted;
|
|
31
|
+
if (hasOwn.call(state, 'visible') && target.visible !== state.visible) target.visible = state.visible;
|
|
32
|
+
if (hasOwn.call(state, 'updated') && target.updated !== state.updated) target.updated = state.updated;
|
|
33
|
+
if (hasOwn.call(state, 'updatedAt') && target.updatedAt !== state.updatedAt) target.updatedAt = state.updatedAt;
|
|
34
|
+
if (hasOwn.call(state, 'icon') && target.icon !== state.icon) target.icon = state.icon;
|
|
35
|
+
|
|
36
|
+
for (const key in state) {
|
|
37
|
+
if (!hasOwn.call(state, key) || knownDynamicStateKeys.has(key)) continue;
|
|
38
|
+
if (target[key] !== state[key]) target[key] = state[key];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return target;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {import('./types.js').NodeDynamicState} state */
|
|
45
|
+
export function cloneDynamicState(state) {
|
|
46
|
+
return mergeDynamicState({}, state);
|
|
47
|
+
}
|
|
@@ -84,6 +84,9 @@ export class IconRegistry {
|
|
|
84
84
|
this.register('control', drawControl);
|
|
85
85
|
this.register('situation', drawSituation);
|
|
86
86
|
this.register('damage', drawDamage);
|
|
87
|
+
this.register('inspector-object', drawInspectorObject);
|
|
88
|
+
this.register('inspector-array', drawInspectorArray);
|
|
89
|
+
this.register('inspector-value', drawInspectorValue);
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -420,3 +423,44 @@ function drawDamage(ctx, x, y, size, color) {
|
|
|
420
423
|
ctx.lineTo(x + size * 0.82, y + size * 0.18);
|
|
421
424
|
ctx.stroke();
|
|
422
425
|
}
|
|
426
|
+
|
|
427
|
+
function drawInspectorObject(ctx, x, y, size, color) {
|
|
428
|
+
ctx.strokeStyle = color;
|
|
429
|
+
ctx.lineWidth = 1.2;
|
|
430
|
+
const left = x + size * 0.24;
|
|
431
|
+
const top = y + size * 0.22;
|
|
432
|
+
const width = size * 0.52;
|
|
433
|
+
const height = size * 0.56;
|
|
434
|
+
ctx.strokeRect(left, top, width, height);
|
|
435
|
+
ctx.beginPath();
|
|
436
|
+
ctx.moveTo(left + width * 0.28, top);
|
|
437
|
+
ctx.lineTo(left + width * 0.28, top + height);
|
|
438
|
+
ctx.moveTo(left + width * 0.72, top);
|
|
439
|
+
ctx.lineTo(left + width * 0.72, top + height);
|
|
440
|
+
ctx.stroke();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function drawInspectorArray(ctx, x, y, size, color) {
|
|
444
|
+
ctx.strokeStyle = color;
|
|
445
|
+
ctx.lineWidth = 1.2;
|
|
446
|
+
const cx = x + size * 0.5;
|
|
447
|
+
const cy = y + size * 0.5;
|
|
448
|
+
ctx.beginPath();
|
|
449
|
+
ctx.arc(cx - size * 0.18, cy, size * 0.18, 0, Math.PI * 2);
|
|
450
|
+
ctx.arc(cx + size * 0.18, cy, size * 0.18, 0, Math.PI * 2);
|
|
451
|
+
ctx.stroke();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function drawInspectorValue(ctx, x, y, size, color) {
|
|
455
|
+
const cx = x + size * 0.5;
|
|
456
|
+
const cy = y + size * 0.5;
|
|
457
|
+
ctx.strokeStyle = color;
|
|
458
|
+
ctx.fillStyle = color;
|
|
459
|
+
ctx.lineWidth = 1.25;
|
|
460
|
+
ctx.beginPath();
|
|
461
|
+
ctx.arc(cx, cy, size * 0.3, 0, Math.PI * 2);
|
|
462
|
+
ctx.stroke();
|
|
463
|
+
ctx.beginPath();
|
|
464
|
+
ctx.arc(cx, cy, size * 0.11, 0, Math.PI * 2);
|
|
465
|
+
ctx.fill();
|
|
466
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cloneDynamicState, mergeDynamicState } from './dynamic-state.js';
|
|
2
|
+
|
|
1
3
|
export class PatchBatcher {
|
|
2
4
|
constructor() {
|
|
3
5
|
/** @type {Map<string, import('./types.js').NodeDynamicState>} */
|
|
@@ -7,8 +9,8 @@ export class PatchBatcher {
|
|
|
7
9
|
/** @param {string} id @param {import('./types.js').NodeDynamicState} state */
|
|
8
10
|
set(id, state) {
|
|
9
11
|
const current = this.pending.get(id);
|
|
10
|
-
if (current)
|
|
11
|
-
else this.pending.set(id,
|
|
12
|
+
if (current) mergeDynamicState(current, state);
|
|
13
|
+
else this.pending.set(id, cloneDynamicState(state));
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/** @param {Array<import('./types.js').DynamicPatch>} patches */
|
|
@@ -26,4 +28,3 @@ export class PatchBatcher {
|
|
|
26
28
|
return this.pending.size;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
|
-
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export const darkTheme = {
|
|
2
2
|
rowHeight: 28,
|
|
3
3
|
indentWidth: 18,
|
|
4
|
-
font: '12px system-ui, sans-serif',
|
|
4
|
+
font: '12px Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
5
|
+
monoFont: '12px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace',
|
|
5
6
|
colors: {
|
|
6
7
|
background: '#0b1020',
|
|
7
8
|
row: '#0b1020',
|
|
@@ -37,6 +38,12 @@ export const darkTheme = {
|
|
|
37
38
|
warning: { icon: 'warning', color: '#facc15' },
|
|
38
39
|
error: { icon: 'error', color: '#ef4444' },
|
|
39
40
|
task: { icon: 'task', color: '#f97316' },
|
|
41
|
+
object: { icon: 'inspector-object', color: '#7dd3fc' },
|
|
42
|
+
array: { icon: 'inspector-array', color: '#a78bfa' },
|
|
43
|
+
string: { icon: 'inspector-value', color: '#94a3b8' },
|
|
44
|
+
number: { icon: 'inspector-value', color: '#34d399' },
|
|
45
|
+
boolean: { icon: 'inspector-value', color: '#22c55e' },
|
|
46
|
+
null: { icon: 'inspector-value', color: '#64748b' },
|
|
40
47
|
},
|
|
41
48
|
statuses: {
|
|
42
49
|
0: { label: 'OK', color: '#22c55e' },
|
package/src/core/tree-model.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TreeIndex } from './tree-index.js';
|
|
2
|
+
import { mergeDynamicState } from './dynamic-state.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Owns structural nodes, expanded state, and per-node dynamic state.
|
|
@@ -68,7 +69,7 @@ export class TreeModel extends EventTarget {
|
|
|
68
69
|
for (const patch of patches) {
|
|
69
70
|
const current = this.dynamicState.get(patch.id);
|
|
70
71
|
if (!current) continue;
|
|
71
|
-
|
|
72
|
+
mergeDynamicState(current, patch.state);
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
package/src/core/types.js
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* @property {boolean} [selected]
|
|
21
21
|
* @property {boolean} [highlighted]
|
|
22
22
|
* @property {boolean} [visible]
|
|
23
|
+
* @property {boolean} [updated]
|
|
24
|
+
* @property {number} [updatedAt]
|
|
25
|
+
* @property {string} [icon]
|
|
23
26
|
*/
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -41,4 +44,3 @@
|
|
|
41
44
|
* @property {string} id
|
|
42
45
|
* @property {NodeDynamicState} state
|
|
43
46
|
*/
|
|
44
|
-
|
|
@@ -61,18 +61,24 @@ export class TreeViewInputController {
|
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
const hit = this.#hitTest(event);
|
|
64
|
-
this.canvas.style.cursor = hit
|
|
64
|
+
this.canvas.style.cursor = cursorForHit(hit);
|
|
65
65
|
const id = hit?.row?.nodeId ?? null;
|
|
66
|
-
|
|
66
|
+
const key = hitKey(hit);
|
|
67
|
+
if (id === this.hoveredId && key === this.hoveredHitKey) return;
|
|
67
68
|
this.hoveredId = id;
|
|
68
|
-
this.
|
|
69
|
+
this.hoveredHitKey = key;
|
|
70
|
+
if (this.controller?.setHoverHit) this.controller.setHoverHit(hit);
|
|
71
|
+
else this.controller?.setHover(id);
|
|
69
72
|
this.onHoverChanged?.(id);
|
|
70
73
|
};
|
|
71
74
|
|
|
72
75
|
#onMouseLeave = () => {
|
|
73
76
|
if (!this.resizeDrag) this.canvas.style.cursor = '';
|
|
74
77
|
this.hoveredId = null;
|
|
75
|
-
this.
|
|
78
|
+
this.hoveredHitKey = null;
|
|
79
|
+
this.controller?.setActiveHit?.(null);
|
|
80
|
+
if (this.controller?.setHoverHit) this.controller.setHoverHit(null);
|
|
81
|
+
else this.controller?.setHover(null);
|
|
76
82
|
this.onHoverChanged?.(null);
|
|
77
83
|
};
|
|
78
84
|
|
|
@@ -118,6 +124,7 @@ export class TreeViewInputController {
|
|
|
118
124
|
|
|
119
125
|
#onMouseDown = (event) => {
|
|
120
126
|
const hit = this.#hitTest(event);
|
|
127
|
+
this.controller?.setActiveHit?.(hit);
|
|
121
128
|
if (this.cellEditor?.handlePointerDown(event, hit)) {
|
|
122
129
|
event.preventDefault();
|
|
123
130
|
return;
|
|
@@ -136,6 +143,7 @@ export class TreeViewInputController {
|
|
|
136
143
|
#onMouseUp = () => {
|
|
137
144
|
this.resizeDrag = null;
|
|
138
145
|
this.canvas.style.cursor = '';
|
|
146
|
+
this.controller?.setActiveHit?.(null);
|
|
139
147
|
};
|
|
140
148
|
|
|
141
149
|
#onKeyDown = (event) => {
|
|
@@ -234,3 +242,18 @@ export class TreeViewInputController {
|
|
|
234
242
|
return { area: 'row', row, x, y, part };
|
|
235
243
|
}
|
|
236
244
|
}
|
|
245
|
+
|
|
246
|
+
function cursorForHit(hit) {
|
|
247
|
+
if (hit?.area === 'header' && hit.part === 'resize') return 'col-resize';
|
|
248
|
+
if (hit?.area === 'header' && hit.part === 'filter') return 'text';
|
|
249
|
+
if (hit?.area !== 'row') return '';
|
|
250
|
+
if (hit.part === 'button' || hit.part === 'checkbox' || hit.part === 'arrayAdd' || hit.part === 'arrayRemove' || hit.part === 'chevron') return 'pointer';
|
|
251
|
+
if (hit.part === 'editor' || hit.part === 'number') return 'text';
|
|
252
|
+
if (hit.part === 'range') return 'ew-resize';
|
|
253
|
+
return '';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function hitKey(hit) {
|
|
257
|
+
if (!hit?.row || !hit.column) return null;
|
|
258
|
+
return `${hit.row.nodeId}:${hit.column.id}:${hit.part}`;
|
|
259
|
+
}
|
|
@@ -78,6 +78,7 @@ export class CellEditorManager {
|
|
|
78
78
|
const rect = this.#clampRectToHost(this.#overlayRect(hit), 4);
|
|
79
79
|
const hostRect = this.host.getBoundingClientRect();
|
|
80
80
|
const element = createEditorElement(data);
|
|
81
|
+
element.className = `vtc-editor vtc-editor-${data.editorType}`;
|
|
81
82
|
Object.assign(element.style, {
|
|
82
83
|
position: 'absolute',
|
|
83
84
|
left: `${rect.x - hostRect.left}px`,
|
|
@@ -90,12 +91,14 @@ export class CellEditorManager {
|
|
|
90
91
|
boxSizing: 'border-box',
|
|
91
92
|
margin: '0',
|
|
92
93
|
outline: 'none',
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
appearance: 'none',
|
|
95
|
+
border: '1px solid rgba(56, 189, 248, 0.65)',
|
|
96
|
+
borderRadius: '6px',
|
|
97
|
+
background: '#0f172a',
|
|
96
98
|
color: '#e5e7eb',
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
boxShadow: '0 0 0 1px rgba(15, 23, 42, 0.8), 0 8px 20px rgba(0, 0, 0, 0.28)',
|
|
100
|
+
font: editorFont(data),
|
|
101
|
+
padding: data.editorType === 'color' ? '0 2px' : data.editorType === 'select' ? '0 22px 0 8px' : '0 8px',
|
|
99
102
|
textAlign: data.editorType === 'number' || data.editorType === 'range' ? 'right' : 'left',
|
|
100
103
|
});
|
|
101
104
|
const commit = () => {
|
|
@@ -154,7 +157,7 @@ export class CellEditorManager {
|
|
|
154
157
|
borderRadius: '4px',
|
|
155
158
|
background: '#0b1020',
|
|
156
159
|
color: '#e5e7eb',
|
|
157
|
-
font:
|
|
160
|
+
font: SANS_FONT,
|
|
158
161
|
padding: '0 8px',
|
|
159
162
|
});
|
|
160
163
|
element.addEventListener('input', () => this.controller.setFilter(element.value));
|
|
@@ -267,7 +270,10 @@ function createEditorElement(data) {
|
|
|
267
270
|
return select;
|
|
268
271
|
}
|
|
269
272
|
const input = document.createElement('input');
|
|
270
|
-
input.type = data.editorType === 'color' ? 'color' :
|
|
273
|
+
input.type = data.editorType === 'color' ? 'color' : 'text';
|
|
274
|
+
if (data.editorType === 'number' || data.editorType === 'range') {
|
|
275
|
+
input.inputMode = data.meta.integer ? 'numeric' : 'decimal';
|
|
276
|
+
}
|
|
271
277
|
input.value = data.value ?? '';
|
|
272
278
|
if (data.meta.min !== undefined) input.min = data.meta.min;
|
|
273
279
|
if (data.meta.max !== undefined) input.max = data.meta.max;
|
|
@@ -288,7 +294,13 @@ function ensureOverlayHost(host) {
|
|
|
288
294
|
}
|
|
289
295
|
|
|
290
296
|
function parseEditorValue(element, data) {
|
|
291
|
-
if (data.editorType === 'number' || data.editorType === 'range')
|
|
297
|
+
if (data.editorType === 'number' || data.editorType === 'range') {
|
|
298
|
+
const value = data.meta.integer ? Number.parseInt(element.value, 10) : Number(element.value);
|
|
299
|
+
if (!Number.isFinite(value)) return data.value;
|
|
300
|
+
const min = Number.isFinite(data.meta.min) ? data.meta.min : -Infinity;
|
|
301
|
+
const max = Number.isFinite(data.meta.max) ? data.meta.max : Infinity;
|
|
302
|
+
return Math.max(min, Math.min(max, value));
|
|
303
|
+
}
|
|
292
304
|
if (data.editorType === 'select') {
|
|
293
305
|
const values = Object.values(data.meta.options ?? {});
|
|
294
306
|
const match = values.find((value) => String(value) === element.value);
|
|
@@ -296,3 +308,10 @@ function parseEditorValue(element, data) {
|
|
|
296
308
|
}
|
|
297
309
|
return element.value;
|
|
298
310
|
}
|
|
311
|
+
|
|
312
|
+
const SANS_FONT = '12px Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif';
|
|
313
|
+
const MONO_FONT = '12px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace';
|
|
314
|
+
|
|
315
|
+
function editorFont(data) {
|
|
316
|
+
return data.editorType === 'number' || data.editorType === 'range' ? MONO_FONT : SANS_FONT;
|
|
317
|
+
}
|
|
@@ -78,7 +78,7 @@ export class TreeRowRenderer {
|
|
|
78
78
|
drawTruncatedText(ctx, column.label, column.x + 10, viewport.headerHeight / 2, column.width - 20);
|
|
79
79
|
}
|
|
80
80
|
if (sort?.columnId === column.id && sort.direction) {
|
|
81
|
-
|
|
81
|
+
this.#drawSortIndicator(ctx, column.x + column.width - 16, viewport.headerHeight / 2, sort.direction, theme);
|
|
82
82
|
}
|
|
83
83
|
ctx.strokeStyle = colors.border;
|
|
84
84
|
ctx.beginPath();
|
|
@@ -112,6 +112,22 @@ export class TreeRowRenderer {
|
|
|
112
112
|
drawTruncatedText(ctx, filterQuery || 'Filter inspector', x + 8, viewport.headerHeight / 2, width - 16);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
#drawSortIndicator(ctx, x, y, direction, theme) {
|
|
116
|
+
ctx.fillStyle = theme.colors.focus;
|
|
117
|
+
ctx.beginPath();
|
|
118
|
+
if (direction === 'asc') {
|
|
119
|
+
ctx.moveTo(x, y + 3);
|
|
120
|
+
ctx.lineTo(x + 5, y - 3);
|
|
121
|
+
ctx.lineTo(x + 10, y + 3);
|
|
122
|
+
} else {
|
|
123
|
+
ctx.moveTo(x, y - 3);
|
|
124
|
+
ctx.lineTo(x + 5, y + 3);
|
|
125
|
+
ctx.lineTo(x + 10, y - 3);
|
|
126
|
+
}
|
|
127
|
+
ctx.closePath();
|
|
128
|
+
ctx.fill();
|
|
129
|
+
}
|
|
130
|
+
|
|
115
131
|
#drawRows(ctx) {
|
|
116
132
|
const { rows, visibleRange, viewport } = this.scene;
|
|
117
133
|
this.renderedRows = visibleRange.count;
|
|
@@ -178,7 +194,7 @@ export class TreeRowRenderer {
|
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
#drawRow(ctx, row) {
|
|
181
|
-
const { columns, nodes, dynamicState, selection, hoverNodeId, focusNodeId, searchMatches, theme, viewport } = this.scene;
|
|
197
|
+
const { columns, nodes, dynamicState, selection, hoverNodeId, hoverPart, activeNodeId, activePart, focusNodeId, searchMatches, theme, viewport } = this.scene;
|
|
182
198
|
const node = nodes[row.nodeIndex];
|
|
183
199
|
const state = dynamicState.get(row.nodeId) ?? {};
|
|
184
200
|
const style = resolveNodeStyle(theme, node, state);
|
|
@@ -198,7 +214,19 @@ export class TreeRowRenderer {
|
|
|
198
214
|
|
|
199
215
|
for (const column of columns) {
|
|
200
216
|
const rect = { x: column.x, y, width: column.width, height: row.height };
|
|
201
|
-
this.#drawCell(ctx, {
|
|
217
|
+
this.#drawCell(ctx, {
|
|
218
|
+
node,
|
|
219
|
+
state,
|
|
220
|
+
row,
|
|
221
|
+
column,
|
|
222
|
+
rect,
|
|
223
|
+
theme,
|
|
224
|
+
style,
|
|
225
|
+
selected,
|
|
226
|
+
hovered,
|
|
227
|
+
hoverPart: hoverNodeId === row.nodeId ? hoverPart : null,
|
|
228
|
+
activePart: activeNodeId === row.nodeId ? activePart : null,
|
|
229
|
+
});
|
|
202
230
|
ctx.strokeStyle = colors.border;
|
|
203
231
|
ctx.beginPath();
|
|
204
232
|
ctx.moveTo(column.x + column.width + 0.5, y);
|
|
@@ -235,7 +263,7 @@ export class TreeRowRenderer {
|
|
|
235
263
|
else this.#drawTextCell(ctx, cell);
|
|
236
264
|
}
|
|
237
265
|
|
|
238
|
-
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style }) {
|
|
266
|
+
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style, hovered, hoverPart, activePart }) {
|
|
239
267
|
const visibleRight = this.scene.viewport.scrollX + this.scene.viewport.viewportWidth;
|
|
240
268
|
rect = { ...rect, width: Math.max(1, Math.min(rect.x + rect.width, visibleRight) - rect.x) };
|
|
241
269
|
const data = node.data ?? {};
|
|
@@ -272,13 +300,22 @@ export class TreeRowRenderer {
|
|
|
272
300
|
if (data.valueType === 'array') {
|
|
273
301
|
ctx.fillStyle = colors.textMuted;
|
|
274
302
|
drawTruncatedText(ctx, data.valueText, editorX, cy, Math.max(20, editorWidth - 58));
|
|
275
|
-
this.#drawSmallButton(ctx, rect.x + rect.width - 54, rect.y + 5, 22, rect.height - 10, '+', theme
|
|
276
|
-
|
|
303
|
+
this.#drawSmallButton(ctx, rect.x + rect.width - 54, rect.y + 5, 22, rect.height - 10, '+', theme, {
|
|
304
|
+
hovered: hovered && hoverPart === 'arrayAdd',
|
|
305
|
+
active: activePart === 'arrayAdd',
|
|
306
|
+
});
|
|
307
|
+
this.#drawSmallButton(ctx, rect.x + rect.width - 28, rect.y + 5, 22, rect.height - 10, '-', theme, {
|
|
308
|
+
hovered: hovered && hoverPart === 'arrayRemove',
|
|
309
|
+
active: activePart === 'arrayRemove',
|
|
310
|
+
});
|
|
277
311
|
} else {
|
|
278
312
|
this.#drawInspectorValueCell(ctx, {
|
|
279
313
|
node,
|
|
280
314
|
rect: { x: editorX, y: rect.y, width: editorWidth, height: rect.height },
|
|
281
315
|
theme,
|
|
316
|
+
hovered,
|
|
317
|
+
hoverPart,
|
|
318
|
+
activePart,
|
|
282
319
|
suppressUpdatedMarker: true,
|
|
283
320
|
});
|
|
284
321
|
}
|
|
@@ -286,7 +323,7 @@ export class TreeRowRenderer {
|
|
|
286
323
|
ctx.globalAlpha = 1;
|
|
287
324
|
}
|
|
288
325
|
|
|
289
|
-
#drawInspectorValueCell(ctx, { node, rect, theme, suppressUpdatedMarker = false }) {
|
|
326
|
+
#drawInspectorValueCell(ctx, { node, rect, theme, hovered = false, hoverPart = null, activePart = null, suppressUpdatedMarker = false }) {
|
|
290
327
|
const data = node.data ?? {};
|
|
291
328
|
const meta = data.meta ?? {};
|
|
292
329
|
const disabled = data.disabled;
|
|
@@ -301,9 +338,12 @@ export class TreeRowRenderer {
|
|
|
301
338
|
ctx.globalAlpha = disabled ? DISABLED_ALPHA : 1;
|
|
302
339
|
|
|
303
340
|
if (data.editorType === 'checkbox') {
|
|
304
|
-
this.#drawCheckbox(ctx, x, rect.y + rect.height / 2 -
|
|
341
|
+
this.#drawCheckbox(ctx, x, rect.y + rect.height / 2 - 8, Boolean(data.value), theme);
|
|
305
342
|
} else if (data.editorType === 'range') {
|
|
306
|
-
this.#drawInspectorRange(ctx, x, rect.y + rect.height / 2 - 4, width, data, theme
|
|
343
|
+
this.#drawInspectorRange(ctx, x, rect.y + rect.height / 2 - 4, width, data, theme, {
|
|
344
|
+
hoveredNumber: hovered && hoverPart === 'number',
|
|
345
|
+
activeNumber: activePart === 'number',
|
|
346
|
+
});
|
|
307
347
|
} else if (data.editorType === 'color') {
|
|
308
348
|
ctx.fillStyle = String(data.value || '#000000');
|
|
309
349
|
ctx.fillRect(x, y + 2, 28, height - 4);
|
|
@@ -311,20 +351,25 @@ export class TreeRowRenderer {
|
|
|
311
351
|
ctx.strokeRect(x + 0.5, y + 2.5, 28, height - 4);
|
|
312
352
|
this.#drawMutedText(ctx, String(data.value ?? ''), x + 38, rect.y + rect.height / 2, width - 38, theme);
|
|
313
353
|
} else if (data.editorType === 'button') {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
354
|
+
const buttonWidth = meta.fullWidthButton ? width : Math.min(width, 140);
|
|
355
|
+
this.#drawControlSurface(ctx, x, y, buttonWidth, height, theme, {
|
|
356
|
+
hovered: hovered && hoverPart === 'button',
|
|
357
|
+
active: activePart === 'button',
|
|
358
|
+
disabled: readonly || disabled,
|
|
359
|
+
});
|
|
317
360
|
ctx.fillStyle = theme.colors.text;
|
|
318
361
|
ctx.textAlign = 'center';
|
|
319
|
-
drawTruncatedText(ctx, meta.button ?? node.label, x +
|
|
362
|
+
drawTruncatedText(ctx, meta.button ?? node.label, x + buttonWidth / 2, rect.y + rect.height / 2, Math.max(10, buttonWidth - 12));
|
|
320
363
|
ctx.textAlign = 'left';
|
|
321
364
|
} else if (data.editorType === 'select') {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
365
|
+
const selectWidth = Math.min(width, 180);
|
|
366
|
+
this.#drawControlSurface(ctx, x, y, selectWidth, height, theme, {
|
|
367
|
+
hovered: hovered && hoverPart === 'editor',
|
|
368
|
+
active: activePart === 'editor',
|
|
369
|
+
disabled: readonly || disabled,
|
|
370
|
+
});
|
|
371
|
+
this.#drawMutedText(ctx, data.valueText, x + 8, rect.y + rect.height / 2, selectWidth - 30, theme);
|
|
372
|
+
this.#drawSelectChevron(ctx, x + selectWidth - 18, rect.y + rect.height / 2, theme, disabled);
|
|
328
373
|
} else {
|
|
329
374
|
this.#drawMutedText(ctx, data.valueText, x, rect.y + rect.height / 2, width, theme, readonly);
|
|
330
375
|
}
|
|
@@ -348,7 +393,7 @@ export class TreeRowRenderer {
|
|
|
348
393
|
}
|
|
349
394
|
|
|
350
395
|
#drawInspectorTypeCell(ctx, { node, rect, theme }) {
|
|
351
|
-
this.#drawMutedText(ctx, node.data?.valueType ?? '', rect.x + 10, rect.y + rect.height / 2, rect.width - 20, theme);
|
|
396
|
+
this.#drawMutedText(ctx, node.data?.valueType ?? '', rect.x + 10, rect.y + rect.height / 2, rect.width - 20, theme, false, theme.monoFont);
|
|
352
397
|
}
|
|
353
398
|
|
|
354
399
|
#drawInspectorDescriptionCell(ctx, { node, rect, theme }) {
|
|
@@ -356,20 +401,18 @@ export class TreeRowRenderer {
|
|
|
356
401
|
}
|
|
357
402
|
|
|
358
403
|
#drawCheckbox(ctx, x, y, checked, theme) {
|
|
359
|
-
ctx
|
|
360
|
-
ctx.
|
|
361
|
-
|
|
362
|
-
ctx.strokeStyle = theme.colors.progressFill;
|
|
363
|
-
ctx.lineWidth = 2;
|
|
364
|
-
ctx.beginPath();
|
|
365
|
-
ctx.moveTo(x + 3, y + 7);
|
|
366
|
-
ctx.lineTo(x + 6, y + 11);
|
|
367
|
-
ctx.lineTo(x + 12, y + 3);
|
|
404
|
+
roundRect(ctx, x, y, 16, 16, 3);
|
|
405
|
+
ctx.fillStyle = theme.colors.progressTrack;
|
|
406
|
+
ctx.fill();
|
|
407
|
+
ctx.strokeStyle = checked ? theme.colors.progressFill : theme.colors.textMuted;
|
|
368
408
|
ctx.stroke();
|
|
369
|
-
|
|
409
|
+
if (!checked) return;
|
|
410
|
+
ctx.fillStyle = theme.colors.progressFill;
|
|
411
|
+
roundRect(ctx, x + 4, y + 4, 8, 8, 1.5);
|
|
412
|
+
ctx.fill();
|
|
370
413
|
}
|
|
371
414
|
|
|
372
|
-
#drawInspectorRange(ctx, x, y, width, data, theme) {
|
|
415
|
+
#drawInspectorRange(ctx, x, y, width, data, theme, state = {}) {
|
|
373
416
|
const meta = data.meta ?? {};
|
|
374
417
|
const min = meta.min ?? 0;
|
|
375
418
|
const max = meta.max ?? 100;
|
|
@@ -382,19 +425,20 @@ export class TreeRowRenderer {
|
|
|
382
425
|
ctx.fillRect(x, y, barWidth, 8);
|
|
383
426
|
ctx.fillStyle = theme.colors.progressFill;
|
|
384
427
|
ctx.fillRect(x, y, barWidth * ratio, 8);
|
|
385
|
-
ctx
|
|
386
|
-
|
|
387
|
-
|
|
428
|
+
this.#drawControlSurface(ctx, x + barWidth + gap, y - 6, valueWidth, 20, theme, {
|
|
429
|
+
hovered: Boolean(state.hoveredNumber),
|
|
430
|
+
active: Boolean(state.activeNumber),
|
|
431
|
+
});
|
|
388
432
|
ctx.fillStyle = theme.colors.text;
|
|
433
|
+
ctx.font = theme.monoFont ?? theme.font;
|
|
389
434
|
ctx.textAlign = 'right';
|
|
390
435
|
drawTruncatedText(ctx, String(data.valueText ?? ''), x + barWidth + gap + valueWidth - 6, y + 4, valueWidth - 10);
|
|
391
436
|
ctx.textAlign = 'left';
|
|
437
|
+
ctx.font = theme.font;
|
|
392
438
|
}
|
|
393
439
|
|
|
394
|
-
#drawSmallButton(ctx, x, y, width, height, label, theme) {
|
|
395
|
-
ctx
|
|
396
|
-
roundRect(ctx, x, y, width, height, 3);
|
|
397
|
-
ctx.fill();
|
|
440
|
+
#drawSmallButton(ctx, x, y, width, height, label, theme, state = {}) {
|
|
441
|
+
this.#drawControlSurface(ctx, x, y, width, height, theme, state);
|
|
398
442
|
ctx.fillStyle = theme.colors.text;
|
|
399
443
|
ctx.textAlign = 'center';
|
|
400
444
|
ctx.textBaseline = 'middle';
|
|
@@ -402,9 +446,34 @@ export class TreeRowRenderer {
|
|
|
402
446
|
ctx.textAlign = 'left';
|
|
403
447
|
}
|
|
404
448
|
|
|
405
|
-
#
|
|
449
|
+
#drawControlSurface(ctx, x, y, width, height, theme, { hovered = false, active = false, disabled = false } = {}) {
|
|
450
|
+
const colors = theme.colors;
|
|
451
|
+
ctx.fillStyle = disabled
|
|
452
|
+
? colors.progressTrack
|
|
453
|
+
: active
|
|
454
|
+
? mixColor(colors.rowHover, colors.focus, 0.36)
|
|
455
|
+
: hovered
|
|
456
|
+
? mixColor(colors.rowHover, colors.focus, 0.18)
|
|
457
|
+
: colors.progressTrack;
|
|
458
|
+
roundRect(ctx, x, y, width, height, 5);
|
|
459
|
+
ctx.fill();
|
|
460
|
+
ctx.strokeStyle = active ? colors.focus : hovered ? mixColor(colors.border, colors.focus, 0.5) : colors.border;
|
|
461
|
+
ctx.stroke();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
#drawSelectChevron(ctx, x, y, theme, disabled = false) {
|
|
465
|
+
ctx.fillStyle = disabled ? theme.colors.textMuted : theme.colors.chevron;
|
|
466
|
+
ctx.beginPath();
|
|
467
|
+
ctx.moveTo(x - 4, y - 2);
|
|
468
|
+
ctx.lineTo(x + 4, y - 2);
|
|
469
|
+
ctx.lineTo(x, y + 3);
|
|
470
|
+
ctx.closePath();
|
|
471
|
+
ctx.fill();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#drawMutedText(ctx, text, x, y, width, theme, readonly = false, font = null) {
|
|
406
475
|
ctx.fillStyle = readonly ? theme.colors.textMuted : theme.colors.text;
|
|
407
|
-
ctx.font = theme.font;
|
|
476
|
+
ctx.font = font ?? theme.font;
|
|
408
477
|
ctx.textBaseline = 'middle';
|
|
409
478
|
ctx.textAlign = 'left';
|
|
410
479
|
drawTruncatedText(ctx, String(text ?? ''), x, y, Math.max(10, width));
|
|
@@ -431,7 +500,7 @@ export class TreeRowRenderer {
|
|
|
431
500
|
roundRect(ctx, x, y, badgeWidth, 16, 8);
|
|
432
501
|
ctx.fill();
|
|
433
502
|
ctx.fillStyle = theme.colors.badgeText;
|
|
434
|
-
ctx.font = '10px
|
|
503
|
+
ctx.font = '10px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace';
|
|
435
504
|
ctx.textAlign = 'center';
|
|
436
505
|
ctx.textBaseline = 'middle';
|
|
437
506
|
drawTruncatedText(ctx, style.status.label, x + badgeWidth / 2, y + 8, badgeWidth - 8);
|
|
@@ -454,7 +523,7 @@ export class TreeRowRenderer {
|
|
|
454
523
|
if (column.kind === 'updated' && typeof value === 'number') value = formatTime(value);
|
|
455
524
|
if (typeof value === 'number') value = Math.round(value).toString();
|
|
456
525
|
ctx.fillStyle = column.kind === 'type' ? resolveNodeStyle(theme, node, state).color : theme.colors.textMuted;
|
|
457
|
-
ctx.font = theme.font;
|
|
526
|
+
ctx.font = column.kind === 'type' || column.kind === 'updated' || typeof value === 'number' ? theme.monoFont ?? theme.font : theme.font;
|
|
458
527
|
ctx.textBaseline = 'middle';
|
|
459
528
|
ctx.textAlign = column.align;
|
|
460
529
|
const x = column.align === 'right' ? rect.x + rect.width - 10 : column.align === 'center' ? rect.x + rect.width / 2 : rect.x + 10;
|
|
@@ -540,6 +609,10 @@ function clamp01(value) {
|
|
|
540
609
|
|
|
541
610
|
function roundRect(ctx, x, y, width, height, radius) {
|
|
542
611
|
ctx.beginPath();
|
|
612
|
+
if (typeof ctx.roundRect === 'function') {
|
|
613
|
+
ctx.roundRect(x, y, width, height, radius);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
543
616
|
ctx.moveTo(x + radius, y);
|
|
544
617
|
ctx.lineTo(x + width - radius, y);
|
|
545
618
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
@@ -551,6 +624,23 @@ function roundRect(ctx, x, y, width, height, radius) {
|
|
|
551
624
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
552
625
|
}
|
|
553
626
|
|
|
627
|
+
function mixColor(a, b, amount) {
|
|
628
|
+
const from = parseHexColor(a);
|
|
629
|
+
const to = parseHexColor(b);
|
|
630
|
+
if (!from || !to) return amount >= 0.5 ? b : a;
|
|
631
|
+
const t = clamp01(amount);
|
|
632
|
+
const value = from.map((channel, index) => Math.round(channel + (to[index] - channel) * t));
|
|
633
|
+
return `rgb(${value[0]}, ${value[1]}, ${value[2]})`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function parseHexColor(value) {
|
|
637
|
+
const text = String(value ?? '').trim();
|
|
638
|
+
const match = /^#([0-9a-f]{6})$/i.exec(text);
|
|
639
|
+
if (!match) return null;
|
|
640
|
+
const number = Number.parseInt(match[1], 16);
|
|
641
|
+
return [(number >> 16) & 255, (number >> 8) & 255, number & 255];
|
|
642
|
+
}
|
|
643
|
+
|
|
554
644
|
function drawTruncatedText(ctx, text, x, y, maxWidth) {
|
|
555
645
|
const value = String(text ?? '');
|
|
556
646
|
const width = Math.max(0, maxWidth);
|
|
@@ -558,22 +648,54 @@ function drawTruncatedText(ctx, text, x, y, maxWidth) {
|
|
|
558
648
|
ctx.fillText(fitText(ctx, value, width), x, y);
|
|
559
649
|
}
|
|
560
650
|
|
|
651
|
+
const TEXT_FIT_CACHE_LIMIT = 6000;
|
|
652
|
+
const textFitCache = new Map();
|
|
653
|
+
|
|
561
654
|
function fitText(ctx, text, maxWidth) {
|
|
562
|
-
|
|
655
|
+
const safeWidth = Math.max(0, Math.floor(maxWidth));
|
|
656
|
+
const cacheKey = `${ctx.font}\u0000${safeWidth}\u0000${text}`;
|
|
657
|
+
const cached = textFitCache.get(cacheKey);
|
|
658
|
+
if (cached !== undefined) return cached;
|
|
659
|
+
|
|
660
|
+
let result = text;
|
|
661
|
+
if (ctx.measureText(text).width <= safeWidth) {
|
|
662
|
+
setTextFitCache(cacheKey, result);
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
563
665
|
const ellipsis = '...';
|
|
564
666
|
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
|
565
|
-
if (ellipsisWidth >
|
|
667
|
+
if (ellipsisWidth > safeWidth) {
|
|
668
|
+
setTextFitCache(cacheKey, '');
|
|
669
|
+
return '';
|
|
670
|
+
}
|
|
566
671
|
let low = 0;
|
|
567
672
|
let high = text.length;
|
|
568
673
|
while (low < high) {
|
|
569
674
|
const mid = Math.ceil((low + high) / 2);
|
|
570
675
|
const candidate = text.slice(0, mid);
|
|
571
|
-
if (ctx.measureText(candidate).width + ellipsisWidth <=
|
|
676
|
+
if (ctx.measureText(candidate).width + ellipsisWidth <= safeWidth) low = mid;
|
|
572
677
|
else high = mid - 1;
|
|
573
678
|
}
|
|
574
|
-
|
|
679
|
+
result = `${text.slice(0, low)}${ellipsis}`;
|
|
680
|
+
setTextFitCache(cacheKey, result);
|
|
681
|
+
return result;
|
|
575
682
|
}
|
|
576
683
|
|
|
684
|
+
function setTextFitCache(key, value) {
|
|
685
|
+
textFitCache.set(key, value);
|
|
686
|
+
if (textFitCache.size <= TEXT_FIT_CACHE_LIMIT) return;
|
|
687
|
+
const firstKey = textFitCache.keys().next().value;
|
|
688
|
+
if (firstKey !== undefined) textFitCache.delete(firstKey);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const timeFormatter = new Intl.DateTimeFormat([], {
|
|
692
|
+
hour: '2-digit',
|
|
693
|
+
minute: '2-digit',
|
|
694
|
+
second: '2-digit',
|
|
695
|
+
});
|
|
696
|
+
|
|
577
697
|
function formatTime(value) {
|
|
578
|
-
|
|
698
|
+
const time = typeof value === 'number' ? value : Number(value);
|
|
699
|
+
if (!Number.isFinite(time)) return '';
|
|
700
|
+
return timeFormatter.format(time);
|
|
579
701
|
}
|
|
@@ -44,6 +44,9 @@ export class TreeViewController {
|
|
|
44
44
|
this.searchHighlights = new Set();
|
|
45
45
|
this.filterQuery = '';
|
|
46
46
|
this.hoverId = null;
|
|
47
|
+
this.hoverPart = null;
|
|
48
|
+
this.activeId = null;
|
|
49
|
+
this.activePart = null;
|
|
47
50
|
this.focusedId = null;
|
|
48
51
|
this.anchorRowIndex = null;
|
|
49
52
|
this.lastPatchCount = 0;
|
|
@@ -63,6 +66,7 @@ export class TreeViewController {
|
|
|
63
66
|
this.canvas = canvas;
|
|
64
67
|
this.renderer.initialize(canvas);
|
|
65
68
|
this.renderer.setScene(this.scene);
|
|
69
|
+
this.#resizeToCanvasClientSize();
|
|
66
70
|
this.#observeCanvasSize();
|
|
67
71
|
return this;
|
|
68
72
|
}
|
|
@@ -469,9 +473,27 @@ export class TreeViewController {
|
|
|
469
473
|
setHover(nodeId) {
|
|
470
474
|
if (this.hoverId === nodeId) return;
|
|
471
475
|
this.hoverId = nodeId;
|
|
476
|
+
this.hoverPart = null;
|
|
472
477
|
this.events.emit('nodehover', { nodeId });
|
|
473
478
|
}
|
|
474
479
|
|
|
480
|
+
setHoverHit(hit) {
|
|
481
|
+
const nodeId = hit?.row?.nodeId ?? null;
|
|
482
|
+
const part = hit?.part ?? null;
|
|
483
|
+
if (this.hoverId === nodeId && this.hoverPart === part) return;
|
|
484
|
+
this.hoverId = nodeId;
|
|
485
|
+
this.hoverPart = part;
|
|
486
|
+
this.events.emit('nodehover', { nodeId, part });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
setActiveHit(hit) {
|
|
490
|
+
const nodeId = hit?.row?.nodeId ?? null;
|
|
491
|
+
const part = hit?.part ?? null;
|
|
492
|
+
if (this.activeId === nodeId && this.activePart === part) return;
|
|
493
|
+
this.activeId = nodeId;
|
|
494
|
+
this.activePart = part;
|
|
495
|
+
}
|
|
496
|
+
|
|
475
497
|
clickNode(nodeId, event = {}) {
|
|
476
498
|
const row = this.rowModel.getRowById(nodeId);
|
|
477
499
|
if (!row) return;
|
|
@@ -588,6 +610,9 @@ export class TreeViewController {
|
|
|
588
610
|
dynamicState: this.model.dynamicState,
|
|
589
611
|
selection: this.selection.selected,
|
|
590
612
|
hoverNodeId: this.hoverId,
|
|
613
|
+
hoverPart: this.hoverPart,
|
|
614
|
+
activeNodeId: this.activeId,
|
|
615
|
+
activePart: this.activePart,
|
|
591
616
|
focusNodeId: this.focusedId,
|
|
592
617
|
searchMatches: this.searchHighlights,
|
|
593
618
|
sort: this.columnModel.sort,
|
|
@@ -661,6 +686,8 @@ export class TreeViewController {
|
|
|
661
686
|
else if (localX >= treeX + 26 && localX <= treeX + 44) part = 'icon';
|
|
662
687
|
else if (localX >= treeX + 48) part = 'label';
|
|
663
688
|
else part = 'cell';
|
|
689
|
+
} else if (column.kind === 'inspectorValue') {
|
|
690
|
+
part = this.#inspectorEditorPart(row, x - column.x, column.width);
|
|
664
691
|
}
|
|
665
692
|
return { area: 'row', part, row, column, x, y: rowY };
|
|
666
693
|
}
|
|
@@ -842,6 +869,13 @@ export class TreeViewController {
|
|
|
842
869
|
this.#resizeObserver.observe(this.canvas);
|
|
843
870
|
}
|
|
844
871
|
|
|
872
|
+
#resizeToCanvasClientSize() {
|
|
873
|
+
if (!this.canvas) return;
|
|
874
|
+
const width = Math.floor(this.canvas.clientWidth || this.canvas.getBoundingClientRect().width || 0);
|
|
875
|
+
const height = Math.floor(this.canvas.clientHeight || this.canvas.getBoundingClientRect().height || 0);
|
|
876
|
+
if (width > 0 && height > 0) this.resize(width, height);
|
|
877
|
+
}
|
|
878
|
+
|
|
845
879
|
#computeInspectorPaneLabelEnd(visibleRange) {
|
|
846
880
|
const column = this.columnModel.columns.find((item) => item.kind === 'inspectorPane');
|
|
847
881
|
if (!column) return 0;
|