@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.
Files changed (111) hide show
  1. package/README.md +32 -0
  2. package/dist/canvas/cell-renderer.d.ts +45 -0
  3. package/dist/canvas/cell-renderer.d.ts.map +1 -0
  4. package/dist/canvas/grid-renderer.d.ts +29 -0
  5. package/dist/canvas/grid-renderer.d.ts.map +1 -0
  6. package/dist/canvas/header-renderer.d.ts +58 -0
  7. package/dist/canvas/header-renderer.d.ts.map +1 -0
  8. package/dist/canvas/hit-testing.d.ts +81 -0
  9. package/dist/canvas/hit-testing.d.ts.map +1 -0
  10. package/dist/canvas/index.d.ts +9 -0
  11. package/dist/canvas/index.d.ts.map +1 -0
  12. package/dist/canvas/renderer.d.ts +140 -0
  13. package/dist/canvas/renderer.d.ts.map +1 -0
  14. package/dist/canvas/selection-renderer.d.ts +55 -0
  15. package/dist/canvas/selection-renderer.d.ts.map +1 -0
  16. package/dist/canvas/text-renderer.d.ts +49 -0
  17. package/dist/canvas/text-renderer.d.ts.map +1 -0
  18. package/dist/canvas/types.d.ts +200 -0
  19. package/dist/canvas/types.d.ts.map +1 -0
  20. package/dist/collaboration/firebase-provider.d.ts +13 -0
  21. package/dist/collaboration/firebase-provider.d.ts.map +1 -0
  22. package/dist/collaboration/index.d.ts +3 -0
  23. package/dist/collaboration/index.d.ts.map +1 -0
  24. package/dist/collaboration/types.d.ts +34 -0
  25. package/dist/collaboration/types.d.ts.map +1 -0
  26. package/dist/event-emitter.d.ts +13 -0
  27. package/dist/event-emitter.d.ts.map +1 -0
  28. package/dist/export/csv.d.ts +5 -0
  29. package/dist/export/csv.d.ts.map +1 -0
  30. package/dist/export/index.d.ts +2 -0
  31. package/dist/export/index.d.ts.map +1 -0
  32. package/dist/features/filter.d.ts +58 -0
  33. package/dist/features/filter.d.ts.map +1 -0
  34. package/dist/features/freeze.d.ts +86 -0
  35. package/dist/features/freeze.d.ts.map +1 -0
  36. package/dist/features/index.d.ts +4 -0
  37. package/dist/features/index.d.ts.map +1 -0
  38. package/dist/features/sort.d.ts +15 -0
  39. package/dist/features/sort.d.ts.map +1 -0
  40. package/dist/format-pool.d.ts +17 -0
  41. package/dist/format-pool.d.ts.map +1 -0
  42. package/dist/formula-graph.d.ts +12 -0
  43. package/dist/formula-graph.d.ts.map +1 -0
  44. package/dist/formula-parser/cell-reference.d.ts +7 -0
  45. package/dist/formula-parser/cell-reference.d.ts.map +1 -0
  46. package/dist/formula-parser/formula-adjust.d.ts +13 -0
  47. package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
  48. package/dist/formula-parser/formula-ranges.d.ts +22 -0
  49. package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
  50. package/dist/formula-parser/index.d.ts +6 -0
  51. package/dist/formula-parser/index.d.ts.map +1 -0
  52. package/dist/formula-parser/parser.d.ts +18 -0
  53. package/dist/formula-parser/parser.d.ts.map +1 -0
  54. package/dist/formula-parser/types.d.ts +33 -0
  55. package/dist/formula-parser/types.d.ts.map +1 -0
  56. package/dist/index.d.ts +15 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.esm.js +5823 -0
  59. package/dist/index.esm.js.map +1 -0
  60. package/dist/index.js +5885 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/sheet.d.ts +119 -0
  63. package/dist/sheet.d.ts.map +1 -0
  64. package/dist/style-pool.d.ts +17 -0
  65. package/dist/style-pool.d.ts.map +1 -0
  66. package/dist/types.d.ts +260 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/utils/cell-key.d.ts +7 -0
  69. package/dist/utils/cell-key.d.ts.map +1 -0
  70. package/dist/utils/format-utils.d.ts +75 -0
  71. package/dist/utils/format-utils.d.ts.map +1 -0
  72. package/dist/utils/range.d.ts +13 -0
  73. package/dist/utils/range.d.ts.map +1 -0
  74. package/dist/workbook.d.ts +155 -0
  75. package/dist/workbook.d.ts.map +1 -0
  76. package/package.json +46 -0
  77. package/src/canvas/cell-renderer.ts +181 -0
  78. package/src/canvas/grid-renderer.ts +238 -0
  79. package/src/canvas/header-renderer.ts +402 -0
  80. package/src/canvas/hit-testing.ts +537 -0
  81. package/src/canvas/index.ts +16 -0
  82. package/src/canvas/renderer.ts +1056 -0
  83. package/src/canvas/selection-renderer.ts +604 -0
  84. package/src/canvas/text-renderer.ts +321 -0
  85. package/src/canvas/types.ts +289 -0
  86. package/src/collaboration/firebase-provider.ts +48 -0
  87. package/src/collaboration/index.ts +5 -0
  88. package/src/collaboration/types.ts +38 -0
  89. package/src/event-emitter.ts +73 -0
  90. package/src/export/csv.ts +101 -0
  91. package/src/export/index.ts +4 -0
  92. package/src/features/filter.ts +231 -0
  93. package/src/features/freeze.ts +271 -0
  94. package/src/features/index.ts +5 -0
  95. package/src/features/sort.ts +282 -0
  96. package/src/format-pool.ts +61 -0
  97. package/src/formula-graph.ts +84 -0
  98. package/src/formula-parser/cell-reference.ts +99 -0
  99. package/src/formula-parser/formula-adjust.ts +129 -0
  100. package/src/formula-parser/formula-ranges.ts +159 -0
  101. package/src/formula-parser/index.ts +8 -0
  102. package/src/formula-parser/parser.ts +438 -0
  103. package/src/formula-parser/types.ts +39 -0
  104. package/src/index.ts +25 -0
  105. package/src/sheet.ts +502 -0
  106. package/src/style-pool.ts +62 -0
  107. package/src/types.ts +291 -0
  108. package/src/utils/cell-key.ts +19 -0
  109. package/src/utils/format-utils.ts +515 -0
  110. package/src/utils/range.ts +53 -0
  111. package/src/workbook.ts +1031 -0
@@ -0,0 +1,604 @@
1
+ // Selection Renderer - Handles selection highlighting and active cell
2
+
3
+ import type { CanvasTheme, Viewport, CellPosition, Rect, FormulaRangeHighlight } from './types';
4
+ import type { Selection, Range } from '../types';
5
+ import type { FreezeRegion } from '../features/freeze';
6
+ import { getCellRegion } from '../features/freeze';
7
+
8
+ /**
9
+ * Renders selection highlighting and active cell border
10
+ */
11
+ export class SelectionRenderer {
12
+ private theme: CanvasTheme;
13
+
14
+ constructor(theme: CanvasTheme) {
15
+ this.theme = theme;
16
+ }
17
+
18
+ /**
19
+ * Main render method for all selection elements
20
+ * @param frozenRows - Number of frozen rows (optional, for freeze pane support)
21
+ * @param frozenCols - Number of frozen columns (optional, for freeze pane support)
22
+ * @param currentRegion - Current freeze region being rendered (optional)
23
+ */
24
+ render(
25
+ ctx: CanvasRenderingContext2D,
26
+ selection: Selection | null,
27
+ activeCell: CellPosition | null,
28
+ viewport: Viewport,
29
+ rowHeights: Map<number, number>,
30
+ colWidths: Map<number, number>,
31
+ defaultRowHeight: number,
32
+ defaultColWidth: number,
33
+ headerWidth: number,
34
+ headerHeight: number,
35
+ formulaRanges?: FormulaRangeHighlight[],
36
+ hiddenRows?: Set<number>,
37
+ hiddenCols?: Set<number>,
38
+ frozenRows?: number,
39
+ frozenCols?: number,
40
+ currentRegion?: FreezeRegion
41
+ ): void {
42
+ const hidRows = hiddenRows ?? new Set<number>();
43
+ const hidCols = hiddenCols ?? new Set<number>();
44
+ const numFrozenRows = frozenRows ?? 0;
45
+ const numFrozenCols = frozenCols ?? 0;
46
+
47
+ // Render formula reference highlights first (behind selection)
48
+ if (formulaRanges && formulaRanges.length > 0) {
49
+ for (const formulaRange of formulaRanges) {
50
+ // Check if this formula range should be rendered in the current region
51
+ if (currentRegion && !this.shouldRenderRangeInRegion(
52
+ { startRow: formulaRange.startRow, endRow: formulaRange.endRow,
53
+ startCol: formulaRange.startCol, endCol: formulaRange.endCol },
54
+ currentRegion, numFrozenRows, numFrozenCols
55
+ )) {
56
+ continue;
57
+ }
58
+
59
+ this.renderFormulaRange(
60
+ ctx,
61
+ formulaRange,
62
+ viewport,
63
+ rowHeights,
64
+ colWidths,
65
+ defaultRowHeight,
66
+ defaultColWidth,
67
+ headerWidth,
68
+ headerHeight,
69
+ hidRows,
70
+ hidCols,
71
+ numFrozenRows,
72
+ numFrozenCols
73
+ );
74
+ }
75
+ }
76
+
77
+ // Render selection ranges (highlight)
78
+ if (selection) {
79
+ for (const range of selection.ranges) {
80
+ // Check if this selection range should be rendered in the current region
81
+ if (currentRegion && !this.shouldRenderRangeInRegion(
82
+ range, currentRegion, numFrozenRows, numFrozenCols
83
+ )) {
84
+ continue;
85
+ }
86
+
87
+ this.renderSelectionRange(
88
+ ctx,
89
+ range,
90
+ viewport,
91
+ rowHeights,
92
+ colWidths,
93
+ defaultRowHeight,
94
+ defaultColWidth,
95
+ headerWidth,
96
+ headerHeight,
97
+ hidRows,
98
+ hidCols,
99
+ numFrozenRows,
100
+ numFrozenCols
101
+ );
102
+ }
103
+ }
104
+
105
+ // Render active cell border (only if not hidden)
106
+ if (activeCell && !hidRows.has(activeCell.row) && !hidCols.has(activeCell.col)) {
107
+ // Check if active cell should be rendered in the current region
108
+ const activeCellRegion = getCellRegion(activeCell.row, activeCell.col, numFrozenRows, numFrozenCols);
109
+ if (!currentRegion || activeCellRegion === currentRegion) {
110
+ this.renderActiveCell(
111
+ ctx,
112
+ activeCell,
113
+ viewport,
114
+ rowHeights,
115
+ colWidths,
116
+ defaultRowHeight,
117
+ defaultColWidth,
118
+ headerWidth,
119
+ headerHeight,
120
+ hidRows,
121
+ hidCols,
122
+ numFrozenRows,
123
+ numFrozenCols
124
+ );
125
+ }
126
+
127
+ // Render fill handle if there's a selection (only in the region containing the end of selection)
128
+ if (selection && selection.ranges.length > 0) {
129
+ const lastRange = selection.ranges[selection.ranges.length - 1];
130
+ const fillHandleRegion = getCellRegion(lastRange.endRow, lastRange.endCol, numFrozenRows, numFrozenCols);
131
+
132
+ if (!currentRegion || fillHandleRegion === currentRegion) {
133
+ this.renderFillHandle(
134
+ ctx,
135
+ lastRange,
136
+ viewport,
137
+ rowHeights,
138
+ colWidths,
139
+ defaultRowHeight,
140
+ defaultColWidth,
141
+ headerWidth,
142
+ headerHeight,
143
+ hidRows,
144
+ hidCols,
145
+ numFrozenRows,
146
+ numFrozenCols
147
+ );
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Determine if a range should be rendered in the current freeze region.
155
+ * A range should be rendered if any of its cells overlap with the current region.
156
+ */
157
+ private shouldRenderRangeInRegion(
158
+ range: Range,
159
+ currentRegion: FreezeRegion,
160
+ frozenRows: number,
161
+ frozenCols: number
162
+ ): boolean {
163
+ // Check each corner of the range to see if any fall in the current region
164
+ const corners = [
165
+ { row: range.startRow, col: range.startCol },
166
+ { row: range.startRow, col: range.endCol },
167
+ { row: range.endRow, col: range.startCol },
168
+ { row: range.endRow, col: range.endCol },
169
+ ];
170
+
171
+ for (const corner of corners) {
172
+ if (getCellRegion(corner.row, corner.col, frozenRows, frozenCols) === currentRegion) {
173
+ return true;
174
+ }
175
+ }
176
+
177
+ // Also check if the range spans across the current region
178
+ // (e.g., a range from frozen to non-frozen area)
179
+ const rangeStartsFrozenRow = range.startRow < frozenRows;
180
+ const rangeEndsFrozenRow = range.endRow < frozenRows;
181
+ const rangeStartsFrozenCol = range.startCol < frozenCols;
182
+ const rangeEndsFrozenCol = range.endCol < frozenCols;
183
+
184
+ switch (currentRegion) {
185
+ case 'top-left':
186
+ // Any range that has cells in frozen rows AND frozen cols
187
+ return rangeStartsFrozenRow && rangeStartsFrozenCol;
188
+ case 'top':
189
+ // Range that has cells in frozen rows AND non-frozen cols
190
+ return rangeStartsFrozenRow && !rangeEndsFrozenCol;
191
+ case 'left':
192
+ // Range that has cells in non-frozen rows AND frozen cols
193
+ return !rangeEndsFrozenRow && rangeStartsFrozenCol;
194
+ case 'main':
195
+ // Range that has cells in non-frozen rows AND non-frozen cols
196
+ return !rangeEndsFrozenRow && !rangeEndsFrozenCol;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get color for formula range by index
202
+ */
203
+ private getFormulaRangeColor(colorIndex: number): { border: string; fill: string } {
204
+ const colors = this.theme.formulaReferenceColors;
205
+ if (colorIndex < colors.length) {
206
+ return colors[colorIndex];
207
+ }
208
+ // Generate a color for indices beyond the palette
209
+ const hue = (colorIndex * 137.508) % 360;
210
+ const saturation = 60 + (colorIndex % 20);
211
+ const lightness = 50 + (colorIndex % 10);
212
+ return {
213
+ border: `hsl(${hue}, ${saturation}%, ${lightness}%)`,
214
+ fill: `hsla(${hue}, ${saturation}%, ${lightness}%, 0.15)`,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Render a formula range highlight (cell references in formulas)
220
+ */
221
+ private renderFormulaRange(
222
+ ctx: CanvasRenderingContext2D,
223
+ formulaRange: FormulaRangeHighlight,
224
+ viewport: Viewport,
225
+ rowHeights: Map<number, number>,
226
+ colWidths: Map<number, number>,
227
+ defaultRowHeight: number,
228
+ defaultColWidth: number,
229
+ headerWidth: number,
230
+ headerHeight: number,
231
+ hiddenRows?: Set<number>,
232
+ hiddenCols?: Set<number>,
233
+ frozenRows?: number,
234
+ frozenCols?: number
235
+ ): void {
236
+ const range: Range = {
237
+ startRow: formulaRange.startRow,
238
+ endRow: formulaRange.endRow,
239
+ startCol: formulaRange.startCol,
240
+ endCol: formulaRange.endCol,
241
+ };
242
+
243
+ const bounds = this.getRangeBounds(
244
+ range,
245
+ viewport,
246
+ rowHeights,
247
+ colWidths,
248
+ defaultRowHeight,
249
+ defaultColWidth,
250
+ headerWidth,
251
+ headerHeight,
252
+ hiddenRows,
253
+ hiddenCols,
254
+ frozenRows,
255
+ frozenCols
256
+ );
257
+
258
+ if (!bounds) return;
259
+
260
+ const color = this.getFormulaRangeColor(formulaRange.colorIndex);
261
+
262
+ // Formula range fill
263
+ ctx.fillStyle = color.fill;
264
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
265
+
266
+ // Formula range border
267
+ ctx.strokeStyle = color.border;
268
+ ctx.lineWidth = 2;
269
+ ctx.strokeRect(
270
+ bounds.x + 1,
271
+ bounds.y + 1,
272
+ bounds.width - 2,
273
+ bounds.height - 2
274
+ );
275
+ }
276
+
277
+ /**
278
+ * Calculate bounds for a range in canvas coordinates
279
+ */
280
+ private getRangeBounds(
281
+ range: Range,
282
+ viewport: Viewport,
283
+ rowHeights: Map<number, number>,
284
+ colWidths: Map<number, number>,
285
+ defaultRowHeight: number,
286
+ defaultColWidth: number,
287
+ headerWidth: number,
288
+ headerHeight: number,
289
+ hiddenRows?: Set<number>,
290
+ hiddenCols?: Set<number>,
291
+ frozenRows?: number,
292
+ frozenCols?: number
293
+ ): Rect | null {
294
+ const { scrollTop, scrollLeft } = viewport;
295
+ const hidRows = hiddenRows ?? new Set<number>();
296
+ const hidCols = hiddenCols ?? new Set<number>();
297
+ const numFrozenRows = frozenRows ?? 0;
298
+ const numFrozenCols = frozenCols ?? 0;
299
+
300
+ // Determine if this range is in frozen area
301
+ const isRowFrozen = range.startRow < numFrozenRows;
302
+ const isColFrozen = range.startCol < numFrozenCols;
303
+
304
+ // Calculate x position (skip hidden columns)
305
+ // If column is frozen, accumulate from 0; otherwise accumulate from frozenCols
306
+ let x = headerWidth;
307
+ const startColAccumulation = isColFrozen ? 0 : numFrozenCols;
308
+ for (let c = startColAccumulation; c < range.startCol; c++) {
309
+ if (!hidCols.has(c)) {
310
+ x += colWidths.get(c) ?? defaultColWidth;
311
+ }
312
+ }
313
+ // Only apply scroll if not in frozen columns
314
+ if (!isColFrozen) {
315
+ x -= scrollLeft;
316
+ }
317
+
318
+ // Calculate y position (skip hidden rows)
319
+ // If row is frozen, accumulate from 0; otherwise accumulate from frozenRows
320
+ let y = headerHeight;
321
+ const startRowAccumulation = isRowFrozen ? 0 : numFrozenRows;
322
+ for (let r = startRowAccumulation; r < range.startRow; r++) {
323
+ if (!hidRows.has(r)) {
324
+ y += rowHeights.get(r) ?? defaultRowHeight;
325
+ }
326
+ }
327
+ // Only apply scroll if not in frozen rows
328
+ if (!isRowFrozen) {
329
+ y -= scrollTop;
330
+ }
331
+
332
+ // Calculate width (skip hidden columns)
333
+ let width = 0;
334
+ for (let c = range.startCol; c <= range.endCol; c++) {
335
+ if (!hidCols.has(c)) {
336
+ width += colWidths.get(c) ?? defaultColWidth;
337
+ }
338
+ }
339
+
340
+ // Calculate height (skip hidden rows)
341
+ let height = 0;
342
+ for (let r = range.startRow; r <= range.endRow; r++) {
343
+ if (!hidRows.has(r)) {
344
+ height += rowHeights.get(r) ?? defaultRowHeight;
345
+ }
346
+ }
347
+
348
+ // Return null if entire range is hidden
349
+ if (width === 0 || height === 0) {
350
+ return null;
351
+ }
352
+
353
+ return { x, y, width, height };
354
+ }
355
+
356
+ /**
357
+ * Render a selection range (highlight fill and border)
358
+ */
359
+ private renderSelectionRange(
360
+ ctx: CanvasRenderingContext2D,
361
+ range: Range,
362
+ viewport: Viewport,
363
+ rowHeights: Map<number, number>,
364
+ colWidths: Map<number, number>,
365
+ defaultRowHeight: number,
366
+ defaultColWidth: number,
367
+ headerWidth: number,
368
+ headerHeight: number,
369
+ hiddenRows?: Set<number>,
370
+ hiddenCols?: Set<number>,
371
+ frozenRows?: number,
372
+ frozenCols?: number
373
+ ): void {
374
+ const bounds = this.getRangeBounds(
375
+ range,
376
+ viewport,
377
+ rowHeights,
378
+ colWidths,
379
+ defaultRowHeight,
380
+ defaultColWidth,
381
+ headerWidth,
382
+ headerHeight,
383
+ hiddenRows,
384
+ hiddenCols,
385
+ frozenRows,
386
+ frozenCols
387
+ );
388
+
389
+ if (!bounds) return;
390
+
391
+ // Selection fill
392
+ ctx.fillStyle = this.theme.selectionFillColor;
393
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
394
+
395
+ // Selection border
396
+ ctx.strokeStyle = this.theme.selectionBorderColor;
397
+ ctx.lineWidth = this.theme.selectionBorderWidth;
398
+ ctx.strokeRect(
399
+ bounds.x + 0.5,
400
+ bounds.y + 0.5,
401
+ bounds.width - 1,
402
+ bounds.height - 1
403
+ );
404
+ }
405
+
406
+ /**
407
+ * Render the active cell border
408
+ */
409
+ private renderActiveCell(
410
+ ctx: CanvasRenderingContext2D,
411
+ cell: CellPosition,
412
+ viewport: Viewport,
413
+ rowHeights: Map<number, number>,
414
+ colWidths: Map<number, number>,
415
+ defaultRowHeight: number,
416
+ defaultColWidth: number,
417
+ headerWidth: number,
418
+ headerHeight: number,
419
+ hiddenRows?: Set<number>,
420
+ hiddenCols?: Set<number>,
421
+ frozenRows?: number,
422
+ frozenCols?: number
423
+ ): void {
424
+ const range: Range = {
425
+ startRow: cell.row,
426
+ endRow: cell.row,
427
+ startCol: cell.col,
428
+ endCol: cell.col,
429
+ };
430
+
431
+ const bounds = this.getRangeBounds(
432
+ range,
433
+ viewport,
434
+ rowHeights,
435
+ colWidths,
436
+ defaultRowHeight,
437
+ defaultColWidth,
438
+ headerWidth,
439
+ headerHeight,
440
+ hiddenRows,
441
+ hiddenCols,
442
+ frozenRows,
443
+ frozenCols
444
+ );
445
+
446
+ if (!bounds) return;
447
+
448
+ // Active cell border only - cell content is already rendered by CellRenderer
449
+ ctx.strokeStyle = this.theme.activeCellBorderColor;
450
+ ctx.lineWidth = this.theme.activeCellBorderWidth;
451
+ ctx.strokeRect(
452
+ bounds.x + 1,
453
+ bounds.y + 1,
454
+ bounds.width - 2,
455
+ bounds.height - 2
456
+ );
457
+ }
458
+
459
+ /**
460
+ * Render the fill handle (small square at bottom-right of selection)
461
+ */
462
+ private renderFillHandle(
463
+ ctx: CanvasRenderingContext2D,
464
+ range: Range,
465
+ viewport: Viewport,
466
+ rowHeights: Map<number, number>,
467
+ colWidths: Map<number, number>,
468
+ defaultRowHeight: number,
469
+ defaultColWidth: number,
470
+ headerWidth: number,
471
+ headerHeight: number,
472
+ hiddenRows?: Set<number>,
473
+ hiddenCols?: Set<number>,
474
+ frozenRows?: number,
475
+ frozenCols?: number
476
+ ): void {
477
+ const bounds = this.getRangeBounds(
478
+ range,
479
+ viewport,
480
+ rowHeights,
481
+ colWidths,
482
+ defaultRowHeight,
483
+ defaultColWidth,
484
+ headerWidth,
485
+ headerHeight,
486
+ hiddenRows,
487
+ hiddenCols,
488
+ frozenRows,
489
+ frozenCols
490
+ );
491
+
492
+ if (!bounds) return;
493
+
494
+ const handleSize = this.theme.fillHandleSize;
495
+ const handleX = bounds.x + bounds.width - handleSize / 2;
496
+ const handleY = bounds.y + bounds.height - handleSize / 2;
497
+
498
+ // Fill handle square
499
+ ctx.fillStyle = this.theme.fillHandleColor;
500
+ ctx.fillRect(
501
+ handleX - handleSize / 2,
502
+ handleY - handleSize / 2,
503
+ handleSize,
504
+ handleSize
505
+ );
506
+
507
+ // White border around fill handle
508
+ ctx.strokeStyle = '#ffffff';
509
+ ctx.lineWidth = 1;
510
+ ctx.strokeRect(
511
+ handleX - handleSize / 2,
512
+ handleY - handleSize / 2,
513
+ handleSize,
514
+ handleSize
515
+ );
516
+ }
517
+
518
+ /**
519
+ * Render copy/paste marching ants border
520
+ */
521
+ renderMarchingAnts(
522
+ ctx: CanvasRenderingContext2D,
523
+ range: Range,
524
+ viewport: Viewport,
525
+ rowHeights: Map<number, number>,
526
+ colWidths: Map<number, number>,
527
+ defaultRowHeight: number,
528
+ defaultColWidth: number,
529
+ headerWidth: number,
530
+ headerHeight: number,
531
+ offset: number
532
+ ): void {
533
+ const bounds = this.getRangeBounds(
534
+ range,
535
+ viewport,
536
+ rowHeights,
537
+ colWidths,
538
+ defaultRowHeight,
539
+ defaultColWidth,
540
+ headerWidth,
541
+ headerHeight
542
+ );
543
+
544
+ if (!bounds) return;
545
+
546
+ ctx.strokeStyle = this.theme.selectionBorderColor;
547
+ ctx.lineWidth = 1;
548
+ ctx.setLineDash([4, 4]);
549
+ ctx.lineDashOffset = offset;
550
+
551
+ ctx.strokeRect(
552
+ bounds.x + 0.5,
553
+ bounds.y + 0.5,
554
+ bounds.width - 1,
555
+ bounds.height - 1
556
+ );
557
+
558
+ ctx.setLineDash([]);
559
+ ctx.lineDashOffset = 0;
560
+ }
561
+
562
+ /**
563
+ * Render fill preview during fill handle drag
564
+ */
565
+ renderFillPreview(
566
+ ctx: CanvasRenderingContext2D,
567
+ range: Range,
568
+ viewport: Viewport,
569
+ rowHeights: Map<number, number>,
570
+ colWidths: Map<number, number>,
571
+ defaultRowHeight: number,
572
+ defaultColWidth: number,
573
+ headerWidth: number,
574
+ headerHeight: number
575
+ ): void {
576
+ const bounds = this.getRangeBounds(
577
+ range,
578
+ viewport,
579
+ rowHeights,
580
+ colWidths,
581
+ defaultRowHeight,
582
+ defaultColWidth,
583
+ headerWidth,
584
+ headerHeight
585
+ );
586
+
587
+ if (!bounds) return;
588
+
589
+ // Dashed border for fill preview
590
+ ctx.strokeStyle = this.theme.selectionBorderColor;
591
+ ctx.lineWidth = 1;
592
+ ctx.setLineDash([3, 3]);
593
+
594
+ ctx.strokeRect(
595
+ bounds.x + 0.5,
596
+ bounds.y + 0.5,
597
+ bounds.width - 1,
598
+ bounds.height - 1
599
+ );
600
+
601
+ ctx.setLineDash([]);
602
+ }
603
+ }
604
+