virtual-tree-canvas 0.1.0 → 0.2.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 +6 -2
- package/src/core/icon-registry.js +238 -0
- package/src/core/theme-manager.js +20 -1
- package/src/core/tree-worker-operations.js +5 -3
- package/src/core/visible-row-model.js +15 -3
- package/src/index.d.ts +108 -0
- package/src/input/tree-view-input-controller.js +1 -0
- package/src/inspector/cell-editor-manager.js +57 -15
- package/src/inspector/model-inspector-builder.js +2 -1
- package/src/renderers/tree-row-renderer.js +125 -32
- package/src/tree-view-controller.js +154 -9
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "virtual-tree-canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "High-performance Canvas2D virtual tree/table widget for very large hierarchical datasets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
7
8
|
"sideEffects": false,
|
|
8
9
|
"exports": {
|
|
9
|
-
".":
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
10
14
|
"./core": "./src/core/index.js",
|
|
11
15
|
"./inspector": "./src/inspector/index.js",
|
|
12
16
|
"./renderers": "./src/renderers/index.js",
|
|
@@ -74,6 +74,16 @@ export class IconRegistry {
|
|
|
74
74
|
this.register('error', drawError);
|
|
75
75
|
this.register('task', drawTask);
|
|
76
76
|
this.register('track', drawTrack);
|
|
77
|
+
this.register('point', drawPoint);
|
|
78
|
+
this.register('munition', drawMunition);
|
|
79
|
+
this.register('air', drawAircraft);
|
|
80
|
+
this.register('ground', drawGroundVehicle);
|
|
81
|
+
this.register('surface', drawSurfaceVehicle);
|
|
82
|
+
this.register('subsurface', drawSubsurfaceVehicle);
|
|
83
|
+
this.register('space', drawSpaceVehicle);
|
|
84
|
+
this.register('control', drawControl);
|
|
85
|
+
this.register('situation', drawSituation);
|
|
86
|
+
this.register('damage', drawDamage);
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -171,3 +181,231 @@ function drawTrack(ctx, x, y, size, color) {
|
|
|
171
181
|
ctx.lineTo(x + size - 1, y + size / 2);
|
|
172
182
|
ctx.stroke();
|
|
173
183
|
}
|
|
184
|
+
|
|
185
|
+
function drawPoint(ctx, x, y, size, color) {
|
|
186
|
+
const cx = x + size / 2;
|
|
187
|
+
const cy = y + size / 2;
|
|
188
|
+
ctx.strokeStyle = color;
|
|
189
|
+
ctx.fillStyle = color;
|
|
190
|
+
ctx.lineWidth = 1.4;
|
|
191
|
+
ctx.beginPath();
|
|
192
|
+
ctx.arc(cx, cy, size * 0.22, 0, Math.PI * 2);
|
|
193
|
+
ctx.fill();
|
|
194
|
+
ctx.beginPath();
|
|
195
|
+
ctx.arc(cx, cy, size * 0.42, 0, Math.PI * 2);
|
|
196
|
+
ctx.stroke();
|
|
197
|
+
ctx.beginPath();
|
|
198
|
+
ctx.moveTo(cx, y + 1);
|
|
199
|
+
ctx.lineTo(cx, y + size * 0.22);
|
|
200
|
+
ctx.moveTo(cx, y + size * 0.78);
|
|
201
|
+
ctx.lineTo(cx, y + size - 1);
|
|
202
|
+
ctx.moveTo(x + 1, cy);
|
|
203
|
+
ctx.lineTo(x + size * 0.22, cy);
|
|
204
|
+
ctx.moveTo(x + size * 0.78, cy);
|
|
205
|
+
ctx.lineTo(x + size - 1, cy);
|
|
206
|
+
ctx.stroke();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function drawMunition(ctx, x, y, size, color) {
|
|
210
|
+
const cx = x + size * 0.5;
|
|
211
|
+
const bodyTop = y + size * 0.25;
|
|
212
|
+
const bodyBottom = y + size * 0.76;
|
|
213
|
+
ctx.fillStyle = color;
|
|
214
|
+
|
|
215
|
+
ctx.beginPath();
|
|
216
|
+
ctx.moveTo(cx, y + 1);
|
|
217
|
+
ctx.lineTo(cx + size * 0.17, bodyTop);
|
|
218
|
+
ctx.lineTo(cx - size * 0.17, bodyTop);
|
|
219
|
+
ctx.closePath();
|
|
220
|
+
ctx.fill();
|
|
221
|
+
|
|
222
|
+
ctx.fillRect(cx - size * 0.11, bodyTop, size * 0.22, bodyBottom - bodyTop);
|
|
223
|
+
|
|
224
|
+
ctx.beginPath();
|
|
225
|
+
ctx.moveTo(cx - size * 0.11, y + size * 0.63);
|
|
226
|
+
ctx.lineTo(x + 1, y + size - 2);
|
|
227
|
+
ctx.lineTo(cx - size * 0.11, y + size * 0.82);
|
|
228
|
+
ctx.closePath();
|
|
229
|
+
ctx.fill();
|
|
230
|
+
|
|
231
|
+
ctx.beginPath();
|
|
232
|
+
ctx.moveTo(cx + size * 0.11, y + size * 0.63);
|
|
233
|
+
ctx.lineTo(x + size - 1, y + size - 2);
|
|
234
|
+
ctx.lineTo(cx + size * 0.11, y + size * 0.82);
|
|
235
|
+
ctx.closePath();
|
|
236
|
+
ctx.fill();
|
|
237
|
+
|
|
238
|
+
ctx.strokeStyle = 'rgba(255,255,255,.35)';
|
|
239
|
+
ctx.lineWidth = 1;
|
|
240
|
+
ctx.beginPath();
|
|
241
|
+
ctx.moveTo(cx, bodyTop + 1);
|
|
242
|
+
ctx.lineTo(cx, bodyBottom - 1);
|
|
243
|
+
ctx.stroke();
|
|
244
|
+
|
|
245
|
+
ctx.strokeStyle = color;
|
|
246
|
+
ctx.beginPath();
|
|
247
|
+
ctx.moveTo(cx - size * 0.12, bodyBottom);
|
|
248
|
+
ctx.lineTo(cx, y + size - 1);
|
|
249
|
+
ctx.lineTo(cx + size * 0.12, bodyBottom);
|
|
250
|
+
ctx.stroke();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function drawGroundVehicle(ctx, x, y, size, color) {
|
|
254
|
+
ctx.fillStyle = color;
|
|
255
|
+
const bodyY = y + size * 0.42;
|
|
256
|
+
ctx.fillRect(x + size * 0.16, bodyY, size * 0.68, size * 0.26);
|
|
257
|
+
ctx.fillRect(x + size * 0.34, y + size * 0.26, size * 0.28, size * 0.2);
|
|
258
|
+
ctx.fillRect(x + size * 0.62, y + size * 0.34, size * 0.3, size * 0.06);
|
|
259
|
+
ctx.beginPath();
|
|
260
|
+
ctx.arc(x + size * 0.28, y + size * 0.76, size * 0.09, 0, Math.PI * 2);
|
|
261
|
+
ctx.arc(x + size * 0.5, y + size * 0.76, size * 0.09, 0, Math.PI * 2);
|
|
262
|
+
ctx.arc(x + size * 0.72, y + size * 0.76, size * 0.09, 0, Math.PI * 2);
|
|
263
|
+
ctx.fill();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function drawSurfaceVehicle(ctx, x, y, size, color) {
|
|
267
|
+
ctx.fillStyle = color;
|
|
268
|
+
ctx.beginPath();
|
|
269
|
+
ctx.moveTo(x + 1, y + size * 0.58);
|
|
270
|
+
ctx.lineTo(x + size * 0.82, y + size * 0.58);
|
|
271
|
+
ctx.lineTo(x + size - 1, y + size * 0.72);
|
|
272
|
+
ctx.lineTo(x + size * 0.18, y + size * 0.82);
|
|
273
|
+
ctx.closePath();
|
|
274
|
+
ctx.fill();
|
|
275
|
+
|
|
276
|
+
ctx.fillRect(x + size * 0.34, y + size * 0.34, size * 0.24, size * 0.2);
|
|
277
|
+
ctx.fillRect(x + size * 0.46, y + size * 0.18, size * 0.08, size * 0.18);
|
|
278
|
+
ctx.strokeStyle = color;
|
|
279
|
+
ctx.lineWidth = 1.2;
|
|
280
|
+
ctx.beginPath();
|
|
281
|
+
ctx.moveTo(x + size * 0.18, y + size * 0.88);
|
|
282
|
+
ctx.quadraticCurveTo(x + size * 0.34, y + size * 0.78, x + size * 0.5, y + size * 0.88);
|
|
283
|
+
ctx.quadraticCurveTo(x + size * 0.66, y + size * 0.98, x + size * 0.82, y + size * 0.88);
|
|
284
|
+
ctx.stroke();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function drawSubsurfaceVehicle(ctx, x, y, size, color) {
|
|
288
|
+
ctx.fillStyle = color;
|
|
289
|
+
ctx.beginPath();
|
|
290
|
+
ctx.ellipse(x + size * 0.5, y + size * 0.58, size * 0.38, size * 0.18, 0, 0, Math.PI * 2);
|
|
291
|
+
ctx.fill();
|
|
292
|
+
ctx.fillRect(x + size * 0.44, y + size * 0.28, size * 0.12, size * 0.18);
|
|
293
|
+
ctx.fillRect(x + size * 0.38, y + size * 0.26, size * 0.24, size * 0.06);
|
|
294
|
+
ctx.strokeStyle = color;
|
|
295
|
+
ctx.lineWidth = 1.2;
|
|
296
|
+
ctx.beginPath();
|
|
297
|
+
ctx.moveTo(x + size * 0.16, y + size * 0.86);
|
|
298
|
+
ctx.quadraticCurveTo(x + size * 0.32, y + size * 0.78, x + size * 0.5, y + size * 0.86);
|
|
299
|
+
ctx.quadraticCurveTo(x + size * 0.68, y + size * 0.94, x + size * 0.84, y + size * 0.86);
|
|
300
|
+
ctx.stroke();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function drawSpaceVehicle(ctx, x, y, size, color) {
|
|
304
|
+
const cx = x + size * 0.5;
|
|
305
|
+
const cy = y + size * 0.5;
|
|
306
|
+
ctx.strokeStyle = color;
|
|
307
|
+
ctx.fillStyle = color;
|
|
308
|
+
ctx.lineWidth = 1.2;
|
|
309
|
+
ctx.beginPath();
|
|
310
|
+
ctx.arc(cx, cy, size * 0.16, 0, Math.PI * 2);
|
|
311
|
+
ctx.fill();
|
|
312
|
+
ctx.strokeRect(x + size * 0.12, y + size * 0.32, size * 0.24, size * 0.36);
|
|
313
|
+
ctx.strokeRect(x + size * 0.64, y + size * 0.32, size * 0.24, size * 0.36);
|
|
314
|
+
ctx.beginPath();
|
|
315
|
+
ctx.moveTo(x + size * 0.36, cy);
|
|
316
|
+
ctx.lineTo(x + size * 0.64, cy);
|
|
317
|
+
ctx.stroke();
|
|
318
|
+
ctx.beginPath();
|
|
319
|
+
ctx.ellipse(cx, cy, size * 0.42, size * 0.18, -0.45, 0, Math.PI * 2);
|
|
320
|
+
ctx.stroke();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function drawControl(ctx, x, y, size, color) {
|
|
324
|
+
ctx.strokeStyle = color;
|
|
325
|
+
ctx.fillStyle = color;
|
|
326
|
+
ctx.lineWidth = 1.3;
|
|
327
|
+
const cx = x + size * 0.5;
|
|
328
|
+
const baseY = y + size * 0.68;
|
|
329
|
+
|
|
330
|
+
ctx.beginPath();
|
|
331
|
+
ctx.moveTo(x + size * 0.22, baseY);
|
|
332
|
+
ctx.lineTo(x + size * 0.78, baseY);
|
|
333
|
+
ctx.lineTo(x + size * 0.88, y + size * 0.88);
|
|
334
|
+
ctx.lineTo(x + size * 0.12, y + size * 0.88);
|
|
335
|
+
ctx.closePath();
|
|
336
|
+
ctx.stroke();
|
|
337
|
+
|
|
338
|
+
ctx.beginPath();
|
|
339
|
+
ctx.moveTo(cx, baseY);
|
|
340
|
+
ctx.lineTo(x + size * 0.42, y + size * 0.32);
|
|
341
|
+
ctx.stroke();
|
|
342
|
+
|
|
343
|
+
ctx.beginPath();
|
|
344
|
+
ctx.arc(x + size * 0.4, y + size * 0.28, size * 0.13, 0, Math.PI * 2);
|
|
345
|
+
ctx.fill();
|
|
346
|
+
|
|
347
|
+
ctx.beginPath();
|
|
348
|
+
ctx.arc(x + size * 0.66, y + size * 0.78, size * 0.055, 0, Math.PI * 2);
|
|
349
|
+
ctx.arc(x + size * 0.78, y + size * 0.78, size * 0.055, 0, Math.PI * 2);
|
|
350
|
+
ctx.fill();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function drawSituation(ctx, x, y, size, color) {
|
|
354
|
+
const cx = x + size * 0.5;
|
|
355
|
+
const cy = y + size * 0.5;
|
|
356
|
+
ctx.strokeStyle = color;
|
|
357
|
+
ctx.fillStyle = color;
|
|
358
|
+
ctx.lineWidth = 1.25;
|
|
359
|
+
|
|
360
|
+
ctx.beginPath();
|
|
361
|
+
ctx.arc(cx, cy, size * 0.42, 0, Math.PI * 2);
|
|
362
|
+
ctx.moveTo(cx - size * 0.42, cy);
|
|
363
|
+
ctx.lineTo(cx + size * 0.42, cy);
|
|
364
|
+
ctx.moveTo(cx, cy - size * 0.42);
|
|
365
|
+
ctx.lineTo(cx, cy + size * 0.42);
|
|
366
|
+
ctx.stroke();
|
|
367
|
+
|
|
368
|
+
ctx.globalAlpha = 0.55;
|
|
369
|
+
ctx.beginPath();
|
|
370
|
+
ctx.arc(cx, cy, size * 0.25, 0, Math.PI * 2);
|
|
371
|
+
ctx.stroke();
|
|
372
|
+
ctx.globalAlpha = 1;
|
|
373
|
+
|
|
374
|
+
ctx.beginPath();
|
|
375
|
+
ctx.moveTo(cx, cy);
|
|
376
|
+
ctx.lineTo(x + size * 0.82, y + size * 0.26);
|
|
377
|
+
ctx.stroke();
|
|
378
|
+
|
|
379
|
+
ctx.beginPath();
|
|
380
|
+
ctx.arc(x + size * 0.68, y + size * 0.38, size * 0.055, 0, Math.PI * 2);
|
|
381
|
+
ctx.arc(x + size * 0.36, y + size * 0.62, size * 0.045, 0, Math.PI * 2);
|
|
382
|
+
ctx.fill();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function drawDamage(ctx, x, y, size, color) {
|
|
386
|
+
const cx = x + size * 0.5;
|
|
387
|
+
const cy = y + size * 0.52;
|
|
388
|
+
ctx.strokeStyle = color;
|
|
389
|
+
ctx.fillStyle = color;
|
|
390
|
+
ctx.lineWidth = 1.4;
|
|
391
|
+
|
|
392
|
+
ctx.beginPath();
|
|
393
|
+
ctx.moveTo(cx, y + 1);
|
|
394
|
+
ctx.lineTo(x + size * 0.9, y + size * 0.34);
|
|
395
|
+
ctx.lineTo(x + size * 0.68, y + size * 0.42);
|
|
396
|
+
ctx.lineTo(x + size - 1, y + size * 0.64);
|
|
397
|
+
ctx.lineTo(x + size * 0.62, y + size * 0.6);
|
|
398
|
+
ctx.lineTo(x + size * 0.72, y + size - 1);
|
|
399
|
+
ctx.lineTo(cx, y + size * 0.72);
|
|
400
|
+
ctx.lineTo(x + size * 0.28, y + size - 1);
|
|
401
|
+
ctx.lineTo(x + size * 0.38, y + size * 0.6);
|
|
402
|
+
ctx.lineTo(x + 1, y + size * 0.64);
|
|
403
|
+
ctx.lineTo(x + size * 0.32, y + size * 0.42);
|
|
404
|
+
ctx.lineTo(x + size * 0.1, y + size * 0.34);
|
|
405
|
+
ctx.closePath();
|
|
406
|
+
ctx.stroke();
|
|
407
|
+
|
|
408
|
+
ctx.beginPath();
|
|
409
|
+
ctx.arc(cx, cy, size * 0.12, 0, Math.PI * 2);
|
|
410
|
+
ctx.fill();
|
|
411
|
+
}
|
|
@@ -22,8 +22,18 @@ export const darkTheme = {
|
|
|
22
22
|
root: { icon: 'folder', color: '#38bdf8' },
|
|
23
23
|
system: { icon: 'folder', color: '#818cf8' },
|
|
24
24
|
platform: { icon: 'aircraft', color: '#60a5fa' },
|
|
25
|
+
air: { icon: 'air', color: '#60a5fa' },
|
|
26
|
+
ground: { icon: 'ground', color: '#a3e635' },
|
|
27
|
+
surface: { icon: 'surface', color: '#22d3ee' },
|
|
28
|
+
subsurface: { icon: 'subsurface', color: '#38bdf8' },
|
|
29
|
+
space: { icon: 'space', color: '#c084fc' },
|
|
30
|
+
munition: { icon: 'munition', color: '#fb7185' },
|
|
25
31
|
sensor: { icon: 'radar', color: '#34d399' },
|
|
26
32
|
track: { icon: 'track', color: '#a78bfa' },
|
|
33
|
+
geometry: { icon: 'point', color: '#38bdf8' },
|
|
34
|
+
control: { icon: 'control', color: '#f59e0b' },
|
|
35
|
+
situation: { icon: 'situation', color: '#2dd4bf' },
|
|
36
|
+
damage: { icon: 'damage', color: '#fb7185' },
|
|
27
37
|
warning: { icon: 'warning', color: '#facc15' },
|
|
28
38
|
error: { icon: 'error', color: '#ef4444' },
|
|
29
39
|
task: { icon: 'task', color: '#f97316' },
|
|
@@ -77,8 +87,18 @@ export const tacticalTheme = {
|
|
|
77
87
|
...darkTheme.types,
|
|
78
88
|
root: { icon: 'folder', color: '#7ddc92' },
|
|
79
89
|
platform: { icon: 'aircraft', color: '#93c572' },
|
|
90
|
+
air: { icon: 'air', color: '#93c572' },
|
|
91
|
+
ground: { icon: 'ground', color: '#c7f36f' },
|
|
92
|
+
surface: { icon: 'surface', color: '#67e8f9' },
|
|
93
|
+
subsurface: { icon: 'subsurface', color: '#7dd3fc' },
|
|
94
|
+
space: { icon: 'space', color: '#d8b4fe' },
|
|
95
|
+
munition: { icon: 'munition', color: '#ff8fab' },
|
|
80
96
|
sensor: { icon: 'radar', color: '#45d483' },
|
|
81
97
|
track: { icon: 'track', color: '#d6f264' },
|
|
98
|
+
geometry: { icon: 'point', color: '#7ddc92' },
|
|
99
|
+
control: { icon: 'control', color: '#ffd166' },
|
|
100
|
+
situation: { icon: 'situation', color: '#8be9d9' },
|
|
101
|
+
damage: { icon: 'damage', color: '#ff8fab' },
|
|
82
102
|
warning: { icon: 'warning', color: '#ffd166' },
|
|
83
103
|
error: { icon: 'error', color: '#ff5c5c' },
|
|
84
104
|
},
|
|
@@ -159,4 +179,3 @@ function mergeTheme(base, override) {
|
|
|
159
179
|
statuses: { ...base.statuses, ...(override.statuses ?? {}) },
|
|
160
180
|
};
|
|
161
181
|
}
|
|
162
|
-
|
|
@@ -59,6 +59,7 @@ export function rebuildWorkerRows(state, options = {}) {
|
|
|
59
59
|
const rowHeight = options.rowHeight ?? 28;
|
|
60
60
|
const indentWidth = options.indentWidth ?? 18;
|
|
61
61
|
const expanded = new Set(options.expandedIds ?? []);
|
|
62
|
+
const filterCollapsed = new Set(options.filterCollapsedIds ?? []);
|
|
62
63
|
const query = normalize(options.filterQuery ?? '');
|
|
63
64
|
const sort = options.sort ?? { columnId: null, direction: null };
|
|
64
65
|
const sortValues = options.sortValues ? new Map(options.sortValues) : null;
|
|
@@ -81,7 +82,8 @@ export function rebuildWorkerRows(state, options = {}) {
|
|
|
81
82
|
if (!isIncluded(id)) return;
|
|
82
83
|
const nodeIndex = state.idToIndex.get(id);
|
|
83
84
|
if (nodeIndex === undefined) return;
|
|
84
|
-
const
|
|
85
|
+
const children = sortIds(state.childrenByParent.get(id) ?? []);
|
|
86
|
+
const expandedRow = (expanded.has(id) || Boolean(query && children.length > 0)) && !filterCollapsed.has(id);
|
|
85
87
|
const rowIndex = rows.length;
|
|
86
88
|
rows.push({
|
|
87
89
|
nodeId: id,
|
|
@@ -94,8 +96,8 @@ export function rebuildWorkerRows(state, options = {}) {
|
|
|
94
96
|
hasChildren: hasChildren(id),
|
|
95
97
|
});
|
|
96
98
|
maxDepth = Math.max(maxDepth, depth);
|
|
97
|
-
if (!expandedRow
|
|
98
|
-
for (const childId of
|
|
99
|
+
if (!expandedRow) return;
|
|
100
|
+
for (const childId of children) visit(childId, depth + 1);
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
for (const rootId of sortIds(state.roots)) visit(rootId, 0);
|
|
@@ -33,6 +33,7 @@ export class VisibleRowModel extends EventTarget {
|
|
|
33
33
|
this.contentHeight = 0;
|
|
34
34
|
this.sortComparator = null;
|
|
35
35
|
this.filterPredicate = null;
|
|
36
|
+
this.filterCollapsed = new Set();
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
setSortComparator(comparator) {
|
|
@@ -41,6 +42,15 @@ export class VisibleRowModel extends EventTarget {
|
|
|
41
42
|
|
|
42
43
|
setFilterPredicate(predicate) {
|
|
43
44
|
this.filterPredicate = predicate;
|
|
45
|
+
this.filterCollapsed.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
collapseFilterBranch(id) {
|
|
49
|
+
this.filterCollapsed.add(id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
expandFilterBranch(id) {
|
|
53
|
+
return this.filterCollapsed.delete(id);
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
applyRows({ rows, contentHeight, contentWidth }) {
|
|
@@ -84,8 +94,10 @@ export class VisibleRowModel extends EventTarget {
|
|
|
84
94
|
if (!subtreeIncluded(id)) return;
|
|
85
95
|
const nodeIndex = this.model.index.idToIndex.get(id);
|
|
86
96
|
if (nodeIndex === undefined) return;
|
|
97
|
+
const children = sortedChildren(id);
|
|
87
98
|
const hasChildren = this.expansion.hasChildren(id);
|
|
88
|
-
const
|
|
99
|
+
const autoExpanded = Boolean(this.filterPredicate && children.length > 0);
|
|
100
|
+
const expanded = (this.expansion.isExpanded(id) || autoExpanded) && !this.filterCollapsed.has(id);
|
|
89
101
|
const rowIndex = this.rows.length;
|
|
90
102
|
this.rows.push({
|
|
91
103
|
nodeId: id,
|
|
@@ -99,8 +111,8 @@ export class VisibleRowModel extends EventTarget {
|
|
|
99
111
|
});
|
|
100
112
|
this.rowIndexById.set(id, rowIndex);
|
|
101
113
|
maxDepth = Math.max(maxDepth, depth);
|
|
102
|
-
if (!expanded
|
|
103
|
-
for (const childId of
|
|
114
|
+
if (!expanded) return;
|
|
115
|
+
for (const childId of children) visit(childId, depth + 1);
|
|
104
116
|
};
|
|
105
117
|
|
|
106
118
|
const roots = this.model.index.roots.filter((rootId) => subtreeIncluded(rootId));
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export type TreeViewAlign = 'start' | 'center' | 'end' | 'nearest';
|
|
2
|
+
|
|
3
|
+
export type TreeNode = {
|
|
4
|
+
id: string;
|
|
5
|
+
parentId?: string | null;
|
|
6
|
+
label?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
image?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
data?: any;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type DynamicPatch = {
|
|
15
|
+
id: string;
|
|
16
|
+
state?: Record<string, any>;
|
|
17
|
+
} & Record<string, any>;
|
|
18
|
+
|
|
19
|
+
export type MetaRule = {
|
|
20
|
+
min?: number;
|
|
21
|
+
max?: number;
|
|
22
|
+
step?: number;
|
|
23
|
+
integer?: boolean;
|
|
24
|
+
readonly?: boolean;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
updated?: boolean;
|
|
27
|
+
description?: string;
|
|
28
|
+
color?: boolean;
|
|
29
|
+
options?: Record<string, string | number | boolean>;
|
|
30
|
+
button?: string;
|
|
31
|
+
label?: string;
|
|
32
|
+
fullWidthButton?: boolean;
|
|
33
|
+
itemType?: 'object' | 'number' | 'string' | 'boolean';
|
|
34
|
+
itemTitle?: (i: number, item: any) => string;
|
|
35
|
+
itemFactory?: () => any;
|
|
36
|
+
type?: string;
|
|
37
|
+
icon?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type Column = {
|
|
41
|
+
id: string;
|
|
42
|
+
label?: string;
|
|
43
|
+
width?: number;
|
|
44
|
+
minWidth?: number;
|
|
45
|
+
align?: 'left' | 'center' | 'right';
|
|
46
|
+
kind?: string;
|
|
47
|
+
sortable?: boolean;
|
|
48
|
+
value?: (node: TreeNode, state: Record<string, any>) => string | number | boolean;
|
|
49
|
+
render?: (ctx: CanvasRenderingContext2D, cell: any) => void;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const themes: Record<string, any>;
|
|
53
|
+
|
|
54
|
+
export class TreeRowRenderer {
|
|
55
|
+
renderedRows: number;
|
|
56
|
+
constructor(options?: Record<string, any>);
|
|
57
|
+
initialize(canvas: HTMLCanvasElement): void;
|
|
58
|
+
setScene(scene: any): void;
|
|
59
|
+
updateDynamicState(patches: DynamicPatch[]): void;
|
|
60
|
+
render(scene?: any, time?: number): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class TreeViewController {
|
|
64
|
+
canvas?: HTMLCanvasElement;
|
|
65
|
+
viewport: {
|
|
66
|
+
viewportWidth: number;
|
|
67
|
+
viewportHeight: number;
|
|
68
|
+
[key: string]: any;
|
|
69
|
+
};
|
|
70
|
+
filterQuery: string;
|
|
71
|
+
rowModel: any;
|
|
72
|
+
expansion: any;
|
|
73
|
+
selection: any;
|
|
74
|
+
constructor(options?: Record<string, any>);
|
|
75
|
+
on(type: string, listener: (event: any) => void): any;
|
|
76
|
+
off(type: string, listener: (event: any) => void): void;
|
|
77
|
+
setData(nodes: TreeNode[]): void;
|
|
78
|
+
setModel(model: any, meta?: Record<string, MetaRule>, options?: Record<string, any>): void;
|
|
79
|
+
setColumns(columns: Column[]): void;
|
|
80
|
+
setDynamicState(patches: DynamicPatch[]): void;
|
|
81
|
+
setTheme(theme: any): void;
|
|
82
|
+
resize(width: number, height: number): void;
|
|
83
|
+
render(time?: number): void;
|
|
84
|
+
renderMeasured(time?: number): any;
|
|
85
|
+
hitTest(clientX: number, clientY: number): any;
|
|
86
|
+
getTooltipForHit(hit: any): any;
|
|
87
|
+
search(query: string, options?: Record<string, any>): any;
|
|
88
|
+
setFilter(queryOrPredicate?: string | ((node: any, state: any) => boolean)): void;
|
|
89
|
+
clearFilter(): void;
|
|
90
|
+
focusNode(nodeId: string, options?: Record<string, any>): boolean;
|
|
91
|
+
scrollToNode(nodeId: string, align?: TreeViewAlign): boolean;
|
|
92
|
+
getSelection(): string[];
|
|
93
|
+
setSelection(ids: string[]): void;
|
|
94
|
+
clearSelection(): void;
|
|
95
|
+
expandAll(): void;
|
|
96
|
+
collapseAll(): void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class TreeViewInputController {
|
|
100
|
+
constructor(options?: Record<string, any>);
|
|
101
|
+
destroy(): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class CellEditorManager {
|
|
105
|
+
constructor(options?: Record<string, any>);
|
|
106
|
+
destroy(): void;
|
|
107
|
+
close(): void;
|
|
108
|
+
}
|
|
@@ -15,11 +15,18 @@ export class CellEditorManager {
|
|
|
15
15
|
window.removeEventListener('mouseup', this.onMouseUp);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
close() {
|
|
19
|
+
this.#removeOverlay();
|
|
20
|
+
this.rangeDrag = null;
|
|
21
|
+
window.removeEventListener('mousemove', this.onMouseMove);
|
|
22
|
+
window.removeEventListener('mouseup', this.onMouseUp);
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
handlePointerDown(event, hit) {
|
|
19
26
|
if (!this.#isEditableHit(hit)) return false;
|
|
20
27
|
const data = hit.row ? this.controller.model.nodes[hit.row.nodeIndex]?.data : null;
|
|
21
28
|
if (!data || data.readonly || data.disabled) return false;
|
|
22
|
-
if (data.editorType === 'range' && hit.part
|
|
29
|
+
if (data.editorType === 'range' && hit.part === 'range') {
|
|
23
30
|
this.rangeDrag = { hit, data };
|
|
24
31
|
this.#updateRangeFromEvent(event);
|
|
25
32
|
window.addEventListener('mousemove', this.onMouseMove);
|
|
@@ -47,7 +54,13 @@ export class CellEditorManager {
|
|
|
47
54
|
this.controller.updateInspectorValue(node.id, !data.value, 'checkbox');
|
|
48
55
|
return true;
|
|
49
56
|
}
|
|
50
|
-
if (data.editorType === 'range'
|
|
57
|
+
if (data.editorType === 'range') {
|
|
58
|
+
if (hit.part === 'range') return true;
|
|
59
|
+
if (hit.part !== 'number') {
|
|
60
|
+
this.#toggleRangeMinMax(node, data);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
51
64
|
if (this.overlay) return true;
|
|
52
65
|
this.#showOverlay(node, hit);
|
|
53
66
|
return true;
|
|
@@ -62,7 +75,7 @@ export class CellEditorManager {
|
|
|
62
75
|
#showOverlay(node, hit, options = {}) {
|
|
63
76
|
this.#removeOverlay();
|
|
64
77
|
const data = node.data;
|
|
65
|
-
const rect = this.#overlayRect(hit);
|
|
78
|
+
const rect = this.#clampRectToHost(this.#overlayRect(hit), 4);
|
|
66
79
|
const hostRect = this.host.getBoundingClientRect();
|
|
67
80
|
const element = createEditorElement(data);
|
|
68
81
|
Object.assign(element.style, {
|
|
@@ -98,7 +111,7 @@ export class CellEditorManager {
|
|
|
98
111
|
ensureOverlayHost(this.host);
|
|
99
112
|
this.host.append(element);
|
|
100
113
|
this.overlay = element;
|
|
101
|
-
element.focus();
|
|
114
|
+
element.focus({ preventScroll: true });
|
|
102
115
|
element.select?.();
|
|
103
116
|
if (options.showPicker && element.showPicker) {
|
|
104
117
|
requestAnimationFrame(() => {
|
|
@@ -113,7 +126,13 @@ export class CellEditorManager {
|
|
|
113
126
|
|
|
114
127
|
#showHeaderFilterOverlay(hit) {
|
|
115
128
|
this.#removeOverlay();
|
|
116
|
-
const
|
|
129
|
+
const headerRect = this.controller.getHeaderClientRect(hit);
|
|
130
|
+
const rect = this.#clampRectToHost({
|
|
131
|
+
x: headerRect.x + 8,
|
|
132
|
+
y: headerRect.y + 5,
|
|
133
|
+
width: Math.max(24, Math.min(headerRect.width, this.controller.viewport.viewportWidth) - 16),
|
|
134
|
+
height: Math.max(20, headerRect.height - 10),
|
|
135
|
+
}, 0);
|
|
117
136
|
const hostRect = this.host.getBoundingClientRect();
|
|
118
137
|
const element = document.createElement('input');
|
|
119
138
|
element.type = 'search';
|
|
@@ -121,12 +140,12 @@ export class CellEditorManager {
|
|
|
121
140
|
element.placeholder = 'Filter inspector';
|
|
122
141
|
Object.assign(element.style, {
|
|
123
142
|
position: 'absolute',
|
|
124
|
-
left: `${rect.x - hostRect.left
|
|
125
|
-
top: `${rect.y - hostRect.top
|
|
126
|
-
width: `${Math.max(24, rect.width
|
|
127
|
-
height: `${Math.max(20, rect.height
|
|
143
|
+
left: `${rect.x - hostRect.left}px`,
|
|
144
|
+
top: `${rect.y - hostRect.top}px`,
|
|
145
|
+
width: `${Math.max(24, rect.width)}px`,
|
|
146
|
+
height: `${Math.max(20, rect.height)}px`,
|
|
128
147
|
minWidth: '0',
|
|
129
|
-
maxWidth: `${Math.max(24, rect.width
|
|
148
|
+
maxWidth: `${Math.max(24, rect.width)}px`,
|
|
130
149
|
zIndex: 20,
|
|
131
150
|
boxSizing: 'border-box',
|
|
132
151
|
margin: '0',
|
|
@@ -147,7 +166,7 @@ export class CellEditorManager {
|
|
|
147
166
|
ensureOverlayHost(this.host);
|
|
148
167
|
this.host.append(element);
|
|
149
168
|
this.overlay = element;
|
|
150
|
-
element.focus();
|
|
169
|
+
element.focus({ preventScroll: true });
|
|
151
170
|
element.select();
|
|
152
171
|
}
|
|
153
172
|
|
|
@@ -184,8 +203,9 @@ export class CellEditorManager {
|
|
|
184
203
|
#overlayRect(hit) {
|
|
185
204
|
if (hit.column?.kind !== 'inspectorPane') return this.controller.getCellClientRect(hit);
|
|
186
205
|
const rect = this.controller.getCellClientRect(hit);
|
|
187
|
-
const
|
|
188
|
-
const
|
|
206
|
+
const visibleWidth = Math.max(1, Math.min(rect.width, this.controller.viewport.viewportWidth));
|
|
207
|
+
const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
|
|
208
|
+
const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
|
|
189
209
|
if (hit.part === 'number') {
|
|
190
210
|
const valueWidth = Math.min(64, Math.max(42, (editorWidth - 20) * 0.28));
|
|
191
211
|
return { x: rect.x + editorLeft + editorWidth - valueWidth - 10, y: rect.y + 4, width: valueWidth, height: rect.height - 8 };
|
|
@@ -196,8 +216,9 @@ export class CellEditorManager {
|
|
|
196
216
|
#rangeBarRect(hit) {
|
|
197
217
|
const rect = this.controller.getCellClientRect(hit);
|
|
198
218
|
if (hit.column?.kind === 'inspectorPane') {
|
|
199
|
-
const
|
|
200
|
-
const
|
|
219
|
+
const visibleWidth = Math.max(1, Math.min(rect.width, this.controller.viewport.viewportWidth));
|
|
220
|
+
const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
|
|
221
|
+
const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
|
|
201
222
|
const valueWidth = Math.min(64, Math.max(42, (editorWidth - 20) * 0.28));
|
|
202
223
|
const barWidth = Math.max(24, editorWidth - 20 - valueWidth - 8);
|
|
203
224
|
return { x: rect.x + editorLeft + 10, y: rect.y + rect.height / 2 - 4, width: barWidth, height: 8 };
|
|
@@ -206,10 +227,31 @@ export class CellEditorManager {
|
|
|
206
227
|
return { x: rect.x + 10, y: rect.y + rect.height / 2 - 4, width: Math.max(24, rect.width - 20 - valueWidth - 8), height: 8 };
|
|
207
228
|
}
|
|
208
229
|
|
|
230
|
+
#clampRectToHost(rect, inset = 0) {
|
|
231
|
+
const hostRect = this.host.getBoundingClientRect();
|
|
232
|
+
const minX = hostRect.left + inset;
|
|
233
|
+
const minY = hostRect.top + inset;
|
|
234
|
+
const maxX = hostRect.right - inset;
|
|
235
|
+
const maxY = hostRect.bottom - inset;
|
|
236
|
+
const width = Math.max(24, Math.min(rect.width, maxX - minX));
|
|
237
|
+
const height = Math.max(18, Math.min(rect.height, maxY - minY));
|
|
238
|
+
const x = Math.max(minX, Math.min(rect.x, maxX - width));
|
|
239
|
+
const y = Math.max(minY, Math.min(rect.y, maxY - height));
|
|
240
|
+
return { x, y, width, height };
|
|
241
|
+
}
|
|
242
|
+
|
|
209
243
|
#removeOverlay() {
|
|
210
244
|
this.overlay?.remove();
|
|
211
245
|
this.overlay = null;
|
|
212
246
|
}
|
|
247
|
+
|
|
248
|
+
#toggleRangeMinMax(node, data) {
|
|
249
|
+
const meta = data.meta ?? {};
|
|
250
|
+
const min = Number.isFinite(meta.min) ? meta.min : 0;
|
|
251
|
+
const max = Number.isFinite(meta.max) ? meta.max : 100;
|
|
252
|
+
const current = typeof data.value === 'number' && Number.isFinite(data.value) ? data.value : min;
|
|
253
|
+
this.controller.updateInspectorValue(node.id, current <= min ? max : min, 'range');
|
|
254
|
+
}
|
|
213
255
|
}
|
|
214
256
|
|
|
215
257
|
function createEditorElement(data) {
|
|
@@ -47,11 +47,13 @@ export class TreeRowRenderer {
|
|
|
47
47
|
ctx.translate(viewport.renderInsetX ?? 0, viewport.renderInsetY ?? 0);
|
|
48
48
|
this.#drawHeader(ctx);
|
|
49
49
|
this.#drawRows(ctx);
|
|
50
|
+
this.#drawScrollIndicator(ctx);
|
|
50
51
|
ctx.restore();
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
#drawHeader(ctx) {
|
|
54
55
|
const { viewport, columns, theme, sort, headerFilter, filterQuery } = this.scene;
|
|
56
|
+
if (viewport.headerHeight <= 0) return;
|
|
55
57
|
const colors = theme.colors;
|
|
56
58
|
ctx.save();
|
|
57
59
|
ctx.fillStyle = colors.row;
|
|
@@ -65,10 +67,10 @@ export class TreeRowRenderer {
|
|
|
65
67
|
this.#drawHeaderFilter(ctx, column, viewport, theme, filterQuery);
|
|
66
68
|
} else {
|
|
67
69
|
ctx.fillStyle = colors.textMuted;
|
|
68
|
-
ctx
|
|
70
|
+
drawTruncatedText(ctx, column.label, column.x + 10, viewport.headerHeight / 2, column.width - 20);
|
|
69
71
|
}
|
|
70
72
|
if (sort?.columnId === column.id && sort.direction) {
|
|
71
|
-
ctx
|
|
73
|
+
drawTruncatedText(ctx, sort.direction === 'asc' ? '^' : 'v', column.x + column.width - 16, viewport.headerHeight / 2, 12);
|
|
72
74
|
}
|
|
73
75
|
ctx.strokeStyle = colors.border;
|
|
74
76
|
ctx.beginPath();
|
|
@@ -89,7 +91,9 @@ export class TreeRowRenderer {
|
|
|
89
91
|
const colors = theme.colors;
|
|
90
92
|
const x = column.x + 8;
|
|
91
93
|
const y = 5;
|
|
92
|
-
const
|
|
94
|
+
const visibleRight = viewport.scrollX + viewport.viewportWidth;
|
|
95
|
+
const visibleWidth = Math.max(1, Math.min(column.x + column.width, visibleRight) - column.x);
|
|
96
|
+
const width = Math.max(40, visibleWidth - 16);
|
|
93
97
|
const height = Math.max(18, viewport.headerHeight - 10);
|
|
94
98
|
ctx.fillStyle = colors.progressTrack;
|
|
95
99
|
roundRect(ctx, x, y, width, height, 4);
|
|
@@ -97,13 +101,16 @@ export class TreeRowRenderer {
|
|
|
97
101
|
ctx.strokeStyle = colors.border;
|
|
98
102
|
ctx.stroke();
|
|
99
103
|
ctx.fillStyle = filterQuery ? colors.text : colors.textMuted;
|
|
100
|
-
ctx
|
|
104
|
+
drawTruncatedText(ctx, filterQuery || 'Filter inspector', x + 8, viewport.headerHeight / 2, width - 16);
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
#drawRows(ctx) {
|
|
104
108
|
const { rows, visibleRange, viewport } = this.scene;
|
|
105
109
|
this.renderedRows = visibleRange.count;
|
|
106
110
|
ctx.save();
|
|
111
|
+
ctx.beginPath();
|
|
112
|
+
ctx.rect(0, viewport.headerHeight, viewport.viewportWidth, viewport.rowViewportHeight);
|
|
113
|
+
ctx.clip();
|
|
107
114
|
ctx.translate(-viewport.scrollX, viewport.headerHeight - viewport.scrollY);
|
|
108
115
|
for (let i = visibleRange.first; i <= visibleRange.last; i++) {
|
|
109
116
|
const row = rows[i];
|
|
@@ -112,6 +119,36 @@ export class TreeRowRenderer {
|
|
|
112
119
|
ctx.restore();
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
#drawScrollIndicator(ctx) {
|
|
123
|
+
const { viewport, theme } = this.scene;
|
|
124
|
+
const maxY = Math.max(0, viewport.contentHeight - viewport.rowViewportHeight);
|
|
125
|
+
if (maxY <= 0 || viewport.viewportHeight <= viewport.headerHeight + 12) return;
|
|
126
|
+
|
|
127
|
+
const trackTop = viewport.headerHeight + 4;
|
|
128
|
+
const trackHeight = Math.max(1, viewport.viewportHeight - viewport.headerHeight - 8);
|
|
129
|
+
const thumbHeight = Math.max(18, Math.min(trackHeight, trackHeight * (viewport.rowViewportHeight / viewport.contentHeight)));
|
|
130
|
+
const thumbY = trackTop + (trackHeight - thumbHeight) * (viewport.scrollY / maxY);
|
|
131
|
+
const x = Math.max(2, viewport.viewportWidth - 4);
|
|
132
|
+
|
|
133
|
+
ctx.save();
|
|
134
|
+
ctx.lineCap = 'round';
|
|
135
|
+
ctx.lineWidth = 2;
|
|
136
|
+
ctx.globalAlpha = 0.28;
|
|
137
|
+
ctx.strokeStyle = theme.colors.guide;
|
|
138
|
+
ctx.beginPath();
|
|
139
|
+
ctx.moveTo(x, trackTop);
|
|
140
|
+
ctx.lineTo(x, trackTop + trackHeight);
|
|
141
|
+
ctx.stroke();
|
|
142
|
+
|
|
143
|
+
ctx.globalAlpha = 0.9;
|
|
144
|
+
ctx.strokeStyle = theme.colors.focus;
|
|
145
|
+
ctx.beginPath();
|
|
146
|
+
ctx.moveTo(x, thumbY);
|
|
147
|
+
ctx.lineTo(x, thumbY + thumbHeight);
|
|
148
|
+
ctx.stroke();
|
|
149
|
+
ctx.restore();
|
|
150
|
+
}
|
|
151
|
+
|
|
115
152
|
#drawRow(ctx, row) {
|
|
116
153
|
const { columns, nodes, dynamicState, selection, hoverNodeId, focusNodeId, searchMatches, theme, viewport } = this.scene;
|
|
117
154
|
const node = nodes[row.nodeIndex];
|
|
@@ -123,10 +160,11 @@ export class TreeRowRenderer {
|
|
|
123
160
|
const hovered = hoverNodeId === row.nodeId;
|
|
124
161
|
const focused = focusNodeId === row.nodeId;
|
|
125
162
|
const y = row.y;
|
|
126
|
-
const
|
|
163
|
+
const visibleX = viewport.scrollX;
|
|
164
|
+
const visibleWidth = viewport.viewportWidth;
|
|
127
165
|
|
|
128
166
|
ctx.fillStyle = selected ? colors.rowSelected : highlighted ? colors.rowHighlighted : hovered ? colors.rowHover : colors.row;
|
|
129
|
-
ctx.fillRect(
|
|
167
|
+
ctx.fillRect(visibleX, y, visibleWidth, row.height);
|
|
130
168
|
|
|
131
169
|
this.#drawIndentGuides(ctx, row, colors);
|
|
132
170
|
|
|
@@ -142,20 +180,15 @@ export class TreeRowRenderer {
|
|
|
142
180
|
|
|
143
181
|
ctx.strokeStyle = colors.border;
|
|
144
182
|
ctx.beginPath();
|
|
145
|
-
ctx.moveTo(
|
|
146
|
-
ctx.lineTo(
|
|
183
|
+
ctx.moveTo(visibleX, y + row.height + 0.5);
|
|
184
|
+
ctx.lineTo(visibleX + visibleWidth, y + row.height + 0.5);
|
|
147
185
|
ctx.stroke();
|
|
148
186
|
|
|
149
|
-
if (state.updated && node.data?.inspector)
|
|
150
|
-
ctx.fillStyle = theme.colors.focus;
|
|
151
|
-
ctx.beginPath();
|
|
152
|
-
ctx.arc(rowWidth - 10, y + row.height / 2, 3, 0, Math.PI * 2);
|
|
153
|
-
ctx.fill();
|
|
154
|
-
}
|
|
187
|
+
if (state.updated && node.data?.inspector) this.#drawUpdatedMarker(ctx, row, theme);
|
|
155
188
|
|
|
156
189
|
if (focused) {
|
|
157
190
|
ctx.strokeStyle = colors.focus;
|
|
158
|
-
ctx.strokeRect(1.5, y + 2.5,
|
|
191
|
+
ctx.strokeRect(visibleX + 1.5, y + 2.5, visibleWidth - 3, row.height - 5);
|
|
159
192
|
}
|
|
160
193
|
}
|
|
161
194
|
|
|
@@ -174,25 +207,35 @@ export class TreeRowRenderer {
|
|
|
174
207
|
else this.#drawTextCell(ctx, cell);
|
|
175
208
|
}
|
|
176
209
|
|
|
177
|
-
#drawInspectorPaneCell(ctx, { node, row, rect, theme }) {
|
|
210
|
+
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style }) {
|
|
211
|
+
const visibleRight = this.scene.viewport.scrollX + this.scene.viewport.viewportWidth;
|
|
212
|
+
rect = { ...rect, width: Math.max(1, Math.min(rect.x + rect.width, visibleRight) - rect.x) };
|
|
178
213
|
const data = node.data ?? {};
|
|
179
214
|
const colors = theme.colors;
|
|
180
215
|
const indentX = rect.x + row.depth * theme.indentWidth;
|
|
181
|
-
const labelX = indentX + 28;
|
|
182
216
|
const cy = rect.y + rect.height / 2;
|
|
183
|
-
|
|
217
|
+
let labelX = indentX + 28;
|
|
218
|
+
if (!row.hasChildren && node.icon) {
|
|
219
|
+
this.iconRegistry.draw(ctx, style.icon, indentX + 3, rect.y + 6, 15, style.color);
|
|
220
|
+
labelX = indentX + 24;
|
|
221
|
+
} else if (!row.hasChildren) {
|
|
222
|
+
labelX = indentX + 24;
|
|
223
|
+
} else {
|
|
224
|
+
this.#drawChevron(ctx, indentX + 10, cy, row, colors);
|
|
225
|
+
}
|
|
184
226
|
ctx.font = theme.font;
|
|
185
227
|
ctx.textBaseline = 'middle';
|
|
186
228
|
ctx.textAlign = 'left';
|
|
187
229
|
ctx.globalAlpha = data.disabled ? 0.45 : 1;
|
|
188
230
|
ctx.fillStyle = data.valueType === 'object' || data.valueType === 'array' ? colors.text : colors.textMuted;
|
|
189
|
-
|
|
231
|
+
const layout = inspectorPaneLayout(rect.width, row.depth, theme.indentWidth, data.editorType, this.scene.inspectorPaneLabelEnd);
|
|
232
|
+
drawTruncatedText(ctx, node.label ?? node.id, labelX, cy, Math.max(20, rect.x + layout.editorLeft - labelX - 8));
|
|
190
233
|
|
|
191
|
-
const editorX = rect.x +
|
|
192
|
-
const editorWidth =
|
|
234
|
+
const editorX = rect.x + layout.editorLeft;
|
|
235
|
+
const editorWidth = layout.editorWidth;
|
|
193
236
|
if (data.valueType === 'array') {
|
|
194
237
|
ctx.fillStyle = colors.textMuted;
|
|
195
|
-
ctx
|
|
238
|
+
drawTruncatedText(ctx, data.valueText, editorX, cy, Math.max(20, editorWidth - 58));
|
|
196
239
|
this.#drawSmallButton(ctx, rect.x + rect.width - 54, rect.y + 5, 22, rect.height - 10, '+', theme);
|
|
197
240
|
this.#drawSmallButton(ctx, rect.x + rect.width - 28, rect.y + 5, 22, rect.height - 10, '-', theme);
|
|
198
241
|
} else if (data.valueType === 'object') {
|
|
@@ -202,12 +245,14 @@ export class TreeRowRenderer {
|
|
|
202
245
|
node,
|
|
203
246
|
rect: { x: editorX, y: rect.y, width: editorWidth, height: rect.height },
|
|
204
247
|
theme,
|
|
248
|
+
suppressUpdatedMarker: true,
|
|
205
249
|
});
|
|
206
250
|
}
|
|
251
|
+
if (data.meta?.updated) this.#drawUpdatedMarker(ctx, row, theme);
|
|
207
252
|
ctx.globalAlpha = 1;
|
|
208
253
|
}
|
|
209
254
|
|
|
210
|
-
#drawInspectorValueCell(ctx, { node, rect, theme }) {
|
|
255
|
+
#drawInspectorValueCell(ctx, { node, rect, theme, suppressUpdatedMarker = false }) {
|
|
211
256
|
const data = node.data ?? {};
|
|
212
257
|
const meta = data.meta ?? {};
|
|
213
258
|
const disabled = data.disabled;
|
|
@@ -237,7 +282,7 @@ export class TreeRowRenderer {
|
|
|
237
282
|
ctx.fill();
|
|
238
283
|
ctx.fillStyle = theme.colors.text;
|
|
239
284
|
ctx.textAlign = 'center';
|
|
240
|
-
ctx
|
|
285
|
+
drawTruncatedText(ctx, meta.button ?? node.label, x + (meta.fullWidthButton ? width : Math.min(width, 140)) / 2, rect.y + rect.height / 2, Math.max(10, (meta.fullWidthButton ? width : Math.min(width, 140)) - 12));
|
|
241
286
|
ctx.textAlign = 'left';
|
|
242
287
|
} else if (data.editorType === 'select') {
|
|
243
288
|
ctx.fillStyle = theme.colors.rowHover;
|
|
@@ -245,12 +290,12 @@ export class TreeRowRenderer {
|
|
|
245
290
|
ctx.fill();
|
|
246
291
|
this.#drawMutedText(ctx, data.valueText, x + 8, rect.y + rect.height / 2, Math.min(width, 180) - 22, theme);
|
|
247
292
|
ctx.fillStyle = theme.colors.chevron;
|
|
248
|
-
ctx
|
|
293
|
+
drawTruncatedText(ctx, 'v', x + Math.min(width, 180) - 14, rect.y + rect.height / 2, 10);
|
|
249
294
|
} else {
|
|
250
295
|
this.#drawMutedText(ctx, data.valueText, x, rect.y + rect.height / 2, width, theme, readonly);
|
|
251
296
|
}
|
|
252
297
|
|
|
253
|
-
if (meta.updated) {
|
|
298
|
+
if (meta.updated && !suppressUpdatedMarker) {
|
|
254
299
|
ctx.fillStyle = theme.colors.focus;
|
|
255
300
|
ctx.beginPath();
|
|
256
301
|
ctx.arc(rect.x + rect.width - 10, rect.y + rect.height / 2, 3, 0, Math.PI * 2);
|
|
@@ -259,6 +304,15 @@ export class TreeRowRenderer {
|
|
|
259
304
|
ctx.globalAlpha = 1;
|
|
260
305
|
}
|
|
261
306
|
|
|
307
|
+
#drawUpdatedMarker(ctx, row, theme) {
|
|
308
|
+
const x = Math.max(6, row.depth * theme.indentWidth - 8);
|
|
309
|
+
const y = row.y + row.height / 2;
|
|
310
|
+
ctx.fillStyle = theme.colors.focus;
|
|
311
|
+
ctx.beginPath();
|
|
312
|
+
ctx.arc(x, y, 3, 0, Math.PI * 2);
|
|
313
|
+
ctx.fill();
|
|
314
|
+
}
|
|
315
|
+
|
|
262
316
|
#drawInspectorTypeCell(ctx, { node, rect, theme }) {
|
|
263
317
|
this.#drawMutedText(ctx, node.data?.valueType ?? '', rect.x + 10, rect.y + rect.height / 2, rect.width - 20, theme);
|
|
264
318
|
}
|
|
@@ -299,7 +353,7 @@ export class TreeRowRenderer {
|
|
|
299
353
|
ctx.fill();
|
|
300
354
|
ctx.fillStyle = theme.colors.text;
|
|
301
355
|
ctx.textAlign = 'right';
|
|
302
|
-
ctx
|
|
356
|
+
drawTruncatedText(ctx, String(data.valueText ?? ''), x + barWidth + gap + valueWidth - 6, y + 4, valueWidth - 10);
|
|
303
357
|
ctx.textAlign = 'left';
|
|
304
358
|
}
|
|
305
359
|
|
|
@@ -310,7 +364,7 @@ export class TreeRowRenderer {
|
|
|
310
364
|
ctx.fillStyle = theme.colors.text;
|
|
311
365
|
ctx.textAlign = 'center';
|
|
312
366
|
ctx.textBaseline = 'middle';
|
|
313
|
-
ctx
|
|
367
|
+
drawTruncatedText(ctx, label, x + width / 2, y + height / 2, width - 4);
|
|
314
368
|
ctx.textAlign = 'left';
|
|
315
369
|
}
|
|
316
370
|
|
|
@@ -319,7 +373,7 @@ export class TreeRowRenderer {
|
|
|
319
373
|
ctx.font = theme.font;
|
|
320
374
|
ctx.textBaseline = 'middle';
|
|
321
375
|
ctx.textAlign = 'left';
|
|
322
|
-
ctx
|
|
376
|
+
drawTruncatedText(ctx, String(text ?? ''), x, y, Math.max(10, width));
|
|
323
377
|
}
|
|
324
378
|
|
|
325
379
|
#drawTreeCell(ctx, { node, row, rect, theme, style }) {
|
|
@@ -332,7 +386,7 @@ export class TreeRowRenderer {
|
|
|
332
386
|
ctx.font = theme.font;
|
|
333
387
|
ctx.textBaseline = 'middle';
|
|
334
388
|
ctx.textAlign = 'left';
|
|
335
|
-
ctx
|
|
389
|
+
drawTruncatedText(ctx, node.label ?? node.id, x + 50, cy, Math.max(40, rect.x + rect.width - x - 56));
|
|
336
390
|
}
|
|
337
391
|
|
|
338
392
|
#drawStatusCell(ctx, { rect, style, theme }) {
|
|
@@ -346,7 +400,7 @@ export class TreeRowRenderer {
|
|
|
346
400
|
ctx.font = '10px system-ui, sans-serif';
|
|
347
401
|
ctx.textAlign = 'center';
|
|
348
402
|
ctx.textBaseline = 'middle';
|
|
349
|
-
ctx
|
|
403
|
+
drawTruncatedText(ctx, style.status.label, x + badgeWidth / 2, y + 8, badgeWidth - 8);
|
|
350
404
|
ctx.textAlign = 'left';
|
|
351
405
|
}
|
|
352
406
|
|
|
@@ -370,7 +424,7 @@ export class TreeRowRenderer {
|
|
|
370
424
|
ctx.textBaseline = 'middle';
|
|
371
425
|
ctx.textAlign = column.align;
|
|
372
426
|
const x = column.align === 'right' ? rect.x + rect.width - 10 : column.align === 'center' ? rect.x + rect.width / 2 : rect.x + 10;
|
|
373
|
-
ctx
|
|
427
|
+
drawTruncatedText(ctx, String(value ?? ''), x, rect.y + rect.height / 2, rect.width - 20);
|
|
374
428
|
ctx.textAlign = 'left';
|
|
375
429
|
}
|
|
376
430
|
|
|
@@ -421,6 +475,22 @@ function resolveNodeStyle(theme, node, state = {}) {
|
|
|
421
475
|
};
|
|
422
476
|
}
|
|
423
477
|
|
|
478
|
+
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
479
|
+
const safeWidth = Math.max(1, width);
|
|
480
|
+
const rightPadding = 14;
|
|
481
|
+
if (editorType === 'checkbox') {
|
|
482
|
+
const editorWidth = 34;
|
|
483
|
+
return { editorLeft: Math.max(64, safeWidth - rightPadding - editorWidth), editorWidth };
|
|
484
|
+
}
|
|
485
|
+
const minEditor = Math.min(170, Math.max(96, safeWidth * 0.45));
|
|
486
|
+
const minLabelEnd = Math.max(88, depth * indentWidth + 104);
|
|
487
|
+
const preferredLeft = Math.max(minLabelEnd, labelEnd, safeWidth * 0.32);
|
|
488
|
+
const maxLeft = Math.max(64, safeWidth - rightPadding - minEditor);
|
|
489
|
+
const editorLeft = Math.max(64, Math.min(preferredLeft, maxLeft));
|
|
490
|
+
const editorWidth = Math.max(56, safeWidth - rightPadding - editorLeft);
|
|
491
|
+
return { editorLeft, editorWidth };
|
|
492
|
+
}
|
|
493
|
+
|
|
424
494
|
function clamp01(value) {
|
|
425
495
|
return Math.max(0, Math.min(1, value));
|
|
426
496
|
}
|
|
@@ -438,6 +508,29 @@ function roundRect(ctx, x, y, width, height, radius) {
|
|
|
438
508
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
439
509
|
}
|
|
440
510
|
|
|
511
|
+
function drawTruncatedText(ctx, text, x, y, maxWidth) {
|
|
512
|
+
const value = String(text ?? '');
|
|
513
|
+
const width = Math.max(0, maxWidth);
|
|
514
|
+
if (!value || width <= 0) return;
|
|
515
|
+
ctx.fillText(fitText(ctx, value, width), x, y);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function fitText(ctx, text, maxWidth) {
|
|
519
|
+
if (ctx.measureText(text).width <= maxWidth) return text;
|
|
520
|
+
const ellipsis = '...';
|
|
521
|
+
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
|
522
|
+
if (ellipsisWidth > maxWidth) return '';
|
|
523
|
+
let low = 0;
|
|
524
|
+
let high = text.length;
|
|
525
|
+
while (low < high) {
|
|
526
|
+
const mid = Math.ceil((low + high) / 2);
|
|
527
|
+
const candidate = text.slice(0, mid);
|
|
528
|
+
if (ctx.measureText(candidate).width + ellipsisWidth <= maxWidth) low = mid;
|
|
529
|
+
else high = mid - 1;
|
|
530
|
+
}
|
|
531
|
+
return `${text.slice(0, low)}${ellipsis}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
441
534
|
function formatTime(value) {
|
|
442
535
|
return new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
443
536
|
}
|
|
@@ -86,17 +86,21 @@ export class TreeViewController {
|
|
|
86
86
|
setModel(model, meta = {}, options = {}) {
|
|
87
87
|
const builder = new ModelInspectorBuilder();
|
|
88
88
|
const presentation = options.presentation ?? options.mode ?? 'table';
|
|
89
|
+
const previousExpanded = new Set(this.model.expanded);
|
|
90
|
+
const shouldPreserveExpansion = Boolean(this.inspector);
|
|
89
91
|
const inspectorOptions = {
|
|
90
92
|
presentation,
|
|
91
93
|
flatRoot: Boolean(options.flatRoot),
|
|
92
94
|
enforceMeta: Boolean(options.enforceMeta),
|
|
93
95
|
filter: Boolean(options.filter),
|
|
96
|
+
markUpdated: options.markUpdated !== false,
|
|
94
97
|
};
|
|
95
98
|
const nodes = builder.build(model, meta, inspectorOptions);
|
|
96
99
|
this.inspector = { model, meta, builder, presentation, options: inspectorOptions };
|
|
97
100
|
this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
|
|
98
101
|
this.model.setTree(nodes);
|
|
99
|
-
this
|
|
102
|
+
if (shouldPreserveExpansion) this.#restoreExpansion(previousExpanded);
|
|
103
|
+
else this.expansion.expandToDepth(this.initialExpandDepth);
|
|
100
104
|
this.searchIndex.rebuild(this.model);
|
|
101
105
|
this.#rebuildRows();
|
|
102
106
|
this.events.emit('modelchange', { model, meta, structural: true });
|
|
@@ -113,7 +117,9 @@ export class TreeViewController {
|
|
|
113
117
|
data.value = newValue;
|
|
114
118
|
data.valueType = Array.isArray(newValue) ? 'array' : newValue === null ? 'null' : typeof newValue === 'object' ? 'object' : typeof newValue;
|
|
115
119
|
data.valueText = formatInspectorValue(newValue, data.meta);
|
|
116
|
-
this.
|
|
120
|
+
if (this.inspector.options.markUpdated !== false) {
|
|
121
|
+
this.setDynamicState([{ id: nodeId, state: { updated: true } }]);
|
|
122
|
+
}
|
|
117
123
|
const detail = { path: data.path, oldValue, newValue, nodeId, editorType };
|
|
118
124
|
this.events.emit('valuechange', detail);
|
|
119
125
|
this.events.emit('modelchange', { model: this.inspector.model, path: data.path, oldValue, newValue, nodeId });
|
|
@@ -275,6 +281,11 @@ export class TreeViewController {
|
|
|
275
281
|
}
|
|
276
282
|
|
|
277
283
|
expand(nodeId) {
|
|
284
|
+
if (this.filterQuery && this.rowModel.expandFilterBranch(nodeId)) {
|
|
285
|
+
this.#rebuildRows();
|
|
286
|
+
this.events.emit('expand', { nodeId, filter: true });
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
278
289
|
if (!this.expansion.expand(nodeId)) return false;
|
|
279
290
|
this.#rebuildRows();
|
|
280
291
|
this.events.emit('expand', { nodeId });
|
|
@@ -282,14 +293,19 @@ export class TreeViewController {
|
|
|
282
293
|
}
|
|
283
294
|
|
|
284
295
|
collapse(nodeId) {
|
|
285
|
-
|
|
296
|
+
const row = this.rowModel.getRowById(nodeId);
|
|
297
|
+
const shouldCollapseFilterBranch = Boolean(this.filterQuery && row?.expanded);
|
|
298
|
+
const changed = this.expansion.collapse(nodeId);
|
|
299
|
+
if (shouldCollapseFilterBranch) this.rowModel.collapseFilterBranch(nodeId);
|
|
300
|
+
if (!changed && !shouldCollapseFilterBranch) return false;
|
|
286
301
|
this.#rebuildRows();
|
|
287
|
-
this.events.emit('collapse', { nodeId });
|
|
302
|
+
this.events.emit('collapse', { nodeId, filter: shouldCollapseFilterBranch });
|
|
288
303
|
return true;
|
|
289
304
|
}
|
|
290
305
|
|
|
291
306
|
toggle(nodeId) {
|
|
292
|
-
|
|
307
|
+
const row = this.rowModel.getRowById(nodeId);
|
|
308
|
+
return (row?.expanded ?? this.expansion.isExpanded(nodeId)) ? this.collapse(nodeId) : this.expand(nodeId);
|
|
293
309
|
}
|
|
294
310
|
|
|
295
311
|
expandAll() {
|
|
@@ -573,12 +589,17 @@ export class TreeViewController {
|
|
|
573
589
|
searchMatches: this.searchHighlights,
|
|
574
590
|
sort: this.columnModel.sort,
|
|
575
591
|
sortValues: this.sortValueSnapshot ? Array.from(this.sortValueSnapshot) : null,
|
|
592
|
+
inspectorPaneLabelEnd: this.#computeInspectorPaneLabelEnd(visibleRange),
|
|
576
593
|
filterQuery: this.filterQuery,
|
|
577
594
|
headerFilter: Boolean(this.inspector?.options?.filter),
|
|
578
595
|
stats: this.getStats(),
|
|
579
596
|
};
|
|
580
597
|
}
|
|
581
598
|
|
|
599
|
+
closeEditor() {
|
|
600
|
+
this.events.emit('editorclose', {});
|
|
601
|
+
}
|
|
602
|
+
|
|
582
603
|
hitTest(clientX, clientY) {
|
|
583
604
|
const localX = clientX - (this.viewport.renderInsetX ?? 0);
|
|
584
605
|
const localY = clientY - (this.viewport.renderInsetY ?? 0);
|
|
@@ -609,7 +630,7 @@ export class TreeViewController {
|
|
|
609
630
|
if (localX >= column.width - 54 && localX <= column.width - 32) part = 'arrayAdd';
|
|
610
631
|
else if (localX >= column.width - 28 && localX <= column.width - 6) part = 'arrayRemove';
|
|
611
632
|
else {
|
|
612
|
-
const editorLeft =
|
|
633
|
+
const editorLeft = this.getInspectorPaneLayout(this.#visibleInspectorPaneWidth(column), row, node?.data?.editorType).editorLeft;
|
|
613
634
|
if (localX >= editorLeft) part = 'editor';
|
|
614
635
|
else {
|
|
615
636
|
const treeX = row.depth * this.rowModel.indentWidth;
|
|
@@ -618,8 +639,8 @@ export class TreeViewController {
|
|
|
618
639
|
}
|
|
619
640
|
return { area: 'row', part, row, column, x, y: rowY };
|
|
620
641
|
}
|
|
621
|
-
const
|
|
622
|
-
if (localX >= editorLeft) part = this.#inspectorEditorPart(row, localX - editorLeft,
|
|
642
|
+
const layout = this.getInspectorPaneLayout(this.#visibleInspectorPaneWidth(column), row, node?.data?.editorType);
|
|
643
|
+
if (localX >= layout.editorLeft) part = this.#inspectorEditorPart(row, localX - layout.editorLeft, layout.editorWidth);
|
|
623
644
|
else {
|
|
624
645
|
const treeX = row.depth * this.rowModel.indentWidth;
|
|
625
646
|
if (localX >= treeX + 4 && localX <= treeX + 22) part = 'chevron';
|
|
@@ -642,13 +663,23 @@ export class TreeViewController {
|
|
|
642
663
|
if (!data?.inspector) return 'cell';
|
|
643
664
|
if (data.editorType === 'checkbox') return 'checkbox';
|
|
644
665
|
if (data.editorType === 'range') {
|
|
645
|
-
const
|
|
666
|
+
const width = Math.max(24, editorWidth - 20);
|
|
667
|
+
const valueWidth = Math.min(64, Math.max(42, width * 0.28));
|
|
668
|
+
const numberLeft = editorWidth - valueWidth - 10;
|
|
646
669
|
return localEditorX >= numberLeft ? 'number' : 'range';
|
|
647
670
|
}
|
|
648
671
|
if (data.editorType === 'button') return 'button';
|
|
649
672
|
return 'editor';
|
|
650
673
|
}
|
|
651
674
|
|
|
675
|
+
#visibleInspectorPaneWidth(column) {
|
|
676
|
+
return Math.max(1, Math.min(column.width, this.viewport.scrollX + this.viewport.viewportWidth - column.x));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
getInspectorPaneLayout(width, row = null, editorType = '') {
|
|
680
|
+
return inspectorPaneLayout(width, row?.depth ?? 0, this.rowModel.indentWidth, editorType, this.#computeInspectorPaneLabelEnd(this.rowModel.getVisibleRange(this.viewport, 4)));
|
|
681
|
+
}
|
|
682
|
+
|
|
652
683
|
getCellClientRect(hit) {
|
|
653
684
|
if (!hit?.row || !hit.column || !this.canvas) return { x: 0, y: 0, width: 0, height: 0 };
|
|
654
685
|
const rect = this.canvas.getBoundingClientRect();
|
|
@@ -671,8 +702,57 @@ export class TreeViewController {
|
|
|
671
702
|
};
|
|
672
703
|
}
|
|
673
704
|
|
|
705
|
+
getTooltipForHit(hit) {
|
|
706
|
+
if (!hit?.row || !hit.column) return null;
|
|
707
|
+
const node = this.model.nodes[hit.row.nodeIndex];
|
|
708
|
+
if (!node) return null;
|
|
709
|
+
const state = this.model.dynamicState.get(hit.row.nodeId) ?? {};
|
|
710
|
+
const rect = this.getCellClientRect(hit);
|
|
711
|
+
let text = '';
|
|
712
|
+
let width = rect.width;
|
|
713
|
+
|
|
714
|
+
if (hit.column.kind === 'inspectorPane') {
|
|
715
|
+
const data = node.data ?? {};
|
|
716
|
+
const visibleWidth = this.#visibleInspectorPaneWidth(hit.column);
|
|
717
|
+
const layout = this.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
|
|
718
|
+
if (hit.part === 'label' || hit.part === 'chevron') {
|
|
719
|
+
const labelX = hit.row.depth * this.rowModel.indentWidth + (hit.row.hasChildren ? 28 : 24);
|
|
720
|
+
text = node.label ?? node.id;
|
|
721
|
+
width = Math.max(0, layout.editorLeft - labelX - 8);
|
|
722
|
+
} else if (hit.part === 'arrayAdd' || hit.part === 'arrayRemove') {
|
|
723
|
+
return null;
|
|
724
|
+
} else {
|
|
725
|
+
text = inspectorTooltipValue(node);
|
|
726
|
+
width = Math.max(0, layout.editorWidth - 20);
|
|
727
|
+
}
|
|
728
|
+
} else if (hit.column.kind === 'tree') {
|
|
729
|
+
const labelX = hit.row.depth * this.rowModel.indentWidth + 50;
|
|
730
|
+
text = node.label ?? node.id;
|
|
731
|
+
width = Math.max(0, hit.column.width - labelX - 6);
|
|
732
|
+
} else if (hit.column.kind === 'inspectorValue') {
|
|
733
|
+
text = inspectorTooltipValue(node);
|
|
734
|
+
width = Math.max(0, hit.column.width - 20);
|
|
735
|
+
} else if (hit.column.kind === 'inspectorType') {
|
|
736
|
+
text = node.data?.valueType ?? '';
|
|
737
|
+
width = Math.max(0, hit.column.width - 20);
|
|
738
|
+
} else if (hit.column.kind === 'inspectorDescription') {
|
|
739
|
+
text = node.data?.meta?.description ?? '';
|
|
740
|
+
width = Math.max(0, hit.column.width - 20);
|
|
741
|
+
} else if (typeof hit.column.value === 'function') {
|
|
742
|
+
const value = hit.column.value(node, state);
|
|
743
|
+
text = value == null ? '' : String(value);
|
|
744
|
+
width = Math.max(0, hit.column.width - 20);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
text = String(text ?? '');
|
|
748
|
+
if (!text || !isProbablyTruncated(text, width)) return null;
|
|
749
|
+
return { text, rect, nodeId: node.id, part: hit.part, columnId: hit.column.id };
|
|
750
|
+
}
|
|
751
|
+
|
|
674
752
|
resize(width, height) {
|
|
675
753
|
this.viewport.resize(width, height);
|
|
754
|
+
this.#fitInspectorPaneColumn(width);
|
|
755
|
+
this.#syncContentSize();
|
|
676
756
|
this.events.emit('viewportchange', this.getViewportState());
|
|
677
757
|
}
|
|
678
758
|
|
|
@@ -726,6 +806,13 @@ export class TreeViewController {
|
|
|
726
806
|
this.setDynamicState(this.patchBatcher.flush());
|
|
727
807
|
}
|
|
728
808
|
|
|
809
|
+
#restoreExpansion(expandedIds) {
|
|
810
|
+
this.model.expanded.clear();
|
|
811
|
+
for (const id of expandedIds) {
|
|
812
|
+
if (this.model.index.getNode(id) && this.expansion.hasChildren(id)) this.model.expanded.add(id);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
729
816
|
#focusedRowIndex() {
|
|
730
817
|
if (this.focusedId) {
|
|
731
818
|
const row = this.rowModel.getRowById(this.focusedId);
|
|
@@ -740,12 +827,41 @@ export class TreeViewController {
|
|
|
740
827
|
const height = this.canvas.clientHeight;
|
|
741
828
|
if (width > 0 && height > 0 && (this.viewport.viewportWidth !== width || this.viewport.viewportHeight !== height)) {
|
|
742
829
|
this.viewport.resize(width, height);
|
|
830
|
+
this.#fitInspectorPaneColumn(width);
|
|
831
|
+
this.#syncContentSize();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
#computeInspectorPaneLabelEnd(visibleRange) {
|
|
836
|
+
const column = this.columnModel.columns.find((item) => item.kind === 'inspectorPane');
|
|
837
|
+
if (!column) return 0;
|
|
838
|
+
let labelEnd = 0;
|
|
839
|
+
for (let i = visibleRange.first; i <= visibleRange.last; i++) {
|
|
840
|
+
const row = this.rowModel.rows[i];
|
|
841
|
+
if (!row) continue;
|
|
842
|
+
const node = this.model.nodes[row.nodeIndex];
|
|
843
|
+
if (!node) continue;
|
|
844
|
+
const indentX = row.depth * this.rowModel.indentWidth;
|
|
845
|
+
const labelX = indentX + (row.hasChildren ? 28 : 24);
|
|
846
|
+
const labelWidth = String(node.label ?? node.id ?? '').length * 6.4;
|
|
847
|
+
labelEnd = Math.max(labelEnd, labelX + labelWidth + 12);
|
|
743
848
|
}
|
|
849
|
+
return Math.min(labelEnd, Math.max(90, this.#visibleInspectorPaneWidth(column) - 72));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
#fitInspectorPaneColumn(width) {
|
|
853
|
+
const column = this.columnModel.columns.length === 1 ? this.columnModel.columns[0] : null;
|
|
854
|
+
if (column?.kind !== 'inspectorPane' && column?.kind !== 'tree') return;
|
|
855
|
+
if (column.kind === 'inspectorPane' && this.inspector?.options?.presentation !== 'pane') return;
|
|
856
|
+
const nextWidth = Math.max(column.minWidth, Math.floor(width));
|
|
857
|
+
if (Math.abs(column.width - nextWidth) < 1) return;
|
|
858
|
+
this.columnModel.resizeColumn(column.id, nextWidth);
|
|
744
859
|
}
|
|
745
860
|
|
|
746
861
|
#workerRowOptions(overrides = {}) {
|
|
747
862
|
return {
|
|
748
863
|
expandedIds: Array.from(this.expansion.model.expanded),
|
|
864
|
+
filterCollapsedIds: Array.from(this.rowModel.filterCollapsed ?? []),
|
|
749
865
|
rowHeight: this.rowModel.rowHeight,
|
|
750
866
|
indentWidth: this.rowModel.indentWidth,
|
|
751
867
|
sort: this.columnModel.sort,
|
|
@@ -777,6 +893,22 @@ export class TreeViewController {
|
|
|
777
893
|
}
|
|
778
894
|
}
|
|
779
895
|
|
|
896
|
+
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
897
|
+
const safeWidth = Math.max(1, width);
|
|
898
|
+
const rightPadding = 14;
|
|
899
|
+
if (editorType === 'checkbox') {
|
|
900
|
+
const editorWidth = 34;
|
|
901
|
+
return { editorLeft: Math.max(64, safeWidth - rightPadding - editorWidth), editorWidth };
|
|
902
|
+
}
|
|
903
|
+
const minEditor = Math.min(170, Math.max(96, safeWidth * 0.45));
|
|
904
|
+
const minLabelEnd = Math.max(88, depth * indentWidth + 104);
|
|
905
|
+
const preferredLeft = Math.max(minLabelEnd, labelEnd, safeWidth * 0.32);
|
|
906
|
+
const maxLeft = Math.max(64, safeWidth - rightPadding - minEditor);
|
|
907
|
+
const editorLeft = Math.max(64, Math.min(preferredLeft, maxLeft));
|
|
908
|
+
const editorWidth = Math.max(56, safeWidth - rightPadding - editorLeft);
|
|
909
|
+
return { editorLeft, editorWidth };
|
|
910
|
+
}
|
|
911
|
+
|
|
780
912
|
function createDefaultArrayItem(meta = {}) {
|
|
781
913
|
if (meta.itemFactory) return meta.itemFactory();
|
|
782
914
|
if (meta.itemType === 'number') return 0;
|
|
@@ -820,6 +952,19 @@ function matchesFilter(node, state, path, query) {
|
|
|
820
952
|
return values.some((value) => String(value ?? '').toLowerCase().includes(query));
|
|
821
953
|
}
|
|
822
954
|
|
|
955
|
+
function inspectorTooltipValue(node) {
|
|
956
|
+
const data = node.data ?? {};
|
|
957
|
+
if (data.editorType === 'checkbox') return '';
|
|
958
|
+
if (data.editorType === 'button') return data.meta?.button ?? node.label ?? '';
|
|
959
|
+
if (data.valueType === 'object') return '';
|
|
960
|
+
return data.valueText ?? data.value ?? '';
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function isProbablyTruncated(text, width) {
|
|
964
|
+
if (width <= 0) return Boolean(text);
|
|
965
|
+
return String(text).length * 6.4 > width;
|
|
966
|
+
}
|
|
967
|
+
|
|
823
968
|
function now() {
|
|
824
969
|
return globalThis.performance?.now?.() ?? Date.now();
|
|
825
970
|
}
|