virtual-tree-canvas 0.2.0 → 0.3.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/package.json
CHANGED
|
@@ -385,27 +385,38 @@ function drawSituation(ctx, x, y, size, color) {
|
|
|
385
385
|
function drawDamage(ctx, x, y, size, color) {
|
|
386
386
|
const cx = x + size * 0.5;
|
|
387
387
|
const cy = y + size * 0.52;
|
|
388
|
-
|
|
389
|
-
ctx.fillStyle = color;
|
|
390
|
-
ctx.lineWidth = 1.4;
|
|
388
|
+
const r = size * 0.42;
|
|
391
389
|
|
|
392
390
|
ctx.beginPath();
|
|
393
391
|
ctx.moveTo(cx, y + 1);
|
|
394
|
-
ctx.lineTo(
|
|
395
|
-
ctx.lineTo(x + size
|
|
396
|
-
ctx.lineTo(
|
|
397
|
-
ctx.lineTo(x + size * 0.
|
|
398
|
-
ctx.lineTo(
|
|
399
|
-
ctx.lineTo(cx, y + size
|
|
400
|
-
ctx.lineTo(
|
|
401
|
-
ctx.lineTo(x + size * 0.
|
|
402
|
-
ctx.lineTo(
|
|
403
|
-
ctx.lineTo(x +
|
|
404
|
-
ctx.lineTo(
|
|
392
|
+
ctx.lineTo(cx + r * 0.28, cy - r * 0.28);
|
|
393
|
+
ctx.lineTo(x + size - 1, y + size * 0.2);
|
|
394
|
+
ctx.lineTo(cx + r * 0.55, cy + r * 0.05);
|
|
395
|
+
ctx.lineTo(x + size * 0.92, y + size * 0.76);
|
|
396
|
+
ctx.lineTo(cx + r * 0.22, cy + r * 0.32);
|
|
397
|
+
ctx.lineTo(cx + r * 0.08, y + size - 1);
|
|
398
|
+
ctx.lineTo(cx - r * 0.18, cy + r * 0.34);
|
|
399
|
+
ctx.lineTo(x + size * 0.14, y + size * 0.86);
|
|
400
|
+
ctx.lineTo(cx - r * 0.42, cy + r * 0.12);
|
|
401
|
+
ctx.lineTo(x + 1, y + size * 0.38);
|
|
402
|
+
ctx.lineTo(cx - r * 0.3, cy - r * 0.18);
|
|
405
403
|
ctx.closePath();
|
|
406
|
-
ctx.
|
|
404
|
+
ctx.fillStyle = color;
|
|
405
|
+
ctx.fill();
|
|
407
406
|
|
|
407
|
+
ctx.fillStyle = '#fff7ed';
|
|
408
408
|
ctx.beginPath();
|
|
409
|
-
ctx.arc(cx, cy, size * 0.
|
|
409
|
+
ctx.arc(cx, cy, size * 0.13, 0, Math.PI * 2);
|
|
410
410
|
ctx.fill();
|
|
411
|
+
|
|
412
|
+
ctx.strokeStyle = color;
|
|
413
|
+
ctx.lineWidth = 1.1;
|
|
414
|
+
ctx.beginPath();
|
|
415
|
+
ctx.moveTo(x + size * 0.08, y + size * 0.08);
|
|
416
|
+
ctx.lineTo(x + size * 0.2, y + size * 0.2);
|
|
417
|
+
ctx.moveTo(x + size * 0.84, y + size * 0.88);
|
|
418
|
+
ctx.lineTo(x + size * 0.94, y + size * 0.98);
|
|
419
|
+
ctx.moveTo(x + size * 0.9, y + size * 0.06);
|
|
420
|
+
ctx.lineTo(x + size * 0.82, y + size * 0.18);
|
|
421
|
+
ctx.stroke();
|
|
411
422
|
}
|
|
@@ -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);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { IconRegistry } from '../core/icon-registry.js';
|
|
2
2
|
|
|
3
|
+
const DISABLED_ALPHA = 0.72;
|
|
4
|
+
const SCROLL_INDICATOR_HOLD_MS = 1600;
|
|
5
|
+
const SCROLL_INDICATOR_FADE_MS = 700;
|
|
6
|
+
|
|
3
7
|
export class TreeRowRenderer {
|
|
4
8
|
constructor({ iconRegistry } = {}) {
|
|
5
9
|
this.canvas = null;
|
|
@@ -7,6 +11,8 @@ export class TreeRowRenderer {
|
|
|
7
11
|
this.scene = null;
|
|
8
12
|
this.iconRegistry = iconRegistry ?? new IconRegistry();
|
|
9
13
|
this.renderedRows = 0;
|
|
14
|
+
this.scrollIndicatorState = null;
|
|
15
|
+
this.scrollIndicatorVisibleUntil = 0;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
/** @param {HTMLCanvasElement} canvas */
|
|
@@ -24,30 +30,32 @@ export class TreeRowRenderer {
|
|
|
24
30
|
// Canvas2D reads the state map directly; patches still stay on the hot path.
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
render(scene) {
|
|
33
|
+
render(scene, time = performance.now()) {
|
|
28
34
|
if (scene?.rows) this.scene = scene;
|
|
29
35
|
if (!this.canvas || !this.ctx || !this.scene) return;
|
|
30
36
|
const { viewport, theme } = this.scene;
|
|
31
37
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
32
|
-
const
|
|
33
|
-
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);
|
|
34
43
|
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
35
44
|
this.canvas.width = width;
|
|
36
45
|
this.canvas.height = height;
|
|
37
|
-
viewport.resize(this.canvas.clientWidth, this.canvas.clientHeight);
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
const ctx = this.ctx;
|
|
41
49
|
const colors = theme.colors;
|
|
42
50
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
43
51
|
ctx.fillStyle = colors.background;
|
|
44
|
-
ctx.fillRect(0, 0,
|
|
52
|
+
ctx.fillRect(0, 0, viewportWidth, viewportHeight);
|
|
45
53
|
|
|
46
54
|
ctx.save();
|
|
47
55
|
ctx.translate(viewport.renderInsetX ?? 0, viewport.renderInsetY ?? 0);
|
|
48
56
|
this.#drawHeader(ctx);
|
|
49
57
|
this.#drawRows(ctx);
|
|
50
|
-
this.#drawScrollIndicator(ctx);
|
|
58
|
+
this.#drawScrollIndicator(ctx, time);
|
|
51
59
|
ctx.restore();
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -119,10 +127,12 @@ export class TreeRowRenderer {
|
|
|
119
127
|
ctx.restore();
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
#drawScrollIndicator(ctx) {
|
|
123
|
-
const { viewport, theme } = this.scene;
|
|
130
|
+
#drawScrollIndicator(ctx, time) {
|
|
131
|
+
const { viewport, theme, rows } = this.scene;
|
|
124
132
|
const maxY = Math.max(0, viewport.contentHeight - viewport.rowViewportHeight);
|
|
125
133
|
if (maxY <= 0 || viewport.viewportHeight <= viewport.headerHeight + 12) return;
|
|
134
|
+
const alpha = this.#scrollIndicatorAlpha(viewport, rows?.length ?? 0, time);
|
|
135
|
+
if (alpha <= 0) return;
|
|
126
136
|
|
|
127
137
|
const trackTop = viewport.headerHeight + 4;
|
|
128
138
|
const trackHeight = Math.max(1, viewport.viewportHeight - viewport.headerHeight - 8);
|
|
@@ -133,14 +143,14 @@ export class TreeRowRenderer {
|
|
|
133
143
|
ctx.save();
|
|
134
144
|
ctx.lineCap = 'round';
|
|
135
145
|
ctx.lineWidth = 2;
|
|
136
|
-
ctx.globalAlpha = 0.28;
|
|
146
|
+
ctx.globalAlpha = 0.28 * alpha;
|
|
137
147
|
ctx.strokeStyle = theme.colors.guide;
|
|
138
148
|
ctx.beginPath();
|
|
139
149
|
ctx.moveTo(x, trackTop);
|
|
140
150
|
ctx.lineTo(x, trackTop + trackHeight);
|
|
141
151
|
ctx.stroke();
|
|
142
152
|
|
|
143
|
-
ctx.globalAlpha = 0.9;
|
|
153
|
+
ctx.globalAlpha = 0.9 * alpha;
|
|
144
154
|
ctx.strokeStyle = theme.colors.focus;
|
|
145
155
|
ctx.beginPath();
|
|
146
156
|
ctx.moveTo(x, thumbY);
|
|
@@ -149,6 +159,24 @@ export class TreeRowRenderer {
|
|
|
149
159
|
ctx.restore();
|
|
150
160
|
}
|
|
151
161
|
|
|
162
|
+
#scrollIndicatorAlpha(viewport, rowCount, time) {
|
|
163
|
+
const nextState = {
|
|
164
|
+
scrollY: viewport.scrollY,
|
|
165
|
+
contentHeight: viewport.contentHeight,
|
|
166
|
+
rowViewportHeight: viewport.rowViewportHeight,
|
|
167
|
+
viewportHeight: viewport.viewportHeight,
|
|
168
|
+
rowCount,
|
|
169
|
+
};
|
|
170
|
+
if (!sameScrollIndicatorState(this.scrollIndicatorState, nextState)) {
|
|
171
|
+
this.scrollIndicatorState = nextState;
|
|
172
|
+
this.scrollIndicatorVisibleUntil = time + SCROLL_INDICATOR_HOLD_MS;
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
if (time <= this.scrollIndicatorVisibleUntil) return 1;
|
|
176
|
+
const fade = 1 - ((time - this.scrollIndicatorVisibleUntil) / SCROLL_INDICATOR_FADE_MS);
|
|
177
|
+
return clamp01(fade);
|
|
178
|
+
}
|
|
179
|
+
|
|
152
180
|
#drawRow(ctx, row) {
|
|
153
181
|
const { columns, nodes, dynamicState, selection, hoverNodeId, focusNodeId, searchMatches, theme, viewport } = this.scene;
|
|
154
182
|
const node = nodes[row.nodeIndex];
|
|
@@ -226,8 +254,16 @@ export class TreeRowRenderer {
|
|
|
226
254
|
ctx.font = theme.font;
|
|
227
255
|
ctx.textBaseline = 'middle';
|
|
228
256
|
ctx.textAlign = 'left';
|
|
229
|
-
ctx.globalAlpha = data.disabled ?
|
|
257
|
+
ctx.globalAlpha = data.disabled ? DISABLED_ALPHA : 1;
|
|
230
258
|
ctx.fillStyle = data.valueType === 'object' || data.valueType === 'array' ? colors.text : colors.textMuted;
|
|
259
|
+
|
|
260
|
+
if (data.valueType === 'object') {
|
|
261
|
+
drawTruncatedText(ctx, node.label ?? node.id, labelX, cy, Math.max(20, rect.x + rect.width - labelX - 8));
|
|
262
|
+
if (data.meta?.updated) this.#drawUpdatedMarker(ctx, row, theme);
|
|
263
|
+
ctx.globalAlpha = 1;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
231
267
|
const layout = inspectorPaneLayout(rect.width, row.depth, theme.indentWidth, data.editorType, this.scene.inspectorPaneLabelEnd);
|
|
232
268
|
drawTruncatedText(ctx, node.label ?? node.id, labelX, cy, Math.max(20, rect.x + layout.editorLeft - labelX - 8));
|
|
233
269
|
|
|
@@ -238,8 +274,6 @@ export class TreeRowRenderer {
|
|
|
238
274
|
drawTruncatedText(ctx, data.valueText, editorX, cy, Math.max(20, editorWidth - 58));
|
|
239
275
|
this.#drawSmallButton(ctx, rect.x + rect.width - 54, rect.y + 5, 22, rect.height - 10, '+', theme);
|
|
240
276
|
this.#drawSmallButton(ctx, rect.x + rect.width - 28, rect.y + 5, 22, rect.height - 10, '-', theme);
|
|
241
|
-
} else if (data.valueType === 'object') {
|
|
242
|
-
// Pane mode uses object rows as folders; the label is enough.
|
|
243
277
|
} else {
|
|
244
278
|
this.#drawInspectorValueCell(ctx, {
|
|
245
279
|
node,
|
|
@@ -264,7 +298,7 @@ export class TreeRowRenderer {
|
|
|
264
298
|
ctx.font = theme.font;
|
|
265
299
|
ctx.textBaseline = 'middle';
|
|
266
300
|
ctx.textAlign = 'left';
|
|
267
|
-
ctx.globalAlpha = disabled ?
|
|
301
|
+
ctx.globalAlpha = disabled ? DISABLED_ALPHA : 1;
|
|
268
302
|
|
|
269
303
|
if (data.editorType === 'checkbox') {
|
|
270
304
|
this.#drawCheckbox(ctx, x, rect.y + rect.height / 2 - 7, Boolean(data.value), theme);
|
|
@@ -475,6 +509,15 @@ function resolveNodeStyle(theme, node, state = {}) {
|
|
|
475
509
|
};
|
|
476
510
|
}
|
|
477
511
|
|
|
512
|
+
function sameScrollIndicatorState(a, b) {
|
|
513
|
+
return Boolean(a) &&
|
|
514
|
+
a.scrollY === b.scrollY &&
|
|
515
|
+
a.contentHeight === b.contentHeight &&
|
|
516
|
+
a.rowViewportHeight === b.rowViewportHeight &&
|
|
517
|
+
a.viewportHeight === b.viewportHeight &&
|
|
518
|
+
a.rowCount === b.rowCount;
|
|
519
|
+
}
|
|
520
|
+
|
|
478
521
|
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
479
522
|
const safeWidth = Math.max(1, width);
|
|
480
523
|
const rightPadding = 14;
|
|
@@ -63,6 +63,7 @@ export class TreeViewController {
|
|
|
63
63
|
this.canvas = canvas;
|
|
64
64
|
this.renderer.initialize(canvas);
|
|
65
65
|
this.renderer.setScene(this.scene);
|
|
66
|
+
this.#observeCanvasSize();
|
|
66
67
|
return this;
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -76,6 +77,9 @@ export class TreeViewController {
|
|
|
76
77
|
|
|
77
78
|
setData(nodes) {
|
|
78
79
|
this.inspector = null;
|
|
80
|
+
if (this.columnModel.columns.length === 1 && this.columnModel.columns[0]?.kind === 'inspectorPane') {
|
|
81
|
+
this.columnModel.setColumns([]);
|
|
82
|
+
}
|
|
79
83
|
this.model.setTree(nodes);
|
|
80
84
|
this.expansion.expandToDepth(this.initialExpandDepth);
|
|
81
85
|
this.searchIndex.rebuild(this.model);
|
|
@@ -561,7 +565,6 @@ export class TreeViewController {
|
|
|
561
565
|
}
|
|
562
566
|
|
|
563
567
|
renderMeasured(time = performance.now()) {
|
|
564
|
-
this.#syncViewportFromCanvas();
|
|
565
568
|
const sceneStart = performance.now();
|
|
566
569
|
this.scene = this.createRenderScene();
|
|
567
570
|
const sceneMs = performance.now() - sceneStart;
|
|
@@ -626,6 +629,11 @@ export class TreeViewController {
|
|
|
626
629
|
if (column.kind === 'inspectorPane') {
|
|
627
630
|
const localX = x - column.x;
|
|
628
631
|
const node = this.model.nodes[row.nodeIndex];
|
|
632
|
+
if (node?.data?.valueType === 'object') {
|
|
633
|
+
const treeX = row.depth * this.rowModel.indentWidth;
|
|
634
|
+
part = localX >= treeX + 4 && localX <= treeX + 22 ? 'chevron' : 'label';
|
|
635
|
+
return { area: 'row', part, row, column, x, y: rowY };
|
|
636
|
+
}
|
|
629
637
|
if (node?.data?.valueType === 'array') {
|
|
630
638
|
if (localX >= column.width - 54 && localX <= column.width - 32) part = 'arrayAdd';
|
|
631
639
|
else if (localX >= column.width - 28 && localX <= column.width - 6) part = 'arrayRemove';
|
|
@@ -718,7 +726,7 @@ export class TreeViewController {
|
|
|
718
726
|
if (hit.part === 'label' || hit.part === 'chevron') {
|
|
719
727
|
const labelX = hit.row.depth * this.rowModel.indentWidth + (hit.row.hasChildren ? 28 : 24);
|
|
720
728
|
text = node.label ?? node.id;
|
|
721
|
-
width = Math.max(0, layout.editorLeft - labelX - 8);
|
|
729
|
+
width = data.valueType === 'object' ? Math.max(0, visibleWidth - labelX - 8) : Math.max(0, layout.editorLeft - labelX - 8);
|
|
722
730
|
} else if (hit.part === 'arrayAdd' || hit.part === 'arrayRemove') {
|
|
723
731
|
return null;
|
|
724
732
|
} else {
|
|
@@ -795,6 +803,7 @@ export class TreeViewController {
|
|
|
795
803
|
}
|
|
796
804
|
|
|
797
805
|
#syncContentSize() {
|
|
806
|
+
if (this.viewport.viewportWidth > 1) this.#fitInspectorPaneColumn(this.viewport.viewportWidth);
|
|
798
807
|
this.viewport.setContentSize(this.columnModel.contentWidth, this.rowModel.contentHeight);
|
|
799
808
|
}
|
|
800
809
|
|
|
@@ -821,15 +830,16 @@ export class TreeViewController {
|
|
|
821
830
|
return Math.max(0, Math.floor(this.viewport.scrollY / this.rowModel.rowHeight));
|
|
822
831
|
}
|
|
823
832
|
|
|
824
|
-
#
|
|
825
|
-
if (!this.canvas) return;
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
this
|
|
832
|
-
}
|
|
833
|
+
#observeCanvasSize() {
|
|
834
|
+
if (!this.canvas || typeof ResizeObserver === 'undefined') return;
|
|
835
|
+
this.#resizeObserver?.disconnect();
|
|
836
|
+
this.#resizeObserver = new ResizeObserver((entries) => {
|
|
837
|
+
const entry = entries[0];
|
|
838
|
+
const width = Math.floor(entry?.contentRect?.width ?? 0);
|
|
839
|
+
const height = Math.floor(entry?.contentRect?.height ?? 0);
|
|
840
|
+
if (width > 0 && height > 0) this.resize(width, height);
|
|
841
|
+
});
|
|
842
|
+
this.#resizeObserver.observe(this.canvas);
|
|
833
843
|
}
|
|
834
844
|
|
|
835
845
|
#computeInspectorPaneLabelEnd(visibleRange) {
|
|
@@ -841,6 +851,7 @@ export class TreeViewController {
|
|
|
841
851
|
if (!row) continue;
|
|
842
852
|
const node = this.model.nodes[row.nodeIndex];
|
|
843
853
|
if (!node) continue;
|
|
854
|
+
if (node.data?.valueType === 'object') continue;
|
|
844
855
|
const indentX = row.depth * this.rowModel.indentWidth;
|
|
845
856
|
const labelX = indentX + (row.hasChildren ? 28 : 24);
|
|
846
857
|
const labelWidth = String(node.label ?? node.id ?? '').length * 6.4;
|
|
@@ -891,6 +902,8 @@ export class TreeViewController {
|
|
|
891
902
|
this.#rebuildRows();
|
|
892
903
|
if (focusId) this.focusedId = focusId;
|
|
893
904
|
}
|
|
905
|
+
|
|
906
|
+
#resizeObserver = null;
|
|
894
907
|
}
|
|
895
908
|
|
|
896
909
|
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|