virtual-tree-canvas 0.2.0 → 0.3.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/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
|
}
|
|
@@ -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,7 +30,7 @@ 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;
|
|
@@ -47,7 +53,7 @@ export class TreeRowRenderer {
|
|
|
47
53
|
ctx.translate(viewport.renderInsetX ?? 0, viewport.renderInsetY ?? 0);
|
|
48
54
|
this.#drawHeader(ctx);
|
|
49
55
|
this.#drawRows(ctx);
|
|
50
|
-
this.#drawScrollIndicator(ctx);
|
|
56
|
+
this.#drawScrollIndicator(ctx, time);
|
|
51
57
|
ctx.restore();
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -119,10 +125,12 @@ export class TreeRowRenderer {
|
|
|
119
125
|
ctx.restore();
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
#drawScrollIndicator(ctx) {
|
|
123
|
-
const { viewport, theme } = this.scene;
|
|
128
|
+
#drawScrollIndicator(ctx, time) {
|
|
129
|
+
const { viewport, theme, rows } = this.scene;
|
|
124
130
|
const maxY = Math.max(0, viewport.contentHeight - viewport.rowViewportHeight);
|
|
125
131
|
if (maxY <= 0 || viewport.viewportHeight <= viewport.headerHeight + 12) return;
|
|
132
|
+
const alpha = this.#scrollIndicatorAlpha(viewport, rows?.length ?? 0, time);
|
|
133
|
+
if (alpha <= 0) return;
|
|
126
134
|
|
|
127
135
|
const trackTop = viewport.headerHeight + 4;
|
|
128
136
|
const trackHeight = Math.max(1, viewport.viewportHeight - viewport.headerHeight - 8);
|
|
@@ -133,14 +141,14 @@ export class TreeRowRenderer {
|
|
|
133
141
|
ctx.save();
|
|
134
142
|
ctx.lineCap = 'round';
|
|
135
143
|
ctx.lineWidth = 2;
|
|
136
|
-
ctx.globalAlpha = 0.28;
|
|
144
|
+
ctx.globalAlpha = 0.28 * alpha;
|
|
137
145
|
ctx.strokeStyle = theme.colors.guide;
|
|
138
146
|
ctx.beginPath();
|
|
139
147
|
ctx.moveTo(x, trackTop);
|
|
140
148
|
ctx.lineTo(x, trackTop + trackHeight);
|
|
141
149
|
ctx.stroke();
|
|
142
150
|
|
|
143
|
-
ctx.globalAlpha = 0.9;
|
|
151
|
+
ctx.globalAlpha = 0.9 * alpha;
|
|
144
152
|
ctx.strokeStyle = theme.colors.focus;
|
|
145
153
|
ctx.beginPath();
|
|
146
154
|
ctx.moveTo(x, thumbY);
|
|
@@ -149,6 +157,24 @@ export class TreeRowRenderer {
|
|
|
149
157
|
ctx.restore();
|
|
150
158
|
}
|
|
151
159
|
|
|
160
|
+
#scrollIndicatorAlpha(viewport, rowCount, time) {
|
|
161
|
+
const nextState = {
|
|
162
|
+
scrollY: viewport.scrollY,
|
|
163
|
+
contentHeight: viewport.contentHeight,
|
|
164
|
+
rowViewportHeight: viewport.rowViewportHeight,
|
|
165
|
+
viewportHeight: viewport.viewportHeight,
|
|
166
|
+
rowCount,
|
|
167
|
+
};
|
|
168
|
+
if (!sameScrollIndicatorState(this.scrollIndicatorState, nextState)) {
|
|
169
|
+
this.scrollIndicatorState = nextState;
|
|
170
|
+
this.scrollIndicatorVisibleUntil = time + SCROLL_INDICATOR_HOLD_MS;
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
if (time <= this.scrollIndicatorVisibleUntil) return 1;
|
|
174
|
+
const fade = 1 - ((time - this.scrollIndicatorVisibleUntil) / SCROLL_INDICATOR_FADE_MS);
|
|
175
|
+
return clamp01(fade);
|
|
176
|
+
}
|
|
177
|
+
|
|
152
178
|
#drawRow(ctx, row) {
|
|
153
179
|
const { columns, nodes, dynamicState, selection, hoverNodeId, focusNodeId, searchMatches, theme, viewport } = this.scene;
|
|
154
180
|
const node = nodes[row.nodeIndex];
|
|
@@ -226,8 +252,16 @@ export class TreeRowRenderer {
|
|
|
226
252
|
ctx.font = theme.font;
|
|
227
253
|
ctx.textBaseline = 'middle';
|
|
228
254
|
ctx.textAlign = 'left';
|
|
229
|
-
ctx.globalAlpha = data.disabled ?
|
|
255
|
+
ctx.globalAlpha = data.disabled ? DISABLED_ALPHA : 1;
|
|
230
256
|
ctx.fillStyle = data.valueType === 'object' || data.valueType === 'array' ? colors.text : colors.textMuted;
|
|
257
|
+
|
|
258
|
+
if (data.valueType === 'object') {
|
|
259
|
+
drawTruncatedText(ctx, node.label ?? node.id, labelX, cy, Math.max(20, rect.x + rect.width - labelX - 8));
|
|
260
|
+
if (data.meta?.updated) this.#drawUpdatedMarker(ctx, row, theme);
|
|
261
|
+
ctx.globalAlpha = 1;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
231
265
|
const layout = inspectorPaneLayout(rect.width, row.depth, theme.indentWidth, data.editorType, this.scene.inspectorPaneLabelEnd);
|
|
232
266
|
drawTruncatedText(ctx, node.label ?? node.id, labelX, cy, Math.max(20, rect.x + layout.editorLeft - labelX - 8));
|
|
233
267
|
|
|
@@ -238,8 +272,6 @@ export class TreeRowRenderer {
|
|
|
238
272
|
drawTruncatedText(ctx, data.valueText, editorX, cy, Math.max(20, editorWidth - 58));
|
|
239
273
|
this.#drawSmallButton(ctx, rect.x + rect.width - 54, rect.y + 5, 22, rect.height - 10, '+', theme);
|
|
240
274
|
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
275
|
} else {
|
|
244
276
|
this.#drawInspectorValueCell(ctx, {
|
|
245
277
|
node,
|
|
@@ -264,7 +296,7 @@ export class TreeRowRenderer {
|
|
|
264
296
|
ctx.font = theme.font;
|
|
265
297
|
ctx.textBaseline = 'middle';
|
|
266
298
|
ctx.textAlign = 'left';
|
|
267
|
-
ctx.globalAlpha = disabled ?
|
|
299
|
+
ctx.globalAlpha = disabled ? DISABLED_ALPHA : 1;
|
|
268
300
|
|
|
269
301
|
if (data.editorType === 'checkbox') {
|
|
270
302
|
this.#drawCheckbox(ctx, x, rect.y + rect.height / 2 - 7, Boolean(data.value), theme);
|
|
@@ -475,6 +507,15 @@ function resolveNodeStyle(theme, node, state = {}) {
|
|
|
475
507
|
};
|
|
476
508
|
}
|
|
477
509
|
|
|
510
|
+
function sameScrollIndicatorState(a, b) {
|
|
511
|
+
return Boolean(a) &&
|
|
512
|
+
a.scrollY === b.scrollY &&
|
|
513
|
+
a.contentHeight === b.contentHeight &&
|
|
514
|
+
a.rowViewportHeight === b.rowViewportHeight &&
|
|
515
|
+
a.viewportHeight === b.viewportHeight &&
|
|
516
|
+
a.rowCount === b.rowCount;
|
|
517
|
+
}
|
|
518
|
+
|
|
478
519
|
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
479
520
|
const safeWidth = Math.max(1, width);
|
|
480
521
|
const rightPadding = 14;
|
|
@@ -76,6 +76,9 @@ export class TreeViewController {
|
|
|
76
76
|
|
|
77
77
|
setData(nodes) {
|
|
78
78
|
this.inspector = null;
|
|
79
|
+
if (this.columnModel.columns.length === 1 && this.columnModel.columns[0]?.kind === 'inspectorPane') {
|
|
80
|
+
this.columnModel.setColumns([]);
|
|
81
|
+
}
|
|
79
82
|
this.model.setTree(nodes);
|
|
80
83
|
this.expansion.expandToDepth(this.initialExpandDepth);
|
|
81
84
|
this.searchIndex.rebuild(this.model);
|
|
@@ -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
|
|
|
@@ -841,6 +850,7 @@ export class TreeViewController {
|
|
|
841
850
|
if (!row) continue;
|
|
842
851
|
const node = this.model.nodes[row.nodeIndex];
|
|
843
852
|
if (!node) continue;
|
|
853
|
+
if (node.data?.valueType === 'object') continue;
|
|
844
854
|
const indentX = row.depth * this.rowModel.indentWidth;
|
|
845
855
|
const labelX = indentX + (row.hasChildren ? 28 : 24);
|
|
846
856
|
const labelWidth = String(node.label ?? node.id ?? '').length * 6.4;
|