virtual-tree-canvas 0.3.0 → 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/canvas2d-renderer.js +6 -4
- package/src/renderers/tree-row-renderer.js +175 -51
- package/src/tree-view-controller.js +46 -9
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
|
+
}
|
|
@@ -28,19 +28,21 @@ export class Canvas2DRenderer {
|
|
|
28
28
|
if (!this.canvas || !this.ctx || !this.scene) return;
|
|
29
29
|
const { viewport } = frameState;
|
|
30
30
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
31
|
-
const
|
|
32
|
-
const
|
|
31
|
+
const viewportWidth = Math.max(0, viewport.viewportWidth);
|
|
32
|
+
const viewportHeight = Math.max(0, viewport.viewportHeight);
|
|
33
|
+
if (viewportWidth <= 0 || viewportHeight <= 0) return;
|
|
34
|
+
const width = Math.floor(viewportWidth * dpr);
|
|
35
|
+
const height = Math.floor(viewportHeight * dpr);
|
|
33
36
|
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
34
37
|
this.canvas.width = width;
|
|
35
38
|
this.canvas.height = height;
|
|
36
|
-
viewport.resize(this.canvas.clientWidth, this.canvas.clientHeight);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const ctx = this.ctx;
|
|
40
42
|
const theme = this.themeManager?.get() ?? {};
|
|
41
43
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
42
44
|
ctx.fillStyle = theme.background ?? '#101419';
|
|
43
|
-
ctx.fillRect(0, 0,
|
|
45
|
+
ctx.fillRect(0, 0, viewportWidth, viewportHeight);
|
|
44
46
|
|
|
45
47
|
this.visibleNodeIndices = cullLayoutNodes(this.scene.layout.nodes, viewport.getWorldBounds());
|
|
46
48
|
const visibleSet = new Set(this.visibleNodeIndices);
|
|
@@ -35,19 +35,21 @@ export class TreeRowRenderer {
|
|
|
35
35
|
if (!this.canvas || !this.ctx || !this.scene) return;
|
|
36
36
|
const { viewport, theme } = this.scene;
|
|
37
37
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
38
|
-
const
|
|
39
|
-
const
|
|
38
|
+
const viewportWidth = Math.max(0, viewport.viewportWidth);
|
|
39
|
+
const viewportHeight = Math.max(0, viewport.viewportHeight);
|
|
40
|
+
if (viewportWidth <= 0 || viewportHeight <= 0) return;
|
|
41
|
+
const width = Math.floor(viewportWidth * dpr);
|
|
42
|
+
const height = Math.floor(viewportHeight * dpr);
|
|
40
43
|
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
41
44
|
this.canvas.width = width;
|
|
42
45
|
this.canvas.height = height;
|
|
43
|
-
viewport.resize(this.canvas.clientWidth, this.canvas.clientHeight);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const ctx = this.ctx;
|
|
47
49
|
const colors = theme.colors;
|
|
48
50
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
49
51
|
ctx.fillStyle = colors.background;
|
|
50
|
-
ctx.fillRect(0, 0,
|
|
52
|
+
ctx.fillRect(0, 0, viewportWidth, viewportHeight);
|
|
51
53
|
|
|
52
54
|
ctx.save();
|
|
53
55
|
ctx.translate(viewport.renderInsetX ?? 0, viewport.renderInsetY ?? 0);
|
|
@@ -76,7 +78,7 @@ export class TreeRowRenderer {
|
|
|
76
78
|
drawTruncatedText(ctx, column.label, column.x + 10, viewport.headerHeight / 2, column.width - 20);
|
|
77
79
|
}
|
|
78
80
|
if (sort?.columnId === column.id && sort.direction) {
|
|
79
|
-
|
|
81
|
+
this.#drawSortIndicator(ctx, column.x + column.width - 16, viewport.headerHeight / 2, sort.direction, theme);
|
|
80
82
|
}
|
|
81
83
|
ctx.strokeStyle = colors.border;
|
|
82
84
|
ctx.beginPath();
|
|
@@ -110,6 +112,22 @@ export class TreeRowRenderer {
|
|
|
110
112
|
drawTruncatedText(ctx, filterQuery || 'Filter inspector', x + 8, viewport.headerHeight / 2, width - 16);
|
|
111
113
|
}
|
|
112
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
|
+
|
|
113
131
|
#drawRows(ctx) {
|
|
114
132
|
const { rows, visibleRange, viewport } = this.scene;
|
|
115
133
|
this.renderedRows = visibleRange.count;
|
|
@@ -176,7 +194,7 @@ export class TreeRowRenderer {
|
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
#drawRow(ctx, row) {
|
|
179
|
-
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;
|
|
180
198
|
const node = nodes[row.nodeIndex];
|
|
181
199
|
const state = dynamicState.get(row.nodeId) ?? {};
|
|
182
200
|
const style = resolveNodeStyle(theme, node, state);
|
|
@@ -196,7 +214,19 @@ export class TreeRowRenderer {
|
|
|
196
214
|
|
|
197
215
|
for (const column of columns) {
|
|
198
216
|
const rect = { x: column.x, y, width: column.width, height: row.height };
|
|
199
|
-
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
|
+
});
|
|
200
230
|
ctx.strokeStyle = colors.border;
|
|
201
231
|
ctx.beginPath();
|
|
202
232
|
ctx.moveTo(column.x + column.width + 0.5, y);
|
|
@@ -233,7 +263,7 @@ export class TreeRowRenderer {
|
|
|
233
263
|
else this.#drawTextCell(ctx, cell);
|
|
234
264
|
}
|
|
235
265
|
|
|
236
|
-
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style }) {
|
|
266
|
+
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style, hovered, hoverPart, activePart }) {
|
|
237
267
|
const visibleRight = this.scene.viewport.scrollX + this.scene.viewport.viewportWidth;
|
|
238
268
|
rect = { ...rect, width: Math.max(1, Math.min(rect.x + rect.width, visibleRight) - rect.x) };
|
|
239
269
|
const data = node.data ?? {};
|
|
@@ -270,13 +300,22 @@ export class TreeRowRenderer {
|
|
|
270
300
|
if (data.valueType === 'array') {
|
|
271
301
|
ctx.fillStyle = colors.textMuted;
|
|
272
302
|
drawTruncatedText(ctx, data.valueText, editorX, cy, Math.max(20, editorWidth - 58));
|
|
273
|
-
this.#drawSmallButton(ctx, rect.x + rect.width - 54, rect.y + 5, 22, rect.height - 10, '+', theme
|
|
274
|
-
|
|
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
|
+
});
|
|
275
311
|
} else {
|
|
276
312
|
this.#drawInspectorValueCell(ctx, {
|
|
277
313
|
node,
|
|
278
314
|
rect: { x: editorX, y: rect.y, width: editorWidth, height: rect.height },
|
|
279
315
|
theme,
|
|
316
|
+
hovered,
|
|
317
|
+
hoverPart,
|
|
318
|
+
activePart,
|
|
280
319
|
suppressUpdatedMarker: true,
|
|
281
320
|
});
|
|
282
321
|
}
|
|
@@ -284,7 +323,7 @@ export class TreeRowRenderer {
|
|
|
284
323
|
ctx.globalAlpha = 1;
|
|
285
324
|
}
|
|
286
325
|
|
|
287
|
-
#drawInspectorValueCell(ctx, { node, rect, theme, suppressUpdatedMarker = false }) {
|
|
326
|
+
#drawInspectorValueCell(ctx, { node, rect, theme, hovered = false, hoverPart = null, activePart = null, suppressUpdatedMarker = false }) {
|
|
288
327
|
const data = node.data ?? {};
|
|
289
328
|
const meta = data.meta ?? {};
|
|
290
329
|
const disabled = data.disabled;
|
|
@@ -299,9 +338,12 @@ export class TreeRowRenderer {
|
|
|
299
338
|
ctx.globalAlpha = disabled ? DISABLED_ALPHA : 1;
|
|
300
339
|
|
|
301
340
|
if (data.editorType === 'checkbox') {
|
|
302
|
-
this.#drawCheckbox(ctx, x, rect.y + rect.height / 2 -
|
|
341
|
+
this.#drawCheckbox(ctx, x, rect.y + rect.height / 2 - 8, Boolean(data.value), theme);
|
|
303
342
|
} else if (data.editorType === 'range') {
|
|
304
|
-
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
|
+
});
|
|
305
347
|
} else if (data.editorType === 'color') {
|
|
306
348
|
ctx.fillStyle = String(data.value || '#000000');
|
|
307
349
|
ctx.fillRect(x, y + 2, 28, height - 4);
|
|
@@ -309,20 +351,25 @@ export class TreeRowRenderer {
|
|
|
309
351
|
ctx.strokeRect(x + 0.5, y + 2.5, 28, height - 4);
|
|
310
352
|
this.#drawMutedText(ctx, String(data.value ?? ''), x + 38, rect.y + rect.height / 2, width - 38, theme);
|
|
311
353
|
} else if (data.editorType === 'button') {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
+
});
|
|
315
360
|
ctx.fillStyle = theme.colors.text;
|
|
316
361
|
ctx.textAlign = 'center';
|
|
317
|
-
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));
|
|
318
363
|
ctx.textAlign = 'left';
|
|
319
364
|
} else if (data.editorType === 'select') {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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);
|
|
326
373
|
} else {
|
|
327
374
|
this.#drawMutedText(ctx, data.valueText, x, rect.y + rect.height / 2, width, theme, readonly);
|
|
328
375
|
}
|
|
@@ -346,7 +393,7 @@ export class TreeRowRenderer {
|
|
|
346
393
|
}
|
|
347
394
|
|
|
348
395
|
#drawInspectorTypeCell(ctx, { node, rect, theme }) {
|
|
349
|
-
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);
|
|
350
397
|
}
|
|
351
398
|
|
|
352
399
|
#drawInspectorDescriptionCell(ctx, { node, rect, theme }) {
|
|
@@ -354,20 +401,18 @@ export class TreeRowRenderer {
|
|
|
354
401
|
}
|
|
355
402
|
|
|
356
403
|
#drawCheckbox(ctx, x, y, checked, theme) {
|
|
357
|
-
ctx
|
|
358
|
-
ctx.
|
|
359
|
-
|
|
360
|
-
ctx.strokeStyle = theme.colors.progressFill;
|
|
361
|
-
ctx.lineWidth = 2;
|
|
362
|
-
ctx.beginPath();
|
|
363
|
-
ctx.moveTo(x + 3, y + 7);
|
|
364
|
-
ctx.lineTo(x + 6, y + 11);
|
|
365
|
-
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;
|
|
366
408
|
ctx.stroke();
|
|
367
|
-
|
|
409
|
+
if (!checked) return;
|
|
410
|
+
ctx.fillStyle = theme.colors.progressFill;
|
|
411
|
+
roundRect(ctx, x + 4, y + 4, 8, 8, 1.5);
|
|
412
|
+
ctx.fill();
|
|
368
413
|
}
|
|
369
414
|
|
|
370
|
-
#drawInspectorRange(ctx, x, y, width, data, theme) {
|
|
415
|
+
#drawInspectorRange(ctx, x, y, width, data, theme, state = {}) {
|
|
371
416
|
const meta = data.meta ?? {};
|
|
372
417
|
const min = meta.min ?? 0;
|
|
373
418
|
const max = meta.max ?? 100;
|
|
@@ -380,19 +425,20 @@ export class TreeRowRenderer {
|
|
|
380
425
|
ctx.fillRect(x, y, barWidth, 8);
|
|
381
426
|
ctx.fillStyle = theme.colors.progressFill;
|
|
382
427
|
ctx.fillRect(x, y, barWidth * ratio, 8);
|
|
383
|
-
ctx
|
|
384
|
-
|
|
385
|
-
|
|
428
|
+
this.#drawControlSurface(ctx, x + barWidth + gap, y - 6, valueWidth, 20, theme, {
|
|
429
|
+
hovered: Boolean(state.hoveredNumber),
|
|
430
|
+
active: Boolean(state.activeNumber),
|
|
431
|
+
});
|
|
386
432
|
ctx.fillStyle = theme.colors.text;
|
|
433
|
+
ctx.font = theme.monoFont ?? theme.font;
|
|
387
434
|
ctx.textAlign = 'right';
|
|
388
435
|
drawTruncatedText(ctx, String(data.valueText ?? ''), x + barWidth + gap + valueWidth - 6, y + 4, valueWidth - 10);
|
|
389
436
|
ctx.textAlign = 'left';
|
|
437
|
+
ctx.font = theme.font;
|
|
390
438
|
}
|
|
391
439
|
|
|
392
|
-
#drawSmallButton(ctx, x, y, width, height, label, theme) {
|
|
393
|
-
ctx
|
|
394
|
-
roundRect(ctx, x, y, width, height, 3);
|
|
395
|
-
ctx.fill();
|
|
440
|
+
#drawSmallButton(ctx, x, y, width, height, label, theme, state = {}) {
|
|
441
|
+
this.#drawControlSurface(ctx, x, y, width, height, theme, state);
|
|
396
442
|
ctx.fillStyle = theme.colors.text;
|
|
397
443
|
ctx.textAlign = 'center';
|
|
398
444
|
ctx.textBaseline = 'middle';
|
|
@@ -400,9 +446,34 @@ export class TreeRowRenderer {
|
|
|
400
446
|
ctx.textAlign = 'left';
|
|
401
447
|
}
|
|
402
448
|
|
|
403
|
-
#
|
|
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) {
|
|
404
475
|
ctx.fillStyle = readonly ? theme.colors.textMuted : theme.colors.text;
|
|
405
|
-
ctx.font = theme.font;
|
|
476
|
+
ctx.font = font ?? theme.font;
|
|
406
477
|
ctx.textBaseline = 'middle';
|
|
407
478
|
ctx.textAlign = 'left';
|
|
408
479
|
drawTruncatedText(ctx, String(text ?? ''), x, y, Math.max(10, width));
|
|
@@ -429,7 +500,7 @@ export class TreeRowRenderer {
|
|
|
429
500
|
roundRect(ctx, x, y, badgeWidth, 16, 8);
|
|
430
501
|
ctx.fill();
|
|
431
502
|
ctx.fillStyle = theme.colors.badgeText;
|
|
432
|
-
ctx.font = '10px
|
|
503
|
+
ctx.font = '10px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace';
|
|
433
504
|
ctx.textAlign = 'center';
|
|
434
505
|
ctx.textBaseline = 'middle';
|
|
435
506
|
drawTruncatedText(ctx, style.status.label, x + badgeWidth / 2, y + 8, badgeWidth - 8);
|
|
@@ -452,7 +523,7 @@ export class TreeRowRenderer {
|
|
|
452
523
|
if (column.kind === 'updated' && typeof value === 'number') value = formatTime(value);
|
|
453
524
|
if (typeof value === 'number') value = Math.round(value).toString();
|
|
454
525
|
ctx.fillStyle = column.kind === 'type' ? resolveNodeStyle(theme, node, state).color : theme.colors.textMuted;
|
|
455
|
-
ctx.font = theme.font;
|
|
526
|
+
ctx.font = column.kind === 'type' || column.kind === 'updated' || typeof value === 'number' ? theme.monoFont ?? theme.font : theme.font;
|
|
456
527
|
ctx.textBaseline = 'middle';
|
|
457
528
|
ctx.textAlign = column.align;
|
|
458
529
|
const x = column.align === 'right' ? rect.x + rect.width - 10 : column.align === 'center' ? rect.x + rect.width / 2 : rect.x + 10;
|
|
@@ -538,6 +609,10 @@ function clamp01(value) {
|
|
|
538
609
|
|
|
539
610
|
function roundRect(ctx, x, y, width, height, radius) {
|
|
540
611
|
ctx.beginPath();
|
|
612
|
+
if (typeof ctx.roundRect === 'function') {
|
|
613
|
+
ctx.roundRect(x, y, width, height, radius);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
541
616
|
ctx.moveTo(x + radius, y);
|
|
542
617
|
ctx.lineTo(x + width - radius, y);
|
|
543
618
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
@@ -549,6 +624,23 @@ function roundRect(ctx, x, y, width, height, radius) {
|
|
|
549
624
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
550
625
|
}
|
|
551
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
|
+
|
|
552
644
|
function drawTruncatedText(ctx, text, x, y, maxWidth) {
|
|
553
645
|
const value = String(text ?? '');
|
|
554
646
|
const width = Math.max(0, maxWidth);
|
|
@@ -556,22 +648,54 @@ function drawTruncatedText(ctx, text, x, y, maxWidth) {
|
|
|
556
648
|
ctx.fillText(fitText(ctx, value, width), x, y);
|
|
557
649
|
}
|
|
558
650
|
|
|
651
|
+
const TEXT_FIT_CACHE_LIMIT = 6000;
|
|
652
|
+
const textFitCache = new Map();
|
|
653
|
+
|
|
559
654
|
function fitText(ctx, text, maxWidth) {
|
|
560
|
-
|
|
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
|
+
}
|
|
561
665
|
const ellipsis = '...';
|
|
562
666
|
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
|
563
|
-
if (ellipsisWidth >
|
|
667
|
+
if (ellipsisWidth > safeWidth) {
|
|
668
|
+
setTextFitCache(cacheKey, '');
|
|
669
|
+
return '';
|
|
670
|
+
}
|
|
564
671
|
let low = 0;
|
|
565
672
|
let high = text.length;
|
|
566
673
|
while (low < high) {
|
|
567
674
|
const mid = Math.ceil((low + high) / 2);
|
|
568
675
|
const candidate = text.slice(0, mid);
|
|
569
|
-
if (ctx.measureText(candidate).width + ellipsisWidth <=
|
|
676
|
+
if (ctx.measureText(candidate).width + ellipsisWidth <= safeWidth) low = mid;
|
|
570
677
|
else high = mid - 1;
|
|
571
678
|
}
|
|
572
|
-
|
|
679
|
+
result = `${text.slice(0, low)}${ellipsis}`;
|
|
680
|
+
setTextFitCache(cacheKey, result);
|
|
681
|
+
return result;
|
|
573
682
|
}
|
|
574
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
|
+
|
|
575
697
|
function formatTime(value) {
|
|
576
|
-
|
|
698
|
+
const time = typeof value === 'number' ? value : Number(value);
|
|
699
|
+
if (!Number.isFinite(time)) return '';
|
|
700
|
+
return timeFormatter.format(time);
|
|
577
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,8 @@ export class TreeViewController {
|
|
|
63
66
|
this.canvas = canvas;
|
|
64
67
|
this.renderer.initialize(canvas);
|
|
65
68
|
this.renderer.setScene(this.scene);
|
|
69
|
+
this.#resizeToCanvasClientSize();
|
|
70
|
+
this.#observeCanvasSize();
|
|
66
71
|
return this;
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -468,9 +473,27 @@ export class TreeViewController {
|
|
|
468
473
|
setHover(nodeId) {
|
|
469
474
|
if (this.hoverId === nodeId) return;
|
|
470
475
|
this.hoverId = nodeId;
|
|
476
|
+
this.hoverPart = null;
|
|
471
477
|
this.events.emit('nodehover', { nodeId });
|
|
472
478
|
}
|
|
473
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
|
+
|
|
474
497
|
clickNode(nodeId, event = {}) {
|
|
475
498
|
const row = this.rowModel.getRowById(nodeId);
|
|
476
499
|
if (!row) return;
|
|
@@ -564,7 +587,6 @@ export class TreeViewController {
|
|
|
564
587
|
}
|
|
565
588
|
|
|
566
589
|
renderMeasured(time = performance.now()) {
|
|
567
|
-
this.#syncViewportFromCanvas();
|
|
568
590
|
const sceneStart = performance.now();
|
|
569
591
|
this.scene = this.createRenderScene();
|
|
570
592
|
const sceneMs = performance.now() - sceneStart;
|
|
@@ -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
|
}
|
|
@@ -830,15 +857,23 @@ export class TreeViewController {
|
|
|
830
857
|
return Math.max(0, Math.floor(this.viewport.scrollY / this.rowModel.rowHeight));
|
|
831
858
|
}
|
|
832
859
|
|
|
833
|
-
#
|
|
860
|
+
#observeCanvasSize() {
|
|
861
|
+
if (!this.canvas || typeof ResizeObserver === 'undefined') return;
|
|
862
|
+
this.#resizeObserver?.disconnect();
|
|
863
|
+
this.#resizeObserver = new ResizeObserver((entries) => {
|
|
864
|
+
const entry = entries[0];
|
|
865
|
+
const width = Math.floor(entry?.contentRect?.width ?? 0);
|
|
866
|
+
const height = Math.floor(entry?.contentRect?.height ?? 0);
|
|
867
|
+
if (width > 0 && height > 0) this.resize(width, height);
|
|
868
|
+
});
|
|
869
|
+
this.#resizeObserver.observe(this.canvas);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
#resizeToCanvasClientSize() {
|
|
834
873
|
if (!this.canvas) return;
|
|
835
|
-
const width = this.canvas.clientWidth;
|
|
836
|
-
const height = this.canvas.clientHeight;
|
|
837
|
-
if (width > 0 && height > 0
|
|
838
|
-
this.viewport.resize(width, height);
|
|
839
|
-
this.#fitInspectorPaneColumn(width);
|
|
840
|
-
this.#syncContentSize();
|
|
841
|
-
}
|
|
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);
|
|
842
877
|
}
|
|
843
878
|
|
|
844
879
|
#computeInspectorPaneLabelEnd(visibleRange) {
|
|
@@ -901,6 +936,8 @@ export class TreeViewController {
|
|
|
901
936
|
this.#rebuildRows();
|
|
902
937
|
if (focusId) this.focusedId = focusId;
|
|
903
938
|
}
|
|
939
|
+
|
|
940
|
+
#resizeObserver = null;
|
|
904
941
|
}
|
|
905
942
|
|
|
906
943
|
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|