virtual-tree-canvas 0.1.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 +6 -2
- package/src/core/icon-registry.js +249 -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 +171 -37
- package/src/tree-view-controller.js +164 -9
|
@@ -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,11 +53,13 @@ export class TreeRowRenderer {
|
|
|
47
53
|
ctx.translate(viewport.renderInsetX ?? 0, viewport.renderInsetY ?? 0);
|
|
48
54
|
this.#drawHeader(ctx);
|
|
49
55
|
this.#drawRows(ctx);
|
|
56
|
+
this.#drawScrollIndicator(ctx, time);
|
|
50
57
|
ctx.restore();
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
#drawHeader(ctx) {
|
|
54
61
|
const { viewport, columns, theme, sort, headerFilter, filterQuery } = this.scene;
|
|
62
|
+
if (viewport.headerHeight <= 0) return;
|
|
55
63
|
const colors = theme.colors;
|
|
56
64
|
ctx.save();
|
|
57
65
|
ctx.fillStyle = colors.row;
|
|
@@ -65,10 +73,10 @@ export class TreeRowRenderer {
|
|
|
65
73
|
this.#drawHeaderFilter(ctx, column, viewport, theme, filterQuery);
|
|
66
74
|
} else {
|
|
67
75
|
ctx.fillStyle = colors.textMuted;
|
|
68
|
-
ctx
|
|
76
|
+
drawTruncatedText(ctx, column.label, column.x + 10, viewport.headerHeight / 2, column.width - 20);
|
|
69
77
|
}
|
|
70
78
|
if (sort?.columnId === column.id && sort.direction) {
|
|
71
|
-
ctx
|
|
79
|
+
drawTruncatedText(ctx, sort.direction === 'asc' ? '^' : 'v', column.x + column.width - 16, viewport.headerHeight / 2, 12);
|
|
72
80
|
}
|
|
73
81
|
ctx.strokeStyle = colors.border;
|
|
74
82
|
ctx.beginPath();
|
|
@@ -89,7 +97,9 @@ export class TreeRowRenderer {
|
|
|
89
97
|
const colors = theme.colors;
|
|
90
98
|
const x = column.x + 8;
|
|
91
99
|
const y = 5;
|
|
92
|
-
const
|
|
100
|
+
const visibleRight = viewport.scrollX + viewport.viewportWidth;
|
|
101
|
+
const visibleWidth = Math.max(1, Math.min(column.x + column.width, visibleRight) - column.x);
|
|
102
|
+
const width = Math.max(40, visibleWidth - 16);
|
|
93
103
|
const height = Math.max(18, viewport.headerHeight - 10);
|
|
94
104
|
ctx.fillStyle = colors.progressTrack;
|
|
95
105
|
roundRect(ctx, x, y, width, height, 4);
|
|
@@ -97,13 +107,16 @@ export class TreeRowRenderer {
|
|
|
97
107
|
ctx.strokeStyle = colors.border;
|
|
98
108
|
ctx.stroke();
|
|
99
109
|
ctx.fillStyle = filterQuery ? colors.text : colors.textMuted;
|
|
100
|
-
ctx
|
|
110
|
+
drawTruncatedText(ctx, filterQuery || 'Filter inspector', x + 8, viewport.headerHeight / 2, width - 16);
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
#drawRows(ctx) {
|
|
104
114
|
const { rows, visibleRange, viewport } = this.scene;
|
|
105
115
|
this.renderedRows = visibleRange.count;
|
|
106
116
|
ctx.save();
|
|
117
|
+
ctx.beginPath();
|
|
118
|
+
ctx.rect(0, viewport.headerHeight, viewport.viewportWidth, viewport.rowViewportHeight);
|
|
119
|
+
ctx.clip();
|
|
107
120
|
ctx.translate(-viewport.scrollX, viewport.headerHeight - viewport.scrollY);
|
|
108
121
|
for (let i = visibleRange.first; i <= visibleRange.last; i++) {
|
|
109
122
|
const row = rows[i];
|
|
@@ -112,6 +125,56 @@ export class TreeRowRenderer {
|
|
|
112
125
|
ctx.restore();
|
|
113
126
|
}
|
|
114
127
|
|
|
128
|
+
#drawScrollIndicator(ctx, time) {
|
|
129
|
+
const { viewport, theme, rows } = this.scene;
|
|
130
|
+
const maxY = Math.max(0, viewport.contentHeight - viewport.rowViewportHeight);
|
|
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;
|
|
134
|
+
|
|
135
|
+
const trackTop = viewport.headerHeight + 4;
|
|
136
|
+
const trackHeight = Math.max(1, viewport.viewportHeight - viewport.headerHeight - 8);
|
|
137
|
+
const thumbHeight = Math.max(18, Math.min(trackHeight, trackHeight * (viewport.rowViewportHeight / viewport.contentHeight)));
|
|
138
|
+
const thumbY = trackTop + (trackHeight - thumbHeight) * (viewport.scrollY / maxY);
|
|
139
|
+
const x = Math.max(2, viewport.viewportWidth - 4);
|
|
140
|
+
|
|
141
|
+
ctx.save();
|
|
142
|
+
ctx.lineCap = 'round';
|
|
143
|
+
ctx.lineWidth = 2;
|
|
144
|
+
ctx.globalAlpha = 0.28 * alpha;
|
|
145
|
+
ctx.strokeStyle = theme.colors.guide;
|
|
146
|
+
ctx.beginPath();
|
|
147
|
+
ctx.moveTo(x, trackTop);
|
|
148
|
+
ctx.lineTo(x, trackTop + trackHeight);
|
|
149
|
+
ctx.stroke();
|
|
150
|
+
|
|
151
|
+
ctx.globalAlpha = 0.9 * alpha;
|
|
152
|
+
ctx.strokeStyle = theme.colors.focus;
|
|
153
|
+
ctx.beginPath();
|
|
154
|
+
ctx.moveTo(x, thumbY);
|
|
155
|
+
ctx.lineTo(x, thumbY + thumbHeight);
|
|
156
|
+
ctx.stroke();
|
|
157
|
+
ctx.restore();
|
|
158
|
+
}
|
|
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
|
+
|
|
115
178
|
#drawRow(ctx, row) {
|
|
116
179
|
const { columns, nodes, dynamicState, selection, hoverNodeId, focusNodeId, searchMatches, theme, viewport } = this.scene;
|
|
117
180
|
const node = nodes[row.nodeIndex];
|
|
@@ -123,10 +186,11 @@ export class TreeRowRenderer {
|
|
|
123
186
|
const hovered = hoverNodeId === row.nodeId;
|
|
124
187
|
const focused = focusNodeId === row.nodeId;
|
|
125
188
|
const y = row.y;
|
|
126
|
-
const
|
|
189
|
+
const visibleX = viewport.scrollX;
|
|
190
|
+
const visibleWidth = viewport.viewportWidth;
|
|
127
191
|
|
|
128
192
|
ctx.fillStyle = selected ? colors.rowSelected : highlighted ? colors.rowHighlighted : hovered ? colors.rowHover : colors.row;
|
|
129
|
-
ctx.fillRect(
|
|
193
|
+
ctx.fillRect(visibleX, y, visibleWidth, row.height);
|
|
130
194
|
|
|
131
195
|
this.#drawIndentGuides(ctx, row, colors);
|
|
132
196
|
|
|
@@ -142,20 +206,15 @@ export class TreeRowRenderer {
|
|
|
142
206
|
|
|
143
207
|
ctx.strokeStyle = colors.border;
|
|
144
208
|
ctx.beginPath();
|
|
145
|
-
ctx.moveTo(
|
|
146
|
-
ctx.lineTo(
|
|
209
|
+
ctx.moveTo(visibleX, y + row.height + 0.5);
|
|
210
|
+
ctx.lineTo(visibleX + visibleWidth, y + row.height + 0.5);
|
|
147
211
|
ctx.stroke();
|
|
148
212
|
|
|
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
|
-
}
|
|
213
|
+
if (state.updated && node.data?.inspector) this.#drawUpdatedMarker(ctx, row, theme);
|
|
155
214
|
|
|
156
215
|
if (focused) {
|
|
157
216
|
ctx.strokeStyle = colors.focus;
|
|
158
|
-
ctx.strokeRect(1.5, y + 2.5,
|
|
217
|
+
ctx.strokeRect(visibleX + 1.5, y + 2.5, visibleWidth - 3, row.height - 5);
|
|
159
218
|
}
|
|
160
219
|
}
|
|
161
220
|
|
|
@@ -174,40 +233,58 @@ export class TreeRowRenderer {
|
|
|
174
233
|
else this.#drawTextCell(ctx, cell);
|
|
175
234
|
}
|
|
176
235
|
|
|
177
|
-
#drawInspectorPaneCell(ctx, { node, row, rect, theme }) {
|
|
236
|
+
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style }) {
|
|
237
|
+
const visibleRight = this.scene.viewport.scrollX + this.scene.viewport.viewportWidth;
|
|
238
|
+
rect = { ...rect, width: Math.max(1, Math.min(rect.x + rect.width, visibleRight) - rect.x) };
|
|
178
239
|
const data = node.data ?? {};
|
|
179
240
|
const colors = theme.colors;
|
|
180
241
|
const indentX = rect.x + row.depth * theme.indentWidth;
|
|
181
|
-
const labelX = indentX + 28;
|
|
182
242
|
const cy = rect.y + rect.height / 2;
|
|
183
|
-
|
|
243
|
+
let labelX = indentX + 28;
|
|
244
|
+
if (!row.hasChildren && node.icon) {
|
|
245
|
+
this.iconRegistry.draw(ctx, style.icon, indentX + 3, rect.y + 6, 15, style.color);
|
|
246
|
+
labelX = indentX + 24;
|
|
247
|
+
} else if (!row.hasChildren) {
|
|
248
|
+
labelX = indentX + 24;
|
|
249
|
+
} else {
|
|
250
|
+
this.#drawChevron(ctx, indentX + 10, cy, row, colors);
|
|
251
|
+
}
|
|
184
252
|
ctx.font = theme.font;
|
|
185
253
|
ctx.textBaseline = 'middle';
|
|
186
254
|
ctx.textAlign = 'left';
|
|
187
|
-
ctx.globalAlpha = data.disabled ?
|
|
255
|
+
ctx.globalAlpha = data.disabled ? DISABLED_ALPHA : 1;
|
|
188
256
|
ctx.fillStyle = data.valueType === 'object' || data.valueType === 'array' ? colors.text : colors.textMuted;
|
|
189
|
-
ctx.fillText(node.label ?? node.id, labelX, cy, 180);
|
|
190
257
|
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
|
|
265
|
+
const layout = inspectorPaneLayout(rect.width, row.depth, theme.indentWidth, data.editorType, this.scene.inspectorPaneLabelEnd);
|
|
266
|
+
drawTruncatedText(ctx, node.label ?? node.id, labelX, cy, Math.max(20, rect.x + layout.editorLeft - labelX - 8));
|
|
267
|
+
|
|
268
|
+
const editorX = rect.x + layout.editorLeft;
|
|
269
|
+
const editorWidth = layout.editorWidth;
|
|
193
270
|
if (data.valueType === 'array') {
|
|
194
271
|
ctx.fillStyle = colors.textMuted;
|
|
195
|
-
ctx
|
|
272
|
+
drawTruncatedText(ctx, data.valueText, editorX, cy, Math.max(20, editorWidth - 58));
|
|
196
273
|
this.#drawSmallButton(ctx, rect.x + rect.width - 54, rect.y + 5, 22, rect.height - 10, '+', theme);
|
|
197
274
|
this.#drawSmallButton(ctx, rect.x + rect.width - 28, rect.y + 5, 22, rect.height - 10, '-', theme);
|
|
198
|
-
} else if (data.valueType === 'object') {
|
|
199
|
-
// Pane mode uses object rows as folders; the label is enough.
|
|
200
275
|
} else {
|
|
201
276
|
this.#drawInspectorValueCell(ctx, {
|
|
202
277
|
node,
|
|
203
278
|
rect: { x: editorX, y: rect.y, width: editorWidth, height: rect.height },
|
|
204
279
|
theme,
|
|
280
|
+
suppressUpdatedMarker: true,
|
|
205
281
|
});
|
|
206
282
|
}
|
|
283
|
+
if (data.meta?.updated) this.#drawUpdatedMarker(ctx, row, theme);
|
|
207
284
|
ctx.globalAlpha = 1;
|
|
208
285
|
}
|
|
209
286
|
|
|
210
|
-
#drawInspectorValueCell(ctx, { node, rect, theme }) {
|
|
287
|
+
#drawInspectorValueCell(ctx, { node, rect, theme, suppressUpdatedMarker = false }) {
|
|
211
288
|
const data = node.data ?? {};
|
|
212
289
|
const meta = data.meta ?? {};
|
|
213
290
|
const disabled = data.disabled;
|
|
@@ -219,7 +296,7 @@ export class TreeRowRenderer {
|
|
|
219
296
|
ctx.font = theme.font;
|
|
220
297
|
ctx.textBaseline = 'middle';
|
|
221
298
|
ctx.textAlign = 'left';
|
|
222
|
-
ctx.globalAlpha = disabled ?
|
|
299
|
+
ctx.globalAlpha = disabled ? DISABLED_ALPHA : 1;
|
|
223
300
|
|
|
224
301
|
if (data.editorType === 'checkbox') {
|
|
225
302
|
this.#drawCheckbox(ctx, x, rect.y + rect.height / 2 - 7, Boolean(data.value), theme);
|
|
@@ -237,7 +314,7 @@ export class TreeRowRenderer {
|
|
|
237
314
|
ctx.fill();
|
|
238
315
|
ctx.fillStyle = theme.colors.text;
|
|
239
316
|
ctx.textAlign = 'center';
|
|
240
|
-
ctx
|
|
317
|
+
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
318
|
ctx.textAlign = 'left';
|
|
242
319
|
} else if (data.editorType === 'select') {
|
|
243
320
|
ctx.fillStyle = theme.colors.rowHover;
|
|
@@ -245,12 +322,12 @@ export class TreeRowRenderer {
|
|
|
245
322
|
ctx.fill();
|
|
246
323
|
this.#drawMutedText(ctx, data.valueText, x + 8, rect.y + rect.height / 2, Math.min(width, 180) - 22, theme);
|
|
247
324
|
ctx.fillStyle = theme.colors.chevron;
|
|
248
|
-
ctx
|
|
325
|
+
drawTruncatedText(ctx, 'v', x + Math.min(width, 180) - 14, rect.y + rect.height / 2, 10);
|
|
249
326
|
} else {
|
|
250
327
|
this.#drawMutedText(ctx, data.valueText, x, rect.y + rect.height / 2, width, theme, readonly);
|
|
251
328
|
}
|
|
252
329
|
|
|
253
|
-
if (meta.updated) {
|
|
330
|
+
if (meta.updated && !suppressUpdatedMarker) {
|
|
254
331
|
ctx.fillStyle = theme.colors.focus;
|
|
255
332
|
ctx.beginPath();
|
|
256
333
|
ctx.arc(rect.x + rect.width - 10, rect.y + rect.height / 2, 3, 0, Math.PI * 2);
|
|
@@ -259,6 +336,15 @@ export class TreeRowRenderer {
|
|
|
259
336
|
ctx.globalAlpha = 1;
|
|
260
337
|
}
|
|
261
338
|
|
|
339
|
+
#drawUpdatedMarker(ctx, row, theme) {
|
|
340
|
+
const x = Math.max(6, row.depth * theme.indentWidth - 8);
|
|
341
|
+
const y = row.y + row.height / 2;
|
|
342
|
+
ctx.fillStyle = theme.colors.focus;
|
|
343
|
+
ctx.beginPath();
|
|
344
|
+
ctx.arc(x, y, 3, 0, Math.PI * 2);
|
|
345
|
+
ctx.fill();
|
|
346
|
+
}
|
|
347
|
+
|
|
262
348
|
#drawInspectorTypeCell(ctx, { node, rect, theme }) {
|
|
263
349
|
this.#drawMutedText(ctx, node.data?.valueType ?? '', rect.x + 10, rect.y + rect.height / 2, rect.width - 20, theme);
|
|
264
350
|
}
|
|
@@ -299,7 +385,7 @@ export class TreeRowRenderer {
|
|
|
299
385
|
ctx.fill();
|
|
300
386
|
ctx.fillStyle = theme.colors.text;
|
|
301
387
|
ctx.textAlign = 'right';
|
|
302
|
-
ctx
|
|
388
|
+
drawTruncatedText(ctx, String(data.valueText ?? ''), x + barWidth + gap + valueWidth - 6, y + 4, valueWidth - 10);
|
|
303
389
|
ctx.textAlign = 'left';
|
|
304
390
|
}
|
|
305
391
|
|
|
@@ -310,7 +396,7 @@ export class TreeRowRenderer {
|
|
|
310
396
|
ctx.fillStyle = theme.colors.text;
|
|
311
397
|
ctx.textAlign = 'center';
|
|
312
398
|
ctx.textBaseline = 'middle';
|
|
313
|
-
ctx
|
|
399
|
+
drawTruncatedText(ctx, label, x + width / 2, y + height / 2, width - 4);
|
|
314
400
|
ctx.textAlign = 'left';
|
|
315
401
|
}
|
|
316
402
|
|
|
@@ -319,7 +405,7 @@ export class TreeRowRenderer {
|
|
|
319
405
|
ctx.font = theme.font;
|
|
320
406
|
ctx.textBaseline = 'middle';
|
|
321
407
|
ctx.textAlign = 'left';
|
|
322
|
-
ctx
|
|
408
|
+
drawTruncatedText(ctx, String(text ?? ''), x, y, Math.max(10, width));
|
|
323
409
|
}
|
|
324
410
|
|
|
325
411
|
#drawTreeCell(ctx, { node, row, rect, theme, style }) {
|
|
@@ -332,7 +418,7 @@ export class TreeRowRenderer {
|
|
|
332
418
|
ctx.font = theme.font;
|
|
333
419
|
ctx.textBaseline = 'middle';
|
|
334
420
|
ctx.textAlign = 'left';
|
|
335
|
-
ctx
|
|
421
|
+
drawTruncatedText(ctx, node.label ?? node.id, x + 50, cy, Math.max(40, rect.x + rect.width - x - 56));
|
|
336
422
|
}
|
|
337
423
|
|
|
338
424
|
#drawStatusCell(ctx, { rect, style, theme }) {
|
|
@@ -346,7 +432,7 @@ export class TreeRowRenderer {
|
|
|
346
432
|
ctx.font = '10px system-ui, sans-serif';
|
|
347
433
|
ctx.textAlign = 'center';
|
|
348
434
|
ctx.textBaseline = 'middle';
|
|
349
|
-
ctx
|
|
435
|
+
drawTruncatedText(ctx, style.status.label, x + badgeWidth / 2, y + 8, badgeWidth - 8);
|
|
350
436
|
ctx.textAlign = 'left';
|
|
351
437
|
}
|
|
352
438
|
|
|
@@ -370,7 +456,7 @@ export class TreeRowRenderer {
|
|
|
370
456
|
ctx.textBaseline = 'middle';
|
|
371
457
|
ctx.textAlign = column.align;
|
|
372
458
|
const x = column.align === 'right' ? rect.x + rect.width - 10 : column.align === 'center' ? rect.x + rect.width / 2 : rect.x + 10;
|
|
373
|
-
ctx
|
|
459
|
+
drawTruncatedText(ctx, String(value ?? ''), x, rect.y + rect.height / 2, rect.width - 20);
|
|
374
460
|
ctx.textAlign = 'left';
|
|
375
461
|
}
|
|
376
462
|
|
|
@@ -421,6 +507,31 @@ function resolveNodeStyle(theme, node, state = {}) {
|
|
|
421
507
|
};
|
|
422
508
|
}
|
|
423
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
|
+
|
|
519
|
+
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
520
|
+
const safeWidth = Math.max(1, width);
|
|
521
|
+
const rightPadding = 14;
|
|
522
|
+
if (editorType === 'checkbox') {
|
|
523
|
+
const editorWidth = 34;
|
|
524
|
+
return { editorLeft: Math.max(64, safeWidth - rightPadding - editorWidth), editorWidth };
|
|
525
|
+
}
|
|
526
|
+
const minEditor = Math.min(170, Math.max(96, safeWidth * 0.45));
|
|
527
|
+
const minLabelEnd = Math.max(88, depth * indentWidth + 104);
|
|
528
|
+
const preferredLeft = Math.max(minLabelEnd, labelEnd, safeWidth * 0.32);
|
|
529
|
+
const maxLeft = Math.max(64, safeWidth - rightPadding - minEditor);
|
|
530
|
+
const editorLeft = Math.max(64, Math.min(preferredLeft, maxLeft));
|
|
531
|
+
const editorWidth = Math.max(56, safeWidth - rightPadding - editorLeft);
|
|
532
|
+
return { editorLeft, editorWidth };
|
|
533
|
+
}
|
|
534
|
+
|
|
424
535
|
function clamp01(value) {
|
|
425
536
|
return Math.max(0, Math.min(1, value));
|
|
426
537
|
}
|
|
@@ -438,6 +549,29 @@ function roundRect(ctx, x, y, width, height, radius) {
|
|
|
438
549
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
439
550
|
}
|
|
440
551
|
|
|
552
|
+
function drawTruncatedText(ctx, text, x, y, maxWidth) {
|
|
553
|
+
const value = String(text ?? '');
|
|
554
|
+
const width = Math.max(0, maxWidth);
|
|
555
|
+
if (!value || width <= 0) return;
|
|
556
|
+
ctx.fillText(fitText(ctx, value, width), x, y);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function fitText(ctx, text, maxWidth) {
|
|
560
|
+
if (ctx.measureText(text).width <= maxWidth) return text;
|
|
561
|
+
const ellipsis = '...';
|
|
562
|
+
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
|
563
|
+
if (ellipsisWidth > maxWidth) return '';
|
|
564
|
+
let low = 0;
|
|
565
|
+
let high = text.length;
|
|
566
|
+
while (low < high) {
|
|
567
|
+
const mid = Math.ceil((low + high) / 2);
|
|
568
|
+
const candidate = text.slice(0, mid);
|
|
569
|
+
if (ctx.measureText(candidate).width + ellipsisWidth <= maxWidth) low = mid;
|
|
570
|
+
else high = mid - 1;
|
|
571
|
+
}
|
|
572
|
+
return `${text.slice(0, low)}${ellipsis}`;
|
|
573
|
+
}
|
|
574
|
+
|
|
441
575
|
function formatTime(value) {
|
|
442
576
|
return new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
443
577
|
}
|