virtual-tree-canvas 0.7.0 → 0.7.1
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/CHANGELOG.md +9 -0
- package/README.md +12 -1
- package/docs/icon-catalog.html +68 -66
- package/docs/icon-catalog.md +111 -111
- package/docs/performance/dynamic-updates-0.7.1.md +38 -0
- package/package.json +3 -2
- package/src/assets/icons.js +10 -4
- package/src/core/dynamic-state.js +19 -29
- package/src/core/patch-batcher.js +4 -0
- package/src/core/theme-manager.js +238 -64
- package/src/core/tree-cell-layout.js +4 -2
- package/src/core/tree-column-model.js +72 -24
- package/src/core/tree-model.js +4 -2
- package/src/core/view-options.js +98 -25
- package/src/core/visible-row-model.js +50 -20
- package/src/index.d.ts +22 -0
- package/src/input/row-actions.js +216 -83
- package/src/input/row-reorder-input.js +112 -39
- package/src/input/tree-tooltip.js +39 -5
- package/src/input/tree-view-input-controller.js +33 -10
- package/src/inspector/cell-editor-manager.js +110 -31
- package/src/inspector/pane-layout.js +11 -3
- package/src/renderers/checkbox.js +12 -13
- package/src/renderers/tree-row-renderer.js +325 -68
- package/src/tree-view-controller.js +919 -213
- package/src/tree-view.js +215 -53
- package/src/view/styles.js +83 -12
|
@@ -20,10 +20,14 @@ export class TreeRowRenderer {
|
|
|
20
20
|
this.stopIconListener = this.iconRegistry.onChange?.(() => this.#scheduleIconRender()) ?? null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* @param {HTMLCanvasElement} canvas
|
|
25
|
+
*/
|
|
24
26
|
initialize(canvas) {
|
|
25
27
|
this.canvas = canvas;
|
|
26
|
-
this.ctx = canvas.getContext('2d', {
|
|
28
|
+
this.ctx = canvas.getContext('2d', {
|
|
29
|
+
alpha: false
|
|
30
|
+
});
|
|
27
31
|
if (!this.ctx) throw new Error('Canvas2D context is not available');
|
|
28
32
|
}
|
|
29
33
|
|
|
@@ -31,7 +35,9 @@ export class TreeRowRenderer {
|
|
|
31
35
|
this.scene = scene;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
setInvalidationHandler(invalidate) {
|
|
38
|
+
setInvalidationHandler(invalidate) {
|
|
39
|
+
this.invalidate = invalidate;
|
|
40
|
+
}
|
|
35
41
|
|
|
36
42
|
destroy() {
|
|
37
43
|
if (this.iconRenderFrame !== null) {
|
|
@@ -87,7 +93,8 @@ export class TreeRowRenderer {
|
|
|
87
93
|
if (this.invalidate) return this.invalidate();
|
|
88
94
|
if (this.iconRenderFrame !== null) return;
|
|
89
95
|
const view = this.canvas.ownerDocument?.defaultView ?? globalThis;
|
|
90
|
-
const schedule =
|
|
96
|
+
const schedule =
|
|
97
|
+
view.requestAnimationFrame?.bind(view) ?? ((callback) => setTimeout(callback, 0));
|
|
91
98
|
this.iconRenderFrame = schedule(() => {
|
|
92
99
|
this.iconRenderFrame = null;
|
|
93
100
|
this.render();
|
|
@@ -103,7 +110,12 @@ export class TreeRowRenderer {
|
|
|
103
110
|
if (key === this.preparedIconThemeKey) return;
|
|
104
111
|
this.preparedIconThemeKey = key;
|
|
105
112
|
for (const [icon, color] of iconColors) {
|
|
106
|
-
this.iconRegistry.prepare?.({
|
|
113
|
+
this.iconRegistry.prepare?.({
|
|
114
|
+
icons: [icon],
|
|
115
|
+
size: 15,
|
|
116
|
+
color,
|
|
117
|
+
pixelRatio
|
|
118
|
+
});
|
|
107
119
|
}
|
|
108
120
|
}
|
|
109
121
|
|
|
@@ -123,14 +135,29 @@ export class TreeRowRenderer {
|
|
|
123
135
|
ctx.textBaseline = 'middle';
|
|
124
136
|
|
|
125
137
|
for (const column of columns) {
|
|
126
|
-
if (
|
|
138
|
+
if (
|
|
139
|
+
headerFilter &&
|
|
140
|
+
column === columns.find((item) => item.kind !== 'rowOrder' && item.id !== '__vtc_actions')
|
|
141
|
+
) {
|
|
127
142
|
this.#drawHeaderFilter(ctx, column, viewport, theme, filterQuery);
|
|
128
143
|
} else {
|
|
129
144
|
ctx.fillStyle = colors.textMuted;
|
|
130
|
-
drawTruncatedText(
|
|
145
|
+
drawTruncatedText(
|
|
146
|
+
ctx,
|
|
147
|
+
column.label,
|
|
148
|
+
column.x + 10,
|
|
149
|
+
viewport.headerHeight / 2,
|
|
150
|
+
column.width - 20
|
|
151
|
+
);
|
|
131
152
|
}
|
|
132
153
|
if (sort?.columnId === column.id && sort.direction) {
|
|
133
|
-
this.#drawSortIndicator(
|
|
154
|
+
this.#drawSortIndicator(
|
|
155
|
+
ctx,
|
|
156
|
+
column.x + column.width - 16,
|
|
157
|
+
viewport.headerHeight / 2,
|
|
158
|
+
sort.direction,
|
|
159
|
+
theme
|
|
160
|
+
);
|
|
134
161
|
}
|
|
135
162
|
ctx.strokeStyle = colors.border;
|
|
136
163
|
ctx.beginPath();
|
|
@@ -151,7 +178,8 @@ export class TreeRowRenderer {
|
|
|
151
178
|
const colors = theme.colors;
|
|
152
179
|
const x = column.x + 8;
|
|
153
180
|
const y = 5;
|
|
154
|
-
const visibleRight =
|
|
181
|
+
const visibleRight =
|
|
182
|
+
viewport.scrollX + (viewport.contentViewportWidth ?? viewport.viewportWidth);
|
|
155
183
|
const visibleWidth = Math.max(1, Math.min(column.x + column.width, visibleRight) - column.x);
|
|
156
184
|
const width = Math.max(40, visibleWidth - 16);
|
|
157
185
|
const height = Math.max(18, viewport.headerHeight - 10);
|
|
@@ -161,7 +189,13 @@ export class TreeRowRenderer {
|
|
|
161
189
|
ctx.strokeStyle = colors.border;
|
|
162
190
|
ctx.stroke();
|
|
163
191
|
ctx.fillStyle = filterQuery ? colors.text : colors.textMuted;
|
|
164
|
-
drawTruncatedText(
|
|
192
|
+
drawTruncatedText(
|
|
193
|
+
ctx,
|
|
194
|
+
filterQuery || 'Filter rows',
|
|
195
|
+
x + 8,
|
|
196
|
+
viewport.headerHeight / 2,
|
|
197
|
+
width - 16
|
|
198
|
+
);
|
|
165
199
|
}
|
|
166
200
|
|
|
167
201
|
#drawSortIndicator(ctx, x, y, direction, theme) {
|
|
@@ -202,7 +236,10 @@ export class TreeRowRenderer {
|
|
|
202
236
|
const visibleWidth = viewport.contentViewportWidth ?? viewport.viewportWidth;
|
|
203
237
|
const height = Math.min(
|
|
204
238
|
viewport.rowViewportHeight,
|
|
205
|
-
stickyRows.reduce(
|
|
239
|
+
stickyRows.reduce(
|
|
240
|
+
(bottom, row, index) => Math.max(bottom, (row.stickyY ?? index * row.height) + row.height),
|
|
241
|
+
0
|
|
242
|
+
)
|
|
206
243
|
);
|
|
207
244
|
|
|
208
245
|
ctx.save();
|
|
@@ -212,11 +249,14 @@ export class TreeRowRenderer {
|
|
|
212
249
|
ctx.translate(-viewport.scrollX, viewport.headerHeight);
|
|
213
250
|
for (let i = 0; i < stickyRows.length; i++) {
|
|
214
251
|
const row = stickyRows[i];
|
|
215
|
-
this.#drawRow(ctx, {
|
|
252
|
+
this.#drawRow(ctx, {
|
|
253
|
+
...row,
|
|
254
|
+
y: row.stickyY ?? i * row.height
|
|
255
|
+
});
|
|
216
256
|
}
|
|
217
257
|
ctx.restore();
|
|
218
258
|
|
|
219
|
-
if (!stickyRows.some(row => row.stickyY > row.y - viewport.scrollY)) return;
|
|
259
|
+
if (!stickyRows.some((row) => row.stickyY > row.y - viewport.scrollY)) return;
|
|
220
260
|
const bottom = viewport.headerHeight + height;
|
|
221
261
|
ctx.strokeStyle = theme.colors.border;
|
|
222
262
|
ctx.beginPath();
|
|
@@ -226,7 +266,20 @@ export class TreeRowRenderer {
|
|
|
226
266
|
}
|
|
227
267
|
|
|
228
268
|
#drawRow(ctx, row) {
|
|
229
|
-
const {
|
|
269
|
+
const {
|
|
270
|
+
columns,
|
|
271
|
+
nodes,
|
|
272
|
+
dynamicState,
|
|
273
|
+
selection,
|
|
274
|
+
hoverNodeId,
|
|
275
|
+
hoverPart,
|
|
276
|
+
activeNodeId,
|
|
277
|
+
activePart,
|
|
278
|
+
focusNodeId,
|
|
279
|
+
searchMatches,
|
|
280
|
+
theme,
|
|
281
|
+
viewport
|
|
282
|
+
} = this.scene;
|
|
230
283
|
const node = nodes[row.nodeIndex];
|
|
231
284
|
const state = dynamicState.get(row.nodeId) ?? {};
|
|
232
285
|
const style = resolveNodeStyle(theme, node, state);
|
|
@@ -239,14 +292,25 @@ export class TreeRowRenderer {
|
|
|
239
292
|
const visibleX = viewport.scrollX;
|
|
240
293
|
const visibleWidth = viewport.contentViewportWidth ?? viewport.viewportWidth;
|
|
241
294
|
|
|
242
|
-
const background = selected
|
|
295
|
+
const background = selected
|
|
296
|
+
? colors.rowSelected
|
|
297
|
+
: highlighted
|
|
298
|
+
? colors.rowHighlighted
|
|
299
|
+
: hovered
|
|
300
|
+
? colors.rowHover
|
|
301
|
+
: colors.row;
|
|
243
302
|
ctx.fillStyle = background;
|
|
244
303
|
ctx.fillRect(visibleX, y, visibleWidth, row.height);
|
|
245
304
|
|
|
246
305
|
this.#drawIndentGuides(ctx, row, colors);
|
|
247
306
|
|
|
248
307
|
for (const column of columns) {
|
|
249
|
-
const rect = {
|
|
308
|
+
const rect = {
|
|
309
|
+
x: column.x,
|
|
310
|
+
y,
|
|
311
|
+
width: column.width,
|
|
312
|
+
height: row.height
|
|
313
|
+
};
|
|
250
314
|
this.#drawCell(ctx, {
|
|
251
315
|
node,
|
|
252
316
|
state,
|
|
@@ -258,7 +322,7 @@ export class TreeRowRenderer {
|
|
|
258
322
|
selected,
|
|
259
323
|
hovered,
|
|
260
324
|
hoverPart: hoverNodeId === row.nodeId ? hoverPart : null,
|
|
261
|
-
activePart: activeNodeId === row.nodeId ? activePart : null
|
|
325
|
+
activePart: activeNodeId === row.nodeId ? activePart : null
|
|
262
326
|
});
|
|
263
327
|
ctx.strokeStyle = colors.border;
|
|
264
328
|
ctx.beginPath();
|
|
@@ -267,10 +331,15 @@ export class TreeRowRenderer {
|
|
|
267
331
|
ctx.stroke();
|
|
268
332
|
}
|
|
269
333
|
|
|
270
|
-
const actions = columns.find(column => column.id === '__vtc_actions');
|
|
334
|
+
const actions = columns.find((column) => column.id === '__vtc_actions');
|
|
271
335
|
if (actions) {
|
|
272
336
|
ctx.fillStyle = background;
|
|
273
|
-
ctx.fillRect(
|
|
337
|
+
ctx.fillRect(
|
|
338
|
+
visibleX + Math.max(0, visibleWidth - actions.width),
|
|
339
|
+
y,
|
|
340
|
+
actions.width,
|
|
341
|
+
row.height
|
|
342
|
+
);
|
|
274
343
|
}
|
|
275
344
|
|
|
276
345
|
ctx.strokeStyle = colors.border;
|
|
@@ -297,7 +366,8 @@ export class TreeRowRenderer {
|
|
|
297
366
|
else if (cell.column.kind === 'inspectorPane') this.#drawInspectorPaneCell(ctx, cell);
|
|
298
367
|
else if (cell.column.kind === 'inspectorValue') this.#drawInspectorValueCell(ctx, cell);
|
|
299
368
|
else if (cell.column.kind === 'inspectorType') this.#drawInspectorTypeCell(ctx, cell);
|
|
300
|
-
else if (cell.column.kind === 'inspectorDescription')
|
|
369
|
+
else if (cell.column.kind === 'inspectorDescription')
|
|
370
|
+
this.#drawInspectorDescriptionCell(ctx, cell);
|
|
301
371
|
else if (cell.column.kind === 'status') this.#drawStatusCell(ctx, cell);
|
|
302
372
|
else if (cell.column.kind === 'progress') this.#drawProgressCell(ctx, cell);
|
|
303
373
|
else this.#drawTextCell(ctx, cell);
|
|
@@ -312,9 +382,16 @@ export class TreeRowRenderer {
|
|
|
312
382
|
ctx.strokeStyle = theme.colors.textMuted;
|
|
313
383
|
ctx.fillStyle = theme.colors.textMuted;
|
|
314
384
|
ctx.lineWidth = 1.5;
|
|
315
|
-
ctx.globalAlpha =
|
|
316
|
-
|
|
317
|
-
|
|
385
|
+
ctx.globalAlpha =
|
|
386
|
+
enabled || this.scene.rowDrag?.(node, this.scene.dynamicState.get(node.id) ?? {}) != null
|
|
387
|
+
? 1
|
|
388
|
+
: 0.3;
|
|
389
|
+
for (const dx of [10, 14])
|
|
390
|
+
for (const dy of [-4, 0, 4]) ctx.fillRect(rect.x + dx, cy + dy, 1.5, 1.5);
|
|
391
|
+
for (const [center, direction, available] of [
|
|
392
|
+
[36, -1, siblings[0] !== node.id],
|
|
393
|
+
[60, 1, siblings.at(-1) !== node.id]
|
|
394
|
+
]) {
|
|
318
395
|
ctx.globalAlpha = enabled && available ? 1 : 0.3;
|
|
319
396
|
ctx.beginPath();
|
|
320
397
|
ctx.moveTo(rect.x + center - 4, cy - direction * 2);
|
|
@@ -341,8 +418,13 @@ export class TreeRowRenderer {
|
|
|
341
418
|
}
|
|
342
419
|
|
|
343
420
|
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style, hovered, hoverPart, activePart }) {
|
|
344
|
-
const visibleRight =
|
|
345
|
-
|
|
421
|
+
const visibleRight =
|
|
422
|
+
this.scene.viewport.scrollX +
|
|
423
|
+
(this.scene.viewport.contentViewportWidth ?? this.scene.viewport.viewportWidth);
|
|
424
|
+
rect = {
|
|
425
|
+
...rect,
|
|
426
|
+
width: Math.max(1, Math.min(rect.x + rect.width, visibleRight) - rect.x)
|
|
427
|
+
};
|
|
346
428
|
const data = node.data ?? {};
|
|
347
429
|
const colors = theme.colors;
|
|
348
430
|
const indentX = rect.x + row.depth * theme.indentWidth;
|
|
@@ -360,52 +442,109 @@ export class TreeRowRenderer {
|
|
|
360
442
|
ctx.textBaseline = 'middle';
|
|
361
443
|
ctx.textAlign = 'left';
|
|
362
444
|
ctx.globalAlpha = data.disabled ? DISABLED_ALPHA : 1;
|
|
363
|
-
ctx.fillStyle =
|
|
445
|
+
ctx.fillStyle =
|
|
446
|
+
data.valueType === 'object' || data.valueType === 'array' ? colors.text : colors.textMuted;
|
|
364
447
|
|
|
365
448
|
if (data.valueType === 'object') {
|
|
366
|
-
drawTruncatedText(
|
|
449
|
+
drawTruncatedText(
|
|
450
|
+
ctx,
|
|
451
|
+
node.label ?? node.id,
|
|
452
|
+
labelX,
|
|
453
|
+
cy,
|
|
454
|
+
Math.max(20, rect.x + rect.width - labelX - 8)
|
|
455
|
+
);
|
|
367
456
|
if (data.meta?.updated) this.#drawUpdatedMarker(ctx, row, theme);
|
|
368
457
|
ctx.globalAlpha = 1;
|
|
369
458
|
return;
|
|
370
459
|
}
|
|
371
460
|
|
|
372
|
-
const layout = inspectorPaneLayout(
|
|
373
|
-
|
|
461
|
+
const layout = inspectorPaneLayout(
|
|
462
|
+
rect.width,
|
|
463
|
+
row.depth,
|
|
464
|
+
theme.indentWidth,
|
|
465
|
+
data.editorType,
|
|
466
|
+
this.scene.inspectorPaneLabelEnd
|
|
467
|
+
);
|
|
468
|
+
drawTruncatedText(
|
|
469
|
+
ctx,
|
|
470
|
+
node.label ?? node.id,
|
|
471
|
+
labelX,
|
|
472
|
+
cy,
|
|
473
|
+
Math.max(20, rect.x + layout.editorLeft - labelX - 8)
|
|
474
|
+
);
|
|
374
475
|
|
|
375
476
|
const editorX = rect.x + layout.editorLeft;
|
|
376
477
|
const editorWidth = layout.editorWidth;
|
|
377
478
|
if (data.valueType === 'array') {
|
|
378
479
|
ctx.fillStyle = colors.textMuted;
|
|
379
480
|
drawTruncatedText(ctx, data.valueText, editorX, cy, Math.max(20, editorWidth - 58));
|
|
380
|
-
this.#drawSmallButton(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
481
|
+
this.#drawSmallButton(
|
|
482
|
+
ctx,
|
|
483
|
+
rect.x + rect.width - 54,
|
|
484
|
+
rect.y + 5,
|
|
485
|
+
22,
|
|
486
|
+
rect.height - 10,
|
|
487
|
+
'+',
|
|
488
|
+
theme,
|
|
489
|
+
{
|
|
490
|
+
hovered: hovered && hoverPart === 'arrayAdd',
|
|
491
|
+
active: activePart === 'arrayAdd'
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
this.#drawSmallButton(
|
|
495
|
+
ctx,
|
|
496
|
+
rect.x + rect.width - 28,
|
|
497
|
+
rect.y + 5,
|
|
498
|
+
22,
|
|
499
|
+
rect.height - 10,
|
|
500
|
+
'-',
|
|
501
|
+
theme,
|
|
502
|
+
{
|
|
503
|
+
hovered: hovered && hoverPart === 'arrayRemove',
|
|
504
|
+
active: activePart === 'arrayRemove'
|
|
505
|
+
}
|
|
506
|
+
);
|
|
388
507
|
} else {
|
|
389
508
|
this.#drawInspectorValueCell(ctx, {
|
|
390
509
|
node,
|
|
391
|
-
rect: {
|
|
510
|
+
rect: {
|
|
511
|
+
x: editorX,
|
|
512
|
+
y: rect.y,
|
|
513
|
+
width: editorWidth,
|
|
514
|
+
height: rect.height
|
|
515
|
+
},
|
|
392
516
|
theme,
|
|
393
517
|
hovered,
|
|
394
518
|
hoverPart,
|
|
395
519
|
activePart,
|
|
396
|
-
suppressUpdatedMarker: true
|
|
520
|
+
suppressUpdatedMarker: true
|
|
397
521
|
});
|
|
398
522
|
}
|
|
399
523
|
if (data.meta?.updated) this.#drawUpdatedMarker(ctx, row, theme);
|
|
400
524
|
ctx.globalAlpha = 1;
|
|
401
525
|
}
|
|
402
526
|
|
|
403
|
-
#drawInspectorValueCell(
|
|
527
|
+
#drawInspectorValueCell(
|
|
528
|
+
ctx,
|
|
529
|
+
{
|
|
530
|
+
node,
|
|
531
|
+
rect,
|
|
532
|
+
theme,
|
|
533
|
+
hovered = false,
|
|
534
|
+
hoverPart = null,
|
|
535
|
+
activePart = null,
|
|
536
|
+
suppressUpdatedMarker = false
|
|
537
|
+
}
|
|
538
|
+
) {
|
|
404
539
|
const data = node.data ?? {};
|
|
405
540
|
const meta = data.meta ?? {};
|
|
406
541
|
const disabled = data.disabled;
|
|
407
542
|
const readonly = data.readonly;
|
|
408
|
-
const valueColor = resolveValueColor(
|
|
543
|
+
const valueColor = resolveValueColor(
|
|
544
|
+
theme,
|
|
545
|
+
data.value,
|
|
546
|
+
data.editorType === 'select' ? 'enum' : data.valueType
|
|
547
|
+
);
|
|
409
548
|
const x = rect.x + 10;
|
|
410
549
|
const y = rect.y + 5;
|
|
411
550
|
const fullWidth = Math.max(24, rect.width - 20);
|
|
@@ -420,7 +559,13 @@ export class TreeRowRenderer {
|
|
|
420
559
|
if (numeric.unit && numeric.unitWidth) {
|
|
421
560
|
ctx.font = theme.unitFont ?? theme.monoFont ?? theme.font;
|
|
422
561
|
ctx.fillStyle = theme.colors.unit ?? theme.colors.textMuted;
|
|
423
|
-
drawTruncatedText(
|
|
562
|
+
drawTruncatedText(
|
|
563
|
+
ctx,
|
|
564
|
+
numeric.unit,
|
|
565
|
+
x + numeric.unitLeft,
|
|
566
|
+
rect.y + rect.height / 2,
|
|
567
|
+
numeric.unitWidth
|
|
568
|
+
);
|
|
424
569
|
ctx.font = theme.font;
|
|
425
570
|
}
|
|
426
571
|
|
|
@@ -429,33 +574,59 @@ export class TreeRowRenderer {
|
|
|
429
574
|
} else if (data.editorType === 'range') {
|
|
430
575
|
this.#drawInspectorRange(ctx, x, rect.y + rect.height / 2 - 4, width, data, theme, {
|
|
431
576
|
hoveredNumber: hovered && hoverPart === 'number',
|
|
432
|
-
activeNumber: activePart === 'number'
|
|
577
|
+
activeNumber: activePart === 'number'
|
|
433
578
|
});
|
|
434
579
|
} else if (data.editorType === 'color') {
|
|
435
580
|
ctx.fillStyle = String(data.value || '#000000');
|
|
436
581
|
ctx.fillRect(x, y + 2, 28, height - 4);
|
|
437
582
|
ctx.strokeStyle = theme.colors.border;
|
|
438
583
|
ctx.strokeRect(x + 0.5, y + 2.5, 28, height - 4);
|
|
439
|
-
this.#drawMutedText(
|
|
584
|
+
this.#drawMutedText(
|
|
585
|
+
ctx,
|
|
586
|
+
String(data.value ?? ''),
|
|
587
|
+
x + 38,
|
|
588
|
+
rect.y + rect.height / 2,
|
|
589
|
+
width - 38,
|
|
590
|
+
theme,
|
|
591
|
+
false,
|
|
592
|
+
null,
|
|
593
|
+
valueColor
|
|
594
|
+
);
|
|
440
595
|
} else if (data.editorType === 'button') {
|
|
441
596
|
const buttonWidth = meta.fullWidthButton ? width : Math.min(width, 140);
|
|
442
597
|
this.#drawControlSurface(ctx, x, y, buttonWidth, height, theme, {
|
|
443
598
|
hovered: hovered && hoverPart === 'button',
|
|
444
599
|
active: activePart === 'button',
|
|
445
|
-
disabled: readonly || disabled
|
|
600
|
+
disabled: readonly || disabled
|
|
446
601
|
});
|
|
447
602
|
ctx.fillStyle = theme.colors.text;
|
|
448
603
|
ctx.textAlign = 'center';
|
|
449
|
-
drawTruncatedText(
|
|
604
|
+
drawTruncatedText(
|
|
605
|
+
ctx,
|
|
606
|
+
meta.button ?? node.label,
|
|
607
|
+
x + buttonWidth / 2,
|
|
608
|
+
rect.y + rect.height / 2,
|
|
609
|
+
Math.max(10, buttonWidth - 12)
|
|
610
|
+
);
|
|
450
611
|
ctx.textAlign = 'left';
|
|
451
612
|
} else if (data.editorType === 'select') {
|
|
452
613
|
const selectWidth = Math.min(width, 180);
|
|
453
614
|
this.#drawControlSurface(ctx, x, y, selectWidth, height, theme, {
|
|
454
615
|
hovered: hovered && hoverPart === 'editor',
|
|
455
616
|
active: activePart === 'editor',
|
|
456
|
-
disabled: readonly || disabled
|
|
617
|
+
disabled: readonly || disabled
|
|
457
618
|
});
|
|
458
|
-
this.#drawMutedText(
|
|
619
|
+
this.#drawMutedText(
|
|
620
|
+
ctx,
|
|
621
|
+
data.valueText,
|
|
622
|
+
x + 8,
|
|
623
|
+
rect.y + rect.height / 2,
|
|
624
|
+
selectWidth - 30,
|
|
625
|
+
theme,
|
|
626
|
+
false,
|
|
627
|
+
null,
|
|
628
|
+
valueColor
|
|
629
|
+
);
|
|
459
630
|
this.#drawSelectChevron(ctx, x + selectWidth - 18, rect.y + rect.height / 2, theme, disabled);
|
|
460
631
|
} else {
|
|
461
632
|
if (numeric.unit) {
|
|
@@ -465,7 +636,17 @@ export class TreeRowRenderer {
|
|
|
465
636
|
drawTruncatedText(ctx, data.valueText, x + width, rect.y + rect.height / 2, width);
|
|
466
637
|
ctx.textAlign = 'left';
|
|
467
638
|
} else {
|
|
468
|
-
this.#drawMutedText(
|
|
639
|
+
this.#drawMutedText(
|
|
640
|
+
ctx,
|
|
641
|
+
data.valueText,
|
|
642
|
+
x,
|
|
643
|
+
rect.y + rect.height / 2,
|
|
644
|
+
width,
|
|
645
|
+
theme,
|
|
646
|
+
false,
|
|
647
|
+
null,
|
|
648
|
+
valueColor
|
|
649
|
+
);
|
|
469
650
|
}
|
|
470
651
|
}
|
|
471
652
|
|
|
@@ -488,14 +669,29 @@ export class TreeRowRenderer {
|
|
|
488
669
|
}
|
|
489
670
|
|
|
490
671
|
#drawInspectorTypeCell(ctx, { node, rect, theme }) {
|
|
491
|
-
this.#drawMutedText(
|
|
672
|
+
this.#drawMutedText(
|
|
673
|
+
ctx,
|
|
674
|
+
node.data?.valueType ?? '',
|
|
675
|
+
rect.x + 10,
|
|
676
|
+
rect.y + rect.height / 2,
|
|
677
|
+
rect.width - 20,
|
|
678
|
+
theme,
|
|
679
|
+
false,
|
|
680
|
+
theme.monoFont
|
|
681
|
+
);
|
|
492
682
|
}
|
|
493
683
|
|
|
494
684
|
#drawInspectorDescriptionCell(ctx, { node, rect, theme }) {
|
|
495
|
-
this.#drawMutedText(
|
|
685
|
+
this.#drawMutedText(
|
|
686
|
+
ctx,
|
|
687
|
+
node.data?.meta?.description ?? '',
|
|
688
|
+
rect.x + 10,
|
|
689
|
+
rect.y + rect.height / 2,
|
|
690
|
+
rect.width - 20,
|
|
691
|
+
theme
|
|
692
|
+
);
|
|
496
693
|
}
|
|
497
694
|
|
|
498
|
-
|
|
499
695
|
#drawInspectorRange(ctx, x, y, width, data, theme, state = {}) {
|
|
500
696
|
const meta = data.meta ?? {};
|
|
501
697
|
const min = meta.min ?? 0;
|
|
@@ -507,12 +703,18 @@ export class TreeRowRenderer {
|
|
|
507
703
|
this.#drawMeterBar(ctx, x, y + 0.5, barWidth, 7, ratio, theme.colors.progressFill, theme);
|
|
508
704
|
this.#drawControlSurface(ctx, x + barWidth + gap, y - 6, valueWidth, 20, theme, {
|
|
509
705
|
hovered: Boolean(state.hoveredNumber),
|
|
510
|
-
active: Boolean(state.activeNumber)
|
|
706
|
+
active: Boolean(state.activeNumber)
|
|
511
707
|
});
|
|
512
708
|
ctx.fillStyle = resolveValueColor(theme, data.value);
|
|
513
709
|
ctx.font = theme.monoFont ?? theme.font;
|
|
514
710
|
ctx.textAlign = 'right';
|
|
515
|
-
drawTruncatedText(
|
|
711
|
+
drawTruncatedText(
|
|
712
|
+
ctx,
|
|
713
|
+
String(data.valueText ?? ''),
|
|
714
|
+
x + barWidth + gap + valueWidth - 6,
|
|
715
|
+
y + 4,
|
|
716
|
+
valueWidth - 10
|
|
717
|
+
);
|
|
516
718
|
ctx.textAlign = 'left';
|
|
517
719
|
ctx.font = theme.font;
|
|
518
720
|
}
|
|
@@ -526,7 +728,15 @@ export class TreeRowRenderer {
|
|
|
526
728
|
ctx.textAlign = 'left';
|
|
527
729
|
}
|
|
528
730
|
|
|
529
|
-
#drawControlSurface(
|
|
731
|
+
#drawControlSurface(
|
|
732
|
+
ctx,
|
|
733
|
+
x,
|
|
734
|
+
y,
|
|
735
|
+
width,
|
|
736
|
+
height,
|
|
737
|
+
theme,
|
|
738
|
+
{ hovered = false, active = false, disabled = false } = {}
|
|
739
|
+
) {
|
|
530
740
|
const colors = theme.colors;
|
|
531
741
|
ctx.fillStyle = disabled
|
|
532
742
|
? colors.progressTrack
|
|
@@ -537,7 +747,11 @@ export class TreeRowRenderer {
|
|
|
537
747
|
: colors.progressTrack;
|
|
538
748
|
roundRect(ctx, x, y, width, height, 5);
|
|
539
749
|
ctx.fill();
|
|
540
|
-
ctx.strokeStyle = active
|
|
750
|
+
ctx.strokeStyle = active
|
|
751
|
+
? colors.focus
|
|
752
|
+
: hovered
|
|
753
|
+
? mixColor(colors.border, colors.focus, 0.5)
|
|
754
|
+
: colors.border;
|
|
541
755
|
ctx.stroke();
|
|
542
756
|
}
|
|
543
757
|
|
|
@@ -561,7 +775,11 @@ export class TreeRowRenderer {
|
|
|
561
775
|
|
|
562
776
|
#drawTreeCell(ctx, { node, row, rect, theme, style }) {
|
|
563
777
|
const colors = theme.colors;
|
|
564
|
-
const layout = treeCellLayout(
|
|
778
|
+
const layout = treeCellLayout(
|
|
779
|
+
row.depth,
|
|
780
|
+
theme.indentWidth,
|
|
781
|
+
this.scene.childrenByParent.size > 1
|
|
782
|
+
);
|
|
565
783
|
const cy = rect.y + rect.height / 2;
|
|
566
784
|
if (row.hasChildren) this.#drawChevron(ctx, rect.x + layout.chevronX, cy, row, colors);
|
|
567
785
|
this.iconRegistry.draw(ctx, style.icon, rect.x + layout.iconX, rect.y + 6, 15, style.color);
|
|
@@ -569,12 +787,21 @@ export class TreeRowRenderer {
|
|
|
569
787
|
ctx.font = theme.font;
|
|
570
788
|
ctx.textBaseline = 'middle';
|
|
571
789
|
ctx.textAlign = 'left';
|
|
572
|
-
drawTruncatedText(
|
|
790
|
+
drawTruncatedText(
|
|
791
|
+
ctx,
|
|
792
|
+
node.label ?? node.id,
|
|
793
|
+
rect.x + layout.labelX,
|
|
794
|
+
cy,
|
|
795
|
+
Math.max(0, rect.width - layout.labelX - 6)
|
|
796
|
+
);
|
|
573
797
|
}
|
|
574
798
|
|
|
575
799
|
#drawStatusCell(ctx, { rect, style, theme }) {
|
|
576
800
|
ctx.font = theme.font;
|
|
577
|
-
const badgeWidth = Math.max(
|
|
801
|
+
const badgeWidth = Math.max(
|
|
802
|
+
0,
|
|
803
|
+
Math.min(ctx.measureText(style.status.label).width + 18, rect.width - 12)
|
|
804
|
+
);
|
|
578
805
|
const x = rect.x + (rect.width - badgeWidth) / 2;
|
|
579
806
|
const y = rect.y + (rect.height - 17) / 2;
|
|
580
807
|
const statusColor = style.status.color;
|
|
@@ -593,7 +820,18 @@ export class TreeRowRenderer {
|
|
|
593
820
|
}
|
|
594
821
|
|
|
595
822
|
#drawProgressCell(ctx, { state, rect, theme, style }) {
|
|
596
|
-
if (state.progress === undefined)
|
|
823
|
+
if (state.progress === undefined)
|
|
824
|
+
return this.#drawTextCell(ctx, {
|
|
825
|
+
state,
|
|
826
|
+
rect,
|
|
827
|
+
theme,
|
|
828
|
+
style,
|
|
829
|
+
column: {
|
|
830
|
+
align: 'right',
|
|
831
|
+
value: () => ''
|
|
832
|
+
},
|
|
833
|
+
node: {}
|
|
834
|
+
});
|
|
597
835
|
const ratio = clamp01(state.progress);
|
|
598
836
|
const barX = rect.x + 10;
|
|
599
837
|
const barHeight = 7;
|
|
@@ -628,16 +866,32 @@ export class TreeRowRenderer {
|
|
|
628
866
|
#drawTextCell(ctx, { node, state, rect, column, theme }) {
|
|
629
867
|
let value = column.value(node, state);
|
|
630
868
|
const numeric = typeof value === 'number';
|
|
631
|
-
const valueType =
|
|
869
|
+
const valueType =
|
|
870
|
+
typeof column.valueType === 'function'
|
|
871
|
+
? column.valueType(value, node, state)
|
|
872
|
+
: column.valueType;
|
|
632
873
|
const valueColor = resolveValueColor(theme, value, valueType);
|
|
633
874
|
if (column.format) value = column.format(value, node, state);
|
|
634
875
|
else if (column.kind === 'updated' && numeric) value = formatTime(value);
|
|
635
876
|
else if (numeric) value = formatInspectorValue(value);
|
|
636
|
-
ctx.fillStyle =
|
|
637
|
-
|
|
877
|
+
ctx.fillStyle =
|
|
878
|
+
column.kind === 'type'
|
|
879
|
+
? resolveNodeStyle(theme, node, state).color
|
|
880
|
+
: column.kind === 'value'
|
|
881
|
+
? valueColor
|
|
882
|
+
: theme.colors.textMuted;
|
|
883
|
+
ctx.font =
|
|
884
|
+
column.kind === 'type' || column.kind === 'updated' || numeric
|
|
885
|
+
? (theme.monoFont ?? theme.font)
|
|
886
|
+
: theme.font;
|
|
638
887
|
ctx.textBaseline = 'middle';
|
|
639
888
|
ctx.textAlign = column.align;
|
|
640
|
-
const x =
|
|
889
|
+
const x =
|
|
890
|
+
column.align === 'right'
|
|
891
|
+
? rect.x + rect.width - 10
|
|
892
|
+
: column.align === 'center'
|
|
893
|
+
? rect.x + rect.width / 2
|
|
894
|
+
: rect.x + 10;
|
|
641
895
|
drawTruncatedText(ctx, String(value ?? ''), x, rect.y + rect.height / 2, rect.width - 20);
|
|
642
896
|
ctx.textAlign = 'left';
|
|
643
897
|
}
|
|
@@ -671,20 +925,23 @@ export class TreeRowRenderer {
|
|
|
671
925
|
ctx.closePath();
|
|
672
926
|
ctx.fill();
|
|
673
927
|
}
|
|
928
|
+
|
|
674
929
|
}
|
|
675
930
|
|
|
676
931
|
function resolveNodeStyle(theme, node, state = {}) {
|
|
677
932
|
const typeRule = theme.types[node?.type ?? ''] ?? {};
|
|
678
|
-
const status = theme.statuses[state.status ?? 0] ?? {
|
|
933
|
+
const status = theme.statuses[state.status ?? 0] ?? {
|
|
934
|
+
label: String(state.status ?? ''),
|
|
935
|
+
color: theme.colors.textMuted
|
|
936
|
+
};
|
|
679
937
|
return {
|
|
680
938
|
icon: state.icon ?? node?.icon ?? typeRule.icon ?? 'placeholder',
|
|
681
939
|
color: state.color ?? typeRule.color ?? theme.colors.progressFill,
|
|
682
940
|
typeColor: typeRule.color ?? theme.colors.progressFill,
|
|
683
|
-
status
|
|
941
|
+
status
|
|
684
942
|
};
|
|
685
943
|
}
|
|
686
944
|
|
|
687
|
-
|
|
688
945
|
function clamp01(value) {
|
|
689
946
|
return Math.max(0, Math.min(1, value));
|
|
690
947
|
}
|
|
@@ -773,7 +1030,7 @@ function setTextFitCache(key, value) {
|
|
|
773
1030
|
const timeFormatter = new Intl.DateTimeFormat([], {
|
|
774
1031
|
hour: '2-digit',
|
|
775
1032
|
minute: '2-digit',
|
|
776
|
-
second: '2-digit'
|
|
1033
|
+
second: '2-digit'
|
|
777
1034
|
});
|
|
778
1035
|
|
|
779
1036
|
function formatTime(value) {
|