virtual-tree-canvas 0.6.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 +32 -0
- package/README.md +90 -3
- package/docs/icon-catalog.html +68 -66
- package/docs/icon-catalog.json +12 -0
- package/docs/icon-catalog.md +111 -107
- package/docs/performance/dynamic-updates-0.7.1.md +38 -0
- package/package.json +3 -2
- package/resources/icons/star-filled.svg +1 -0
- package/resources/icons/trash.svg +1 -0
- package/src/assets/icons.js +12 -4
- package/src/core/dynamic-state.js +19 -29
- package/src/core/patch-batcher.js +4 -0
- package/src/core/theme-manager.js +238 -61
- package/src/core/tree-cell-layout.js +11 -0
- package/src/core/tree-column-model.js +81 -22
- package/src/core/tree-model.js +4 -2
- package/src/core/types.js +1 -0
- package/src/core/view-options.js +98 -18
- package/src/core/visible-row-model.js +51 -21
- package/src/index.d.ts +70 -1
- package/src/input/row-actions.js +248 -0
- package/src/input/row-reorder-input.js +134 -36
- package/src/input/tree-tooltip.js +39 -5
- package/src/input/tree-view-input-controller.js +35 -10
- package/src/inspector/cell-editor-manager.js +128 -35
- package/src/inspector/pane-layout.js +20 -0
- package/src/renderers/checkbox.js +13 -0
- package/src/renderers/tree-row-renderer.js +356 -107
- package/src/tree-view-controller.js +968 -236
- package/src/tree-view.js +218 -54
- package/src/view/styles.js +83 -12
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { drawCheckbox } from './checkbox.js';
|
|
2
|
+
import { treeCellLayout } from '../core/tree-cell-layout.js';
|
|
3
|
+
import { formatInspectorValue } from '../inspector/editor-resolver.js';
|
|
4
|
+
import { inspectorPaneLayout } from '../inspector/pane-layout.js';
|
|
1
5
|
import { numericLayout } from '../inspector/numeric-layout.js';
|
|
2
6
|
import { IconRegistry } from '../core/icon-registry.js';
|
|
3
7
|
|
|
@@ -16,10 +20,14 @@ export class TreeRowRenderer {
|
|
|
16
20
|
this.stopIconListener = this.iconRegistry.onChange?.(() => this.#scheduleIconRender()) ?? null;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* @param {HTMLCanvasElement} canvas
|
|
25
|
+
*/
|
|
20
26
|
initialize(canvas) {
|
|
21
27
|
this.canvas = canvas;
|
|
22
|
-
this.ctx = canvas.getContext('2d', {
|
|
28
|
+
this.ctx = canvas.getContext('2d', {
|
|
29
|
+
alpha: false
|
|
30
|
+
});
|
|
23
31
|
if (!this.ctx) throw new Error('Canvas2D context is not available');
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -27,7 +35,9 @@ export class TreeRowRenderer {
|
|
|
27
35
|
this.scene = scene;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
setInvalidationHandler(invalidate) {
|
|
38
|
+
setInvalidationHandler(invalidate) {
|
|
39
|
+
this.invalidate = invalidate;
|
|
40
|
+
}
|
|
31
41
|
|
|
32
42
|
destroy() {
|
|
33
43
|
if (this.iconRenderFrame !== null) {
|
|
@@ -51,7 +61,7 @@ export class TreeRowRenderer {
|
|
|
51
61
|
if (scene?.rows) this.scene = scene;
|
|
52
62
|
if (!this.canvas || !this.ctx || !this.scene) return;
|
|
53
63
|
const { viewport, theme } = this.scene;
|
|
54
|
-
const dpr = Math.max(1,
|
|
64
|
+
const dpr = Math.max(1, this.canvas.ownerDocument?.defaultView?.devicePixelRatio || 1);
|
|
55
65
|
const viewportWidth = Math.max(0, viewport.viewportWidth);
|
|
56
66
|
const viewportHeight = Math.max(0, viewport.viewportHeight);
|
|
57
67
|
if (viewportWidth <= 0 || viewportHeight <= 0) return;
|
|
@@ -83,7 +93,8 @@ export class TreeRowRenderer {
|
|
|
83
93
|
if (this.invalidate) return this.invalidate();
|
|
84
94
|
if (this.iconRenderFrame !== null) return;
|
|
85
95
|
const view = this.canvas.ownerDocument?.defaultView ?? globalThis;
|
|
86
|
-
const schedule =
|
|
96
|
+
const schedule =
|
|
97
|
+
view.requestAnimationFrame?.bind(view) ?? ((callback) => setTimeout(callback, 0));
|
|
87
98
|
this.iconRenderFrame = schedule(() => {
|
|
88
99
|
this.iconRenderFrame = null;
|
|
89
100
|
this.render();
|
|
@@ -99,7 +110,12 @@ export class TreeRowRenderer {
|
|
|
99
110
|
if (key === this.preparedIconThemeKey) return;
|
|
100
111
|
this.preparedIconThemeKey = key;
|
|
101
112
|
for (const [icon, color] of iconColors) {
|
|
102
|
-
this.iconRegistry.prepare?.({
|
|
113
|
+
this.iconRegistry.prepare?.({
|
|
114
|
+
icons: [icon],
|
|
115
|
+
size: 15,
|
|
116
|
+
color,
|
|
117
|
+
pixelRatio
|
|
118
|
+
});
|
|
103
119
|
}
|
|
104
120
|
}
|
|
105
121
|
|
|
@@ -119,14 +135,29 @@ export class TreeRowRenderer {
|
|
|
119
135
|
ctx.textBaseline = 'middle';
|
|
120
136
|
|
|
121
137
|
for (const column of columns) {
|
|
122
|
-
if (
|
|
138
|
+
if (
|
|
139
|
+
headerFilter &&
|
|
140
|
+
column === columns.find((item) => item.kind !== 'rowOrder' && item.id !== '__vtc_actions')
|
|
141
|
+
) {
|
|
123
142
|
this.#drawHeaderFilter(ctx, column, viewport, theme, filterQuery);
|
|
124
143
|
} else {
|
|
125
144
|
ctx.fillStyle = colors.textMuted;
|
|
126
|
-
drawTruncatedText(
|
|
145
|
+
drawTruncatedText(
|
|
146
|
+
ctx,
|
|
147
|
+
column.label,
|
|
148
|
+
column.x + 10,
|
|
149
|
+
viewport.headerHeight / 2,
|
|
150
|
+
column.width - 20
|
|
151
|
+
);
|
|
127
152
|
}
|
|
128
153
|
if (sort?.columnId === column.id && sort.direction) {
|
|
129
|
-
this.#drawSortIndicator(
|
|
154
|
+
this.#drawSortIndicator(
|
|
155
|
+
ctx,
|
|
156
|
+
column.x + column.width - 16,
|
|
157
|
+
viewport.headerHeight / 2,
|
|
158
|
+
sort.direction,
|
|
159
|
+
theme
|
|
160
|
+
);
|
|
130
161
|
}
|
|
131
162
|
ctx.strokeStyle = colors.border;
|
|
132
163
|
ctx.beginPath();
|
|
@@ -147,7 +178,8 @@ export class TreeRowRenderer {
|
|
|
147
178
|
const colors = theme.colors;
|
|
148
179
|
const x = column.x + 8;
|
|
149
180
|
const y = 5;
|
|
150
|
-
const visibleRight =
|
|
181
|
+
const visibleRight =
|
|
182
|
+
viewport.scrollX + (viewport.contentViewportWidth ?? viewport.viewportWidth);
|
|
151
183
|
const visibleWidth = Math.max(1, Math.min(column.x + column.width, visibleRight) - column.x);
|
|
152
184
|
const width = Math.max(40, visibleWidth - 16);
|
|
153
185
|
const height = Math.max(18, viewport.headerHeight - 10);
|
|
@@ -157,7 +189,13 @@ export class TreeRowRenderer {
|
|
|
157
189
|
ctx.strokeStyle = colors.border;
|
|
158
190
|
ctx.stroke();
|
|
159
191
|
ctx.fillStyle = filterQuery ? colors.text : colors.textMuted;
|
|
160
|
-
drawTruncatedText(
|
|
192
|
+
drawTruncatedText(
|
|
193
|
+
ctx,
|
|
194
|
+
filterQuery || 'Filter rows',
|
|
195
|
+
x + 8,
|
|
196
|
+
viewport.headerHeight / 2,
|
|
197
|
+
width - 16
|
|
198
|
+
);
|
|
161
199
|
}
|
|
162
200
|
|
|
163
201
|
#drawSortIndicator(ctx, x, y, direction, theme) {
|
|
@@ -198,7 +236,10 @@ export class TreeRowRenderer {
|
|
|
198
236
|
const visibleWidth = viewport.contentViewportWidth ?? viewport.viewportWidth;
|
|
199
237
|
const height = Math.min(
|
|
200
238
|
viewport.rowViewportHeight,
|
|
201
|
-
stickyRows.reduce(
|
|
239
|
+
stickyRows.reduce(
|
|
240
|
+
(bottom, row, index) => Math.max(bottom, (row.stickyY ?? index * row.height) + row.height),
|
|
241
|
+
0
|
|
242
|
+
)
|
|
202
243
|
);
|
|
203
244
|
|
|
204
245
|
ctx.save();
|
|
@@ -208,10 +249,14 @@ export class TreeRowRenderer {
|
|
|
208
249
|
ctx.translate(-viewport.scrollX, viewport.headerHeight);
|
|
209
250
|
for (let i = 0; i < stickyRows.length; i++) {
|
|
210
251
|
const row = stickyRows[i];
|
|
211
|
-
this.#drawRow(ctx, {
|
|
252
|
+
this.#drawRow(ctx, {
|
|
253
|
+
...row,
|
|
254
|
+
y: row.stickyY ?? i * row.height
|
|
255
|
+
});
|
|
212
256
|
}
|
|
213
257
|
ctx.restore();
|
|
214
258
|
|
|
259
|
+
if (!stickyRows.some((row) => row.stickyY > row.y - viewport.scrollY)) return;
|
|
215
260
|
const bottom = viewport.headerHeight + height;
|
|
216
261
|
ctx.strokeStyle = theme.colors.border;
|
|
217
262
|
ctx.beginPath();
|
|
@@ -221,7 +266,20 @@ export class TreeRowRenderer {
|
|
|
221
266
|
}
|
|
222
267
|
|
|
223
268
|
#drawRow(ctx, row) {
|
|
224
|
-
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;
|
|
225
283
|
const node = nodes[row.nodeIndex];
|
|
226
284
|
const state = dynamicState.get(row.nodeId) ?? {};
|
|
227
285
|
const style = resolveNodeStyle(theme, node, state);
|
|
@@ -234,13 +292,25 @@ export class TreeRowRenderer {
|
|
|
234
292
|
const visibleX = viewport.scrollX;
|
|
235
293
|
const visibleWidth = viewport.contentViewportWidth ?? viewport.viewportWidth;
|
|
236
294
|
|
|
237
|
-
|
|
295
|
+
const background = selected
|
|
296
|
+
? colors.rowSelected
|
|
297
|
+
: highlighted
|
|
298
|
+
? colors.rowHighlighted
|
|
299
|
+
: hovered
|
|
300
|
+
? colors.rowHover
|
|
301
|
+
: colors.row;
|
|
302
|
+
ctx.fillStyle = background;
|
|
238
303
|
ctx.fillRect(visibleX, y, visibleWidth, row.height);
|
|
239
304
|
|
|
240
305
|
this.#drawIndentGuides(ctx, row, colors);
|
|
241
306
|
|
|
242
307
|
for (const column of columns) {
|
|
243
|
-
const rect = {
|
|
308
|
+
const rect = {
|
|
309
|
+
x: column.x,
|
|
310
|
+
y,
|
|
311
|
+
width: column.width,
|
|
312
|
+
height: row.height
|
|
313
|
+
};
|
|
244
314
|
this.#drawCell(ctx, {
|
|
245
315
|
node,
|
|
246
316
|
state,
|
|
@@ -252,7 +322,7 @@ export class TreeRowRenderer {
|
|
|
252
322
|
selected,
|
|
253
323
|
hovered,
|
|
254
324
|
hoverPart: hoverNodeId === row.nodeId ? hoverPart : null,
|
|
255
|
-
activePart: activeNodeId === row.nodeId ? activePart : null
|
|
325
|
+
activePart: activeNodeId === row.nodeId ? activePart : null
|
|
256
326
|
});
|
|
257
327
|
ctx.strokeStyle = colors.border;
|
|
258
328
|
ctx.beginPath();
|
|
@@ -261,6 +331,17 @@ export class TreeRowRenderer {
|
|
|
261
331
|
ctx.stroke();
|
|
262
332
|
}
|
|
263
333
|
|
|
334
|
+
const actions = columns.find((column) => column.id === '__vtc_actions');
|
|
335
|
+
if (actions) {
|
|
336
|
+
ctx.fillStyle = background;
|
|
337
|
+
ctx.fillRect(
|
|
338
|
+
visibleX + Math.max(0, visibleWidth - actions.width),
|
|
339
|
+
y,
|
|
340
|
+
actions.width,
|
|
341
|
+
row.height
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
264
345
|
ctx.strokeStyle = colors.border;
|
|
265
346
|
ctx.beginPath();
|
|
266
347
|
ctx.moveTo(visibleX, y + row.height + 0.5);
|
|
@@ -285,13 +366,15 @@ export class TreeRowRenderer {
|
|
|
285
366
|
else if (cell.column.kind === 'inspectorPane') this.#drawInspectorPaneCell(ctx, cell);
|
|
286
367
|
else if (cell.column.kind === 'inspectorValue') this.#drawInspectorValueCell(ctx, cell);
|
|
287
368
|
else if (cell.column.kind === 'inspectorType') this.#drawInspectorTypeCell(ctx, cell);
|
|
288
|
-
else if (cell.column.kind === 'inspectorDescription')
|
|
369
|
+
else if (cell.column.kind === 'inspectorDescription')
|
|
370
|
+
this.#drawInspectorDescriptionCell(ctx, cell);
|
|
289
371
|
else if (cell.column.kind === 'status') this.#drawStatusCell(ctx, cell);
|
|
290
372
|
else if (cell.column.kind === 'progress') this.#drawProgressCell(ctx, cell);
|
|
291
373
|
else this.#drawTextCell(ctx, cell);
|
|
292
374
|
}
|
|
293
375
|
|
|
294
376
|
#drawRowOrder(ctx, { node, row, rect, theme }) {
|
|
377
|
+
if (node.reorderable === false) return;
|
|
295
378
|
const siblings = this.scene.childrenByParent?.get(node.parentId ?? null) ?? [];
|
|
296
379
|
const enabled = this.scene.canReorderRows && siblings.length > 1;
|
|
297
380
|
const cy = rect.y + row.height / 2;
|
|
@@ -299,9 +382,16 @@ export class TreeRowRenderer {
|
|
|
299
382
|
ctx.strokeStyle = theme.colors.textMuted;
|
|
300
383
|
ctx.fillStyle = theme.colors.textMuted;
|
|
301
384
|
ctx.lineWidth = 1.5;
|
|
302
|
-
ctx.globalAlpha =
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
]) {
|
|
305
395
|
ctx.globalAlpha = enabled && available ? 1 : 0.3;
|
|
306
396
|
ctx.beginPath();
|
|
307
397
|
ctx.moveTo(rect.x + center - 4, cy - direction * 2);
|
|
@@ -328,8 +418,13 @@ export class TreeRowRenderer {
|
|
|
328
418
|
}
|
|
329
419
|
|
|
330
420
|
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style, hovered, hoverPart, activePart }) {
|
|
331
|
-
const visibleRight =
|
|
332
|
-
|
|
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
|
+
};
|
|
333
428
|
const data = node.data ?? {};
|
|
334
429
|
const colors = theme.colors;
|
|
335
430
|
const indentX = rect.x + row.depth * theme.indentWidth;
|
|
@@ -347,51 +442,109 @@ export class TreeRowRenderer {
|
|
|
347
442
|
ctx.textBaseline = 'middle';
|
|
348
443
|
ctx.textAlign = 'left';
|
|
349
444
|
ctx.globalAlpha = data.disabled ? DISABLED_ALPHA : 1;
|
|
350
|
-
ctx.fillStyle =
|
|
445
|
+
ctx.fillStyle =
|
|
446
|
+
data.valueType === 'object' || data.valueType === 'array' ? colors.text : colors.textMuted;
|
|
351
447
|
|
|
352
448
|
if (data.valueType === 'object') {
|
|
353
|
-
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
|
+
);
|
|
354
456
|
if (data.meta?.updated) this.#drawUpdatedMarker(ctx, row, theme);
|
|
355
457
|
ctx.globalAlpha = 1;
|
|
356
458
|
return;
|
|
357
459
|
}
|
|
358
460
|
|
|
359
|
-
const layout = inspectorPaneLayout(
|
|
360
|
-
|
|
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
|
+
);
|
|
361
475
|
|
|
362
476
|
const editorX = rect.x + layout.editorLeft;
|
|
363
477
|
const editorWidth = layout.editorWidth;
|
|
364
478
|
if (data.valueType === 'array') {
|
|
365
479
|
ctx.fillStyle = colors.textMuted;
|
|
366
480
|
drawTruncatedText(ctx, data.valueText, editorX, cy, Math.max(20, editorWidth - 58));
|
|
367
|
-
this.#drawSmallButton(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
);
|
|
375
507
|
} else {
|
|
376
508
|
this.#drawInspectorValueCell(ctx, {
|
|
377
509
|
node,
|
|
378
|
-
rect: {
|
|
510
|
+
rect: {
|
|
511
|
+
x: editorX,
|
|
512
|
+
y: rect.y,
|
|
513
|
+
width: editorWidth,
|
|
514
|
+
height: rect.height
|
|
515
|
+
},
|
|
379
516
|
theme,
|
|
380
517
|
hovered,
|
|
381
518
|
hoverPart,
|
|
382
519
|
activePart,
|
|
383
|
-
suppressUpdatedMarker: true
|
|
520
|
+
suppressUpdatedMarker: true
|
|
384
521
|
});
|
|
385
522
|
}
|
|
386
523
|
if (data.meta?.updated) this.#drawUpdatedMarker(ctx, row, theme);
|
|
387
524
|
ctx.globalAlpha = 1;
|
|
388
525
|
}
|
|
389
526
|
|
|
390
|
-
#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
|
+
) {
|
|
391
539
|
const data = node.data ?? {};
|
|
392
540
|
const meta = data.meta ?? {};
|
|
393
541
|
const disabled = data.disabled;
|
|
394
542
|
const readonly = data.readonly;
|
|
543
|
+
const valueColor = resolveValueColor(
|
|
544
|
+
theme,
|
|
545
|
+
data.value,
|
|
546
|
+
data.editorType === 'select' ? 'enum' : data.valueType
|
|
547
|
+
);
|
|
395
548
|
const x = rect.x + 10;
|
|
396
549
|
const y = rect.y + 5;
|
|
397
550
|
const fullWidth = Math.max(24, rect.width - 20);
|
|
@@ -406,52 +559,94 @@ export class TreeRowRenderer {
|
|
|
406
559
|
if (numeric.unit && numeric.unitWidth) {
|
|
407
560
|
ctx.font = theme.unitFont ?? theme.monoFont ?? theme.font;
|
|
408
561
|
ctx.fillStyle = theme.colors.unit ?? theme.colors.textMuted;
|
|
409
|
-
drawTruncatedText(
|
|
562
|
+
drawTruncatedText(
|
|
563
|
+
ctx,
|
|
564
|
+
numeric.unit,
|
|
565
|
+
x + numeric.unitLeft,
|
|
566
|
+
rect.y + rect.height / 2,
|
|
567
|
+
numeric.unitWidth
|
|
568
|
+
);
|
|
410
569
|
ctx.font = theme.font;
|
|
411
570
|
}
|
|
412
571
|
|
|
413
572
|
if (data.editorType === 'checkbox') {
|
|
414
|
-
|
|
573
|
+
drawCheckbox(ctx, x, rect.y + rect.height / 2 - 8, Boolean(data.value), theme);
|
|
415
574
|
} else if (data.editorType === 'range') {
|
|
416
575
|
this.#drawInspectorRange(ctx, x, rect.y + rect.height / 2 - 4, width, data, theme, {
|
|
417
576
|
hoveredNumber: hovered && hoverPart === 'number',
|
|
418
|
-
activeNumber: activePart === 'number'
|
|
577
|
+
activeNumber: activePart === 'number'
|
|
419
578
|
});
|
|
420
579
|
} else if (data.editorType === 'color') {
|
|
421
580
|
ctx.fillStyle = String(data.value || '#000000');
|
|
422
581
|
ctx.fillRect(x, y + 2, 28, height - 4);
|
|
423
582
|
ctx.strokeStyle = theme.colors.border;
|
|
424
583
|
ctx.strokeRect(x + 0.5, y + 2.5, 28, height - 4);
|
|
425
|
-
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
|
+
);
|
|
426
595
|
} else if (data.editorType === 'button') {
|
|
427
596
|
const buttonWidth = meta.fullWidthButton ? width : Math.min(width, 140);
|
|
428
597
|
this.#drawControlSurface(ctx, x, y, buttonWidth, height, theme, {
|
|
429
598
|
hovered: hovered && hoverPart === 'button',
|
|
430
599
|
active: activePart === 'button',
|
|
431
|
-
disabled: readonly || disabled
|
|
600
|
+
disabled: readonly || disabled
|
|
432
601
|
});
|
|
433
602
|
ctx.fillStyle = theme.colors.text;
|
|
434
603
|
ctx.textAlign = 'center';
|
|
435
|
-
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
|
+
);
|
|
436
611
|
ctx.textAlign = 'left';
|
|
437
612
|
} else if (data.editorType === 'select') {
|
|
438
613
|
const selectWidth = Math.min(width, 180);
|
|
439
614
|
this.#drawControlSurface(ctx, x, y, selectWidth, height, theme, {
|
|
440
615
|
hovered: hovered && hoverPart === 'editor',
|
|
441
616
|
active: activePart === 'editor',
|
|
442
|
-
disabled: readonly || disabled
|
|
617
|
+
disabled: readonly || disabled
|
|
443
618
|
});
|
|
444
|
-
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
|
+
);
|
|
445
630
|
this.#drawSelectChevron(ctx, x + selectWidth - 18, rect.y + rect.height / 2, theme, disabled);
|
|
446
631
|
} else {
|
|
447
632
|
if (numeric.unit) {
|
|
448
633
|
ctx.font = theme.monoFont ?? theme.font;
|
|
449
|
-
ctx.fillStyle =
|
|
634
|
+
ctx.fillStyle = valueColor;
|
|
450
635
|
ctx.textAlign = 'right';
|
|
451
636
|
drawTruncatedText(ctx, data.valueText, x + width, rect.y + rect.height / 2, width);
|
|
452
637
|
ctx.textAlign = 'left';
|
|
453
638
|
} else {
|
|
454
|
-
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
|
+
);
|
|
455
650
|
}
|
|
456
651
|
}
|
|
457
652
|
|
|
@@ -474,23 +669,27 @@ export class TreeRowRenderer {
|
|
|
474
669
|
}
|
|
475
670
|
|
|
476
671
|
#drawInspectorTypeCell(ctx, { node, rect, theme }) {
|
|
477
|
-
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
|
+
);
|
|
478
682
|
}
|
|
479
683
|
|
|
480
684
|
#drawInspectorDescriptionCell(ctx, { node, rect, theme }) {
|
|
481
|
-
this.#drawMutedText(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
ctx.stroke();
|
|
490
|
-
if (!checked) return;
|
|
491
|
-
ctx.fillStyle = theme.colors.progressFill;
|
|
492
|
-
roundRect(ctx, x + 4, y + 4, 8, 8, 1.5);
|
|
493
|
-
ctx.fill();
|
|
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
|
+
);
|
|
494
693
|
}
|
|
495
694
|
|
|
496
695
|
#drawInspectorRange(ctx, x, y, width, data, theme, state = {}) {
|
|
@@ -504,12 +703,18 @@ export class TreeRowRenderer {
|
|
|
504
703
|
this.#drawMeterBar(ctx, x, y + 0.5, barWidth, 7, ratio, theme.colors.progressFill, theme);
|
|
505
704
|
this.#drawControlSurface(ctx, x + barWidth + gap, y - 6, valueWidth, 20, theme, {
|
|
506
705
|
hovered: Boolean(state.hoveredNumber),
|
|
507
|
-
active: Boolean(state.activeNumber)
|
|
706
|
+
active: Boolean(state.activeNumber)
|
|
508
707
|
});
|
|
509
|
-
ctx.fillStyle = theme.
|
|
708
|
+
ctx.fillStyle = resolveValueColor(theme, data.value);
|
|
510
709
|
ctx.font = theme.monoFont ?? theme.font;
|
|
511
710
|
ctx.textAlign = 'right';
|
|
512
|
-
drawTruncatedText(
|
|
711
|
+
drawTruncatedText(
|
|
712
|
+
ctx,
|
|
713
|
+
String(data.valueText ?? ''),
|
|
714
|
+
x + barWidth + gap + valueWidth - 6,
|
|
715
|
+
y + 4,
|
|
716
|
+
valueWidth - 10
|
|
717
|
+
);
|
|
513
718
|
ctx.textAlign = 'left';
|
|
514
719
|
ctx.font = theme.font;
|
|
515
720
|
}
|
|
@@ -523,7 +728,15 @@ export class TreeRowRenderer {
|
|
|
523
728
|
ctx.textAlign = 'left';
|
|
524
729
|
}
|
|
525
730
|
|
|
526
|
-
#drawControlSurface(
|
|
731
|
+
#drawControlSurface(
|
|
732
|
+
ctx,
|
|
733
|
+
x,
|
|
734
|
+
y,
|
|
735
|
+
width,
|
|
736
|
+
height,
|
|
737
|
+
theme,
|
|
738
|
+
{ hovered = false, active = false, disabled = false } = {}
|
|
739
|
+
) {
|
|
527
740
|
const colors = theme.colors;
|
|
528
741
|
ctx.fillStyle = disabled
|
|
529
742
|
? colors.progressTrack
|
|
@@ -534,7 +747,11 @@ export class TreeRowRenderer {
|
|
|
534
747
|
: colors.progressTrack;
|
|
535
748
|
roundRect(ctx, x, y, width, height, 5);
|
|
536
749
|
ctx.fill();
|
|
537
|
-
ctx.strokeStyle = active
|
|
750
|
+
ctx.strokeStyle = active
|
|
751
|
+
? colors.focus
|
|
752
|
+
: hovered
|
|
753
|
+
? mixColor(colors.border, colors.focus, 0.5)
|
|
754
|
+
: colors.border;
|
|
538
755
|
ctx.stroke();
|
|
539
756
|
}
|
|
540
757
|
|
|
@@ -548,8 +765,8 @@ export class TreeRowRenderer {
|
|
|
548
765
|
ctx.fill();
|
|
549
766
|
}
|
|
550
767
|
|
|
551
|
-
#drawMutedText(ctx, text, x, y, width, theme, readonly = false, font = null) {
|
|
552
|
-
ctx.fillStyle = readonly ? theme.colors.textMuted : theme.colors.text;
|
|
768
|
+
#drawMutedText(ctx, text, x, y, width, theme, readonly = false, font = null, color = null) {
|
|
769
|
+
ctx.fillStyle = color ?? (readonly ? theme.colors.textMuted : theme.colors.text);
|
|
553
770
|
ctx.font = font ?? theme.font;
|
|
554
771
|
ctx.textBaseline = 'middle';
|
|
555
772
|
ctx.textAlign = 'left';
|
|
@@ -558,19 +775,33 @@ export class TreeRowRenderer {
|
|
|
558
775
|
|
|
559
776
|
#drawTreeCell(ctx, { node, row, rect, theme, style }) {
|
|
560
777
|
const colors = theme.colors;
|
|
561
|
-
const
|
|
778
|
+
const layout = treeCellLayout(
|
|
779
|
+
row.depth,
|
|
780
|
+
theme.indentWidth,
|
|
781
|
+
this.scene.childrenByParent.size > 1
|
|
782
|
+
);
|
|
562
783
|
const cy = rect.y + rect.height / 2;
|
|
563
|
-
this.#drawChevron(ctx, x +
|
|
564
|
-
this.iconRegistry.draw(ctx, style.icon, x +
|
|
784
|
+
if (row.hasChildren) this.#drawChevron(ctx, rect.x + layout.chevronX, cy, row, colors);
|
|
785
|
+
this.iconRegistry.draw(ctx, style.icon, rect.x + layout.iconX, rect.y + 6, 15, style.color);
|
|
565
786
|
ctx.fillStyle = colors.text;
|
|
566
787
|
ctx.font = theme.font;
|
|
567
788
|
ctx.textBaseline = 'middle';
|
|
568
789
|
ctx.textAlign = 'left';
|
|
569
|
-
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
|
+
);
|
|
570
797
|
}
|
|
571
798
|
|
|
572
799
|
#drawStatusCell(ctx, { rect, style, theme }) {
|
|
573
|
-
|
|
800
|
+
ctx.font = theme.font;
|
|
801
|
+
const badgeWidth = Math.max(
|
|
802
|
+
0,
|
|
803
|
+
Math.min(ctx.measureText(style.status.label).width + 18, rect.width - 12)
|
|
804
|
+
);
|
|
574
805
|
const x = rect.x + (rect.width - badgeWidth) / 2;
|
|
575
806
|
const y = rect.y + (rect.height - 17) / 2;
|
|
576
807
|
const statusColor = style.status.color;
|
|
@@ -581,7 +812,7 @@ export class TreeRowRenderer {
|
|
|
581
812
|
ctx.strokeStyle = mixColor(theme.colors.border, statusColor, 0.42);
|
|
582
813
|
ctx.stroke();
|
|
583
814
|
ctx.fillStyle = mixColor(theme.colors.text, statusColor, 0.36);
|
|
584
|
-
ctx.font =
|
|
815
|
+
ctx.font = theme.font;
|
|
585
816
|
ctx.textAlign = 'center';
|
|
586
817
|
ctx.textBaseline = 'middle';
|
|
587
818
|
drawTruncatedText(ctx, style.status.label, x + badgeWidth / 2, y + 8.5, badgeWidth - 8);
|
|
@@ -589,7 +820,18 @@ export class TreeRowRenderer {
|
|
|
589
820
|
}
|
|
590
821
|
|
|
591
822
|
#drawProgressCell(ctx, { state, rect, theme, style }) {
|
|
592
|
-
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
|
+
});
|
|
593
835
|
const ratio = clamp01(state.progress);
|
|
594
836
|
const barX = rect.x + 10;
|
|
595
837
|
const barHeight = 7;
|
|
@@ -623,13 +865,33 @@ export class TreeRowRenderer {
|
|
|
623
865
|
|
|
624
866
|
#drawTextCell(ctx, { node, state, rect, column, theme }) {
|
|
625
867
|
let value = column.value(node, state);
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
868
|
+
const numeric = typeof value === 'number';
|
|
869
|
+
const valueType =
|
|
870
|
+
typeof column.valueType === 'function'
|
|
871
|
+
? column.valueType(value, node, state)
|
|
872
|
+
: column.valueType;
|
|
873
|
+
const valueColor = resolveValueColor(theme, value, valueType);
|
|
874
|
+
if (column.format) value = column.format(value, node, state);
|
|
875
|
+
else if (column.kind === 'updated' && numeric) value = formatTime(value);
|
|
876
|
+
else if (numeric) value = formatInspectorValue(value);
|
|
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;
|
|
630
887
|
ctx.textBaseline = 'middle';
|
|
631
888
|
ctx.textAlign = column.align;
|
|
632
|
-
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;
|
|
633
895
|
drawTruncatedText(ctx, String(value ?? ''), x, rect.y + rect.height / 2, rect.width - 20);
|
|
634
896
|
ctx.textAlign = 'left';
|
|
635
897
|
}
|
|
@@ -648,13 +910,8 @@ export class TreeRowRenderer {
|
|
|
648
910
|
}
|
|
649
911
|
|
|
650
912
|
#drawChevron(ctx, x, y, row, colors) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
ctx.beginPath();
|
|
654
|
-
ctx.arc(x, y, 1.5, 0, Math.PI * 2);
|
|
655
|
-
ctx.fill();
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
913
|
+
if (!row.hasChildren) return;
|
|
914
|
+
ctx.fillStyle = colors.chevron;
|
|
658
915
|
ctx.beginPath();
|
|
659
916
|
if (row.expanded) {
|
|
660
917
|
ctx.moveTo(x - 5, y - 2);
|
|
@@ -668,35 +925,23 @@ export class TreeRowRenderer {
|
|
|
668
925
|
ctx.closePath();
|
|
669
926
|
ctx.fill();
|
|
670
927
|
}
|
|
928
|
+
|
|
671
929
|
}
|
|
672
930
|
|
|
673
931
|
function resolveNodeStyle(theme, node, state = {}) {
|
|
674
932
|
const typeRule = theme.types[node?.type ?? ''] ?? {};
|
|
675
|
-
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
|
+
};
|
|
676
937
|
return {
|
|
677
938
|
icon: state.icon ?? node?.icon ?? typeRule.icon ?? 'placeholder',
|
|
678
939
|
color: state.color ?? typeRule.color ?? theme.colors.progressFill,
|
|
679
940
|
typeColor: typeRule.color ?? theme.colors.progressFill,
|
|
680
|
-
status
|
|
941
|
+
status
|
|
681
942
|
};
|
|
682
943
|
}
|
|
683
944
|
|
|
684
|
-
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
685
|
-
const safeWidth = Math.max(1, width);
|
|
686
|
-
const rightPadding = 14;
|
|
687
|
-
if (editorType === 'checkbox') {
|
|
688
|
-
const editorWidth = 34;
|
|
689
|
-
return { editorLeft: Math.max(64, safeWidth - rightPadding - editorWidth), editorWidth };
|
|
690
|
-
}
|
|
691
|
-
const minEditor = Math.min(170, Math.max(96, safeWidth * 0.45));
|
|
692
|
-
const minLabelEnd = Math.max(88, depth * indentWidth + 104);
|
|
693
|
-
const preferredLeft = Math.max(minLabelEnd, labelEnd, safeWidth * 0.32);
|
|
694
|
-
const maxLeft = Math.max(64, safeWidth - rightPadding - minEditor);
|
|
695
|
-
const editorLeft = Math.max(64, Math.min(preferredLeft, maxLeft));
|
|
696
|
-
const editorWidth = Math.max(56, safeWidth - rightPadding - editorLeft);
|
|
697
|
-
return { editorLeft, editorWidth };
|
|
698
|
-
}
|
|
699
|
-
|
|
700
945
|
function clamp01(value) {
|
|
701
946
|
return Math.max(0, Math.min(1, value));
|
|
702
947
|
}
|
|
@@ -785,7 +1030,7 @@ function setTextFitCache(key, value) {
|
|
|
785
1030
|
const timeFormatter = new Intl.DateTimeFormat([], {
|
|
786
1031
|
hour: '2-digit',
|
|
787
1032
|
minute: '2-digit',
|
|
788
|
-
second: '2-digit'
|
|
1033
|
+
second: '2-digit'
|
|
789
1034
|
});
|
|
790
1035
|
|
|
791
1036
|
function formatTime(value) {
|
|
@@ -793,3 +1038,7 @@ function formatTime(value) {
|
|
|
793
1038
|
if (!Number.isFinite(time)) return '';
|
|
794
1039
|
return timeFormatter.format(time);
|
|
795
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
function resolveValueColor(theme, value, type) {
|
|
1043
|
+
return theme.valueColors?.[type ?? (value === null ? 'null' : typeof value)] ?? theme.colors.text;
|
|
1044
|
+
}
|