virtual-tree-canvas 0.6.0 → 0.7.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 +23 -0
- package/README.md +79 -3
- package/docs/icon-catalog.json +12 -0
- package/docs/icon-catalog.md +5 -1
- package/package.json +1 -1
- package/resources/icons/star-filled.svg +1 -0
- package/resources/icons/trash.svg +1 -0
- package/src/assets/icons.js +2 -0
- package/src/core/theme-manager.js +3 -0
- package/src/core/tree-cell-layout.js +9 -0
- package/src/core/tree-column-model.js +13 -2
- package/src/core/types.js +1 -0
- package/src/core/view-options.js +10 -3
- package/src/core/visible-row-model.js +1 -1
- package/src/index.d.ts +48 -1
- package/src/input/row-actions.js +115 -0
- package/src/input/row-reorder-input.js +34 -9
- package/src/input/tree-view-input-controller.js +5 -3
- package/src/inspector/cell-editor-manager.js +23 -9
- package/src/inspector/pane-layout.js +12 -0
- package/src/renderers/checkbox.js +14 -0
- package/src/renderers/tree-row-renderer.js +47 -55
- package/src/tree-view-controller.js +67 -41
- package/src/tree-view.js +6 -4
- package/src/view/styles.js +2 -2
|
@@ -11,14 +11,18 @@ export class RowReorderInput {
|
|
|
11
11
|
this.previousTouchAction = this.canvas.style.touchAction;
|
|
12
12
|
this.canvas.addEventListener('pointerdown', this.onDown);
|
|
13
13
|
this.canvas.addEventListener('keydown', this.onKey);
|
|
14
|
+
this.canvas.addEventListener('lostpointercapture', this.onCancel);
|
|
14
15
|
this.view.addEventListener('pointermove', this.onMove);
|
|
15
16
|
this.view.addEventListener('pointerup', this.onUp);
|
|
16
17
|
this.view.addEventListener('pointercancel', this.onCancel);
|
|
17
18
|
this.view.addEventListener('blur', this.cancel);
|
|
18
|
-
this.stops = ['
|
|
19
|
+
this.stops = ['filterchange', 'sortchange', 'rowreorderchange'].map(type => controller.on(type, () => {
|
|
19
20
|
this.cancel();
|
|
20
21
|
this.syncMode();
|
|
21
22
|
}));
|
|
23
|
+
this.stops.push(controller.on('datachange', () => {
|
|
24
|
+
if (this.gesture && !controller.model.index.getNode(this.gesture.id)) this.cancel();
|
|
25
|
+
}));
|
|
22
26
|
this.stops.push(controller.on('rowreorder', event => {
|
|
23
27
|
if (!this.status) return;
|
|
24
28
|
const { nodeId, toIndex, siblingOrder } = event.detail;
|
|
@@ -37,21 +41,28 @@ export class RowReorderInput {
|
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
syncMode() {
|
|
40
|
-
this.canvas.style.touchAction = this.controller.canReorderRows() ? 'none' : this.previousTouchAction;
|
|
44
|
+
this.canvas.style.touchAction = (this.controller.canReorderRows() || this.controller.rowDrag) ? 'none' : this.previousTouchAction;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
onDown = event => {
|
|
44
|
-
if (event.button !== 0 ||
|
|
48
|
+
if (event.button !== 0 || event.isPrimary === false || this.gesture) return;
|
|
49
|
+
this.suppressClick = false;
|
|
45
50
|
const point = this.point(event);
|
|
46
51
|
const hit = this.controller.hitTest(point.x, point.y);
|
|
47
|
-
if (!hit?.row
|
|
48
|
-
this.
|
|
52
|
+
if (!hit?.row) return;
|
|
53
|
+
const node = this.controller.model.index.getNode(hit.row.nodeId);
|
|
54
|
+
const payload = this.controller.rowDrag?.(node, this.controller.model.dynamicState.get(node.id) ?? {}) ?? null;
|
|
55
|
+
const orderPart = ['rowDrag', 'rowUp', 'rowDown'].includes(hit.part);
|
|
56
|
+
if (orderPart && node.reorderable === false) return;
|
|
57
|
+
if (orderPart ? !this.controller.canReorderRows() && !(payload != null && hit.part === 'rowDrag') : payload == null || !['label', 'row', 'icon', 'cell'].includes(hit.part)) return;
|
|
58
|
+
this.canvas.focus({ preventScroll: true });
|
|
49
59
|
this.controller.cellEditor?.close?.();
|
|
50
60
|
this.controller.focusedId = hit.row.nodeId;
|
|
51
|
-
this.gesture = { id: hit.row.nodeId, part: hit.part, pointerId: event.pointerId,
|
|
61
|
+
this.gesture = { id: hit.row.nodeId, part: orderPart ? hit.part : 'rowDrag', payload, pointerId: event.pointerId,
|
|
52
62
|
startX: point.x, startY: point.y, point, dragging: false };
|
|
53
63
|
this.canvas.setPointerCapture?.(event.pointerId);
|
|
54
|
-
event.
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
if (orderPart) event.preventDefault();
|
|
55
66
|
};
|
|
56
67
|
|
|
57
68
|
onMove = event => {
|
|
@@ -60,7 +71,9 @@ export class RowReorderInput {
|
|
|
60
71
|
gesture.point = this.point(event);
|
|
61
72
|
if (gesture.part !== 'rowDrag') return;
|
|
62
73
|
if (!gesture.dragging && Math.hypot(gesture.point.x - gesture.startX, gesture.point.y - gesture.startY) < 5) return;
|
|
74
|
+
if (!gesture.dragging && gesture.payload != null) this.emitDrag('rowdragstart', event);
|
|
63
75
|
gesture.dragging = true;
|
|
76
|
+
if (gesture.payload != null) this.emitDrag('rowdragmove', event);
|
|
64
77
|
this.canvas.style.cursor = 'grabbing';
|
|
65
78
|
this.updateTarget();
|
|
66
79
|
this.scheduleScroll();
|
|
@@ -73,7 +86,9 @@ export class RowReorderInput {
|
|
|
73
86
|
gesture.point = this.point(event);
|
|
74
87
|
const target = gesture.dragging ? this.controller.getRowDropTarget(gesture.id, gesture.point.x, gesture.point.y) : null;
|
|
75
88
|
const hit = this.controller.hitTest(gesture.point.x, gesture.point.y);
|
|
76
|
-
this.
|
|
89
|
+
if (gesture.dragging && gesture.payload != null) this.emitDrag('rowdragend', event);
|
|
90
|
+
this.suppressClick = gesture.dragging;
|
|
91
|
+
this.cancel(false);
|
|
77
92
|
if (target) this.controller.moveRow(gesture.id, target.index, { source: 'pointer' });
|
|
78
93
|
else if (!gesture.dragging && hit?.row?.nodeId === gesture.id && hit.part === gesture.part) {
|
|
79
94
|
if (gesture.part === 'rowUp') this.controller.moveRowBy(gesture.id, -1, { source: 'button' });
|
|
@@ -102,6 +117,7 @@ export class RowReorderInput {
|
|
|
102
117
|
scheduleScroll() {
|
|
103
118
|
if (this.frame !== null || !this.gesture?.dragging) return;
|
|
104
119
|
const viewport = this.controller.viewport;
|
|
120
|
+
if (this.gesture.point.x < 0 || this.gesture.point.x > viewport.viewportWidth || this.gesture.point.y < 0 || this.gesture.point.y > viewport.viewportHeight) return;
|
|
105
121
|
const y = this.gesture.point.y - viewport.renderInsetY;
|
|
106
122
|
const top = viewport.headerHeight;
|
|
107
123
|
const bottom = top + viewport.rowViewportHeight;
|
|
@@ -118,8 +134,16 @@ export class RowReorderInput {
|
|
|
118
134
|
});
|
|
119
135
|
}
|
|
120
136
|
|
|
121
|
-
|
|
137
|
+
emitDrag(type, originalEvent) {
|
|
138
|
+
const gesture = this.gesture;
|
|
139
|
+
if (gesture) this.controller.events.emit(type, { nodeId: gesture.id, payload: gesture.payload,
|
|
140
|
+
label: this.controller.model.index.getNode(gesture.id)?.label ?? gesture.id, originalEvent });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
cancel = (notify = true) => {
|
|
122
144
|
const gesture = this.gesture;
|
|
145
|
+
if (gesture?.dragging) this.suppressClick = true;
|
|
146
|
+
if (notify && gesture?.dragging && gesture.payload != null) this.emitDrag('rowdragcancel');
|
|
123
147
|
this.gesture = null;
|
|
124
148
|
if (this.frame !== null) (this.view.cancelAnimationFrame?.bind(this.view) ?? clearTimeout)(this.frame);
|
|
125
149
|
this.frame = null;
|
|
@@ -132,6 +156,7 @@ export class RowReorderInput {
|
|
|
132
156
|
this.cancel();
|
|
133
157
|
this.canvas.removeEventListener('pointerdown', this.onDown);
|
|
134
158
|
this.canvas.removeEventListener('keydown', this.onKey);
|
|
159
|
+
this.canvas.removeEventListener('lostpointercapture', this.onCancel);
|
|
135
160
|
this.view.removeEventListener('pointermove', this.onMove);
|
|
136
161
|
this.view.removeEventListener('pointerup', this.onUp);
|
|
137
162
|
this.view.removeEventListener('pointercancel', this.onCancel);
|
|
@@ -18,7 +18,7 @@ export class TreeViewInputController {
|
|
|
18
18
|
this.canvas.addEventListener('wheel', this.#onWheel, { passive: false });
|
|
19
19
|
this.canvas.addEventListener('mousedown', this.#onMouseDown);
|
|
20
20
|
this.canvas.addEventListener('mousemove', this.#onMouseMove);
|
|
21
|
-
window.addEventListener('mouseup', this.#onMouseUp);
|
|
21
|
+
(this.canvas.ownerDocument.defaultView ?? window).addEventListener('mouseup', this.#onMouseUp);
|
|
22
22
|
this.canvas.addEventListener('mouseleave', this.#onMouseLeave);
|
|
23
23
|
this.canvas.addEventListener('click', this.#onClick);
|
|
24
24
|
this.canvas.addEventListener('dblclick', this.#onDoubleClick);
|
|
@@ -29,7 +29,7 @@ export class TreeViewInputController {
|
|
|
29
29
|
this.canvas.removeEventListener('wheel', this.#onWheel);
|
|
30
30
|
this.canvas.removeEventListener('mousedown', this.#onMouseDown);
|
|
31
31
|
this.canvas.removeEventListener('mousemove', this.#onMouseMove);
|
|
32
|
-
window.removeEventListener('mouseup', this.#onMouseUp);
|
|
32
|
+
(this.canvas.ownerDocument.defaultView ?? window).removeEventListener('mouseup', this.#onMouseUp);
|
|
33
33
|
this.canvas.removeEventListener('mouseleave', this.#onMouseLeave);
|
|
34
34
|
this.canvas.removeEventListener('click', this.#onClick);
|
|
35
35
|
this.canvas.removeEventListener('dblclick', this.#onDoubleClick);
|
|
@@ -71,7 +71,7 @@ export class TreeViewInputController {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
#onClick = (event) => {
|
|
74
|
-
this.
|
|
74
|
+
if (this.controller.rowReorderInput?.suppressClick) { this.controller.rowReorderInput.suppressClick = false; event.preventDefault(); return; }
|
|
75
75
|
const hit = this.#hitTest(event);
|
|
76
76
|
if (!hit) return;
|
|
77
77
|
if (['rowDrag', 'rowUp', 'rowDown'].includes(hit.part)) return;
|
|
@@ -85,6 +85,7 @@ export class TreeViewInputController {
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
if (this.cellEditor?.handleClick(event, hit)) return;
|
|
88
|
+
this.canvas.focus({ preventScroll: true });
|
|
88
89
|
this.controller.clickNode(hit.row.nodeId, {
|
|
89
90
|
shiftKey: event.shiftKey,
|
|
90
91
|
ctrlKey: event.ctrlKey,
|
|
@@ -99,6 +100,7 @@ export class TreeViewInputController {
|
|
|
99
100
|
};
|
|
100
101
|
|
|
101
102
|
#onMouseDown = (event) => {
|
|
103
|
+
if (this.controller.rowReorderInput?.gesture) return;
|
|
102
104
|
const hit = this.#hitTest(event);
|
|
103
105
|
if (['rowDrag', 'rowUp', 'rowDown'].includes(hit?.part)) return;
|
|
104
106
|
this.controller.setActiveHit(hit);
|
|
@@ -6,11 +6,13 @@ export class CellEditorManager {
|
|
|
6
6
|
if (!controller.canvas) throw new Error('CellEditorManager requires an initialized TreeViewController canvas');
|
|
7
7
|
this.controller = controller;
|
|
8
8
|
this.canvas = controller.canvas;
|
|
9
|
+
this.view = this.canvas.ownerDocument.defaultView;
|
|
9
10
|
this.host = host ?? controller.canvas.parentElement ?? document.body;
|
|
10
11
|
this.editable = true;
|
|
11
12
|
this.overlay = null;
|
|
12
13
|
this.rangeDrag = null;
|
|
13
14
|
this.overlayKind = null;
|
|
15
|
+
this.overlayNode = null;
|
|
14
16
|
this.stopFilter = controller.on('filterchange', ({ detail }) => {
|
|
15
17
|
if (this.overlayKind === 'filter' && this.overlay) this.overlay.value = detail.query;
|
|
16
18
|
});
|
|
@@ -26,15 +28,25 @@ export class CellEditorManager {
|
|
|
26
28
|
destroy() {
|
|
27
29
|
this.stopFilter();
|
|
28
30
|
this.#removeOverlay();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
this.view.removeEventListener('mousemove', this.onMouseMove);
|
|
32
|
+
this.view.removeEventListener('mouseup', this.onMouseUp);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
close() {
|
|
34
36
|
this.#removeOverlay();
|
|
35
37
|
this.rangeDrag = null;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
this.view.removeEventListener('mousemove', this.onMouseMove);
|
|
39
|
+
this.view.removeEventListener('mouseup', this.onMouseUp);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Keep an in-progress edit while values refresh; cancel if its editor changes. */
|
|
43
|
+
reconcile(nodes) {
|
|
44
|
+
const previous = this.overlayNode ?? this.rangeDrag?.node;
|
|
45
|
+
if (!previous) return;
|
|
46
|
+
const next = nodes.find(node => node.id === previous.id);
|
|
47
|
+
const signature = data => JSON.stringify([data.editorType, data.readonly, data.disabled,
|
|
48
|
+
...['min', 'max', 'step', 'integer', 'options', 'unit'].map(key => data.meta?.[key])]);
|
|
49
|
+
if (!next || signature(previous.data) !== signature(next.data)) this.close();
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
handlePointerDown(event, hit) {
|
|
@@ -42,10 +54,10 @@ export class CellEditorManager {
|
|
|
42
54
|
const data = hit.row ? this.controller.model.nodes[hit.row.nodeIndex]?.data : null;
|
|
43
55
|
if (!data || data.readonly || data.disabled) return false;
|
|
44
56
|
if (data.editorType === 'range' && hit.part === 'range') {
|
|
45
|
-
this.rangeDrag = { hit, data };
|
|
57
|
+
this.rangeDrag = { hit, data, node: this.controller.model.nodes[hit.row.nodeIndex] };
|
|
46
58
|
this.#updateRangeFromEvent(event);
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
this.view.addEventListener('mousemove', this.onMouseMove);
|
|
60
|
+
this.view.addEventListener('mouseup', this.onMouseUp);
|
|
49
61
|
return true;
|
|
50
62
|
}
|
|
51
63
|
if (shouldOpenOverlayOnPointerDown(data, hit)) {
|
|
@@ -137,6 +149,7 @@ export class CellEditorManager {
|
|
|
137
149
|
this.host.append(element);
|
|
138
150
|
this.overlay = element;
|
|
139
151
|
this.overlayKind = 'value';
|
|
152
|
+
this.overlayNode = node;
|
|
140
153
|
element.focus({ preventScroll: true });
|
|
141
154
|
element.select?.();
|
|
142
155
|
if (options.showPicker && element.showPicker) {
|
|
@@ -219,8 +232,8 @@ export class CellEditorManager {
|
|
|
219
232
|
|
|
220
233
|
#onMouseUp() {
|
|
221
234
|
this.rangeDrag = null;
|
|
222
|
-
|
|
223
|
-
|
|
235
|
+
this.view.removeEventListener('mousemove', this.onMouseMove);
|
|
236
|
+
this.view.removeEventListener('mouseup', this.onMouseUp);
|
|
224
237
|
}
|
|
225
238
|
|
|
226
239
|
#isEditableHit(hit) {
|
|
@@ -273,6 +286,7 @@ export class CellEditorManager {
|
|
|
273
286
|
const overlay = this.overlay;
|
|
274
287
|
this.overlay = null;
|
|
275
288
|
this.overlayKind = null;
|
|
289
|
+
this.overlayNode = null;
|
|
276
290
|
overlay?.remove();
|
|
277
291
|
}
|
|
278
292
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
2
|
+
const safeWidth = Math.max(1, width);
|
|
3
|
+
const rightPadding = 14;
|
|
4
|
+
const minEditor = Math.min(170, Math.max(96, safeWidth * 0.45));
|
|
5
|
+
const minLabelEnd = Math.max(88, depth * indentWidth + 104);
|
|
6
|
+
const preferredLeft = Math.max(minLabelEnd, labelEnd, safeWidth * 0.32);
|
|
7
|
+
const maxLeft = Math.max(64, safeWidth - rightPadding - minEditor);
|
|
8
|
+
const editorLeft = Math.max(64, Math.min(preferredLeft, maxLeft));
|
|
9
|
+
const editorWidth = Math.max(56, safeWidth - rightPadding - editorLeft);
|
|
10
|
+
return { editorLeft, editorWidth };
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function drawCheckbox(ctx, x, y, checked, theme) {
|
|
2
|
+
ctx.beginPath();
|
|
3
|
+
ctx.roundRect(x, y, 16, 16, 3);
|
|
4
|
+
ctx.fillStyle = theme.colors.progressTrack;
|
|
5
|
+
ctx.fill();
|
|
6
|
+
ctx.strokeStyle = checked ? theme.colors.progressFill : theme.colors.textMuted;
|
|
7
|
+
ctx.stroke();
|
|
8
|
+
if (!checked) return;
|
|
9
|
+
ctx.fillStyle = theme.colors.progressFill;
|
|
10
|
+
ctx.beginPath();
|
|
11
|
+
ctx.roundRect(x + 4, y + 4, 8, 8, 1.5);
|
|
12
|
+
ctx.fill();
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -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
|
|
|
@@ -51,7 +55,7 @@ export class TreeRowRenderer {
|
|
|
51
55
|
if (scene?.rows) this.scene = scene;
|
|
52
56
|
if (!this.canvas || !this.ctx || !this.scene) return;
|
|
53
57
|
const { viewport, theme } = this.scene;
|
|
54
|
-
const dpr = Math.max(1,
|
|
58
|
+
const dpr = Math.max(1, this.canvas.ownerDocument?.defaultView?.devicePixelRatio || 1);
|
|
55
59
|
const viewportWidth = Math.max(0, viewport.viewportWidth);
|
|
56
60
|
const viewportHeight = Math.max(0, viewport.viewportHeight);
|
|
57
61
|
if (viewportWidth <= 0 || viewportHeight <= 0) return;
|
|
@@ -119,7 +123,7 @@ export class TreeRowRenderer {
|
|
|
119
123
|
ctx.textBaseline = 'middle';
|
|
120
124
|
|
|
121
125
|
for (const column of columns) {
|
|
122
|
-
if (headerFilter && column === columns.find(item => item.kind !== 'rowOrder')) {
|
|
126
|
+
if (headerFilter && column === columns.find(item => item.kind !== 'rowOrder' && item.id !== '__vtc_actions')) {
|
|
123
127
|
this.#drawHeaderFilter(ctx, column, viewport, theme, filterQuery);
|
|
124
128
|
} else {
|
|
125
129
|
ctx.fillStyle = colors.textMuted;
|
|
@@ -212,6 +216,7 @@ export class TreeRowRenderer {
|
|
|
212
216
|
}
|
|
213
217
|
ctx.restore();
|
|
214
218
|
|
|
219
|
+
if (!stickyRows.some(row => row.stickyY > row.y - viewport.scrollY)) return;
|
|
215
220
|
const bottom = viewport.headerHeight + height;
|
|
216
221
|
ctx.strokeStyle = theme.colors.border;
|
|
217
222
|
ctx.beginPath();
|
|
@@ -234,7 +239,8 @@ export class TreeRowRenderer {
|
|
|
234
239
|
const visibleX = viewport.scrollX;
|
|
235
240
|
const visibleWidth = viewport.contentViewportWidth ?? viewport.viewportWidth;
|
|
236
241
|
|
|
237
|
-
|
|
242
|
+
const background = selected ? colors.rowSelected : highlighted ? colors.rowHighlighted : hovered ? colors.rowHover : colors.row;
|
|
243
|
+
ctx.fillStyle = background;
|
|
238
244
|
ctx.fillRect(visibleX, y, visibleWidth, row.height);
|
|
239
245
|
|
|
240
246
|
this.#drawIndentGuides(ctx, row, colors);
|
|
@@ -261,6 +267,12 @@ export class TreeRowRenderer {
|
|
|
261
267
|
ctx.stroke();
|
|
262
268
|
}
|
|
263
269
|
|
|
270
|
+
const actions = columns.find(column => column.id === '__vtc_actions');
|
|
271
|
+
if (actions) {
|
|
272
|
+
ctx.fillStyle = background;
|
|
273
|
+
ctx.fillRect(visibleX + Math.max(0, visibleWidth - actions.width), y, actions.width, row.height);
|
|
274
|
+
}
|
|
275
|
+
|
|
264
276
|
ctx.strokeStyle = colors.border;
|
|
265
277
|
ctx.beginPath();
|
|
266
278
|
ctx.moveTo(visibleX, y + row.height + 0.5);
|
|
@@ -292,6 +304,7 @@ export class TreeRowRenderer {
|
|
|
292
304
|
}
|
|
293
305
|
|
|
294
306
|
#drawRowOrder(ctx, { node, row, rect, theme }) {
|
|
307
|
+
if (node.reorderable === false) return;
|
|
295
308
|
const siblings = this.scene.childrenByParent?.get(node.parentId ?? null) ?? [];
|
|
296
309
|
const enabled = this.scene.canReorderRows && siblings.length > 1;
|
|
297
310
|
const cy = rect.y + row.height / 2;
|
|
@@ -299,7 +312,7 @@ export class TreeRowRenderer {
|
|
|
299
312
|
ctx.strokeStyle = theme.colors.textMuted;
|
|
300
313
|
ctx.fillStyle = theme.colors.textMuted;
|
|
301
314
|
ctx.lineWidth = 1.5;
|
|
302
|
-
ctx.globalAlpha = enabled ? 1 : 0.3;
|
|
315
|
+
ctx.globalAlpha = enabled || this.scene.rowDrag?.(node, this.scene.dynamicState.get(node.id) ?? {}) != null ? 1 : 0.3;
|
|
303
316
|
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
317
|
for (const [center, direction, available] of [[36, -1, siblings[0] !== node.id], [60, 1, siblings.at(-1) !== node.id]]) {
|
|
305
318
|
ctx.globalAlpha = enabled && available ? 1 : 0.3;
|
|
@@ -392,6 +405,7 @@ export class TreeRowRenderer {
|
|
|
392
405
|
const meta = data.meta ?? {};
|
|
393
406
|
const disabled = data.disabled;
|
|
394
407
|
const readonly = data.readonly;
|
|
408
|
+
const valueColor = resolveValueColor(theme, data.value, data.editorType === 'select' ? 'enum' : data.valueType);
|
|
395
409
|
const x = rect.x + 10;
|
|
396
410
|
const y = rect.y + 5;
|
|
397
411
|
const fullWidth = Math.max(24, rect.width - 20);
|
|
@@ -411,7 +425,7 @@ export class TreeRowRenderer {
|
|
|
411
425
|
}
|
|
412
426
|
|
|
413
427
|
if (data.editorType === 'checkbox') {
|
|
414
|
-
|
|
428
|
+
drawCheckbox(ctx, x, rect.y + rect.height / 2 - 8, Boolean(data.value), theme);
|
|
415
429
|
} else if (data.editorType === 'range') {
|
|
416
430
|
this.#drawInspectorRange(ctx, x, rect.y + rect.height / 2 - 4, width, data, theme, {
|
|
417
431
|
hoveredNumber: hovered && hoverPart === 'number',
|
|
@@ -422,7 +436,7 @@ export class TreeRowRenderer {
|
|
|
422
436
|
ctx.fillRect(x, y + 2, 28, height - 4);
|
|
423
437
|
ctx.strokeStyle = theme.colors.border;
|
|
424
438
|
ctx.strokeRect(x + 0.5, y + 2.5, 28, height - 4);
|
|
425
|
-
this.#drawMutedText(ctx, String(data.value ?? ''), x + 38, rect.y + rect.height / 2, width - 38, theme);
|
|
439
|
+
this.#drawMutedText(ctx, String(data.value ?? ''), x + 38, rect.y + rect.height / 2, width - 38, theme, false, null, valueColor);
|
|
426
440
|
} else if (data.editorType === 'button') {
|
|
427
441
|
const buttonWidth = meta.fullWidthButton ? width : Math.min(width, 140);
|
|
428
442
|
this.#drawControlSurface(ctx, x, y, buttonWidth, height, theme, {
|
|
@@ -441,17 +455,17 @@ export class TreeRowRenderer {
|
|
|
441
455
|
active: activePart === 'editor',
|
|
442
456
|
disabled: readonly || disabled,
|
|
443
457
|
});
|
|
444
|
-
this.#drawMutedText(ctx, data.valueText, x + 8, rect.y + rect.height / 2, selectWidth - 30, theme);
|
|
458
|
+
this.#drawMutedText(ctx, data.valueText, x + 8, rect.y + rect.height / 2, selectWidth - 30, theme, false, null, valueColor);
|
|
445
459
|
this.#drawSelectChevron(ctx, x + selectWidth - 18, rect.y + rect.height / 2, theme, disabled);
|
|
446
460
|
} else {
|
|
447
461
|
if (numeric.unit) {
|
|
448
462
|
ctx.font = theme.monoFont ?? theme.font;
|
|
449
|
-
ctx.fillStyle =
|
|
463
|
+
ctx.fillStyle = valueColor;
|
|
450
464
|
ctx.textAlign = 'right';
|
|
451
465
|
drawTruncatedText(ctx, data.valueText, x + width, rect.y + rect.height / 2, width);
|
|
452
466
|
ctx.textAlign = 'left';
|
|
453
467
|
} else {
|
|
454
|
-
this.#drawMutedText(ctx, data.valueText, x, rect.y + rect.height / 2, width, theme,
|
|
468
|
+
this.#drawMutedText(ctx, data.valueText, x, rect.y + rect.height / 2, width, theme, false, null, valueColor);
|
|
455
469
|
}
|
|
456
470
|
}
|
|
457
471
|
|
|
@@ -481,17 +495,6 @@ export class TreeRowRenderer {
|
|
|
481
495
|
this.#drawMutedText(ctx, node.data?.meta?.description ?? '', rect.x + 10, rect.y + rect.height / 2, rect.width - 20, theme);
|
|
482
496
|
}
|
|
483
497
|
|
|
484
|
-
#drawCheckbox(ctx, x, y, checked, theme) {
|
|
485
|
-
roundRect(ctx, x, y, 16, 16, 3);
|
|
486
|
-
ctx.fillStyle = theme.colors.progressTrack;
|
|
487
|
-
ctx.fill();
|
|
488
|
-
ctx.strokeStyle = checked ? theme.colors.progressFill : theme.colors.textMuted;
|
|
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();
|
|
494
|
-
}
|
|
495
498
|
|
|
496
499
|
#drawInspectorRange(ctx, x, y, width, data, theme, state = {}) {
|
|
497
500
|
const meta = data.meta ?? {};
|
|
@@ -506,7 +509,7 @@ export class TreeRowRenderer {
|
|
|
506
509
|
hovered: Boolean(state.hoveredNumber),
|
|
507
510
|
active: Boolean(state.activeNumber),
|
|
508
511
|
});
|
|
509
|
-
ctx.fillStyle = theme.
|
|
512
|
+
ctx.fillStyle = resolveValueColor(theme, data.value);
|
|
510
513
|
ctx.font = theme.monoFont ?? theme.font;
|
|
511
514
|
ctx.textAlign = 'right';
|
|
512
515
|
drawTruncatedText(ctx, String(data.valueText ?? ''), x + barWidth + gap + valueWidth - 6, y + 4, valueWidth - 10);
|
|
@@ -548,8 +551,8 @@ export class TreeRowRenderer {
|
|
|
548
551
|
ctx.fill();
|
|
549
552
|
}
|
|
550
553
|
|
|
551
|
-
#drawMutedText(ctx, text, x, y, width, theme, readonly = false, font = null) {
|
|
552
|
-
ctx.fillStyle = readonly ? theme.colors.textMuted : theme.colors.text;
|
|
554
|
+
#drawMutedText(ctx, text, x, y, width, theme, readonly = false, font = null, color = null) {
|
|
555
|
+
ctx.fillStyle = color ?? (readonly ? theme.colors.textMuted : theme.colors.text);
|
|
553
556
|
ctx.font = font ?? theme.font;
|
|
554
557
|
ctx.textBaseline = 'middle';
|
|
555
558
|
ctx.textAlign = 'left';
|
|
@@ -558,19 +561,20 @@ export class TreeRowRenderer {
|
|
|
558
561
|
|
|
559
562
|
#drawTreeCell(ctx, { node, row, rect, theme, style }) {
|
|
560
563
|
const colors = theme.colors;
|
|
561
|
-
const
|
|
564
|
+
const layout = treeCellLayout(row.depth, theme.indentWidth, this.scene.childrenByParent.size > 1);
|
|
562
565
|
const cy = rect.y + rect.height / 2;
|
|
563
|
-
this.#drawChevron(ctx, x +
|
|
564
|
-
this.iconRegistry.draw(ctx, style.icon, x +
|
|
566
|
+
if (row.hasChildren) this.#drawChevron(ctx, rect.x + layout.chevronX, cy, row, colors);
|
|
567
|
+
this.iconRegistry.draw(ctx, style.icon, rect.x + layout.iconX, rect.y + 6, 15, style.color);
|
|
565
568
|
ctx.fillStyle = colors.text;
|
|
566
569
|
ctx.font = theme.font;
|
|
567
570
|
ctx.textBaseline = 'middle';
|
|
568
571
|
ctx.textAlign = 'left';
|
|
569
|
-
drawTruncatedText(ctx, node.label ?? node.id, x +
|
|
572
|
+
drawTruncatedText(ctx, node.label ?? node.id, rect.x + layout.labelX, cy, Math.max(0, rect.width - layout.labelX - 6));
|
|
570
573
|
}
|
|
571
574
|
|
|
572
575
|
#drawStatusCell(ctx, { rect, style, theme }) {
|
|
573
|
-
|
|
576
|
+
ctx.font = theme.font;
|
|
577
|
+
const badgeWidth = Math.max(0, Math.min(ctx.measureText(style.status.label).width + 18, rect.width - 12));
|
|
574
578
|
const x = rect.x + (rect.width - badgeWidth) / 2;
|
|
575
579
|
const y = rect.y + (rect.height - 17) / 2;
|
|
576
580
|
const statusColor = style.status.color;
|
|
@@ -581,7 +585,7 @@ export class TreeRowRenderer {
|
|
|
581
585
|
ctx.strokeStyle = mixColor(theme.colors.border, statusColor, 0.42);
|
|
582
586
|
ctx.stroke();
|
|
583
587
|
ctx.fillStyle = mixColor(theme.colors.text, statusColor, 0.36);
|
|
584
|
-
ctx.font =
|
|
588
|
+
ctx.font = theme.font;
|
|
585
589
|
ctx.textAlign = 'center';
|
|
586
590
|
ctx.textBaseline = 'middle';
|
|
587
591
|
drawTruncatedText(ctx, style.status.label, x + badgeWidth / 2, y + 8.5, badgeWidth - 8);
|
|
@@ -623,10 +627,14 @@ export class TreeRowRenderer {
|
|
|
623
627
|
|
|
624
628
|
#drawTextCell(ctx, { node, state, rect, column, theme }) {
|
|
625
629
|
let value = column.value(node, state);
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
+
const numeric = typeof value === 'number';
|
|
631
|
+
const valueType = typeof column.valueType === 'function' ? column.valueType(value, node, state) : column.valueType;
|
|
632
|
+
const valueColor = resolveValueColor(theme, value, valueType);
|
|
633
|
+
if (column.format) value = column.format(value, node, state);
|
|
634
|
+
else if (column.kind === 'updated' && numeric) value = formatTime(value);
|
|
635
|
+
else if (numeric) value = formatInspectorValue(value);
|
|
636
|
+
ctx.fillStyle = column.kind === 'type' ? resolveNodeStyle(theme, node, state).color : column.kind === 'value' ? valueColor : theme.colors.textMuted;
|
|
637
|
+
ctx.font = column.kind === 'type' || column.kind === 'updated' || numeric ? theme.monoFont ?? theme.font : theme.font;
|
|
630
638
|
ctx.textBaseline = 'middle';
|
|
631
639
|
ctx.textAlign = column.align;
|
|
632
640
|
const x = column.align === 'right' ? rect.x + rect.width - 10 : column.align === 'center' ? rect.x + rect.width / 2 : rect.x + 10;
|
|
@@ -648,13 +656,8 @@ export class TreeRowRenderer {
|
|
|
648
656
|
}
|
|
649
657
|
|
|
650
658
|
#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
|
-
}
|
|
659
|
+
if (!row.hasChildren) return;
|
|
660
|
+
ctx.fillStyle = colors.chevron;
|
|
658
661
|
ctx.beginPath();
|
|
659
662
|
if (row.expanded) {
|
|
660
663
|
ctx.moveTo(x - 5, y - 2);
|
|
@@ -681,21 +684,6 @@ function resolveNodeStyle(theme, node, state = {}) {
|
|
|
681
684
|
};
|
|
682
685
|
}
|
|
683
686
|
|
|
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
687
|
|
|
700
688
|
function clamp01(value) {
|
|
701
689
|
return Math.max(0, Math.min(1, value));
|
|
@@ -793,3 +781,7 @@ function formatTime(value) {
|
|
|
793
781
|
if (!Number.isFinite(time)) return '';
|
|
794
782
|
return timeFormatter.format(time);
|
|
795
783
|
}
|
|
784
|
+
|
|
785
|
+
function resolveValueColor(theme, value, type) {
|
|
786
|
+
return theme.valueColors?.[type ?? (value === null ? 'null' : typeof value)] ?? theme.colors.text;
|
|
787
|
+
}
|