virtual-tree-canvas 0.3.4 → 0.3.6
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 +1 -1
- package/src/core/icon-registry.js +76 -20
- package/src/core/theme-manager.js +47 -14
- package/src/index.d.ts +11 -3
- package/src/input/tree-view-input-controller.js +24 -133
- package/src/inspector/cell-editor-manager.js +5 -3
- package/src/renderers/tree-row-renderer.js +36 -77
- package/src/tree-view-controller.js +333 -0
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
|
@@ -62,6 +62,8 @@ export class TreeRowRenderer {
|
|
|
62
62
|
|
|
63
63
|
export class TreeViewController {
|
|
64
64
|
canvas?: HTMLCanvasElement;
|
|
65
|
+
inputController?: TreeViewInputController | null;
|
|
66
|
+
cellEditor?: CellEditorManager | null;
|
|
65
67
|
viewport: {
|
|
66
68
|
viewportWidth: number;
|
|
67
69
|
viewportHeight: number;
|
|
@@ -71,7 +73,11 @@ export class TreeViewController {
|
|
|
71
73
|
rowModel: any;
|
|
72
74
|
expansion: any;
|
|
73
75
|
selection: any;
|
|
74
|
-
constructor(options?: Record<string, any>);
|
|
76
|
+
constructor(options?: Record<string, any> & { nativeScrollbars?: boolean });
|
|
77
|
+
initialize(canvas: HTMLCanvasElement): this;
|
|
78
|
+
attachCellEditor(options?: { host?: HTMLElement | null }): CellEditorManager;
|
|
79
|
+
attachInput(options?: { cellEditor?: CellEditorManager | null }): TreeViewInputController;
|
|
80
|
+
destroy(): void;
|
|
75
81
|
on(type: string, listener: (event: any) => void): any;
|
|
76
82
|
off(type: string, listener: (event: any) => void): void;
|
|
77
83
|
setData(nodes: TreeNode[]): void;
|
|
@@ -79,6 +85,7 @@ export class TreeViewController {
|
|
|
79
85
|
setColumns(columns: Column[]): void;
|
|
80
86
|
setDynamicState(patches: DynamicPatch[]): void;
|
|
81
87
|
setTheme(theme: any): void;
|
|
88
|
+
setLayoutMetrics(options?: { rowHeight?: number; indentWidth?: number; headerHeight?: number }): void;
|
|
82
89
|
resize(width: number, height: number): void;
|
|
83
90
|
render(time?: number): void;
|
|
84
91
|
renderMeasured(time?: number): any;
|
|
@@ -89,6 +96,7 @@ export class TreeViewController {
|
|
|
89
96
|
clearFilter(): void;
|
|
90
97
|
focusNode(nodeId: string, options?: Record<string, any>): boolean;
|
|
91
98
|
scrollToNode(nodeId: string, align?: TreeViewAlign): boolean;
|
|
99
|
+
scrollTo(x: number, y: number): void;
|
|
92
100
|
getSelection(): string[];
|
|
93
101
|
setSelection(ids: string[]): void;
|
|
94
102
|
clearSelection(): void;
|
|
@@ -97,12 +105,12 @@ export class TreeViewController {
|
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
export class TreeViewInputController {
|
|
100
|
-
constructor(options?:
|
|
108
|
+
constructor(options: { controller: TreeViewController; cellEditor?: CellEditorManager | null });
|
|
101
109
|
destroy(): void;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
export class CellEditorManager {
|
|
105
|
-
constructor(options?:
|
|
113
|
+
constructor(options: { controller: TreeViewController; host?: HTMLElement | null });
|
|
106
114
|
destroy(): void;
|
|
107
115
|
close(): void;
|
|
108
116
|
}
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
export class TreeViewInputController {
|
|
2
2
|
/**
|
|
3
3
|
* @param {{
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* rowModel: import('../core/visible-row-model.js').VisibleRowModel,
|
|
7
|
-
* expansion: import('../core/tree-expansion-manager.js').TreeExpansionManager,
|
|
8
|
-
* selection: import('../core/selection-manager.js').TreeSelectionManager,
|
|
9
|
-
* controller?: import('../tree-view-controller.js').TreeViewController,
|
|
10
|
-
* onRowsChanged?: () => void,
|
|
11
|
-
* onSelectionChanged?: () => void,
|
|
12
|
-
* onHoverChanged?: (id: string | null) => void
|
|
4
|
+
* controller: import('../tree-view-controller.js').TreeViewController,
|
|
5
|
+
* cellEditor?: import('../inspector/cell-editor-manager.js').CellEditorManager | null
|
|
13
6
|
* }} options
|
|
14
7
|
*/
|
|
15
8
|
constructor(options) {
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
if (!options?.controller) throw new TypeError('TreeViewInputController requires a TreeViewController');
|
|
10
|
+
if (!options.controller.canvas) throw new Error('TreeViewInputController requires an initialized TreeViewController canvas');
|
|
11
|
+
this.controller = options.controller;
|
|
12
|
+
this.canvas = options.controller.canvas;
|
|
18
13
|
this.hoveredId = null;
|
|
19
14
|
this.resizeDrag = null;
|
|
20
15
|
this.cellEditor = options.cellEditor ?? null;
|
|
@@ -44,11 +39,7 @@ export class TreeViewInputController {
|
|
|
44
39
|
#onWheel = (event) => {
|
|
45
40
|
event.preventDefault();
|
|
46
41
|
this.cellEditor?.close?.();
|
|
47
|
-
|
|
48
|
-
this.controller.scrollBy(event.shiftKey ? event.deltaY : event.deltaX, event.deltaY);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
this.viewport.scrollBy(event.shiftKey ? event.deltaY : event.deltaX, event.deltaY);
|
|
42
|
+
this.controller.scrollBy(event.shiftKey ? event.deltaY : event.deltaX, event.deltaY);
|
|
52
43
|
};
|
|
53
44
|
|
|
54
45
|
#onMouseMove = (event) => {
|
|
@@ -56,7 +47,7 @@ export class TreeViewInputController {
|
|
|
56
47
|
const rect = this.canvas.getBoundingClientRect();
|
|
57
48
|
const clientX = event.clientX - rect.left;
|
|
58
49
|
const nextWidth = this.resizeDrag.startWidth + (clientX - this.resizeDrag.startX);
|
|
59
|
-
this.controller
|
|
50
|
+
this.controller.resizeColumn(this.resizeDrag.columnId, nextWidth);
|
|
60
51
|
event.preventDefault();
|
|
61
52
|
return;
|
|
62
53
|
}
|
|
@@ -67,19 +58,15 @@ export class TreeViewInputController {
|
|
|
67
58
|
if (id === this.hoveredId && key === this.hoveredHitKey) return;
|
|
68
59
|
this.hoveredId = id;
|
|
69
60
|
this.hoveredHitKey = key;
|
|
70
|
-
|
|
71
|
-
else this.controller?.setHover(id);
|
|
72
|
-
this.onHoverChanged?.(id);
|
|
61
|
+
this.controller.setHoverHit(hit);
|
|
73
62
|
};
|
|
74
63
|
|
|
75
64
|
#onMouseLeave = () => {
|
|
76
65
|
if (!this.resizeDrag) this.canvas.style.cursor = '';
|
|
77
66
|
this.hoveredId = null;
|
|
78
67
|
this.hoveredHitKey = null;
|
|
79
|
-
this.controller
|
|
80
|
-
|
|
81
|
-
else this.controller?.setHover(null);
|
|
82
|
-
this.onHoverChanged?.(null);
|
|
68
|
+
this.controller.setActiveHit(null);
|
|
69
|
+
this.controller.setHoverHit(null);
|
|
83
70
|
};
|
|
84
71
|
|
|
85
72
|
#onClick = (event) => {
|
|
@@ -88,43 +75,30 @@ export class TreeViewInputController {
|
|
|
88
75
|
if (!hit) return;
|
|
89
76
|
if (hit.area === 'header') {
|
|
90
77
|
if (hit.part === 'filter' && this.cellEditor?.handleHeaderClick(event, hit)) return;
|
|
91
|
-
if (!this.resizeDrag && hit.part === 'label' && hit.column) this.controller
|
|
78
|
+
if (!this.resizeDrag && hit.part === 'label' && hit.column) this.controller.sortBy(hit.column.id);
|
|
92
79
|
return;
|
|
93
80
|
}
|
|
94
81
|
if (hit.part === 'chevron' && hit.row.hasChildren) {
|
|
95
|
-
|
|
96
|
-
this.controller.toggle(hit.row.nodeId);
|
|
97
|
-
this.onRowsChanged?.();
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
this.expansion.toggle(hit.row.nodeId);
|
|
101
|
-
this.rowModel.rebuild();
|
|
102
|
-
this.viewport.setContentSize(this.rowModel.contentWidth, this.rowModel.contentHeight);
|
|
103
|
-
this.onRowsChanged?.();
|
|
82
|
+
this.controller.toggle(hit.row.nodeId);
|
|
104
83
|
return;
|
|
105
84
|
}
|
|
106
85
|
if (this.cellEditor?.handleClick(event, hit)) return;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
this.onSelectionChanged?.();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
this.#selectRow(hit.row.rowIndex, event);
|
|
86
|
+
this.controller.clickNode(hit.row.nodeId, {
|
|
87
|
+
shiftKey: event.shiftKey,
|
|
88
|
+
ctrlKey: event.ctrlKey,
|
|
89
|
+
metaKey: event.metaKey,
|
|
90
|
+
multi: this.canvas.dataset.multi === 'true',
|
|
91
|
+
});
|
|
118
92
|
};
|
|
119
93
|
|
|
120
94
|
#onDoubleClick = (event) => {
|
|
121
95
|
const hit = this.#hitTest(event);
|
|
122
|
-
if (hit?.area === 'row') this.controller
|
|
96
|
+
if (hit?.area === 'row') this.controller.doubleClickNode(hit.row.nodeId, event);
|
|
123
97
|
};
|
|
124
98
|
|
|
125
99
|
#onMouseDown = (event) => {
|
|
126
100
|
const hit = this.#hitTest(event);
|
|
127
|
-
this.controller
|
|
101
|
+
this.controller.setActiveHit(hit);
|
|
128
102
|
if (this.cellEditor?.handlePointerDown(event, hit)) {
|
|
129
103
|
event.preventDefault();
|
|
130
104
|
return;
|
|
@@ -143,103 +117,20 @@ export class TreeViewInputController {
|
|
|
143
117
|
#onMouseUp = () => {
|
|
144
118
|
this.resizeDrag = null;
|
|
145
119
|
this.canvas.style.cursor = '';
|
|
146
|
-
this.controller
|
|
120
|
+
this.controller.setActiveHit(null);
|
|
147
121
|
};
|
|
148
122
|
|
|
149
123
|
#onKeyDown = (event) => {
|
|
150
|
-
if (this.controller
|
|
124
|
+
if (this.controller.handleKey(event)) {
|
|
151
125
|
event.preventDefault();
|
|
152
|
-
this.onSelectionChanged?.();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (!this.rowModel.rows.length) return;
|
|
156
|
-
const currentRow = this.#focusedRowIndex();
|
|
157
|
-
let target = currentRow;
|
|
158
|
-
if (event.key === 'ArrowDown') target = Math.min(this.rowModel.rows.length - 1, currentRow + 1);
|
|
159
|
-
else if (event.key === 'ArrowUp') target = Math.max(0, currentRow - 1);
|
|
160
|
-
else if (event.key === 'Home') target = 0;
|
|
161
|
-
else if (event.key === 'End') target = this.rowModel.rows.length - 1;
|
|
162
|
-
else if (event.key === 'ArrowRight') {
|
|
163
|
-
const row = this.rowModel.getRow(currentRow);
|
|
164
|
-
if (row?.hasChildren && !row.expanded) this.#toggleAndRebuild(row.nodeId);
|
|
165
|
-
else if (row?.expanded) target = Math.min(this.rowModel.rows.length - 1, currentRow + 1);
|
|
166
|
-
} else if (event.key === 'ArrowLeft') {
|
|
167
|
-
const row = this.rowModel.getRow(currentRow);
|
|
168
|
-
if (row?.expanded) this.#toggleAndRebuild(row.nodeId);
|
|
169
|
-
else {
|
|
170
|
-
const node = row ? this.rowModel.model.index.getNode(row.nodeId) : null;
|
|
171
|
-
const parentRow = node?.parentId ? this.rowModel.getRowById(node.parentId) : null;
|
|
172
|
-
if (parentRow) target = parentRow.rowIndex;
|
|
173
|
-
}
|
|
174
|
-
} else if (event.key === 'Enter' || event.key === ' ') {
|
|
175
|
-
const row = this.rowModel.getRow(currentRow);
|
|
176
|
-
if (row) this.selection.toggle(row.nodeId);
|
|
177
|
-
this.onSelectionChanged?.();
|
|
178
|
-
event.preventDefault();
|
|
179
|
-
return;
|
|
180
|
-
} else return;
|
|
181
|
-
|
|
182
|
-
const row = this.rowModel.getRow(target);
|
|
183
|
-
if (row) {
|
|
184
|
-
this.selection.select(row.nodeId);
|
|
185
|
-
this.anchorRowIndex = target;
|
|
186
|
-
this.viewport.scrollRowIntoView(target);
|
|
187
|
-
this.onSelectionChanged?.();
|
|
188
126
|
}
|
|
189
|
-
event.preventDefault();
|
|
190
127
|
};
|
|
191
128
|
|
|
192
|
-
#selectRow(rowIndex, event) {
|
|
193
|
-
const row = this.rowModel.getRow(rowIndex);
|
|
194
|
-
if (!row) return;
|
|
195
|
-
if (event.shiftKey && this.anchorRowIndex !== null) {
|
|
196
|
-
this.selection.selected.clear();
|
|
197
|
-
const start = Math.min(this.anchorRowIndex, rowIndex);
|
|
198
|
-
const end = Math.max(this.anchorRowIndex, rowIndex);
|
|
199
|
-
for (let i = start; i <= end; i++) this.selection.selected.add(this.rowModel.rows[i].nodeId);
|
|
200
|
-
this.selection.focused = row.nodeId;
|
|
201
|
-
this.selection.dispatchEvent(new Event('change'));
|
|
202
|
-
} else if (event.ctrlKey || event.metaKey || this.canvas.dataset.multi === 'true') {
|
|
203
|
-
this.selection.toggle(row.nodeId);
|
|
204
|
-
this.anchorRowIndex = rowIndex;
|
|
205
|
-
} else {
|
|
206
|
-
this.selection.select(row.nodeId);
|
|
207
|
-
this.anchorRowIndex = rowIndex;
|
|
208
|
-
}
|
|
209
|
-
this.onSelectionChanged?.();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
#focusedRowIndex() {
|
|
213
|
-
if (this.selection.focused) {
|
|
214
|
-
const row = this.rowModel.getRowById(this.selection.focused);
|
|
215
|
-
if (row) return row.rowIndex;
|
|
216
|
-
}
|
|
217
|
-
return Math.max(0, Math.floor(this.viewport.scrollY / this.rowModel.rowHeight));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
#toggleAndRebuild(id) {
|
|
221
|
-
this.expansion.toggle(id);
|
|
222
|
-
this.rowModel.rebuild();
|
|
223
|
-
this.viewport.setContentSize(this.rowModel.contentWidth, this.rowModel.contentHeight);
|
|
224
|
-
this.onRowsChanged?.();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
129
|
#hitTest(event) {
|
|
228
130
|
const rect = this.canvas.getBoundingClientRect();
|
|
229
131
|
const clientX = event.clientX - rect.left;
|
|
230
132
|
const clientY = event.clientY - rect.top;
|
|
231
|
-
|
|
232
|
-
const x = clientX + this.viewport.scrollX;
|
|
233
|
-
const y = clientY - (this.viewport.headerHeight ?? 0) + this.viewport.scrollY;
|
|
234
|
-
if (clientY < (this.viewport.headerHeight ?? 0)) return { area: 'header', part: 'header', x, y: clientY };
|
|
235
|
-
const rowIndex = Math.floor(y / this.rowModel.rowHeight);
|
|
236
|
-
const row = this.rowModel.getRow(rowIndex);
|
|
237
|
-
if (!row) return null;
|
|
238
|
-
const rowX = row.depth * this.rowModel.indentWidth;
|
|
239
|
-
const chevronLeft = rowX + 4;
|
|
240
|
-
const chevronRight = chevronLeft + 18;
|
|
241
|
-
const part = x >= chevronLeft && x <= chevronRight ? 'chevron' : x <= rowX + 42 ? 'icon' : 'body';
|
|
242
|
-
return { area: 'row', row, x, y, part };
|
|
133
|
+
return this.controller.hitTest(clientX, clientY);
|
|
243
134
|
}
|
|
244
135
|
}
|
|
245
136
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export class CellEditorManager {
|
|
2
|
-
constructor({ controller,
|
|
2
|
+
constructor({ controller, host = null }) {
|
|
3
|
+
if (!controller) throw new TypeError('CellEditorManager requires a TreeViewController');
|
|
4
|
+
if (!controller.canvas) throw new Error('CellEditorManager requires an initialized TreeViewController canvas');
|
|
3
5
|
this.controller = controller;
|
|
4
|
-
this.canvas = canvas;
|
|
5
|
-
this.host = host ?? canvas.parentElement ?? document.body;
|
|
6
|
+
this.canvas = controller.canvas;
|
|
7
|
+
this.host = host ?? controller.canvas.parentElement ?? document.body;
|
|
6
8
|
this.overlay = null;
|
|
7
9
|
this.rangeDrag = null;
|
|
8
10
|
this.onMouseMove = this.#onMouseMove.bind(this);
|
|
@@ -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;
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
TreeViewViewport,
|
|
13
13
|
VisibleRowModel,
|
|
14
14
|
} from './core/index.js';
|
|
15
|
+
import { CellEditorManager } from './inspector/cell-editor-manager.js';
|
|
15
16
|
import { formatInspectorValue, getAtPath, inspectorColumns, inspectorPaneColumns, ModelInspectorBuilder, setAtPath } from './inspector/index.js';
|
|
17
|
+
import { TreeViewInputController } from './input/tree-view-input-controller.js';
|
|
16
18
|
import { TreeRowRenderer } from './renderers/index.js';
|
|
17
19
|
|
|
18
20
|
export class TreeViewController {
|
|
@@ -40,6 +42,7 @@ export class TreeViewController {
|
|
|
40
42
|
this.themeManager = options.themeManager ?? new ThemeManager();
|
|
41
43
|
this.iconRegistry = options.iconRegistry ?? new IconRegistry();
|
|
42
44
|
this.renderer = options.renderer ?? new TreeRowRenderer({ themeManager: this.themeManager, iconRegistry: this.iconRegistry });
|
|
45
|
+
this.nativeScrollbars = options.nativeScrollbars !== false;
|
|
43
46
|
this.initialExpandDepth = options.initialExpandDepth ?? 1;
|
|
44
47
|
this.searchHighlights = new Set();
|
|
45
48
|
this.filterQuery = '';
|
|
@@ -57,20 +60,61 @@ export class TreeViewController {
|
|
|
57
60
|
this.workerRevision = 0;
|
|
58
61
|
this.sortValueSnapshot = null;
|
|
59
62
|
this.inspector = null;
|
|
63
|
+
this.inputController = null;
|
|
64
|
+
this.cellEditor = null;
|
|
60
65
|
this.scene = this.createRenderScene();
|
|
61
66
|
|
|
62
67
|
if (options.canvas) this.initialize(options.canvas);
|
|
68
|
+
if (options.editable !== false && (options.host || options.editable)) this.attachCellEditor({ host: options.host });
|
|
69
|
+
if (options.canvas && options.input !== false) this.attachInput();
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
initialize(canvas) {
|
|
73
|
+
if (!canvas || typeof canvas.getContext !== 'function') {
|
|
74
|
+
throw new TypeError('TreeViewController.initialize requires an HTMLCanvasElement');
|
|
75
|
+
}
|
|
66
76
|
this.canvas = canvas;
|
|
67
77
|
this.renderer.initialize(canvas);
|
|
68
78
|
this.renderer.setScene(this.scene);
|
|
69
79
|
this.#resizeToCanvasClientSize();
|
|
70
80
|
this.#observeCanvasSize();
|
|
81
|
+
this.#setupNativeScrollbars();
|
|
71
82
|
return this;
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
attachCellEditor(options = {}) {
|
|
86
|
+
if (!this.canvas) throw new Error('TreeViewController.attachCellEditor requires an initialized canvas');
|
|
87
|
+
this.cellEditor?.destroy?.();
|
|
88
|
+
this.cellEditor = new CellEditorManager({
|
|
89
|
+
controller: this,
|
|
90
|
+
host: options.host,
|
|
91
|
+
});
|
|
92
|
+
return this.cellEditor;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
attachInput(options = {}) {
|
|
96
|
+
if (!this.canvas) throw new Error('TreeViewController.attachInput requires an initialized canvas');
|
|
97
|
+
this.inputController?.destroy?.();
|
|
98
|
+
this.inputController = new TreeViewInputController({
|
|
99
|
+
controller: this,
|
|
100
|
+
cellEditor: options.cellEditor ?? this.cellEditor,
|
|
101
|
+
});
|
|
102
|
+
return this.inputController;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
destroy() {
|
|
106
|
+
this.inputController?.destroy?.();
|
|
107
|
+
this.cellEditor?.destroy?.();
|
|
108
|
+
this.disableWorkers();
|
|
109
|
+
this.#resizeObserver?.disconnect();
|
|
110
|
+
this.#resizeObserver = null;
|
|
111
|
+
this.#nativeScroll?.destroy?.();
|
|
112
|
+
this.#nativeScroll = null;
|
|
113
|
+
this.inputController = null;
|
|
114
|
+
this.cellEditor = null;
|
|
115
|
+
this.canvas = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
74
118
|
on(type, listener) {
|
|
75
119
|
return this.events.on(type, listener);
|
|
76
120
|
}
|
|
@@ -281,9 +325,28 @@ export class TreeViewController {
|
|
|
281
325
|
if (beforeRowHeight !== nextTheme.rowHeight || beforeIndentWidth !== nextTheme.indentWidth) {
|
|
282
326
|
this.#rebuildRows();
|
|
283
327
|
}
|
|
328
|
+
this.#nativeScroll?.applyTheme?.(nextTheme);
|
|
284
329
|
this.events.emit('themechange', { theme: nextTheme });
|
|
285
330
|
}
|
|
286
331
|
|
|
332
|
+
setLayoutMetrics(options = {}) {
|
|
333
|
+
const rowHeight = options.rowHeight ?? this.rowModel.rowHeight;
|
|
334
|
+
const indentWidth = options.indentWidth ?? this.rowModel.indentWidth;
|
|
335
|
+
const headerHeight = options.headerHeight ?? this.viewport.headerHeight;
|
|
336
|
+
assertPositiveNumber(rowHeight, 'rowHeight');
|
|
337
|
+
assertPositiveNumber(indentWidth, 'indentWidth');
|
|
338
|
+
assertNonNegativeNumber(headerHeight, 'headerHeight');
|
|
339
|
+
const rowsChanged = this.rowModel.rowHeight !== rowHeight || this.rowModel.indentWidth !== indentWidth;
|
|
340
|
+
this.rowModel.rowHeight = rowHeight;
|
|
341
|
+
this.rowModel.indentWidth = indentWidth;
|
|
342
|
+
this.viewport.rowHeight = rowHeight;
|
|
343
|
+
this.viewport.indentWidth = indentWidth;
|
|
344
|
+
this.viewport.headerHeight = headerHeight;
|
|
345
|
+
if (rowsChanged) this.#rebuildRows();
|
|
346
|
+
else this.#syncContentSize();
|
|
347
|
+
this.events.emit('layoutchange', this.getViewportState());
|
|
348
|
+
}
|
|
349
|
+
|
|
287
350
|
registerIcon(name, imageOrUrl) {
|
|
288
351
|
return this.iconRegistry.register(name, imageOrUrl);
|
|
289
352
|
}
|
|
@@ -582,6 +645,11 @@ export class TreeViewController {
|
|
|
582
645
|
this.events.emit('viewportchange', this.getViewportState());
|
|
583
646
|
}
|
|
584
647
|
|
|
648
|
+
scrollTo(x, y) {
|
|
649
|
+
this.viewport.scrollTo(x, y);
|
|
650
|
+
this.events.emit('viewportchange', this.getViewportState());
|
|
651
|
+
}
|
|
652
|
+
|
|
585
653
|
render(time = performance.now()) {
|
|
586
654
|
this.renderMeasured(time);
|
|
587
655
|
}
|
|
@@ -876,6 +944,132 @@ export class TreeViewController {
|
|
|
876
944
|
if (width > 0 && height > 0) this.resize(width, height);
|
|
877
945
|
}
|
|
878
946
|
|
|
947
|
+
#setupNativeScrollbars() {
|
|
948
|
+
if (!this.nativeScrollbars || !this.canvas || typeof document === 'undefined') return;
|
|
949
|
+
const host = this.canvas.parentElement;
|
|
950
|
+
if (!host) return;
|
|
951
|
+
|
|
952
|
+
this.#nativeScroll?.destroy?.();
|
|
953
|
+
ensureNativeScrollbarStyles(document);
|
|
954
|
+
|
|
955
|
+
const previousHostPosition = host.style.position;
|
|
956
|
+
let hostPositionChanged = false;
|
|
957
|
+
if (typeof getComputedStyle === 'function' && getComputedStyle(host).position === 'static') {
|
|
958
|
+
host.style.position = 'relative';
|
|
959
|
+
hostPositionChanged = true;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const vertical = document.createElement('div');
|
|
963
|
+
const verticalSpacer = document.createElement('div');
|
|
964
|
+
const horizontal = document.createElement('div');
|
|
965
|
+
const horizontalSpacer = document.createElement('div');
|
|
966
|
+
const corner = document.createElement('div');
|
|
967
|
+
|
|
968
|
+
vertical.className = 'virtual-tree-canvas-scrollbar virtual-tree-canvas-scrollbar-y';
|
|
969
|
+
horizontal.className = 'virtual-tree-canvas-scrollbar virtual-tree-canvas-scrollbar-x';
|
|
970
|
+
corner.className = 'virtual-tree-canvas-scrollbar-corner';
|
|
971
|
+
vertical.appendChild(verticalSpacer);
|
|
972
|
+
horizontal.appendChild(horizontalSpacer);
|
|
973
|
+
applyNativeScrollbarTheme([vertical, horizontal, corner], this.themeManager.get());
|
|
974
|
+
|
|
975
|
+
const size = nativeScrollbarSize();
|
|
976
|
+
Object.assign(vertical.style, {
|
|
977
|
+
position: 'absolute',
|
|
978
|
+
top: `${this.viewport.headerHeight}px`,
|
|
979
|
+
right: '0',
|
|
980
|
+
bottom: '0',
|
|
981
|
+
width: `${size}px`,
|
|
982
|
+
overflowX: 'hidden',
|
|
983
|
+
overflowY: 'auto',
|
|
984
|
+
zIndex: '4',
|
|
985
|
+
});
|
|
986
|
+
Object.assign(horizontal.style, {
|
|
987
|
+
position: 'absolute',
|
|
988
|
+
left: '0',
|
|
989
|
+
right: '0',
|
|
990
|
+
bottom: '0',
|
|
991
|
+
height: `${size}px`,
|
|
992
|
+
overflowX: 'auto',
|
|
993
|
+
overflowY: 'hidden',
|
|
994
|
+
zIndex: '4',
|
|
995
|
+
});
|
|
996
|
+
Object.assign(corner.style, {
|
|
997
|
+
position: 'absolute',
|
|
998
|
+
right: '0',
|
|
999
|
+
bottom: '0',
|
|
1000
|
+
width: `${size}px`,
|
|
1001
|
+
height: `${size}px`,
|
|
1002
|
+
zIndex: '4',
|
|
1003
|
+
background: 'transparent',
|
|
1004
|
+
pointerEvents: 'none',
|
|
1005
|
+
});
|
|
1006
|
+
Object.assign(verticalSpacer.style, {
|
|
1007
|
+
width: '1px',
|
|
1008
|
+
minHeight: '1px',
|
|
1009
|
+
});
|
|
1010
|
+
Object.assign(horizontalSpacer.style, {
|
|
1011
|
+
height: '1px',
|
|
1012
|
+
minWidth: '1px',
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
host.appendChild(vertical);
|
|
1016
|
+
host.appendChild(horizontal);
|
|
1017
|
+
host.appendChild(corner);
|
|
1018
|
+
|
|
1019
|
+
let syncing = false;
|
|
1020
|
+
const sync = () => {
|
|
1021
|
+
const maxX = Math.max(0, this.viewport.contentWidth - this.viewport.viewportWidth);
|
|
1022
|
+
const maxY = Math.max(0, this.viewport.contentHeight - this.viewport.rowViewportHeight);
|
|
1023
|
+
const showX = maxX > 0;
|
|
1024
|
+
const showY = maxY > 0;
|
|
1025
|
+
|
|
1026
|
+
vertical.style.display = showY ? 'block' : 'none';
|
|
1027
|
+
horizontal.style.display = showX ? 'block' : 'none';
|
|
1028
|
+
corner.style.display = showX && showY ? 'block' : 'none';
|
|
1029
|
+
vertical.style.bottom = showX ? `${size}px` : '0';
|
|
1030
|
+
horizontal.style.right = showY ? `${size}px` : '0';
|
|
1031
|
+
vertical.style.top = `${this.viewport.headerHeight}px`;
|
|
1032
|
+
|
|
1033
|
+
verticalSpacer.style.height = `${Math.ceil(maxY + vertical.clientHeight)}px`;
|
|
1034
|
+
horizontalSpacer.style.width = `${Math.ceil(maxX + horizontal.clientWidth)}px`;
|
|
1035
|
+
|
|
1036
|
+
syncing = true;
|
|
1037
|
+
vertical.scrollTop = this.viewport.scrollY;
|
|
1038
|
+
horizontal.scrollLeft = this.viewport.scrollX;
|
|
1039
|
+
syncing = false;
|
|
1040
|
+
};
|
|
1041
|
+
const onVerticalScroll = () => {
|
|
1042
|
+
if (syncing) return;
|
|
1043
|
+
this.viewport.scrollTo(this.viewport.scrollX, vertical.scrollTop);
|
|
1044
|
+
this.events.emit('viewportchange', this.getViewportState());
|
|
1045
|
+
};
|
|
1046
|
+
const onHorizontalScroll = () => {
|
|
1047
|
+
if (syncing) return;
|
|
1048
|
+
this.viewport.scrollTo(horizontal.scrollLeft, this.viewport.scrollY);
|
|
1049
|
+
this.events.emit('viewportchange', this.getViewportState());
|
|
1050
|
+
};
|
|
1051
|
+
const onViewportChange = () => sync();
|
|
1052
|
+
|
|
1053
|
+
vertical.addEventListener('scroll', onVerticalScroll, { passive: true });
|
|
1054
|
+
horizontal.addEventListener('scroll', onHorizontalScroll, { passive: true });
|
|
1055
|
+
this.viewport.addEventListener('change', onViewportChange);
|
|
1056
|
+
sync();
|
|
1057
|
+
|
|
1058
|
+
this.#nativeScroll = {
|
|
1059
|
+
sync,
|
|
1060
|
+
applyTheme: (theme) => applyNativeScrollbarTheme([vertical, horizontal, corner], theme),
|
|
1061
|
+
destroy: () => {
|
|
1062
|
+
vertical.removeEventListener('scroll', onVerticalScroll);
|
|
1063
|
+
horizontal.removeEventListener('scroll', onHorizontalScroll);
|
|
1064
|
+
this.viewport.removeEventListener('change', onViewportChange);
|
|
1065
|
+
vertical.remove();
|
|
1066
|
+
horizontal.remove();
|
|
1067
|
+
corner.remove();
|
|
1068
|
+
if (hostPositionChanged) host.style.position = previousHostPosition;
|
|
1069
|
+
},
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
879
1073
|
#computeInspectorPaneLabelEnd(visibleRange) {
|
|
880
1074
|
const column = this.columnModel.columns.find((item) => item.kind === 'inspectorPane');
|
|
881
1075
|
if (!column) return 0;
|
|
@@ -938,6 +1132,145 @@ export class TreeViewController {
|
|
|
938
1132
|
}
|
|
939
1133
|
|
|
940
1134
|
#resizeObserver = null;
|
|
1135
|
+
#nativeScroll = null;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const NATIVE_SCROLLBAR_STYLE_ID = 'virtual-tree-canvas-native-scrollbar-style';
|
|
1139
|
+
|
|
1140
|
+
function ensureNativeScrollbarStyles(doc) {
|
|
1141
|
+
if (!doc || typeof doc.createElement !== 'function') return;
|
|
1142
|
+
if (typeof doc.getElementById === 'function' && doc.getElementById(NATIVE_SCROLLBAR_STYLE_ID)) return;
|
|
1143
|
+
const style = doc.createElement('style');
|
|
1144
|
+
style.id = NATIVE_SCROLLBAR_STYLE_ID;
|
|
1145
|
+
style.textContent = `
|
|
1146
|
+
.virtual-tree-canvas-scrollbar {
|
|
1147
|
+
--vtc-scrollbar-size: 12px;
|
|
1148
|
+
--vtc-scrollbar-track: rgba(15, 23, 42, 0.32);
|
|
1149
|
+
--vtc-scrollbar-thumb: rgba(148, 163, 184, 0.58);
|
|
1150
|
+
--vtc-scrollbar-thumb-hover: rgba(148, 163, 184, 0.78);
|
|
1151
|
+
--vtc-scrollbar-thumb-active: rgba(56, 189, 248, 0.76);
|
|
1152
|
+
--vtc-scrollbar-thumb-border: rgba(255, 255, 255, 0.08);
|
|
1153
|
+
background: var(--vtc-scrollbar-track);
|
|
1154
|
+
scrollbar-color: var(--vtc-scrollbar-thumb) var(--vtc-scrollbar-track);
|
|
1155
|
+
scrollbar-width: thin;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
.virtual-tree-canvas-scrollbar:hover {
|
|
1159
|
+
scrollbar-color: var(--vtc-scrollbar-thumb-hover) var(--vtc-scrollbar-track);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar {
|
|
1163
|
+
width: var(--vtc-scrollbar-size);
|
|
1164
|
+
height: var(--vtc-scrollbar-size);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar-track {
|
|
1168
|
+
background: var(--vtc-scrollbar-track);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar-thumb {
|
|
1172
|
+
min-width: 28px;
|
|
1173
|
+
min-height: 28px;
|
|
1174
|
+
border: 3px solid transparent;
|
|
1175
|
+
border-radius: 999px;
|
|
1176
|
+
background:
|
|
1177
|
+
linear-gradient(180deg, var(--vtc-scrollbar-thumb-highlight), transparent 52%),
|
|
1178
|
+
var(--vtc-scrollbar-thumb);
|
|
1179
|
+
background-clip: content-box;
|
|
1180
|
+
box-shadow: inset 0 0 0 1px var(--vtc-scrollbar-thumb-border);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.virtual-tree-canvas-scrollbar:hover::-webkit-scrollbar-thumb {
|
|
1184
|
+
background:
|
|
1185
|
+
linear-gradient(180deg, var(--vtc-scrollbar-thumb-highlight), transparent 52%),
|
|
1186
|
+
var(--vtc-scrollbar-thumb-hover);
|
|
1187
|
+
background-clip: content-box;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar-thumb:active {
|
|
1191
|
+
background:
|
|
1192
|
+
linear-gradient(180deg, var(--vtc-scrollbar-thumb-highlight), transparent 52%),
|
|
1193
|
+
var(--vtc-scrollbar-thumb-active);
|
|
1194
|
+
background-clip: content-box;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
.virtual-tree-canvas-scrollbar::-webkit-scrollbar-corner,
|
|
1198
|
+
.virtual-tree-canvas-scrollbar-corner {
|
|
1199
|
+
background: var(--vtc-scrollbar-track);
|
|
1200
|
+
}
|
|
1201
|
+
`;
|
|
1202
|
+
const target = doc.head ?? doc.body ?? doc.documentElement;
|
|
1203
|
+
target?.appendChild?.(style);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function applyNativeScrollbarTheme(elements, theme) {
|
|
1207
|
+
const colors = theme?.colors ?? {};
|
|
1208
|
+
const track = alphaHex(colors.row ?? colors.background ?? '#0b1020', 0.74);
|
|
1209
|
+
const thumb = mixHex(colors.borderStrong ?? colors.border ?? '#334155', colors.textMuted ?? colors.focus ?? '#94a3b8', 0.55);
|
|
1210
|
+
const hover = mixHex(thumb, colors.focus ?? '#38bdf8', 0.32);
|
|
1211
|
+
const active = mixHex(thumb, colors.focus ?? '#38bdf8', 0.52);
|
|
1212
|
+
const highlight = alphaHex('#ffffff', 0.2);
|
|
1213
|
+
const border = alphaHex('#ffffff', 0.09);
|
|
1214
|
+
for (const element of elements) {
|
|
1215
|
+
if (!element?.style?.setProperty) continue;
|
|
1216
|
+
element.style.setProperty('--vtc-scrollbar-track', track);
|
|
1217
|
+
element.style.setProperty('--vtc-scrollbar-thumb', thumb);
|
|
1218
|
+
element.style.setProperty('--vtc-scrollbar-thumb-hover', hover);
|
|
1219
|
+
element.style.setProperty('--vtc-scrollbar-thumb-active', active);
|
|
1220
|
+
element.style.setProperty('--vtc-scrollbar-thumb-highlight', highlight);
|
|
1221
|
+
element.style.setProperty('--vtc-scrollbar-thumb-border', border);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function mixHex(a, b, amount) {
|
|
1226
|
+
const from = parseHexColor(a);
|
|
1227
|
+
const to = parseHexColor(b);
|
|
1228
|
+
if (!from || !to) return amount >= 0.5 ? b : a;
|
|
1229
|
+
const t = Math.max(0, Math.min(1, amount));
|
|
1230
|
+
const value = from.map((channel, index) => Math.round(channel + (to[index] - channel) * t));
|
|
1231
|
+
return `rgb(${value[0]}, ${value[1]}, ${value[2]})`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function alphaHex(value, alpha) {
|
|
1235
|
+
const color = parseHexColor(value);
|
|
1236
|
+
if (!color) return value;
|
|
1237
|
+
return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${Math.max(0, Math.min(1, alpha))})`;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function parseHexColor(value) {
|
|
1241
|
+
const text = String(value ?? '').trim();
|
|
1242
|
+
const match = /^#([0-9a-f]{6})$/i.exec(text);
|
|
1243
|
+
if (!match) return null;
|
|
1244
|
+
const number = Number.parseInt(match[1], 16);
|
|
1245
|
+
return [(number >> 16) & 255, (number >> 8) & 255, number & 255];
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function nativeScrollbarSize() {
|
|
1249
|
+
if (typeof document === 'undefined') return 14;
|
|
1250
|
+
const probe = document.createElement('div');
|
|
1251
|
+
Object.assign(probe.style, {
|
|
1252
|
+
position: 'absolute',
|
|
1253
|
+
top: '-9999px',
|
|
1254
|
+
width: '100px',
|
|
1255
|
+
height: '100px',
|
|
1256
|
+
overflow: 'scroll',
|
|
1257
|
+
});
|
|
1258
|
+
document.body?.appendChild(probe);
|
|
1259
|
+
const size = Math.max(10, probe.offsetWidth - probe.clientWidth || 14);
|
|
1260
|
+
probe.remove();
|
|
1261
|
+
return size;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function assertPositiveNumber(value, name) {
|
|
1265
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
1266
|
+
throw new TypeError(`${name} must be a positive number`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function assertNonNegativeNumber(value, name) {
|
|
1271
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
1272
|
+
throw new TypeError(`${name} must be a non-negative number`);
|
|
1273
|
+
}
|
|
941
1274
|
}
|
|
942
1275
|
|
|
943
1276
|
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|