virtual-tree-canvas 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -110,19 +110,86 @@ function drawFolder(ctx, x, y, size, color) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function drawAircraft(ctx, x, y, size, color) {
|
|
113
|
+
const cx = x + size * 0.5;
|
|
114
|
+
ctx.save();
|
|
115
|
+
ctx.strokeStyle = color;
|
|
113
116
|
ctx.fillStyle = color;
|
|
117
|
+
ctx.lineWidth = 1.35;
|
|
118
|
+
ctx.lineCap = 'round';
|
|
119
|
+
ctx.lineJoin = 'round';
|
|
120
|
+
|
|
114
121
|
ctx.beginPath();
|
|
115
|
-
ctx.moveTo(
|
|
116
|
-
ctx.lineTo(
|
|
117
|
-
ctx.lineTo(
|
|
118
|
-
ctx.lineTo(
|
|
119
|
-
ctx.lineTo(
|
|
120
|
-
ctx.lineTo(x + size * 0.46, y + size - 1);
|
|
121
|
-
ctx.lineTo(x + size * 0.42, y + size * 0.78);
|
|
122
|
-
ctx.lineTo(x + 1, y + size * 0.72);
|
|
123
|
-
ctx.lineTo(x + size * 0.38, y + size * 0.58);
|
|
122
|
+
ctx.moveTo(cx, y + 1.5);
|
|
123
|
+
ctx.lineTo(cx + size * 0.08, y + size * 0.56);
|
|
124
|
+
ctx.lineTo(cx + size * 0.05, y + size - 2);
|
|
125
|
+
ctx.lineTo(cx - size * 0.05, y + size - 2);
|
|
126
|
+
ctx.lineTo(cx - size * 0.08, y + size * 0.56);
|
|
124
127
|
ctx.closePath();
|
|
125
128
|
ctx.fill();
|
|
129
|
+
|
|
130
|
+
ctx.beginPath();
|
|
131
|
+
ctx.moveTo(cx - size * 0.08, y + size * 0.54);
|
|
132
|
+
ctx.lineTo(x + 1.5, y + size * 0.76);
|
|
133
|
+
ctx.lineTo(cx - size * 0.03, y + size * 0.68);
|
|
134
|
+
ctx.lineTo(cx + size * 0.03, y + size * 0.68);
|
|
135
|
+
ctx.lineTo(x + size - 1.5, y + size * 0.76);
|
|
136
|
+
ctx.lineTo(cx + size * 0.08, y + size * 0.54);
|
|
137
|
+
ctx.stroke();
|
|
138
|
+
|
|
139
|
+
ctx.beginPath();
|
|
140
|
+
ctx.moveTo(cx - size * 0.05, y + size * 0.82);
|
|
141
|
+
ctx.lineTo(x + size * 0.25, y + size - 1.5);
|
|
142
|
+
ctx.moveTo(cx + size * 0.05, y + size * 0.82);
|
|
143
|
+
ctx.lineTo(x + size * 0.75, y + size - 1.5);
|
|
144
|
+
ctx.stroke();
|
|
145
|
+
ctx.restore();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function drawTask(ctx, x, y, size, color) {
|
|
149
|
+
ctx.save();
|
|
150
|
+
ctx.strokeStyle = color;
|
|
151
|
+
ctx.fillStyle = color;
|
|
152
|
+
ctx.lineWidth = 1.35;
|
|
153
|
+
ctx.lineCap = 'round';
|
|
154
|
+
ctx.lineJoin = 'round';
|
|
155
|
+
|
|
156
|
+
roundIconRect(ctx, x + 3.5, y + 2.5, size - 7, size - 5, 2);
|
|
157
|
+
ctx.stroke();
|
|
158
|
+
ctx.beginPath();
|
|
159
|
+
ctx.moveTo(x + size * 0.4, y + 2.5);
|
|
160
|
+
ctx.lineTo(x + size * 0.6, y + 2.5);
|
|
161
|
+
ctx.stroke();
|
|
162
|
+
|
|
163
|
+
ctx.beginPath();
|
|
164
|
+
ctx.moveTo(x + 5.2, y + size * 0.48);
|
|
165
|
+
ctx.lineTo(x + size * 0.38, y + size * 0.63);
|
|
166
|
+
ctx.lineTo(x + size * 0.6, y + size * 0.38);
|
|
167
|
+
ctx.stroke();
|
|
168
|
+
|
|
169
|
+
ctx.beginPath();
|
|
170
|
+
ctx.moveTo(x + size * 0.62, y + size * 0.6);
|
|
171
|
+
ctx.lineTo(x + size - 4.5, y + size * 0.6);
|
|
172
|
+
ctx.moveTo(x + 5, y + size * 0.78);
|
|
173
|
+
ctx.lineTo(x + size - 4.5, y + size * 0.78);
|
|
174
|
+
ctx.stroke();
|
|
175
|
+
ctx.restore();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function roundIconRect(ctx, x, y, width, height, radius) {
|
|
179
|
+
ctx.beginPath();
|
|
180
|
+
if (typeof ctx.roundRect === 'function') {
|
|
181
|
+
ctx.roundRect(x, y, width, height, radius);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
ctx.moveTo(x + radius, y);
|
|
185
|
+
ctx.lineTo(x + width - radius, y);
|
|
186
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
187
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
188
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
189
|
+
ctx.lineTo(x + radius, y + height);
|
|
190
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
191
|
+
ctx.lineTo(x, y + radius);
|
|
192
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
126
193
|
}
|
|
127
194
|
|
|
128
195
|
function drawRadar(ctx, x, y, size, color) {
|
|
@@ -163,17 +230,6 @@ function drawError(ctx, x, y, size, color) {
|
|
|
163
230
|
ctx.stroke();
|
|
164
231
|
}
|
|
165
232
|
|
|
166
|
-
function drawTask(ctx, x, y, size, color) {
|
|
167
|
-
ctx.strokeStyle = color;
|
|
168
|
-
ctx.lineWidth = 1.5;
|
|
169
|
-
ctx.strokeRect(x + 3, y + 2, size - 6, size - 4);
|
|
170
|
-
ctx.beginPath();
|
|
171
|
-
ctx.moveTo(x + 5, y + size * 0.52);
|
|
172
|
-
ctx.lineTo(x + size * 0.42, y + size - 5);
|
|
173
|
-
ctx.lineTo(x + size - 5, y + 5);
|
|
174
|
-
ctx.stroke();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
233
|
function drawBus(ctx, x, y, size, color) {
|
|
178
234
|
const left = x + size * 0.22;
|
|
179
235
|
const right = x + size * 0.82;
|
|
@@ -4,20 +4,32 @@ export const darkTheme = {
|
|
|
4
4
|
font: '12px Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
5
5
|
monoFont: '12px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace',
|
|
6
6
|
colors: {
|
|
7
|
-
background: '#
|
|
8
|
-
row: '#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
background: '#0a0f1c',
|
|
8
|
+
row: '#0a0f1c',
|
|
9
|
+
rowAlt: '#0c1322',
|
|
10
|
+
rowGroup: '#101827',
|
|
11
|
+
rowHover: '#111b2d',
|
|
12
|
+
rowActive: '#15243a',
|
|
13
|
+
rowSelected: '#16345f',
|
|
14
|
+
rowHighlighted: '#27144a',
|
|
15
|
+
rowFocus: '#38bdf8',
|
|
16
|
+
text: '#e8edf4',
|
|
17
|
+
textStrong: '#f8fafc',
|
|
18
|
+
textMuted: '#8ea0b5',
|
|
19
|
+
textSubtle: '#617086',
|
|
20
|
+
guide: '#1b2637',
|
|
21
|
+
chevron: '#8ea0b5',
|
|
16
22
|
focus: '#38bdf8',
|
|
17
|
-
progressTrack: '#
|
|
18
|
-
progressFill: '#
|
|
23
|
+
progressTrack: '#182235',
|
|
24
|
+
progressFill: '#3dd889',
|
|
25
|
+
control: '#121b2b',
|
|
26
|
+
controlHover: '#17263a',
|
|
27
|
+
controlActive: '#1c3350',
|
|
28
|
+
badgeFill: '#132132',
|
|
19
29
|
badgeText: '#ffffff',
|
|
20
|
-
border: '#
|
|
30
|
+
border: '#1b2637',
|
|
31
|
+
borderStrong: '#2b3b52',
|
|
32
|
+
shadow: 'rgba(0,0,0,.24)',
|
|
21
33
|
},
|
|
22
34
|
types: {
|
|
23
35
|
root: { icon: 'folder', color: '#38bdf8' },
|
|
@@ -58,18 +70,30 @@ export const lightTheme = {
|
|
|
58
70
|
colors: {
|
|
59
71
|
background: '#f8fafc',
|
|
60
72
|
row: '#ffffff',
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
rowAlt: '#f8fafc',
|
|
74
|
+
rowGroup: '#f1f5f9',
|
|
75
|
+
rowHover: '#edf2f7',
|
|
76
|
+
rowActive: '#e0f2fe',
|
|
77
|
+
rowSelected: '#dbeafe',
|
|
63
78
|
rowHighlighted: '#f3e8ff',
|
|
79
|
+
rowFocus: '#0284c7',
|
|
64
80
|
text: '#0f172a',
|
|
81
|
+
textStrong: '#020617',
|
|
65
82
|
textMuted: '#64748b',
|
|
83
|
+
textSubtle: '#94a3b8',
|
|
66
84
|
guide: '#cbd5e1',
|
|
67
85
|
chevron: '#64748b',
|
|
68
86
|
focus: '#0284c7',
|
|
69
87
|
progressTrack: '#e2e8f0',
|
|
70
88
|
progressFill: '#16a34a',
|
|
89
|
+
control: '#ffffff',
|
|
90
|
+
controlHover: '#f1f5f9',
|
|
91
|
+
controlActive: '#e0f2fe',
|
|
71
92
|
badgeText: '#ffffff',
|
|
93
|
+
badgeFill: '#e2e8f0',
|
|
72
94
|
border: '#e2e8f0',
|
|
95
|
+
borderStrong: '#cbd5e1',
|
|
96
|
+
shadow: 'rgba(15,23,42,.1)',
|
|
73
97
|
},
|
|
74
98
|
};
|
|
75
99
|
|
|
@@ -78,18 +102,27 @@ export const tacticalTheme = {
|
|
|
78
102
|
colors: {
|
|
79
103
|
background: '#050806',
|
|
80
104
|
row: '#050806',
|
|
105
|
+
rowAlt: '#07100a',
|
|
106
|
+
rowGroup: '#0a1710',
|
|
81
107
|
rowHover: '#0d1f13',
|
|
108
|
+
rowActive: '#102c1a',
|
|
82
109
|
rowSelected: '#12351f',
|
|
83
110
|
rowHighlighted: '#372b0a',
|
|
111
|
+
rowFocus: '#7ddc92',
|
|
84
112
|
text: '#d7ffe3',
|
|
113
|
+
textStrong: '#edfff2',
|
|
85
114
|
textMuted: '#7fa88c',
|
|
115
|
+
textSubtle: '#5f806a',
|
|
86
116
|
guide: '#183321',
|
|
87
117
|
chevron: '#7fa88c',
|
|
88
118
|
focus: '#7ddc92',
|
|
89
119
|
progressTrack: '#102217',
|
|
90
120
|
progressFill: '#45d483',
|
|
91
121
|
badgeText: '#031006',
|
|
122
|
+
badgeFill: '#102217',
|
|
92
123
|
border: '#183321',
|
|
124
|
+
borderStrong: '#295337',
|
|
125
|
+
shadow: 'rgba(0,0,0,.3)',
|
|
93
126
|
},
|
|
94
127
|
types: {
|
|
95
128
|
...darkTheme.types,
|
package/src/index.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export class TreeViewController {
|
|
|
71
71
|
rowModel: any;
|
|
72
72
|
expansion: any;
|
|
73
73
|
selection: any;
|
|
74
|
-
constructor(options?: Record<string, any>);
|
|
74
|
+
constructor(options?: Record<string, any> & { nativeScrollbars?: boolean });
|
|
75
75
|
on(type: string, listener: (event: any) => void): any;
|
|
76
76
|
off(type: string, listener: (event: any) => void): void;
|
|
77
77
|
setData(nodes: TreeNode[]): void;
|
|
@@ -89,6 +89,7 @@ export class TreeViewController {
|
|
|
89
89
|
clearFilter(): void;
|
|
90
90
|
focusNode(nodeId: string, options?: Record<string, any>): boolean;
|
|
91
91
|
scrollToNode(nodeId: string, align?: TreeViewAlign): boolean;
|
|
92
|
+
scrollTo(x: number, y: number): void;
|
|
92
93
|
getSelection(): string[];
|
|
93
94
|
setSelection(ids: string[]): void;
|
|
94
95
|
clearSelection(): void;
|
|
@@ -101,7 +101,10 @@ export class CellEditorManager {
|
|
|
101
101
|
padding: data.editorType === 'color' ? '0 2px' : data.editorType === 'select' ? '0 22px 0 8px' : '0 8px',
|
|
102
102
|
textAlign: data.editorType === 'number' || data.editorType === 'range' ? 'right' : 'left',
|
|
103
103
|
});
|
|
104
|
+
let committed = false;
|
|
104
105
|
const commit = () => {
|
|
106
|
+
if (committed) return;
|
|
107
|
+
committed = true;
|
|
105
108
|
const nextValue = parseEditorValue(element, data);
|
|
106
109
|
this.controller.updateInspectorValue(node.id, nextValue, data.editorType);
|
|
107
110
|
this.#removeOverlay();
|
|
@@ -110,6 +113,9 @@ export class CellEditorManager {
|
|
|
110
113
|
if (event.key === 'Enter') commit();
|
|
111
114
|
else if (event.key === 'Escape') this.#removeOverlay();
|
|
112
115
|
});
|
|
116
|
+
if (data.editorType === 'select') {
|
|
117
|
+
element.addEventListener('change', commit);
|
|
118
|
+
}
|
|
113
119
|
element.addEventListener('blur', commit);
|
|
114
120
|
ensureOverlayHost(this.host);
|
|
115
121
|
this.host.append(element);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { IconRegistry } from '../core/icon-registry.js';
|
|
2
2
|
|
|
3
3
|
const DISABLED_ALPHA = 0.72;
|
|
4
|
-
const SCROLL_INDICATOR_HOLD_MS = 1600;
|
|
5
|
-
const SCROLL_INDICATOR_FADE_MS = 700;
|
|
6
4
|
|
|
7
5
|
export class TreeRowRenderer {
|
|
8
6
|
constructor({ iconRegistry } = {}) {
|
|
@@ -11,8 +9,6 @@ export class TreeRowRenderer {
|
|
|
11
9
|
this.scene = null;
|
|
12
10
|
this.iconRegistry = iconRegistry ?? new IconRegistry();
|
|
13
11
|
this.renderedRows = 0;
|
|
14
|
-
this.scrollIndicatorState = null;
|
|
15
|
-
this.scrollIndicatorVisibleUntil = 0;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
/** @param {HTMLCanvasElement} canvas */
|
|
@@ -55,7 +51,6 @@ export class TreeRowRenderer {
|
|
|
55
51
|
ctx.translate(viewport.renderInsetX ?? 0, viewport.renderInsetY ?? 0);
|
|
56
52
|
this.#drawHeader(ctx);
|
|
57
53
|
this.#drawRows(ctx);
|
|
58
|
-
this.#drawScrollIndicator(ctx, time);
|
|
59
54
|
ctx.restore();
|
|
60
55
|
}
|
|
61
56
|
|
|
@@ -143,56 +138,6 @@ export class TreeRowRenderer {
|
|
|
143
138
|
ctx.restore();
|
|
144
139
|
}
|
|
145
140
|
|
|
146
|
-
#drawScrollIndicator(ctx, time) {
|
|
147
|
-
const { viewport, theme, rows } = this.scene;
|
|
148
|
-
const maxY = Math.max(0, viewport.contentHeight - viewport.rowViewportHeight);
|
|
149
|
-
if (maxY <= 0 || viewport.viewportHeight <= viewport.headerHeight + 12) return;
|
|
150
|
-
const alpha = this.#scrollIndicatorAlpha(viewport, rows?.length ?? 0, time);
|
|
151
|
-
if (alpha <= 0) return;
|
|
152
|
-
|
|
153
|
-
const trackTop = viewport.headerHeight + 4;
|
|
154
|
-
const trackHeight = Math.max(1, viewport.viewportHeight - viewport.headerHeight - 8);
|
|
155
|
-
const thumbHeight = Math.max(18, Math.min(trackHeight, trackHeight * (viewport.rowViewportHeight / viewport.contentHeight)));
|
|
156
|
-
const thumbY = trackTop + (trackHeight - thumbHeight) * (viewport.scrollY / maxY);
|
|
157
|
-
const x = Math.max(2, viewport.viewportWidth - 4);
|
|
158
|
-
|
|
159
|
-
ctx.save();
|
|
160
|
-
ctx.lineCap = 'round';
|
|
161
|
-
ctx.lineWidth = 2;
|
|
162
|
-
ctx.globalAlpha = 0.28 * alpha;
|
|
163
|
-
ctx.strokeStyle = theme.colors.guide;
|
|
164
|
-
ctx.beginPath();
|
|
165
|
-
ctx.moveTo(x, trackTop);
|
|
166
|
-
ctx.lineTo(x, trackTop + trackHeight);
|
|
167
|
-
ctx.stroke();
|
|
168
|
-
|
|
169
|
-
ctx.globalAlpha = 0.9 * alpha;
|
|
170
|
-
ctx.strokeStyle = theme.colors.focus;
|
|
171
|
-
ctx.beginPath();
|
|
172
|
-
ctx.moveTo(x, thumbY);
|
|
173
|
-
ctx.lineTo(x, thumbY + thumbHeight);
|
|
174
|
-
ctx.stroke();
|
|
175
|
-
ctx.restore();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
#scrollIndicatorAlpha(viewport, rowCount, time) {
|
|
179
|
-
const nextState = {
|
|
180
|
-
scrollY: viewport.scrollY,
|
|
181
|
-
contentHeight: viewport.contentHeight,
|
|
182
|
-
rowViewportHeight: viewport.rowViewportHeight,
|
|
183
|
-
viewportHeight: viewport.viewportHeight,
|
|
184
|
-
rowCount,
|
|
185
|
-
};
|
|
186
|
-
if (!sameScrollIndicatorState(this.scrollIndicatorState, nextState)) {
|
|
187
|
-
this.scrollIndicatorState = nextState;
|
|
188
|
-
this.scrollIndicatorVisibleUntil = time + SCROLL_INDICATOR_HOLD_MS;
|
|
189
|
-
return 1;
|
|
190
|
-
}
|
|
191
|
-
if (time <= this.scrollIndicatorVisibleUntil) return 1;
|
|
192
|
-
const fade = 1 - ((time - this.scrollIndicatorVisibleUntil) / SCROLL_INDICATOR_FADE_MS);
|
|
193
|
-
return clamp01(fade);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
141
|
#drawRow(ctx, row) {
|
|
197
142
|
const { columns, nodes, dynamicState, selection, hoverNodeId, hoverPart, activeNodeId, activePart, focusNodeId, searchMatches, theme, viewport } = this.scene;
|
|
198
143
|
const node = nodes[row.nodeIndex];
|
|
@@ -421,10 +366,7 @@ export class TreeRowRenderer {
|
|
|
421
366
|
const valueWidth = Math.min(64, Math.max(42, width * 0.28));
|
|
422
367
|
const gap = 8;
|
|
423
368
|
const barWidth = Math.max(24, width - valueWidth - gap);
|
|
424
|
-
ctx.
|
|
425
|
-
ctx.fillRect(x, y, barWidth, 8);
|
|
426
|
-
ctx.fillStyle = theme.colors.progressFill;
|
|
427
|
-
ctx.fillRect(x, y, barWidth * ratio, 8);
|
|
369
|
+
this.#drawMeterBar(ctx, x, y + 0.5, barWidth, 7, ratio, theme.colors.progressFill, theme);
|
|
428
370
|
this.#drawControlSurface(ctx, x + barWidth + gap, y - 6, valueWidth, 20, theme, {
|
|
429
371
|
hovered: Boolean(state.hoveredNumber),
|
|
430
372
|
active: Boolean(state.activeNumber),
|
|
@@ -495,27 +437,53 @@ export class TreeRowRenderer {
|
|
|
495
437
|
#drawStatusCell(ctx, { rect, style, theme }) {
|
|
496
438
|
const badgeWidth = Math.min(58, rect.width - 12);
|
|
497
439
|
const x = rect.x + (rect.width - badgeWidth) / 2;
|
|
498
|
-
const y = rect.y + (rect.height -
|
|
499
|
-
|
|
500
|
-
|
|
440
|
+
const y = rect.y + (rect.height - 17) / 2;
|
|
441
|
+
const statusColor = style.status.color;
|
|
442
|
+
const baseFill = theme.colors.badgeFill ?? theme.colors.progressTrack;
|
|
443
|
+
ctx.fillStyle = mixColor(baseFill, statusColor, 0.18);
|
|
444
|
+
roundRect(ctx, x, y, badgeWidth, 17, 8);
|
|
501
445
|
ctx.fill();
|
|
502
|
-
ctx.
|
|
446
|
+
ctx.strokeStyle = mixColor(theme.colors.border, statusColor, 0.42);
|
|
447
|
+
ctx.stroke();
|
|
448
|
+
ctx.fillStyle = mixColor(theme.colors.text, statusColor, 0.36);
|
|
503
449
|
ctx.font = '10px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace';
|
|
504
450
|
ctx.textAlign = 'center';
|
|
505
451
|
ctx.textBaseline = 'middle';
|
|
506
|
-
drawTruncatedText(ctx, style.status.label, x + badgeWidth / 2, y + 8, badgeWidth - 8);
|
|
452
|
+
drawTruncatedText(ctx, style.status.label, x + badgeWidth / 2, y + 8.5, badgeWidth - 8);
|
|
507
453
|
ctx.textAlign = 'left';
|
|
508
454
|
}
|
|
509
455
|
|
|
510
456
|
#drawProgressCell(ctx, { state, rect, theme, style }) {
|
|
511
457
|
if (state.progress === undefined) return this.#drawTextCell(ctx, { state, rect, theme, style, column: { align: 'right', value: () => '' }, node: {} });
|
|
458
|
+
const ratio = clamp01(state.progress);
|
|
512
459
|
const barX = rect.x + 10;
|
|
513
|
-
const
|
|
460
|
+
const barHeight = 7;
|
|
461
|
+
const barY = rect.y + (rect.height - barHeight) / 2;
|
|
514
462
|
const barWidth = Math.max(20, rect.width - 20);
|
|
463
|
+
const fillColor = state.color ?? theme.colors.progressFill;
|
|
464
|
+
this.#drawMeterBar(ctx, barX, barY, barWidth, barHeight, ratio, fillColor, theme);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#drawMeterBar(ctx, x, y, width, height, ratio, fillColor, theme) {
|
|
468
|
+
const radius = height / 2;
|
|
515
469
|
ctx.fillStyle = theme.colors.progressTrack;
|
|
516
|
-
ctx
|
|
517
|
-
ctx.
|
|
518
|
-
ctx.
|
|
470
|
+
roundRect(ctx, x, y, width, height, radius);
|
|
471
|
+
ctx.fill();
|
|
472
|
+
ctx.strokeStyle = mixColor(theme.colors.border, fillColor, 0.22);
|
|
473
|
+
ctx.stroke();
|
|
474
|
+
|
|
475
|
+
const fillWidth = width * clamp01(ratio);
|
|
476
|
+
if (fillWidth <= 0) return;
|
|
477
|
+
ctx.save();
|
|
478
|
+
roundRect(ctx, x, y, width, height, radius);
|
|
479
|
+
ctx.clip();
|
|
480
|
+
ctx.fillStyle = fillColor;
|
|
481
|
+
roundRect(ctx, x, y, Math.max(1, fillWidth), height, Math.min(radius, fillWidth / 2));
|
|
482
|
+
ctx.fill();
|
|
483
|
+
ctx.globalAlpha = 0.28;
|
|
484
|
+
ctx.fillStyle = mixColor(fillColor, '#ffffff', 0.38);
|
|
485
|
+
ctx.fillRect(x + 1, y + 1, Math.max(0, fillWidth - 2), 1);
|
|
486
|
+
ctx.restore();
|
|
519
487
|
}
|
|
520
488
|
|
|
521
489
|
#drawTextCell(ctx, { node, state, rect, column, theme }) {
|
|
@@ -578,15 +546,6 @@ function resolveNodeStyle(theme, node, state = {}) {
|
|
|
578
546
|
};
|
|
579
547
|
}
|
|
580
548
|
|
|
581
|
-
function sameScrollIndicatorState(a, b) {
|
|
582
|
-
return Boolean(a) &&
|
|
583
|
-
a.scrollY === b.scrollY &&
|
|
584
|
-
a.contentHeight === b.contentHeight &&
|
|
585
|
-
a.rowViewportHeight === b.rowViewportHeight &&
|
|
586
|
-
a.viewportHeight === b.viewportHeight &&
|
|
587
|
-
a.rowCount === b.rowCount;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
549
|
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
591
550
|
const safeWidth = Math.max(1, width);
|
|
592
551
|
const rightPadding = 14;
|
|
@@ -40,6 +40,7 @@ export class TreeViewController {
|
|
|
40
40
|
this.themeManager = options.themeManager ?? new ThemeManager();
|
|
41
41
|
this.iconRegistry = options.iconRegistry ?? new IconRegistry();
|
|
42
42
|
this.renderer = options.renderer ?? new TreeRowRenderer({ themeManager: this.themeManager, iconRegistry: this.iconRegistry });
|
|
43
|
+
this.nativeScrollbars = options.nativeScrollbars !== false;
|
|
43
44
|
this.initialExpandDepth = options.initialExpandDepth ?? 1;
|
|
44
45
|
this.searchHighlights = new Set();
|
|
45
46
|
this.filterQuery = '';
|
|
@@ -68,6 +69,7 @@ export class TreeViewController {
|
|
|
68
69
|
this.renderer.setScene(this.scene);
|
|
69
70
|
this.#resizeToCanvasClientSize();
|
|
70
71
|
this.#observeCanvasSize();
|
|
72
|
+
this.#setupNativeScrollbars();
|
|
71
73
|
return this;
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -281,6 +283,7 @@ export class TreeViewController {
|
|
|
281
283
|
if (beforeRowHeight !== nextTheme.rowHeight || beforeIndentWidth !== nextTheme.indentWidth) {
|
|
282
284
|
this.#rebuildRows();
|
|
283
285
|
}
|
|
286
|
+
this.#nativeScroll?.applyTheme?.(nextTheme);
|
|
284
287
|
this.events.emit('themechange', { theme: nextTheme });
|
|
285
288
|
}
|
|
286
289
|
|
|
@@ -582,6 +585,11 @@ export class TreeViewController {
|
|
|
582
585
|
this.events.emit('viewportchange', this.getViewportState());
|
|
583
586
|
}
|
|
584
587
|
|
|
588
|
+
scrollTo(x, y) {
|
|
589
|
+
this.viewport.scrollTo(x, y);
|
|
590
|
+
this.events.emit('viewportchange', this.getViewportState());
|
|
591
|
+
}
|
|
592
|
+
|
|
585
593
|
render(time = performance.now()) {
|
|
586
594
|
this.renderMeasured(time);
|
|
587
595
|
}
|
|
@@ -876,6 +884,132 @@ export class TreeViewController {
|
|
|
876
884
|
if (width > 0 && height > 0) this.resize(width, height);
|
|
877
885
|
}
|
|
878
886
|
|
|
887
|
+
#setupNativeScrollbars() {
|
|
888
|
+
if (!this.nativeScrollbars || !this.canvas || typeof document === 'undefined') return;
|
|
889
|
+
const host = this.canvas.parentElement;
|
|
890
|
+
if (!host) return;
|
|
891
|
+
|
|
892
|
+
this.#nativeScroll?.destroy?.();
|
|
893
|
+
ensureNativeScrollbarStyles(document);
|
|
894
|
+
|
|
895
|
+
const previousHostPosition = host.style.position;
|
|
896
|
+
let hostPositionChanged = false;
|
|
897
|
+
if (typeof getComputedStyle === 'function' && getComputedStyle(host).position === 'static') {
|
|
898
|
+
host.style.position = 'relative';
|
|
899
|
+
hostPositionChanged = true;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const vertical = document.createElement('div');
|
|
903
|
+
const verticalSpacer = document.createElement('div');
|
|
904
|
+
const horizontal = document.createElement('div');
|
|
905
|
+
const horizontalSpacer = document.createElement('div');
|
|
906
|
+
const corner = document.createElement('div');
|
|
907
|
+
|
|
908
|
+
vertical.className = 'virtual-tree-canvas-scrollbar virtual-tree-canvas-scrollbar-y';
|
|
909
|
+
horizontal.className = 'virtual-tree-canvas-scrollbar virtual-tree-canvas-scrollbar-x';
|
|
910
|
+
corner.className = 'virtual-tree-canvas-scrollbar-corner';
|
|
911
|
+
vertical.appendChild(verticalSpacer);
|
|
912
|
+
horizontal.appendChild(horizontalSpacer);
|
|
913
|
+
applyNativeScrollbarTheme([vertical, horizontal, corner], this.themeManager.get());
|
|
914
|
+
|
|
915
|
+
const size = nativeScrollbarSize();
|
|
916
|
+
Object.assign(vertical.style, {
|
|
917
|
+
position: 'absolute',
|
|
918
|
+
top: `${this.viewport.headerHeight}px`,
|
|
919
|
+
right: '0',
|
|
920
|
+
bottom: '0',
|
|
921
|
+
width: `${size}px`,
|
|
922
|
+
overflowX: 'hidden',
|
|
923
|
+
overflowY: 'auto',
|
|
924
|
+
zIndex: '4',
|
|
925
|
+
});
|
|
926
|
+
Object.assign(horizontal.style, {
|
|
927
|
+
position: 'absolute',
|
|
928
|
+
left: '0',
|
|
929
|
+
right: '0',
|
|
930
|
+
bottom: '0',
|
|
931
|
+
height: `${size}px`,
|
|
932
|
+
overflowX: 'auto',
|
|
933
|
+
overflowY: 'hidden',
|
|
934
|
+
zIndex: '4',
|
|
935
|
+
});
|
|
936
|
+
Object.assign(corner.style, {
|
|
937
|
+
position: 'absolute',
|
|
938
|
+
right: '0',
|
|
939
|
+
bottom: '0',
|
|
940
|
+
width: `${size}px`,
|
|
941
|
+
height: `${size}px`,
|
|
942
|
+
zIndex: '4',
|
|
943
|
+
background: 'transparent',
|
|
944
|
+
pointerEvents: 'none',
|
|
945
|
+
});
|
|
946
|
+
Object.assign(verticalSpacer.style, {
|
|
947
|
+
width: '1px',
|
|
948
|
+
minHeight: '1px',
|
|
949
|
+
});
|
|
950
|
+
Object.assign(horizontalSpacer.style, {
|
|
951
|
+
height: '1px',
|
|
952
|
+
minWidth: '1px',
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
host.appendChild(vertical);
|
|
956
|
+
host.appendChild(horizontal);
|
|
957
|
+
host.appendChild(corner);
|
|
958
|
+
|
|
959
|
+
let syncing = false;
|
|
960
|
+
const sync = () => {
|
|
961
|
+
const maxX = Math.max(0, this.viewport.contentWidth - this.viewport.viewportWidth);
|
|
962
|
+
const maxY = Math.max(0, this.viewport.contentHeight - this.viewport.rowViewportHeight);
|
|
963
|
+
const showX = maxX > 0;
|
|
964
|
+
const showY = maxY > 0;
|
|
965
|
+
|
|
966
|
+
vertical.style.display = showY ? 'block' : 'none';
|
|
967
|
+
horizontal.style.display = showX ? 'block' : 'none';
|
|
968
|
+
corner.style.display = showX && showY ? 'block' : 'none';
|
|
969
|
+
vertical.style.bottom = showX ? `${size}px` : '0';
|
|
970
|
+
horizontal.style.right = showY ? `${size}px` : '0';
|
|
971
|
+
vertical.style.top = `${this.viewport.headerHeight}px`;
|
|
972
|
+
|
|
973
|
+
verticalSpacer.style.height = `${Math.ceil(maxY + vertical.clientHeight)}px`;
|
|
974
|
+
horizontalSpacer.style.width = `${Math.ceil(maxX + horizontal.clientWidth)}px`;
|
|
975
|
+
|
|
976
|
+
syncing = true;
|
|
977
|
+
vertical.scrollTop = this.viewport.scrollY;
|
|
978
|
+
horizontal.scrollLeft = this.viewport.scrollX;
|
|
979
|
+
syncing = false;
|
|
980
|
+
};
|
|
981
|
+
const onVerticalScroll = () => {
|
|
982
|
+
if (syncing) return;
|
|
983
|
+
this.viewport.scrollTo(this.viewport.scrollX, vertical.scrollTop);
|
|
984
|
+
this.events.emit('viewportchange', this.getViewportState());
|
|
985
|
+
};
|
|
986
|
+
const onHorizontalScroll = () => {
|
|
987
|
+
if (syncing) return;
|
|
988
|
+
this.viewport.scrollTo(horizontal.scrollLeft, this.viewport.scrollY);
|
|
989
|
+
this.events.emit('viewportchange', this.getViewportState());
|
|
990
|
+
};
|
|
991
|
+
const onViewportChange = () => sync();
|
|
992
|
+
|
|
993
|
+
vertical.addEventListener('scroll', onVerticalScroll, { passive: true });
|
|
994
|
+
horizontal.addEventListener('scroll', onHorizontalScroll, { passive: true });
|
|
995
|
+
this.viewport.addEventListener('change', onViewportChange);
|
|
996
|
+
sync();
|
|
997
|
+
|
|
998
|
+
this.#nativeScroll = {
|
|
999
|
+
sync,
|
|
1000
|
+
applyTheme: (theme) => applyNativeScrollbarTheme([vertical, horizontal, corner], theme),
|
|
1001
|
+
destroy: () => {
|
|
1002
|
+
vertical.removeEventListener('scroll', onVerticalScroll);
|
|
1003
|
+
horizontal.removeEventListener('scroll', onHorizontalScroll);
|
|
1004
|
+
this.viewport.removeEventListener('change', onViewportChange);
|
|
1005
|
+
vertical.remove();
|
|
1006
|
+
horizontal.remove();
|
|
1007
|
+
corner.remove();
|
|
1008
|
+
if (hostPositionChanged) host.style.position = previousHostPosition;
|
|
1009
|
+
},
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
879
1013
|
#computeInspectorPaneLabelEnd(visibleRange) {
|
|
880
1014
|
const column = this.columnModel.columns.find((item) => item.kind === 'inspectorPane');
|
|
881
1015
|
if (!column) return 0;
|
|
@@ -938,6 +1072,133 @@ export class TreeViewController {
|
|
|
938
1072
|
}
|
|
939
1073
|
|
|
940
1074
|
#resizeObserver = null;
|
|
1075
|
+
#nativeScroll = null;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const NATIVE_SCROLLBAR_STYLE_ID = 'virtual-tree-canvas-native-scrollbar-style';
|
|
1079
|
+
|
|
1080
|
+
function ensureNativeScrollbarStyles(doc) {
|
|
1081
|
+
if (!doc || typeof doc.createElement !== 'function') return;
|
|
1082
|
+
if (typeof doc.getElementById === 'function' && doc.getElementById(NATIVE_SCROLLBAR_STYLE_ID)) return;
|
|
1083
|
+
const style = doc.createElement('style');
|
|
1084
|
+
style.id = NATIVE_SCROLLBAR_STYLE_ID;
|
|
1085
|
+
style.textContent = `
|
|
1086
|
+
.virtual-tree-canvas-scrollbar {
|
|
1087
|
+
--vtc-scrollbar-size: 12px;
|
|
1088
|
+
--vtc-scrollbar-track: rgba(15, 23, 42, 0.32);
|
|
1089
|
+
--vtc-scrollbar-thumb: rgba(148, 163, 184, 0.58);
|
|
1090
|
+
--vtc-scrollbar-thumb-hover: rgba(148, 163, 184, 0.78);
|
|
1091
|
+
--vtc-scrollbar-thumb-active: rgba(56, 189, 248, 0.76);
|
|
1092
|
+
--vtc-scrollbar-thumb-border: rgba(255, 255, 255, 0.08);
|
|
1093
|
+
background: var(--vtc-scrollbar-track);
|
|
1094
|
+
scrollbar-color: var(--vtc-scrollbar-thumb) var(--vtc-scrollbar-track);
|
|
1095
|
+
scrollbar-width: thin;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.virtual-tree-canvas-scrollbar:hover {
|
|
1099
|
+
scrollbar-color: var(--vtc-scrollbar-thumb-hover) var(--vtc-scrollbar-track);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar {
|
|
1103
|
+
width: var(--vtc-scrollbar-size);
|
|
1104
|
+
height: var(--vtc-scrollbar-size);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar-track {
|
|
1108
|
+
background: var(--vtc-scrollbar-track);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar-thumb {
|
|
1112
|
+
min-width: 28px;
|
|
1113
|
+
min-height: 28px;
|
|
1114
|
+
border: 3px solid transparent;
|
|
1115
|
+
border-radius: 999px;
|
|
1116
|
+
background:
|
|
1117
|
+
linear-gradient(180deg, var(--vtc-scrollbar-thumb-highlight), transparent 52%),
|
|
1118
|
+
var(--vtc-scrollbar-thumb);
|
|
1119
|
+
background-clip: content-box;
|
|
1120
|
+
box-shadow: inset 0 0 0 1px var(--vtc-scrollbar-thumb-border);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.virtual-tree-canvas-scrollbar:hover::-webkit-scrollbar-thumb {
|
|
1124
|
+
background:
|
|
1125
|
+
linear-gradient(180deg, var(--vtc-scrollbar-thumb-highlight), transparent 52%),
|
|
1126
|
+
var(--vtc-scrollbar-thumb-hover);
|
|
1127
|
+
background-clip: content-box;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar-thumb:active {
|
|
1131
|
+
background:
|
|
1132
|
+
linear-gradient(180deg, var(--vtc-scrollbar-thumb-highlight), transparent 52%),
|
|
1133
|
+
var(--vtc-scrollbar-thumb-active);
|
|
1134
|
+
background-clip: content-box;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar-corner,
|
|
1138
|
+
.virtual-tree-canvas-scrollbar-corner {
|
|
1139
|
+
background: var(--vtc-scrollbar-track);
|
|
1140
|
+
}
|
|
1141
|
+
`;
|
|
1142
|
+
const target = doc.head ?? doc.body ?? doc.documentElement;
|
|
1143
|
+
target?.appendChild?.(style);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function applyNativeScrollbarTheme(elements, theme) {
|
|
1147
|
+
const colors = theme?.colors ?? {};
|
|
1148
|
+
const track = alphaHex(colors.row ?? colors.background ?? '#0b1020', 0.74);
|
|
1149
|
+
const thumb = mixHex(colors.borderStrong ?? colors.border ?? '#334155', colors.textMuted ?? colors.focus ?? '#94a3b8', 0.55);
|
|
1150
|
+
const hover = mixHex(thumb, colors.focus ?? '#38bdf8', 0.32);
|
|
1151
|
+
const active = mixHex(thumb, colors.focus ?? '#38bdf8', 0.52);
|
|
1152
|
+
const highlight = alphaHex('#ffffff', 0.2);
|
|
1153
|
+
const border = alphaHex('#ffffff', 0.09);
|
|
1154
|
+
for (const element of elements) {
|
|
1155
|
+
if (!element?.style?.setProperty) continue;
|
|
1156
|
+
element.style.setProperty('--vtc-scrollbar-track', track);
|
|
1157
|
+
element.style.setProperty('--vtc-scrollbar-thumb', thumb);
|
|
1158
|
+
element.style.setProperty('--vtc-scrollbar-thumb-hover', hover);
|
|
1159
|
+
element.style.setProperty('--vtc-scrollbar-thumb-active', active);
|
|
1160
|
+
element.style.setProperty('--vtc-scrollbar-thumb-highlight', highlight);
|
|
1161
|
+
element.style.setProperty('--vtc-scrollbar-thumb-border', border);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function mixHex(a, b, amount) {
|
|
1166
|
+
const from = parseHexColor(a);
|
|
1167
|
+
const to = parseHexColor(b);
|
|
1168
|
+
if (!from || !to) return amount >= 0.5 ? b : a;
|
|
1169
|
+
const t = Math.max(0, Math.min(1, amount));
|
|
1170
|
+
const value = from.map((channel, index) => Math.round(channel + (to[index] - channel) * t));
|
|
1171
|
+
return `rgb(${value[0]}, ${value[1]}, ${value[2]})`;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function alphaHex(value, alpha) {
|
|
1175
|
+
const color = parseHexColor(value);
|
|
1176
|
+
if (!color) return value;
|
|
1177
|
+
return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${Math.max(0, Math.min(1, alpha))})`;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function parseHexColor(value) {
|
|
1181
|
+
const text = String(value ?? '').trim();
|
|
1182
|
+
const match = /^#([0-9a-f]{6})$/i.exec(text);
|
|
1183
|
+
if (!match) return null;
|
|
1184
|
+
const number = Number.parseInt(match[1], 16);
|
|
1185
|
+
return [(number >> 16) & 255, (number >> 8) & 255, number & 255];
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function nativeScrollbarSize() {
|
|
1189
|
+
if (typeof document === 'undefined') return 14;
|
|
1190
|
+
const probe = document.createElement('div');
|
|
1191
|
+
Object.assign(probe.style, {
|
|
1192
|
+
position: 'absolute',
|
|
1193
|
+
top: '-9999px',
|
|
1194
|
+
width: '100px',
|
|
1195
|
+
height: '100px',
|
|
1196
|
+
overflow: 'scroll',
|
|
1197
|
+
});
|
|
1198
|
+
document.body?.appendChild(probe);
|
|
1199
|
+
const size = Math.max(10, probe.offsetWidth - probe.clientWidth || 14);
|
|
1200
|
+
probe.remove();
|
|
1201
|
+
return size;
|
|
941
1202
|
}
|
|
942
1203
|
|
|
943
1204
|
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|