virtual-tree-canvas 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +115 -28
- package/docs/icon-catalog.html +67 -0
- package/docs/icon-catalog.json +380 -0
- package/docs/icon-catalog.md +167 -0
- package/examples/generate-tree.js +86 -0
- package/examples/reorder-table.html +33 -0
- package/examples/units.html +43 -0
- package/package.json +7 -3
- package/resources/icons/bicycle.svg +1 -0
- package/resources/icons/bird.svg +1 -0
- package/resources/icons/box.svg +1 -0
- package/resources/icons/building.svg +1 -0
- package/resources/icons/calendar.svg +1 -0
- package/resources/icons/car.svg +1 -0
- package/resources/icons/cat.svg +1 -0
- package/resources/icons/child.svg +1 -0
- package/resources/icons/clock.svg +1 -0
- package/resources/icons/cloud.svg +1 -0
- package/resources/icons/data-bus.svg +1 -0
- package/resources/icons/database.svg +1 -0
- package/resources/icons/dog.svg +1 -0
- package/resources/icons/drone.svg +1 -0
- package/resources/icons/fish.svg +1 -0
- package/resources/icons/flower.svg +1 -0
- package/resources/icons/fog.svg +1 -0
- package/resources/icons/globe.svg +1 -0
- package/resources/icons/grip.svg +1 -0
- package/resources/icons/heart.svg +1 -0
- package/resources/icons/helicopter.svg +1 -0
- package/resources/icons/insect.svg +1 -0
- package/resources/icons/leaf.svg +1 -0
- package/resources/icons/link.svg +1 -0
- package/resources/icons/lock.svg +1 -0
- package/resources/icons/moon.svg +1 -0
- package/resources/icons/network.svg +1 -0
- package/resources/icons/people.svg +1 -0
- package/resources/icons/person.svg +1 -0
- package/resources/icons/pin.svg +1 -0
- package/resources/icons/radar.svg +1 -0
- package/resources/icons/rain.svg +1 -0
- package/resources/icons/seedling.svg +1 -0
- package/resources/icons/sensor.svg +1 -0
- package/resources/icons/server.svg +1 -0
- package/resources/icons/ship.svg +1 -0
- package/resources/icons/snow.svg +1 -0
- package/resources/icons/star.svg +1 -0
- package/resources/icons/storm.svg +1 -0
- package/resources/icons/sun.svg +1 -0
- package/resources/icons/thermometer.svg +1 -0
- package/resources/icons/tree.svg +1 -0
- package/resources/icons/wind.svg +1 -0
- package/src/assets/icons.js +73 -25
- package/src/core/icon-registry.js +6 -8
- package/src/core/theme-manager.js +15 -4
- package/src/core/tree-column-model.js +16 -3
- package/src/core/tree-model.js +20 -0
- package/src/core/view-options.js +30 -0
- package/src/index.d.ts +166 -5
- package/src/index.js +3 -0
- package/src/input/index.js +1 -0
- package/src/input/row-reorder-input.js +143 -0
- package/src/input/tree-filter-bar.js +55 -0
- package/src/input/tree-tooltip.js +55 -0
- package/src/input/tree-view-input-controller.js +5 -0
- package/src/inspector/cell-editor-manager.js +42 -22
- package/src/inspector/editor-resolver.js +6 -1
- package/src/inspector/numeric-layout.js +16 -0
- package/src/renderers/tree-row-renderer.js +79 -13
- package/src/tree-view-controller.js +261 -34
- package/src/tree-view.js +181 -0
- package/src/view/styles.js +14 -0
- package/src/assets/icons/air.svg +0 -1
- package/src/assets/icons/radar.svg +0 -1
- /package/{src/assets → resources}/icons/aircraft.svg +0 -0
- /package/{src/assets/icons/bus.svg → resources/icons/bus-vehicle.svg} +0 -0
- /package/{src/assets → resources}/icons/control.svg +0 -0
- /package/{src/assets → resources}/icons/damage.svg +0 -0
- /package/{src/assets → resources}/icons/error.svg +0 -0
- /package/{src/assets → resources}/icons/folder.svg +0 -0
- /package/{src/assets → resources}/icons/ground.svg +0 -0
- /package/{src/assets → resources}/icons/inspector-array.svg +0 -0
- /package/{src/assets → resources}/icons/inspector-object.svg +0 -0
- /package/{src/assets → resources}/icons/inspector-value.svg +0 -0
- /package/{src/assets → resources}/icons/munition.svg +0 -0
- /package/{src/assets → resources}/icons/placeholder.svg +0 -0
- /package/{src/assets → resources}/icons/point.svg +0 -0
- /package/{src/assets → resources}/icons/situation.svg +0 -0
- /package/{src/assets → resources}/icons/space.svg +0 -0
- /package/{src/assets → resources}/icons/subsurface.svg +0 -0
- /package/{src/assets → resources}/icons/surface.svg +0 -0
- /package/{src/assets → resources}/icons/task.svg +0 -0
- /package/{src/assets → resources}/icons/track.svg +0 -0
- /package/{src/assets → resources}/icons/warning.svg +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Shared geometry for canvas drawing, hit testing and the numeric editor. */
|
|
2
|
+
export function numericLayout(width, data = {}) {
|
|
3
|
+
width = Math.max(0, width);
|
|
4
|
+
const numeric = typeof data.value === 'number' && !data.meta?.options && !data.meta?.button;
|
|
5
|
+
const unit = numeric && typeof data.meta?.unit === 'string' ? data.meta.unit.trim() : '';
|
|
6
|
+
// Reserve a stable slot: changes to the number must not move the unit.
|
|
7
|
+
const gap = unit ? 6 : 0;
|
|
8
|
+
const unitWidth = unit ? Math.min(Math.max(24, [...unit].length * 7), 96, Math.max(0, width - 48)) : 0;
|
|
9
|
+
const contentWidth = Math.max(0, width - unitWidth - gap);
|
|
10
|
+
const valueWidth = Math.min(64, Math.max(42, contentWidth * 0.28), Math.max(0, contentWidth - 32));
|
|
11
|
+
return {
|
|
12
|
+
unit, unitWidth, unitLeft: contentWidth + gap, contentWidth,
|
|
13
|
+
valueWidth, numberLeft: contentWidth - valueWidth,
|
|
14
|
+
barWidth: Math.max(0, contentWidth - valueWidth - 8),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { numericLayout } from '../inspector/numeric-layout.js';
|
|
1
2
|
import { IconRegistry } from '../core/icon-registry.js';
|
|
2
3
|
|
|
3
4
|
const DISABLED_ALPHA = 0.72;
|
|
@@ -9,7 +10,8 @@ export class TreeRowRenderer {
|
|
|
9
10
|
this.scene = null;
|
|
10
11
|
this.iconRegistry = iconRegistry ?? new IconRegistry();
|
|
11
12
|
this.renderedRows = 0;
|
|
12
|
-
this.
|
|
13
|
+
this.iconRenderFrame = null;
|
|
14
|
+
this.invalidate = null;
|
|
13
15
|
this.preparedIconThemeKey = null;
|
|
14
16
|
this.stopIconListener = this.iconRegistry.onChange?.(() => this.#scheduleIconRender()) ?? null;
|
|
15
17
|
}
|
|
@@ -25,7 +27,15 @@ export class TreeRowRenderer {
|
|
|
25
27
|
this.scene = scene;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
setInvalidationHandler(invalidate) { this.invalidate = invalidate; }
|
|
31
|
+
|
|
28
32
|
destroy() {
|
|
33
|
+
if (this.iconRenderFrame !== null) {
|
|
34
|
+
const view = this.canvas?.ownerDocument?.defaultView ?? globalThis;
|
|
35
|
+
(view.cancelAnimationFrame?.bind(view) ?? clearTimeout)(this.iconRenderFrame);
|
|
36
|
+
}
|
|
37
|
+
this.iconRenderFrame = null;
|
|
38
|
+
this.invalidate = null;
|
|
29
39
|
this.stopIconListener?.();
|
|
30
40
|
this.stopIconListener = null;
|
|
31
41
|
this.canvas = null;
|
|
@@ -64,15 +74,18 @@ export class TreeRowRenderer {
|
|
|
64
74
|
this.#drawHeader(ctx);
|
|
65
75
|
this.#drawRows(ctx);
|
|
66
76
|
this.#drawStickyRows(ctx);
|
|
77
|
+
this.#drawRowDrop(ctx);
|
|
67
78
|
ctx.restore();
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
#scheduleIconRender() {
|
|
71
|
-
if (this.
|
|
72
|
-
this.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
if (!this.canvas) return;
|
|
83
|
+
if (this.invalidate) return this.invalidate();
|
|
84
|
+
if (this.iconRenderFrame !== null) return;
|
|
85
|
+
const view = this.canvas.ownerDocument?.defaultView ?? globalThis;
|
|
86
|
+
const schedule = view.requestAnimationFrame?.bind(view) ?? (callback => setTimeout(callback, 0));
|
|
87
|
+
this.iconRenderFrame = schedule(() => {
|
|
88
|
+
this.iconRenderFrame = null;
|
|
76
89
|
this.render();
|
|
77
90
|
});
|
|
78
91
|
}
|
|
@@ -106,7 +119,7 @@ export class TreeRowRenderer {
|
|
|
106
119
|
ctx.textBaseline = 'middle';
|
|
107
120
|
|
|
108
121
|
for (const column of columns) {
|
|
109
|
-
if (headerFilter && column.kind
|
|
122
|
+
if (headerFilter && column === columns.find(item => item.kind !== 'rowOrder')) {
|
|
110
123
|
this.#drawHeaderFilter(ctx, column, viewport, theme, filterQuery);
|
|
111
124
|
} else {
|
|
112
125
|
ctx.fillStyle = colors.textMuted;
|
|
@@ -144,7 +157,7 @@ export class TreeRowRenderer {
|
|
|
144
157
|
ctx.strokeStyle = colors.border;
|
|
145
158
|
ctx.stroke();
|
|
146
159
|
ctx.fillStyle = filterQuery ? colors.text : colors.textMuted;
|
|
147
|
-
drawTruncatedText(ctx, filterQuery || 'Filter
|
|
160
|
+
drawTruncatedText(ctx, filterQuery || 'Filter rows', x + 8, viewport.headerHeight / 2, width - 16);
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
#drawSortIndicator(ctx, x, y, direction, theme) {
|
|
@@ -267,7 +280,8 @@ export class TreeRowRenderer {
|
|
|
267
280
|
cell.column.render(ctx, cell);
|
|
268
281
|
return;
|
|
269
282
|
}
|
|
270
|
-
if (cell.column.kind === '
|
|
283
|
+
if (cell.column.kind === 'rowOrder') this.#drawRowOrder(ctx, cell);
|
|
284
|
+
else if (cell.column.kind === 'tree') this.#drawTreeCell(ctx, cell);
|
|
271
285
|
else if (cell.column.kind === 'inspectorPane') this.#drawInspectorPaneCell(ctx, cell);
|
|
272
286
|
else if (cell.column.kind === 'inspectorValue') this.#drawInspectorValueCell(ctx, cell);
|
|
273
287
|
else if (cell.column.kind === 'inspectorType') this.#drawInspectorTypeCell(ctx, cell);
|
|
@@ -277,6 +291,42 @@ export class TreeRowRenderer {
|
|
|
277
291
|
else this.#drawTextCell(ctx, cell);
|
|
278
292
|
}
|
|
279
293
|
|
|
294
|
+
#drawRowOrder(ctx, { node, row, rect, theme }) {
|
|
295
|
+
const siblings = this.scene.childrenByParent?.get(node.parentId ?? null) ?? [];
|
|
296
|
+
const enabled = this.scene.canReorderRows && siblings.length > 1;
|
|
297
|
+
const cy = rect.y + row.height / 2;
|
|
298
|
+
ctx.save();
|
|
299
|
+
ctx.strokeStyle = theme.colors.textMuted;
|
|
300
|
+
ctx.fillStyle = theme.colors.textMuted;
|
|
301
|
+
ctx.lineWidth = 1.5;
|
|
302
|
+
ctx.globalAlpha = enabled ? 1 : 0.3;
|
|
303
|
+
for (const dx of [10, 14]) for (const dy of [-4, 0, 4]) ctx.fillRect(rect.x + dx, cy + dy, 1.5, 1.5);
|
|
304
|
+
for (const [center, direction, available] of [[36, -1, siblings[0] !== node.id], [60, 1, siblings.at(-1) !== node.id]]) {
|
|
305
|
+
ctx.globalAlpha = enabled && available ? 1 : 0.3;
|
|
306
|
+
ctx.beginPath();
|
|
307
|
+
ctx.moveTo(rect.x + center - 4, cy - direction * 2);
|
|
308
|
+
ctx.lineTo(rect.x + center, cy + direction * 2);
|
|
309
|
+
ctx.lineTo(rect.x + center + 4, cy - direction * 2);
|
|
310
|
+
ctx.stroke();
|
|
311
|
+
}
|
|
312
|
+
ctx.restore();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#drawRowDrop(ctx) {
|
|
316
|
+
const { rowDrop, viewport, theme } = this.scene;
|
|
317
|
+
if (!rowDrop) return;
|
|
318
|
+
const y = viewport.headerHeight + rowDrop.y - viewport.scrollY;
|
|
319
|
+
if (y < viewport.headerHeight || y > viewport.headerHeight + viewport.rowViewportHeight) return;
|
|
320
|
+
ctx.save();
|
|
321
|
+
ctx.strokeStyle = theme.colors.focus;
|
|
322
|
+
ctx.lineWidth = 2;
|
|
323
|
+
ctx.beginPath();
|
|
324
|
+
ctx.moveTo(3, y);
|
|
325
|
+
ctx.lineTo(viewport.contentViewportWidth - 3, y);
|
|
326
|
+
ctx.stroke();
|
|
327
|
+
ctx.restore();
|
|
328
|
+
}
|
|
329
|
+
|
|
280
330
|
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style, hovered, hoverPart, activePart }) {
|
|
281
331
|
const visibleRight = this.scene.viewport.scrollX + (this.scene.viewport.contentViewportWidth ?? this.scene.viewport.viewportWidth);
|
|
282
332
|
rect = { ...rect, width: Math.max(1, Math.min(rect.x + rect.width, visibleRight) - rect.x) };
|
|
@@ -344,13 +394,22 @@ export class TreeRowRenderer {
|
|
|
344
394
|
const readonly = data.readonly;
|
|
345
395
|
const x = rect.x + 10;
|
|
346
396
|
const y = rect.y + 5;
|
|
347
|
-
const
|
|
397
|
+
const fullWidth = Math.max(24, rect.width - 20);
|
|
398
|
+
const numeric = numericLayout(fullWidth, data);
|
|
399
|
+
const width = numeric.contentWidth;
|
|
348
400
|
const height = rect.height - 10;
|
|
349
401
|
ctx.font = theme.font;
|
|
350
402
|
ctx.textBaseline = 'middle';
|
|
351
403
|
ctx.textAlign = 'left';
|
|
352
404
|
ctx.globalAlpha = disabled ? DISABLED_ALPHA : 1;
|
|
353
405
|
|
|
406
|
+
if (numeric.unit && numeric.unitWidth) {
|
|
407
|
+
ctx.font = theme.unitFont ?? theme.monoFont ?? theme.font;
|
|
408
|
+
ctx.fillStyle = theme.colors.unit ?? theme.colors.textMuted;
|
|
409
|
+
drawTruncatedText(ctx, numeric.unit, x + numeric.unitLeft, rect.y + rect.height / 2, numeric.unitWidth);
|
|
410
|
+
ctx.font = theme.font;
|
|
411
|
+
}
|
|
412
|
+
|
|
354
413
|
if (data.editorType === 'checkbox') {
|
|
355
414
|
this.#drawCheckbox(ctx, x, rect.y + rect.height / 2 - 8, Boolean(data.value), theme);
|
|
356
415
|
} else if (data.editorType === 'range') {
|
|
@@ -385,7 +444,15 @@ export class TreeRowRenderer {
|
|
|
385
444
|
this.#drawMutedText(ctx, data.valueText, x + 8, rect.y + rect.height / 2, selectWidth - 30, theme);
|
|
386
445
|
this.#drawSelectChevron(ctx, x + selectWidth - 18, rect.y + rect.height / 2, theme, disabled);
|
|
387
446
|
} else {
|
|
388
|
-
|
|
447
|
+
if (numeric.unit) {
|
|
448
|
+
ctx.font = theme.monoFont ?? theme.font;
|
|
449
|
+
ctx.fillStyle = readonly ? theme.colors.textMuted : theme.colors.text;
|
|
450
|
+
ctx.textAlign = 'right';
|
|
451
|
+
drawTruncatedText(ctx, data.valueText, x + width, rect.y + rect.height / 2, width);
|
|
452
|
+
ctx.textAlign = 'left';
|
|
453
|
+
} else {
|
|
454
|
+
this.#drawMutedText(ctx, data.valueText, x, rect.y + rect.height / 2, width, theme, readonly);
|
|
455
|
+
}
|
|
389
456
|
}
|
|
390
457
|
|
|
391
458
|
if (meta.updated && !suppressUpdatedMarker) {
|
|
@@ -432,9 +499,8 @@ export class TreeRowRenderer {
|
|
|
432
499
|
const max = meta.max ?? 100;
|
|
433
500
|
const value = typeof data.value === 'number' ? data.value : min;
|
|
434
501
|
const ratio = max === min ? 0 : clamp01((value - min) / (max - min));
|
|
435
|
-
const valueWidth =
|
|
502
|
+
const { valueWidth, barWidth } = numericLayout(width);
|
|
436
503
|
const gap = 8;
|
|
437
|
-
const barWidth = Math.max(24, width - valueWidth - gap);
|
|
438
504
|
this.#drawMeterBar(ctx, x, y + 0.5, barWidth, 7, ratio, theme.colors.progressFill, theme);
|
|
439
505
|
this.#drawControlSurface(ctx, x + barWidth + gap, y - 6, valueWidth, 20, theme, {
|
|
440
506
|
hovered: Boolean(state.hoveredNumber),
|