@pagent-libs/core 0.1.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/README.md +32 -0
- package/dist/canvas/cell-renderer.d.ts +45 -0
- package/dist/canvas/cell-renderer.d.ts.map +1 -0
- package/dist/canvas/grid-renderer.d.ts +29 -0
- package/dist/canvas/grid-renderer.d.ts.map +1 -0
- package/dist/canvas/header-renderer.d.ts +58 -0
- package/dist/canvas/header-renderer.d.ts.map +1 -0
- package/dist/canvas/hit-testing.d.ts +81 -0
- package/dist/canvas/hit-testing.d.ts.map +1 -0
- package/dist/canvas/index.d.ts +9 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/renderer.d.ts +140 -0
- package/dist/canvas/renderer.d.ts.map +1 -0
- package/dist/canvas/selection-renderer.d.ts +55 -0
- package/dist/canvas/selection-renderer.d.ts.map +1 -0
- package/dist/canvas/text-renderer.d.ts +49 -0
- package/dist/canvas/text-renderer.d.ts.map +1 -0
- package/dist/canvas/types.d.ts +200 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/collaboration/firebase-provider.d.ts +13 -0
- package/dist/collaboration/firebase-provider.d.ts.map +1 -0
- package/dist/collaboration/index.d.ts +3 -0
- package/dist/collaboration/index.d.ts.map +1 -0
- package/dist/collaboration/types.d.ts +34 -0
- package/dist/collaboration/types.d.ts.map +1 -0
- package/dist/event-emitter.d.ts +13 -0
- package/dist/event-emitter.d.ts.map +1 -0
- package/dist/export/csv.d.ts +5 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/features/filter.d.ts +58 -0
- package/dist/features/filter.d.ts.map +1 -0
- package/dist/features/freeze.d.ts +86 -0
- package/dist/features/freeze.d.ts.map +1 -0
- package/dist/features/index.d.ts +4 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/sort.d.ts +15 -0
- package/dist/features/sort.d.ts.map +1 -0
- package/dist/format-pool.d.ts +17 -0
- package/dist/format-pool.d.ts.map +1 -0
- package/dist/formula-graph.d.ts +12 -0
- package/dist/formula-graph.d.ts.map +1 -0
- package/dist/formula-parser/cell-reference.d.ts +7 -0
- package/dist/formula-parser/cell-reference.d.ts.map +1 -0
- package/dist/formula-parser/formula-adjust.d.ts +13 -0
- package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
- package/dist/formula-parser/formula-ranges.d.ts +22 -0
- package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
- package/dist/formula-parser/index.d.ts +6 -0
- package/dist/formula-parser/index.d.ts.map +1 -0
- package/dist/formula-parser/parser.d.ts +18 -0
- package/dist/formula-parser/parser.d.ts.map +1 -0
- package/dist/formula-parser/types.d.ts +33 -0
- package/dist/formula-parser/types.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +5823 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +5885 -0
- package/dist/index.js.map +1 -0
- package/dist/sheet.d.ts +119 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/style-pool.d.ts +17 -0
- package/dist/style-pool.d.ts.map +1 -0
- package/dist/types.d.ts +260 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/cell-key.d.ts +7 -0
- package/dist/utils/cell-key.d.ts.map +1 -0
- package/dist/utils/format-utils.d.ts +75 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/range.d.ts +13 -0
- package/dist/utils/range.d.ts.map +1 -0
- package/dist/workbook.d.ts +155 -0
- package/dist/workbook.d.ts.map +1 -0
- package/package.json +46 -0
- package/src/canvas/cell-renderer.ts +181 -0
- package/src/canvas/grid-renderer.ts +238 -0
- package/src/canvas/header-renderer.ts +402 -0
- package/src/canvas/hit-testing.ts +537 -0
- package/src/canvas/index.ts +16 -0
- package/src/canvas/renderer.ts +1056 -0
- package/src/canvas/selection-renderer.ts +604 -0
- package/src/canvas/text-renderer.ts +321 -0
- package/src/canvas/types.ts +289 -0
- package/src/collaboration/firebase-provider.ts +48 -0
- package/src/collaboration/index.ts +5 -0
- package/src/collaboration/types.ts +38 -0
- package/src/event-emitter.ts +73 -0
- package/src/export/csv.ts +101 -0
- package/src/export/index.ts +4 -0
- package/src/features/filter.ts +231 -0
- package/src/features/freeze.ts +271 -0
- package/src/features/index.ts +5 -0
- package/src/features/sort.ts +282 -0
- package/src/format-pool.ts +61 -0
- package/src/formula-graph.ts +84 -0
- package/src/formula-parser/cell-reference.ts +99 -0
- package/src/formula-parser/formula-adjust.ts +129 -0
- package/src/formula-parser/formula-ranges.ts +159 -0
- package/src/formula-parser/index.ts +8 -0
- package/src/formula-parser/parser.ts +438 -0
- package/src/formula-parser/types.ts +39 -0
- package/src/index.ts +25 -0
- package/src/sheet.ts +502 -0
- package/src/style-pool.ts +62 -0
- package/src/types.ts +291 -0
- package/src/utils/cell-key.ts +19 -0
- package/src/utils/format-utils.ts +515 -0
- package/src/utils/range.ts +53 -0
- package/src/workbook.ts +1031 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
// Main Canvas Renderer - Coordinates all sub-renderers
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CanvasRendererConfig,
|
|
5
|
+
RenderState,
|
|
6
|
+
Viewport,
|
|
7
|
+
CellPosition,
|
|
8
|
+
DirtyRegion,
|
|
9
|
+
CanvasTheme,
|
|
10
|
+
Rect,
|
|
11
|
+
} from './types';
|
|
12
|
+
import { DEFAULT_THEME } from './types';
|
|
13
|
+
import { TextRenderer } from './text-renderer';
|
|
14
|
+
import { GridRenderer } from './grid-renderer';
|
|
15
|
+
import { CellRenderer } from './cell-renderer';
|
|
16
|
+
import { HeaderRenderer } from './header-renderer';
|
|
17
|
+
import { SelectionRenderer } from './selection-renderer';
|
|
18
|
+
import { HitTester } from './hit-testing';
|
|
19
|
+
import {
|
|
20
|
+
calculateFreezeDimensions,
|
|
21
|
+
type FreezeRegion,
|
|
22
|
+
type FreezeDimensions
|
|
23
|
+
} from '../features/freeze';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Main canvas renderer that coordinates all rendering operations
|
|
27
|
+
*/
|
|
28
|
+
export class CanvasRenderer {
|
|
29
|
+
private canvas: HTMLCanvasElement;
|
|
30
|
+
private ctx: CanvasRenderingContext2D;
|
|
31
|
+
private devicePixelRatio: number;
|
|
32
|
+
|
|
33
|
+
// Configuration
|
|
34
|
+
private defaultRowHeight: number;
|
|
35
|
+
private defaultColWidth: number;
|
|
36
|
+
private headerHeight: number;
|
|
37
|
+
private headerWidth: number;
|
|
38
|
+
private theme: CanvasTheme;
|
|
39
|
+
|
|
40
|
+
// Viewport state
|
|
41
|
+
private viewport: Viewport;
|
|
42
|
+
|
|
43
|
+
// Current render state
|
|
44
|
+
private renderState: RenderState | null = null;
|
|
45
|
+
|
|
46
|
+
// Sub-renderers
|
|
47
|
+
private textRenderer: TextRenderer;
|
|
48
|
+
private gridRenderer: GridRenderer;
|
|
49
|
+
private cellRenderer: CellRenderer;
|
|
50
|
+
private headerRenderer: HeaderRenderer;
|
|
51
|
+
private selectionRenderer: SelectionRenderer;
|
|
52
|
+
|
|
53
|
+
// Hit tester
|
|
54
|
+
private hitTester: HitTester;
|
|
55
|
+
|
|
56
|
+
// Dirty tracking
|
|
57
|
+
private isDirty: boolean = true;
|
|
58
|
+
private dirtyRegions: DirtyRegion[] = [];
|
|
59
|
+
|
|
60
|
+
// Animation frame handle
|
|
61
|
+
private animationFrameId: number | null = null;
|
|
62
|
+
|
|
63
|
+
// Freeze panes state
|
|
64
|
+
private freezeDimensions: FreezeDimensions = { frozenWidth: 0, frozenHeight: 0 };
|
|
65
|
+
private frozenRows: number = 0;
|
|
66
|
+
private frozenCols: number = 0;
|
|
67
|
+
|
|
68
|
+
constructor(config: CanvasRendererConfig) {
|
|
69
|
+
this.canvas = config.canvas;
|
|
70
|
+
const ctx = this.canvas.getContext('2d');
|
|
71
|
+
if (!ctx) {
|
|
72
|
+
throw new Error('Failed to get 2D context from canvas');
|
|
73
|
+
}
|
|
74
|
+
this.ctx = ctx;
|
|
75
|
+
|
|
76
|
+
this.devicePixelRatio = config.devicePixelRatio ?? window.devicePixelRatio ?? 1;
|
|
77
|
+
this.defaultRowHeight = config.defaultRowHeight;
|
|
78
|
+
this.defaultColWidth = config.defaultColWidth;
|
|
79
|
+
this.headerHeight = config.headerHeight;
|
|
80
|
+
this.headerWidth = config.headerWidth;
|
|
81
|
+
this.theme = { ...DEFAULT_THEME, ...config.theme };
|
|
82
|
+
|
|
83
|
+
// Initialize viewport
|
|
84
|
+
this.viewport = {
|
|
85
|
+
scrollTop: 0,
|
|
86
|
+
scrollLeft: 0,
|
|
87
|
+
width: this.canvas.width / this.devicePixelRatio,
|
|
88
|
+
height: this.canvas.height / this.devicePixelRatio,
|
|
89
|
+
startRow: 0,
|
|
90
|
+
endRow: 0,
|
|
91
|
+
startCol: 0,
|
|
92
|
+
endCol: 0,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Initialize sub-renderers
|
|
96
|
+
this.textRenderer = new TextRenderer(this.theme);
|
|
97
|
+
this.gridRenderer = new GridRenderer(this.theme);
|
|
98
|
+
this.cellRenderer = new CellRenderer(this.theme, this.textRenderer);
|
|
99
|
+
this.headerRenderer = new HeaderRenderer(this.theme);
|
|
100
|
+
this.selectionRenderer = new SelectionRenderer(this.theme);
|
|
101
|
+
|
|
102
|
+
// Initialize hit tester
|
|
103
|
+
this.hitTester = new HitTester(
|
|
104
|
+
this.headerWidth,
|
|
105
|
+
this.headerHeight,
|
|
106
|
+
this.defaultRowHeight,
|
|
107
|
+
this.defaultColWidth
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Set up canvas for high DPI
|
|
111
|
+
this.setupCanvas();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Set up canvas for high DPI displays
|
|
116
|
+
*/
|
|
117
|
+
private setupCanvas(): void {
|
|
118
|
+
const { width, height } = this.canvas.getBoundingClientRect();
|
|
119
|
+
|
|
120
|
+
// Set actual size in memory (scaled for device pixel ratio)
|
|
121
|
+
this.canvas.width = width * this.devicePixelRatio;
|
|
122
|
+
this.canvas.height = height * this.devicePixelRatio;
|
|
123
|
+
|
|
124
|
+
// Scale the context to match device pixel ratio
|
|
125
|
+
this.ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
|
|
126
|
+
|
|
127
|
+
// Update viewport dimensions
|
|
128
|
+
this.viewport.width = width;
|
|
129
|
+
this.viewport.height = height;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resize the canvas
|
|
134
|
+
*/
|
|
135
|
+
resize(width: number, height: number): void {
|
|
136
|
+
this.canvas.style.width = `${width}px`;
|
|
137
|
+
this.canvas.style.height = `${height}px`;
|
|
138
|
+
this.canvas.width = width * this.devicePixelRatio;
|
|
139
|
+
this.canvas.height = height * this.devicePixelRatio;
|
|
140
|
+
|
|
141
|
+
this.ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
|
|
142
|
+
|
|
143
|
+
this.viewport.width = width;
|
|
144
|
+
this.viewport.height = height;
|
|
145
|
+
|
|
146
|
+
this.invalidate();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Set the viewport scroll position
|
|
151
|
+
*/
|
|
152
|
+
setViewport(scrollTop: number, scrollLeft: number): void {
|
|
153
|
+
if (this.viewport.scrollTop === scrollTop && this.viewport.scrollLeft === scrollLeft) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.viewport.scrollTop = scrollTop;
|
|
158
|
+
this.viewport.scrollLeft = scrollLeft;
|
|
159
|
+
|
|
160
|
+
// Update hit tester
|
|
161
|
+
this.hitTester.setScroll(scrollTop, scrollLeft);
|
|
162
|
+
|
|
163
|
+
// Recalculate visible range if we have render state
|
|
164
|
+
if (this.renderState) {
|
|
165
|
+
this.calculateVisibleRange(this.renderState);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.invalidate();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Calculate the visible range based on current viewport and optionally provided dimensions.
|
|
173
|
+
* This can be called before setState to get an accurate viewport for cell loading.
|
|
174
|
+
*/
|
|
175
|
+
calculateVisibleRangeForDimensions(
|
|
176
|
+
rowCount: number,
|
|
177
|
+
colCount: number,
|
|
178
|
+
rowHeights?: Map<number, number>,
|
|
179
|
+
colWidths?: Map<number, number>
|
|
180
|
+
): void {
|
|
181
|
+
const { scrollTop, scrollLeft, width, height } = this.viewport;
|
|
182
|
+
const defaultRowHeight = this.defaultRowHeight;
|
|
183
|
+
const defaultColWidth = this.defaultColWidth;
|
|
184
|
+
|
|
185
|
+
// Calculate visible rows
|
|
186
|
+
let y = 0;
|
|
187
|
+
let startRow = 0;
|
|
188
|
+
while (y < scrollTop && startRow < rowCount) {
|
|
189
|
+
y += rowHeights?.get(startRow) ?? defaultRowHeight;
|
|
190
|
+
startRow++;
|
|
191
|
+
}
|
|
192
|
+
if (startRow > 0) startRow--;
|
|
193
|
+
|
|
194
|
+
let endRow = startRow;
|
|
195
|
+
const visibleHeight = height - this.headerHeight;
|
|
196
|
+
while (y < scrollTop + visibleHeight && endRow < rowCount) {
|
|
197
|
+
y += rowHeights?.get(endRow) ?? defaultRowHeight;
|
|
198
|
+
endRow++;
|
|
199
|
+
}
|
|
200
|
+
endRow = Math.min(endRow + 1, rowCount);
|
|
201
|
+
|
|
202
|
+
// Calculate visible columns
|
|
203
|
+
let x = 0;
|
|
204
|
+
let startCol = 0;
|
|
205
|
+
while (x < scrollLeft && startCol < colCount) {
|
|
206
|
+
x += colWidths?.get(startCol) ?? defaultColWidth;
|
|
207
|
+
startCol++;
|
|
208
|
+
}
|
|
209
|
+
if (startCol > 0) startCol--;
|
|
210
|
+
|
|
211
|
+
let endCol = startCol;
|
|
212
|
+
const visibleWidth = width - this.headerWidth;
|
|
213
|
+
while (x < scrollLeft + visibleWidth && endCol < colCount) {
|
|
214
|
+
x += colWidths?.get(endCol) ?? defaultColWidth;
|
|
215
|
+
endCol++;
|
|
216
|
+
}
|
|
217
|
+
endCol = Math.min(endCol + 1, colCount);
|
|
218
|
+
|
|
219
|
+
this.viewport.startRow = startRow;
|
|
220
|
+
this.viewport.endRow = endRow;
|
|
221
|
+
this.viewport.startCol = startCol;
|
|
222
|
+
this.viewport.endCol = endCol;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Update the render state
|
|
227
|
+
*/
|
|
228
|
+
setState(state: RenderState): void {
|
|
229
|
+
this.renderState = state;
|
|
230
|
+
|
|
231
|
+
// Calculate freeze dimensions
|
|
232
|
+
this.frozenRows = state.frozenRows ?? 0;
|
|
233
|
+
this.frozenCols = state.frozenCols ?? 0;
|
|
234
|
+
this.freezeDimensions = calculateFreezeDimensions(
|
|
235
|
+
this.frozenRows,
|
|
236
|
+
this.frozenCols,
|
|
237
|
+
state.rowHeights,
|
|
238
|
+
state.colWidths,
|
|
239
|
+
this.defaultRowHeight,
|
|
240
|
+
this.defaultColWidth,
|
|
241
|
+
state.hiddenRows,
|
|
242
|
+
state.hiddenCols
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Update hit tester with row/col dimensions, hidden rows/cols, and freeze config
|
|
246
|
+
this.hitTester.setDimensions(
|
|
247
|
+
state.rowHeights,
|
|
248
|
+
state.colWidths,
|
|
249
|
+
this.defaultRowHeight,
|
|
250
|
+
this.defaultColWidth,
|
|
251
|
+
state.rowCount,
|
|
252
|
+
state.colCount,
|
|
253
|
+
state.hiddenRows,
|
|
254
|
+
state.hiddenCols
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Update hit tester with freeze config
|
|
258
|
+
this.hitTester.setFreezeConfig(
|
|
259
|
+
this.frozenRows,
|
|
260
|
+
this.frozenCols,
|
|
261
|
+
this.freezeDimensions.frozenWidth,
|
|
262
|
+
this.freezeDimensions.frozenHeight
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Calculate visible range immediately so getViewport() returns correct values
|
|
266
|
+
this.calculateVisibleRange(state);
|
|
267
|
+
|
|
268
|
+
this.invalidate();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Mark the canvas as needing redraw
|
|
273
|
+
*/
|
|
274
|
+
invalidate(region?: DirtyRegion): void {
|
|
275
|
+
this.isDirty = true;
|
|
276
|
+
if (region) {
|
|
277
|
+
this.dirtyRegions.push(region);
|
|
278
|
+
} else {
|
|
279
|
+
this.dirtyRegions = [{ type: 'all' }];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Schedule a render on the next animation frame
|
|
283
|
+
if (this.animationFrameId === null) {
|
|
284
|
+
this.animationFrameId = requestAnimationFrame(() => {
|
|
285
|
+
this.animationFrameId = null;
|
|
286
|
+
this.render();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Force an immediate render
|
|
293
|
+
*/
|
|
294
|
+
renderNow(): void {
|
|
295
|
+
if (this.animationFrameId !== null) {
|
|
296
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
297
|
+
this.animationFrameId = null;
|
|
298
|
+
}
|
|
299
|
+
this.render();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Main render method
|
|
304
|
+
*/
|
|
305
|
+
private render(): void {
|
|
306
|
+
if (!this.isDirty) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!this.renderState) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const state = this.renderState;
|
|
315
|
+
const { frozenWidth, frozenHeight } = this.freezeDimensions;
|
|
316
|
+
const hasFrozenRows = this.frozenRows > 0;
|
|
317
|
+
const hasFrozenCols = this.frozenCols > 0;
|
|
318
|
+
|
|
319
|
+
// Calculate visible range
|
|
320
|
+
this.calculateVisibleRange(state);
|
|
321
|
+
|
|
322
|
+
// Clear the canvas
|
|
323
|
+
this.ctx.clearRect(0, 0, this.viewport.width, this.viewport.height);
|
|
324
|
+
|
|
325
|
+
// Save context state
|
|
326
|
+
this.ctx.save();
|
|
327
|
+
|
|
328
|
+
// Render in order (back to front):
|
|
329
|
+
// For freeze panes, we render 4 regions in order:
|
|
330
|
+
// 1. Main scrollable area (scrolls both ways)
|
|
331
|
+
// 2. Left frozen column area (scrolls vertically only)
|
|
332
|
+
// 3. Top frozen row area (scrolls horizontally only)
|
|
333
|
+
// 4. Top-left corner (never scrolls)
|
|
334
|
+
|
|
335
|
+
// --- MAIN SCROLLABLE AREA ---
|
|
336
|
+
this.renderRegion(state, 'main');
|
|
337
|
+
|
|
338
|
+
// --- LEFT FROZEN AREA (if frozen cols exist) ---
|
|
339
|
+
if (hasFrozenCols) {
|
|
340
|
+
this.renderRegion(state, 'left');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --- TOP FROZEN AREA (if frozen rows exist) ---
|
|
344
|
+
if (hasFrozenRows) {
|
|
345
|
+
this.renderRegion(state, 'top');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// --- TOP-LEFT FROZEN CORNER (if both frozen rows and cols exist) ---
|
|
349
|
+
if (hasFrozenRows && hasFrozenCols) {
|
|
350
|
+
this.renderRegion(state, 'top-left');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// --- FREEZE DIVIDER LINES ---
|
|
354
|
+
if (hasFrozenRows || hasFrozenCols) {
|
|
355
|
+
this.renderFreezeDividers(frozenWidth, frozenHeight);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// --- HEADERS ---
|
|
359
|
+
this.renderHeaders(state);
|
|
360
|
+
|
|
361
|
+
// --- CORNER CELL ---
|
|
362
|
+
this.renderCornerCell();
|
|
363
|
+
|
|
364
|
+
// Restore context state
|
|
365
|
+
this.ctx.restore();
|
|
366
|
+
|
|
367
|
+
// Reset dirty state
|
|
368
|
+
this.isDirty = false;
|
|
369
|
+
this.dirtyRegions = [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Render a specific freeze region (cells, grid lines, selection)
|
|
374
|
+
*/
|
|
375
|
+
private renderRegion(state: RenderState, region: FreezeRegion): void {
|
|
376
|
+
const { frozenWidth, frozenHeight } = this.freezeDimensions;
|
|
377
|
+
const { scrollTop, scrollLeft, width, height } = this.viewport;
|
|
378
|
+
|
|
379
|
+
// Calculate clip rect based on region
|
|
380
|
+
let clipX: number, clipY: number, clipW: number, clipH: number;
|
|
381
|
+
let effectiveScrollTop: number, effectiveScrollLeft: number;
|
|
382
|
+
let startRow: number, endRow: number, startCol: number, endCol: number;
|
|
383
|
+
|
|
384
|
+
switch (region) {
|
|
385
|
+
case 'top-left':
|
|
386
|
+
// Never scrolls, shows frozen rows and cols
|
|
387
|
+
clipX = this.headerWidth;
|
|
388
|
+
clipY = this.headerHeight;
|
|
389
|
+
clipW = frozenWidth;
|
|
390
|
+
clipH = frozenHeight;
|
|
391
|
+
effectiveScrollTop = 0;
|
|
392
|
+
effectiveScrollLeft = 0;
|
|
393
|
+
startRow = 0;
|
|
394
|
+
endRow = this.frozenRows;
|
|
395
|
+
startCol = 0;
|
|
396
|
+
endCol = this.frozenCols;
|
|
397
|
+
break;
|
|
398
|
+
|
|
399
|
+
case 'top':
|
|
400
|
+
// Scrolls horizontally only, shows frozen rows
|
|
401
|
+
clipX = this.headerWidth + frozenWidth;
|
|
402
|
+
clipY = this.headerHeight;
|
|
403
|
+
clipW = width - this.headerWidth - frozenWidth;
|
|
404
|
+
clipH = frozenHeight;
|
|
405
|
+
effectiveScrollTop = 0;
|
|
406
|
+
effectiveScrollLeft = scrollLeft;
|
|
407
|
+
startRow = 0;
|
|
408
|
+
endRow = this.frozenRows;
|
|
409
|
+
startCol = this.viewport.startCol;
|
|
410
|
+
endCol = this.viewport.endCol;
|
|
411
|
+
// Ensure we don't render frozen cols in this region
|
|
412
|
+
if (startCol < this.frozenCols) startCol = this.frozenCols;
|
|
413
|
+
break;
|
|
414
|
+
|
|
415
|
+
case 'left':
|
|
416
|
+
// Scrolls vertically only, shows frozen cols
|
|
417
|
+
clipX = this.headerWidth;
|
|
418
|
+
clipY = this.headerHeight + frozenHeight;
|
|
419
|
+
clipW = frozenWidth;
|
|
420
|
+
clipH = height - this.headerHeight - frozenHeight;
|
|
421
|
+
effectiveScrollTop = scrollTop;
|
|
422
|
+
effectiveScrollLeft = 0;
|
|
423
|
+
startRow = this.viewport.startRow;
|
|
424
|
+
endRow = this.viewport.endRow;
|
|
425
|
+
startCol = 0;
|
|
426
|
+
endCol = this.frozenCols;
|
|
427
|
+
// Ensure we don't render frozen rows in this region
|
|
428
|
+
if (startRow < this.frozenRows) startRow = this.frozenRows;
|
|
429
|
+
break;
|
|
430
|
+
|
|
431
|
+
case 'main':
|
|
432
|
+
default:
|
|
433
|
+
// Scrolls both ways
|
|
434
|
+
clipX = this.headerWidth + frozenWidth;
|
|
435
|
+
clipY = this.headerHeight + frozenHeight;
|
|
436
|
+
clipW = width - this.headerWidth - frozenWidth;
|
|
437
|
+
clipH = height - this.headerHeight - frozenHeight;
|
|
438
|
+
effectiveScrollTop = scrollTop;
|
|
439
|
+
effectiveScrollLeft = scrollLeft;
|
|
440
|
+
startRow = this.viewport.startRow;
|
|
441
|
+
endRow = this.viewport.endRow;
|
|
442
|
+
startCol = this.viewport.startCol;
|
|
443
|
+
endCol = this.viewport.endCol;
|
|
444
|
+
// Ensure we don't render frozen rows/cols in main region
|
|
445
|
+
if (startRow < this.frozenRows) startRow = this.frozenRows;
|
|
446
|
+
if (startCol < this.frozenCols) startCol = this.frozenCols;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Skip if clip region is invalid
|
|
451
|
+
if (clipW <= 0 || clipH <= 0) return;
|
|
452
|
+
|
|
453
|
+
// Render cells, grid lines, and selection for this region
|
|
454
|
+
this.ctx.save();
|
|
455
|
+
this.ctx.beginPath();
|
|
456
|
+
this.ctx.rect(clipX, clipY, clipW, clipH);
|
|
457
|
+
this.ctx.clip();
|
|
458
|
+
|
|
459
|
+
// Render cells
|
|
460
|
+
this.renderCellsInRegion(
|
|
461
|
+
state,
|
|
462
|
+
startRow, endRow, startCol, endCol,
|
|
463
|
+
effectiveScrollTop, effectiveScrollLeft,
|
|
464
|
+
region
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// Render grid lines
|
|
468
|
+
this.renderGridLinesInRegion(
|
|
469
|
+
state,
|
|
470
|
+
startRow, endRow, startCol, endCol,
|
|
471
|
+
effectiveScrollTop, effectiveScrollLeft,
|
|
472
|
+
region
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Render selection
|
|
476
|
+
this.renderSelectionInRegion(
|
|
477
|
+
state,
|
|
478
|
+
effectiveScrollTop, effectiveScrollLeft,
|
|
479
|
+
region
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
this.ctx.restore();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Render freeze divider lines
|
|
487
|
+
*/
|
|
488
|
+
private renderFreezeDividers(frozenWidth: number, frozenHeight: number): void {
|
|
489
|
+
const { width, height } = this.viewport;
|
|
490
|
+
|
|
491
|
+
this.ctx.save();
|
|
492
|
+
this.ctx.strokeStyle = this.theme.freezeDividerColor;
|
|
493
|
+
this.ctx.lineWidth = this.theme.freezeDividerWidth;
|
|
494
|
+
|
|
495
|
+
// Vertical divider (after frozen columns)
|
|
496
|
+
if (this.frozenCols > 0 && frozenWidth > 0) {
|
|
497
|
+
const dividerX = this.headerWidth + frozenWidth;
|
|
498
|
+
this.ctx.beginPath();
|
|
499
|
+
this.ctx.moveTo(dividerX, this.headerHeight);
|
|
500
|
+
this.ctx.lineTo(dividerX, height);
|
|
501
|
+
this.ctx.stroke();
|
|
502
|
+
|
|
503
|
+
// Add subtle shadow
|
|
504
|
+
this.ctx.fillStyle = this.theme.freezeShadowColor;
|
|
505
|
+
this.ctx.fillRect(dividerX, this.headerHeight, 4, height - this.headerHeight);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Horizontal divider (after frozen rows)
|
|
509
|
+
if (this.frozenRows > 0 && frozenHeight > 0) {
|
|
510
|
+
const dividerY = this.headerHeight + frozenHeight;
|
|
511
|
+
this.ctx.beginPath();
|
|
512
|
+
this.ctx.moveTo(this.headerWidth, dividerY);
|
|
513
|
+
this.ctx.lineTo(width, dividerY);
|
|
514
|
+
this.ctx.stroke();
|
|
515
|
+
|
|
516
|
+
// Add subtle shadow
|
|
517
|
+
this.ctx.fillStyle = this.theme.freezeShadowColor;
|
|
518
|
+
this.ctx.fillRect(this.headerWidth, dividerY, width - this.headerWidth, 4);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.ctx.restore();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Calculate which rows and columns are visible
|
|
526
|
+
*/
|
|
527
|
+
private calculateVisibleRange(state: RenderState): void {
|
|
528
|
+
const { scrollTop, scrollLeft, width, height } = this.viewport;
|
|
529
|
+
const hiddenRows = state.hiddenRows ?? new Set<number>();
|
|
530
|
+
const hiddenCols = state.hiddenCols ?? new Set<number>();
|
|
531
|
+
|
|
532
|
+
// Calculate visible rows (skip hidden rows)
|
|
533
|
+
// Start from frozenRows since frozen rows don't scroll
|
|
534
|
+
let y = 0;
|
|
535
|
+
let startRow = this.frozenRows;
|
|
536
|
+
while (y < scrollTop && startRow < state.rowCount) {
|
|
537
|
+
if (!hiddenRows.has(startRow)) {
|
|
538
|
+
y += state.rowHeights.get(startRow) ?? this.defaultRowHeight;
|
|
539
|
+
}
|
|
540
|
+
startRow++;
|
|
541
|
+
}
|
|
542
|
+
if (startRow > this.frozenRows) startRow--; // Include partially visible row
|
|
543
|
+
|
|
544
|
+
let endRow = startRow;
|
|
545
|
+
const visibleHeight = height - this.headerHeight - this.freezeDimensions.frozenHeight;
|
|
546
|
+
while (y < scrollTop + visibleHeight && endRow < state.rowCount) {
|
|
547
|
+
if (!hiddenRows.has(endRow)) {
|
|
548
|
+
y += state.rowHeights.get(endRow) ?? this.defaultRowHeight;
|
|
549
|
+
}
|
|
550
|
+
endRow++;
|
|
551
|
+
}
|
|
552
|
+
endRow = Math.min(endRow + 1, state.rowCount); // Include partially visible row
|
|
553
|
+
|
|
554
|
+
// Calculate visible columns (skip hidden columns)
|
|
555
|
+
// Start from frozenCols since frozen columns don't scroll
|
|
556
|
+
let x = 0;
|
|
557
|
+
let startCol = this.frozenCols;
|
|
558
|
+
while (x < scrollLeft && startCol < state.colCount) {
|
|
559
|
+
if (!hiddenCols.has(startCol)) {
|
|
560
|
+
x += state.colWidths.get(startCol) ?? this.defaultColWidth;
|
|
561
|
+
}
|
|
562
|
+
startCol++;
|
|
563
|
+
}
|
|
564
|
+
if (startCol > this.frozenCols) startCol--; // Include partially visible column
|
|
565
|
+
|
|
566
|
+
let endCol = startCol;
|
|
567
|
+
const visibleWidth = width - this.headerWidth - this.freezeDimensions.frozenWidth;
|
|
568
|
+
while (x < scrollLeft + visibleWidth && endCol < state.colCount) {
|
|
569
|
+
if (!hiddenCols.has(endCol)) {
|
|
570
|
+
x += state.colWidths.get(endCol) ?? this.defaultColWidth;
|
|
571
|
+
}
|
|
572
|
+
endCol++;
|
|
573
|
+
}
|
|
574
|
+
endCol = Math.min(endCol + 1, state.colCount); // Include partially visible column
|
|
575
|
+
|
|
576
|
+
this.viewport.startRow = startRow;
|
|
577
|
+
this.viewport.endRow = endRow;
|
|
578
|
+
this.viewport.startCol = startCol;
|
|
579
|
+
this.viewport.endCol = endCol;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Render cells in a specific region
|
|
584
|
+
*/
|
|
585
|
+
private renderCellsInRegion(
|
|
586
|
+
state: RenderState,
|
|
587
|
+
startRow: number,
|
|
588
|
+
endRow: number,
|
|
589
|
+
startCol: number,
|
|
590
|
+
endCol: number,
|
|
591
|
+
effectiveScrollTop: number,
|
|
592
|
+
effectiveScrollLeft: number,
|
|
593
|
+
region: FreezeRegion
|
|
594
|
+
): void {
|
|
595
|
+
const hiddenRows = state.hiddenRows ?? new Set<number>();
|
|
596
|
+
const hiddenCols = state.hiddenCols ?? new Set<number>();
|
|
597
|
+
const { frozenWidth, frozenHeight } = this.freezeDimensions;
|
|
598
|
+
|
|
599
|
+
// Calculate starting Y position based on region
|
|
600
|
+
let startY: number;
|
|
601
|
+
if (region === 'top-left' || region === 'top') {
|
|
602
|
+
// Frozen rows - start at header
|
|
603
|
+
startY = this.headerHeight;
|
|
604
|
+
for (let r = 0; r < startRow; r++) {
|
|
605
|
+
if (!hiddenRows.has(r)) {
|
|
606
|
+
startY += state.rowHeights.get(r) ?? this.defaultRowHeight;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
// Non-frozen rows - account for frozen area and scroll
|
|
611
|
+
startY = this.headerHeight + frozenHeight;
|
|
612
|
+
for (let r = this.frozenRows; r < startRow; r++) {
|
|
613
|
+
if (!hiddenRows.has(r)) {
|
|
614
|
+
startY += state.rowHeights.get(r) ?? this.defaultRowHeight;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
startY -= effectiveScrollTop;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Calculate starting X position based on region
|
|
621
|
+
let startX: number;
|
|
622
|
+
if (region === 'top-left' || region === 'left') {
|
|
623
|
+
// Frozen cols - start at header
|
|
624
|
+
startX = this.headerWidth;
|
|
625
|
+
for (let c = 0; c < startCol; c++) {
|
|
626
|
+
if (!hiddenCols.has(c)) {
|
|
627
|
+
startX += state.colWidths.get(c) ?? this.defaultColWidth;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
// Non-frozen cols - account for frozen area and scroll
|
|
632
|
+
startX = this.headerWidth + frozenWidth;
|
|
633
|
+
for (let c = this.frozenCols; c < startCol; c++) {
|
|
634
|
+
if (!hiddenCols.has(c)) {
|
|
635
|
+
startX += state.colWidths.get(c) ?? this.defaultColWidth;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
startX -= effectiveScrollLeft;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Render visible cells (skip hidden and filtered rows/cols)
|
|
642
|
+
let y = startY;
|
|
643
|
+
for (let row = startRow; row < endRow; row++) {
|
|
644
|
+
// Skip hidden rows
|
|
645
|
+
if (hiddenRows.has(row)) continue;
|
|
646
|
+
// Skip filtered rows (only show rows that pass all filters)
|
|
647
|
+
if (state.filteredRows && !state.filteredRows.has(row)) continue;
|
|
648
|
+
|
|
649
|
+
const rowHeight = state.rowHeights.get(row) ?? this.defaultRowHeight;
|
|
650
|
+
let x = startX;
|
|
651
|
+
|
|
652
|
+
for (let col = startCol; col < endCol; col++) {
|
|
653
|
+
// Skip hidden columns
|
|
654
|
+
if (hiddenCols.has(col)) continue;
|
|
655
|
+
|
|
656
|
+
const colWidth = state.colWidths.get(col) ?? this.defaultColWidth;
|
|
657
|
+
const cellKey = `${row}:${col}`;
|
|
658
|
+
const cell = state.cells.get(cellKey);
|
|
659
|
+
const style = cell?.styleId ? state.styles.get(cell.styleId) : undefined;
|
|
660
|
+
const format = cell?.formatId ? state.formats.get(cell.formatId) : undefined;
|
|
661
|
+
|
|
662
|
+
const bounds: Rect = { x, y, width: colWidth, height: rowHeight };
|
|
663
|
+
|
|
664
|
+
// Skip rendering if this is the editing cell
|
|
665
|
+
const isEditing = state.editingCell?.row === row && state.editingCell?.col === col;
|
|
666
|
+
if (!isEditing) {
|
|
667
|
+
this.cellRenderer.renderCell(this.ctx, cell, bounds, style, format);
|
|
668
|
+
} else {
|
|
669
|
+
// Render empty cell background for editing cell
|
|
670
|
+
this.cellRenderer.renderEmptyCell(this.ctx, bounds);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
x += colWidth;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
y += rowHeight;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Render grid lines in a specific region
|
|
682
|
+
*/
|
|
683
|
+
private renderGridLinesInRegion(
|
|
684
|
+
state: RenderState,
|
|
685
|
+
startRow: number,
|
|
686
|
+
endRow: number,
|
|
687
|
+
startCol: number,
|
|
688
|
+
endCol: number,
|
|
689
|
+
effectiveScrollTop: number,
|
|
690
|
+
effectiveScrollLeft: number,
|
|
691
|
+
region: FreezeRegion
|
|
692
|
+
): void {
|
|
693
|
+
const { frozenWidth, frozenHeight } = this.freezeDimensions;
|
|
694
|
+
|
|
695
|
+
// Create a modified viewport for this region
|
|
696
|
+
const regionViewport: Viewport = {
|
|
697
|
+
...this.viewport,
|
|
698
|
+
scrollTop: effectiveScrollTop,
|
|
699
|
+
scrollLeft: effectiveScrollLeft,
|
|
700
|
+
startRow,
|
|
701
|
+
endRow,
|
|
702
|
+
startCol,
|
|
703
|
+
endCol,
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Determine header offset based on region
|
|
707
|
+
let headerWidthOffset = this.headerWidth;
|
|
708
|
+
let headerHeightOffset = this.headerHeight;
|
|
709
|
+
|
|
710
|
+
if (region === 'main' || region === 'top') {
|
|
711
|
+
headerWidthOffset = this.headerWidth + frozenWidth;
|
|
712
|
+
}
|
|
713
|
+
if (region === 'main' || region === 'left') {
|
|
714
|
+
headerHeightOffset = this.headerHeight + frozenHeight;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
this.gridRenderer.renderGridLines(
|
|
718
|
+
this.ctx,
|
|
719
|
+
regionViewport,
|
|
720
|
+
state.rowHeights,
|
|
721
|
+
state.colWidths,
|
|
722
|
+
this.defaultRowHeight,
|
|
723
|
+
this.defaultColWidth,
|
|
724
|
+
headerWidthOffset,
|
|
725
|
+
headerHeightOffset,
|
|
726
|
+
startRow,
|
|
727
|
+
endRow,
|
|
728
|
+
startCol,
|
|
729
|
+
endCol,
|
|
730
|
+
state.hiddenRows,
|
|
731
|
+
state.hiddenCols
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Render selection in a specific region
|
|
737
|
+
*/
|
|
738
|
+
private renderSelectionInRegion(
|
|
739
|
+
state: RenderState,
|
|
740
|
+
effectiveScrollTop: number,
|
|
741
|
+
effectiveScrollLeft: number,
|
|
742
|
+
region: FreezeRegion
|
|
743
|
+
): void {
|
|
744
|
+
if (!state.selection && !state.activeCell && (!state.formulaRanges || state.formulaRanges.length === 0)) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const { frozenWidth, frozenHeight } = this.freezeDimensions;
|
|
749
|
+
|
|
750
|
+
// Create a modified viewport for this region
|
|
751
|
+
const regionViewport: Viewport = {
|
|
752
|
+
...this.viewport,
|
|
753
|
+
scrollTop: effectiveScrollTop,
|
|
754
|
+
scrollLeft: effectiveScrollLeft,
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// Determine header offset based on region
|
|
758
|
+
let headerWidthOffset = this.headerWidth;
|
|
759
|
+
let headerHeightOffset = this.headerHeight;
|
|
760
|
+
|
|
761
|
+
if (region === 'main' || region === 'top') {
|
|
762
|
+
headerWidthOffset = this.headerWidth + frozenWidth;
|
|
763
|
+
}
|
|
764
|
+
if (region === 'main' || region === 'left') {
|
|
765
|
+
headerHeightOffset = this.headerHeight + frozenHeight;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
this.selectionRenderer.render(
|
|
769
|
+
this.ctx,
|
|
770
|
+
state.selection,
|
|
771
|
+
state.activeCell,
|
|
772
|
+
regionViewport,
|
|
773
|
+
state.rowHeights,
|
|
774
|
+
state.colWidths,
|
|
775
|
+
this.defaultRowHeight,
|
|
776
|
+
this.defaultColWidth,
|
|
777
|
+
headerWidthOffset,
|
|
778
|
+
headerHeightOffset,
|
|
779
|
+
state.formulaRanges,
|
|
780
|
+
state.hiddenRows,
|
|
781
|
+
state.hiddenCols,
|
|
782
|
+
this.frozenRows,
|
|
783
|
+
this.frozenCols,
|
|
784
|
+
region
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Render row and column headers
|
|
791
|
+
*/
|
|
792
|
+
private renderHeaders(state: RenderState): void {
|
|
793
|
+
const { startRow, endRow, startCol, endCol, scrollTop, scrollLeft } = this.viewport;
|
|
794
|
+
const { frozenWidth, frozenHeight } = this.freezeDimensions;
|
|
795
|
+
const hasFrozenRows = this.frozenRows > 0;
|
|
796
|
+
const hasFrozenCols = this.frozenCols > 0;
|
|
797
|
+
|
|
798
|
+
// --- COLUMN HEADERS ---
|
|
799
|
+
if (hasFrozenCols) {
|
|
800
|
+
// Render frozen column headers (don't scroll horizontally)
|
|
801
|
+
this.ctx.save();
|
|
802
|
+
this.ctx.beginPath();
|
|
803
|
+
this.ctx.rect(this.headerWidth, 0, frozenWidth, this.headerHeight);
|
|
804
|
+
this.ctx.clip();
|
|
805
|
+
|
|
806
|
+
this.headerRenderer.renderColumnHeaders(
|
|
807
|
+
this.ctx,
|
|
808
|
+
0,
|
|
809
|
+
this.frozenCols,
|
|
810
|
+
0, // No horizontal scroll for frozen columns
|
|
811
|
+
state.colWidths,
|
|
812
|
+
this.defaultColWidth,
|
|
813
|
+
this.headerWidth,
|
|
814
|
+
this.headerHeight,
|
|
815
|
+
state.selection,
|
|
816
|
+
state.activeCell,
|
|
817
|
+
state.hiddenCols,
|
|
818
|
+
undefined, // No skip - render from column 0
|
|
819
|
+
state.filters
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
this.ctx.restore();
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Render scrollable column headers
|
|
826
|
+
this.ctx.save();
|
|
827
|
+
this.ctx.beginPath();
|
|
828
|
+
const colHeaderX = this.headerWidth + frozenWidth;
|
|
829
|
+
const colHeaderWidth = this.viewport.width - this.headerWidth - frozenWidth;
|
|
830
|
+
this.ctx.rect(colHeaderX, 0, colHeaderWidth, this.headerHeight);
|
|
831
|
+
this.ctx.clip();
|
|
832
|
+
|
|
833
|
+
this.headerRenderer.renderColumnHeaders(
|
|
834
|
+
this.ctx,
|
|
835
|
+
hasFrozenCols ? this.frozenCols : startCol,
|
|
836
|
+
endCol,
|
|
837
|
+
scrollLeft,
|
|
838
|
+
state.colWidths,
|
|
839
|
+
this.defaultColWidth,
|
|
840
|
+
this.headerWidth + frozenWidth, // Start after frozen area
|
|
841
|
+
this.headerHeight,
|
|
842
|
+
state.selection,
|
|
843
|
+
state.activeCell,
|
|
844
|
+
state.hiddenCols,
|
|
845
|
+
hasFrozenCols ? this.frozenCols : undefined, // Skip accumulating frozen column widths
|
|
846
|
+
state.filters
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
this.ctx.restore();
|
|
850
|
+
|
|
851
|
+
// --- ROW HEADERS ---
|
|
852
|
+
if (hasFrozenRows) {
|
|
853
|
+
// Render frozen row headers (don't scroll vertically)
|
|
854
|
+
this.ctx.save();
|
|
855
|
+
this.ctx.beginPath();
|
|
856
|
+
this.ctx.rect(0, this.headerHeight, this.headerWidth, frozenHeight);
|
|
857
|
+
this.ctx.clip();
|
|
858
|
+
|
|
859
|
+
this.headerRenderer.renderRowHeaders(
|
|
860
|
+
this.ctx,
|
|
861
|
+
0,
|
|
862
|
+
this.frozenRows,
|
|
863
|
+
0, // No vertical scroll for frozen rows
|
|
864
|
+
state.rowHeights,
|
|
865
|
+
this.defaultRowHeight,
|
|
866
|
+
this.headerWidth,
|
|
867
|
+
this.headerHeight,
|
|
868
|
+
state.selection,
|
|
869
|
+
state.activeCell,
|
|
870
|
+
state.hiddenRows,
|
|
871
|
+
undefined // No skip - render from row 0
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
this.ctx.restore();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Render scrollable row headers
|
|
878
|
+
this.ctx.save();
|
|
879
|
+
this.ctx.beginPath();
|
|
880
|
+
const rowHeaderY = this.headerHeight + frozenHeight;
|
|
881
|
+
const rowHeaderHeight = this.viewport.height - this.headerHeight - frozenHeight;
|
|
882
|
+
this.ctx.rect(0, rowHeaderY, this.headerWidth, rowHeaderHeight);
|
|
883
|
+
this.ctx.clip();
|
|
884
|
+
|
|
885
|
+
this.headerRenderer.renderRowHeaders(
|
|
886
|
+
this.ctx,
|
|
887
|
+
hasFrozenRows ? this.frozenRows : startRow,
|
|
888
|
+
endRow,
|
|
889
|
+
scrollTop,
|
|
890
|
+
state.rowHeights,
|
|
891
|
+
this.defaultRowHeight,
|
|
892
|
+
this.headerWidth,
|
|
893
|
+
this.headerHeight + frozenHeight, // Start after frozen area
|
|
894
|
+
state.selection,
|
|
895
|
+
state.activeCell,
|
|
896
|
+
state.hiddenRows,
|
|
897
|
+
hasFrozenRows ? this.frozenRows : undefined // Skip accumulating frozen row heights
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
this.ctx.restore();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Render the corner cell
|
|
905
|
+
*/
|
|
906
|
+
private renderCornerCell(): void {
|
|
907
|
+
this.headerRenderer.renderCornerCell(
|
|
908
|
+
this.ctx,
|
|
909
|
+
this.headerWidth,
|
|
910
|
+
this.headerHeight
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Get the cell at a canvas point
|
|
916
|
+
*/
|
|
917
|
+
getCellAtPoint(x: number, y: number): CellPosition | null {
|
|
918
|
+
return this.hitTester.getCellAt(x, y);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Get header at a canvas point
|
|
923
|
+
*/
|
|
924
|
+
getHeaderAtPoint(x: number, y: number) {
|
|
925
|
+
return this.hitTester.getHeaderAt(x, y);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Get resize handle at a canvas point
|
|
930
|
+
*/
|
|
931
|
+
getResizeHandleAtPoint(x: number, y: number) {
|
|
932
|
+
return this.hitTester.getResizeHandleAt(x, y);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Check if point is on fill handle
|
|
937
|
+
*/
|
|
938
|
+
isFillHandleAtPoint(x: number, y: number): boolean {
|
|
939
|
+
if (!this.renderState?.selection?.activeCell) {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
return this.hitTester.getFillHandleAt(
|
|
943
|
+
x,
|
|
944
|
+
y,
|
|
945
|
+
this.renderState.selection.ranges[0]
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Get cell bounds in canvas coordinates (accounting for freeze panes)
|
|
951
|
+
*/
|
|
952
|
+
getCellBounds(row: number, col: number): Rect | null {
|
|
953
|
+
if (!this.renderState) return null;
|
|
954
|
+
|
|
955
|
+
const { scrollTop, scrollLeft } = this.viewport;
|
|
956
|
+
const { frozenWidth, frozenHeight } = this.freezeDimensions;
|
|
957
|
+
const state = this.renderState;
|
|
958
|
+
const hiddenRows = state.hiddenRows ?? new Set<number>();
|
|
959
|
+
const hiddenCols = state.hiddenCols ?? new Set<number>();
|
|
960
|
+
|
|
961
|
+
// If the cell itself is hidden, return null
|
|
962
|
+
if (hiddenRows.has(row) || hiddenCols.has(col)) {
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Determine if cell is in frozen area
|
|
967
|
+
const isRowFrozen = row < this.frozenRows;
|
|
968
|
+
const isColFrozen = col < this.frozenCols;
|
|
969
|
+
|
|
970
|
+
// Calculate x position (skip hidden columns)
|
|
971
|
+
let x: number;
|
|
972
|
+
if (isColFrozen) {
|
|
973
|
+
// Cell is in frozen columns - no scroll offset
|
|
974
|
+
x = this.headerWidth;
|
|
975
|
+
for (let c = 0; c < col; c++) {
|
|
976
|
+
if (!hiddenCols.has(c)) {
|
|
977
|
+
x += state.colWidths.get(c) ?? this.defaultColWidth;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
} else {
|
|
981
|
+
// Cell is in scrollable columns - apply scroll offset
|
|
982
|
+
x = this.headerWidth + frozenWidth;
|
|
983
|
+
for (let c = this.frozenCols; c < col; c++) {
|
|
984
|
+
if (!hiddenCols.has(c)) {
|
|
985
|
+
x += state.colWidths.get(c) ?? this.defaultColWidth;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
x -= scrollLeft;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Calculate y position (skip hidden rows)
|
|
992
|
+
let y: number;
|
|
993
|
+
if (isRowFrozen) {
|
|
994
|
+
// Cell is in frozen rows - no scroll offset
|
|
995
|
+
y = this.headerHeight;
|
|
996
|
+
for (let r = 0; r < row; r++) {
|
|
997
|
+
if (!hiddenRows.has(r)) {
|
|
998
|
+
y += state.rowHeights.get(r) ?? this.defaultRowHeight;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
} else {
|
|
1002
|
+
// Cell is in scrollable rows - apply scroll offset
|
|
1003
|
+
y = this.headerHeight + frozenHeight;
|
|
1004
|
+
for (let r = this.frozenRows; r < row; r++) {
|
|
1005
|
+
if (!hiddenRows.has(r)) {
|
|
1006
|
+
y += state.rowHeights.get(r) ?? this.defaultRowHeight;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
y -= scrollTop;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const width = state.colWidths.get(col) ?? this.defaultColWidth;
|
|
1013
|
+
const height = state.rowHeights.get(row) ?? this.defaultRowHeight;
|
|
1014
|
+
|
|
1015
|
+
return { x, y, width, height };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Get the current viewport
|
|
1020
|
+
*/
|
|
1021
|
+
getViewport(): Viewport {
|
|
1022
|
+
return { ...this.viewport };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Get the hit tester
|
|
1027
|
+
*/
|
|
1028
|
+
getHitTester(): HitTester {
|
|
1029
|
+
return this.hitTester;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Get the current freeze dimensions
|
|
1034
|
+
*/
|
|
1035
|
+
getFreezeDimensions(): FreezeDimensions {
|
|
1036
|
+
return { ...this.freezeDimensions };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Get frozen row/col counts
|
|
1041
|
+
*/
|
|
1042
|
+
getFreezeConfig(): { frozenRows: number; frozenCols: number } {
|
|
1043
|
+
return { frozenRows: this.frozenRows, frozenCols: this.frozenCols };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Cleanup resources
|
|
1048
|
+
*/
|
|
1049
|
+
destroy(): void {
|
|
1050
|
+
if (this.animationFrameId !== null) {
|
|
1051
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
1052
|
+
this.animationFrameId = null;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|