argent-grid 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +70 -27
- package/e2e/advanced.spec.ts +1 -1
- package/e2e/benchmark.spec.ts +7 -7
- package/e2e/cell-renderers.spec.ts +152 -0
- package/e2e/debug-streaming.spec.ts +31 -0
- package/e2e/dnd.spec.ts +73 -0
- package/e2e/screenshots.spec.ts +1 -1
- package/e2e/visual.spec.ts +30 -9
- package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
- package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
- package/package.json +5 -5
- package/plan.md +30 -34
- package/src/lib/components/argent-grid.component.css +258 -549
- package/src/lib/components/argent-grid.component.html +272 -306
- package/src/lib/components/argent-grid.component.ts +585 -135
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +2 -2
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +7 -2
- package/src/lib/rendering/canvas-renderer.spec.ts +148 -1
- package/src/lib/rendering/canvas-renderer.ts +177 -286
- package/src/lib/rendering/render/cells.ts +122 -5
- package/src/lib/rendering/render/column-utils.ts +27 -5
- package/src/lib/rendering/render/hit-test.ts +6 -11
- package/src/lib/rendering/render/index.ts +15 -6
- package/src/lib/rendering/render/lines.ts +12 -6
- package/src/lib/rendering/render/primitives.ts +269 -7
- package/src/lib/rendering/render/types.ts +2 -1
- package/src/lib/rendering/render/walk.ts +39 -19
- package/src/lib/services/grid.service.spec.ts +76 -0
- package/src/lib/services/grid.service.ts +451 -114
- package/src/lib/themes/theme-quartz.ts +2 -2
- package/src/lib/types/ag-grid-types.ts +500 -0
- package/src/stories/Advanced.stories.ts +78 -17
- package/src/stories/ArgentGrid.stories.ts +50 -26
- package/src/stories/Benchmark.stories.ts +17 -15
- package/src/stories/CellRenderers.stories.ts +205 -31
- package/src/stories/Filtering.stories.ts +56 -16
- package/src/stories/Grouping.stories.ts +86 -13
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +23 -10
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +69 -29
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -0
|
@@ -1,41 +1,36 @@
|
|
|
1
1
|
import { Column, GridApi, IRowNode } from '../types/ag-grid-types';
|
|
2
|
-
|
|
2
|
+
import { LiveDataHandler } from './live-data-handler';
|
|
3
3
|
// Import new rendering modules from the index
|
|
4
4
|
import {
|
|
5
|
-
// Blitting
|
|
6
|
-
BlitState,
|
|
7
5
|
ColumnPrepResult,
|
|
8
|
-
calculateBlit,
|
|
9
6
|
// Theme
|
|
10
7
|
DEFAULT_THEME,
|
|
11
|
-
|
|
8
|
+
drawCell,
|
|
12
9
|
drawColumnLines,
|
|
13
|
-
drawGroupIndicator,
|
|
14
10
|
drawRangeSelectionBorder,
|
|
15
11
|
// Lines
|
|
16
12
|
drawRowLines,
|
|
17
|
-
drawSparkline,
|
|
18
13
|
// Types
|
|
19
14
|
GridTheme,
|
|
15
|
+
getCellValue,
|
|
20
16
|
getCenterColumnOffset,
|
|
21
17
|
getColumnAtX,
|
|
22
18
|
getColumnDef,
|
|
23
|
-
getColumnX,
|
|
24
19
|
getFontFromTheme,
|
|
25
20
|
getFormattedValue,
|
|
26
21
|
getPinnedWidths,
|
|
22
|
+
getPositionedColumns,
|
|
27
23
|
getRowAtY,
|
|
28
24
|
getValueByPath,
|
|
29
25
|
getVisibleRowRange,
|
|
26
|
+
isColumnVisible,
|
|
30
27
|
mergeTheme,
|
|
28
|
+
PositionedColumn,
|
|
31
29
|
performHitTest,
|
|
32
30
|
prepColumn,
|
|
33
|
-
// Cells
|
|
34
|
-
truncateText,
|
|
35
31
|
walkRows,
|
|
36
32
|
} from './render';
|
|
37
33
|
import { DamageTracker } from './utils/damage-tracker';
|
|
38
|
-
import { LiveDataHandler } from './live-data-handler';
|
|
39
34
|
|
|
40
35
|
/**
|
|
41
36
|
* CanvasRenderer - High-performance canvas rendering engine for ArgentGrid
|
|
@@ -55,7 +50,6 @@ export class CanvasRenderer<TData = any> {
|
|
|
55
50
|
private canvas: HTMLCanvasElement;
|
|
56
51
|
private ctx: CanvasRenderingContext2D;
|
|
57
52
|
private gridApi: GridApi<TData>;
|
|
58
|
-
private rowHeight: number;
|
|
59
53
|
private scrollTop = 0;
|
|
60
54
|
private scrollLeft = 0;
|
|
61
55
|
|
|
@@ -67,10 +61,13 @@ export class CanvasRenderer<TData = any> {
|
|
|
67
61
|
}
|
|
68
62
|
|
|
69
63
|
private animationFrameId: number | null = null;
|
|
70
|
-
|
|
64
|
+
// When a render is already in-flight and another is requested, coalesce it here
|
|
65
|
+
// so it fires immediately after the current frame completes rather than being dropped.
|
|
66
|
+
private nextRenderPending = false;
|
|
71
67
|
private rowBuffer = 5;
|
|
72
68
|
private viewportHeight = 0;
|
|
73
69
|
private viewportWidth = 0;
|
|
70
|
+
private scrollbarWidth = 0;
|
|
74
71
|
|
|
75
72
|
// Theme system
|
|
76
73
|
private theme: GridTheme;
|
|
@@ -84,25 +81,18 @@ export class CanvasRenderer<TData = any> {
|
|
|
84
81
|
// Damage tracking
|
|
85
82
|
private damageTracker = new DamageTracker();
|
|
86
83
|
|
|
87
|
-
// Blitting state
|
|
88
|
-
private blitState = new BlitState();
|
|
89
|
-
|
|
90
84
|
// Live data handling
|
|
91
85
|
private liveDataHandler: LiveDataHandler<TData>;
|
|
92
86
|
|
|
93
87
|
// Column prep results cache
|
|
94
88
|
private columnPreps: Map<string, ColumnPrepResult<TData>> = new Map();
|
|
95
89
|
|
|
96
|
-
/**
|
|
97
|
-
* Column positions cache for O(1) lookup
|
|
98
|
-
*/
|
|
99
|
-
private columnPositions: Map<string, number> = new Map();
|
|
100
|
-
|
|
101
90
|
// Event listener references for cleanup
|
|
102
91
|
private scrollListener?: (e: Event) => void;
|
|
103
92
|
private resizeListener?: () => void;
|
|
104
93
|
private mousedownListener?: (e: MouseEvent) => void;
|
|
105
94
|
private mousemoveListener?: (e: MouseEvent) => void;
|
|
95
|
+
private mouseleaveListener?: (e: MouseEvent) => void;
|
|
106
96
|
private clickListener?: (e: MouseEvent) => void;
|
|
107
97
|
private dblclickListener?: (e: MouseEvent) => void;
|
|
108
98
|
private mouseupListener?: (e: MouseEvent) => void;
|
|
@@ -123,7 +113,6 @@ export class CanvasRenderer<TData = any> {
|
|
|
123
113
|
this.canvas = canvas;
|
|
124
114
|
this.ctx = canvas.getContext('2d')!;
|
|
125
115
|
this.gridApi = gridApi;
|
|
126
|
-
this.rowHeight = rowHeight;
|
|
127
116
|
this.theme = mergeTheme(DEFAULT_THEME, { rowHeight }, theme || {});
|
|
128
117
|
this.liveDataHandler = new LiveDataHandler(gridApi);
|
|
129
118
|
|
|
@@ -135,7 +124,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
135
124
|
* Update the theme
|
|
136
125
|
*/
|
|
137
126
|
setTheme(theme: Partial<GridTheme>): void {
|
|
138
|
-
this.theme = mergeTheme(DEFAULT_THEME, { rowHeight: this.rowHeight }, theme);
|
|
127
|
+
this.theme = mergeTheme(DEFAULT_THEME, { rowHeight: this.theme.rowHeight }, theme);
|
|
139
128
|
this.damageTracker.markAllDirty();
|
|
140
129
|
this.scheduleRender();
|
|
141
130
|
}
|
|
@@ -205,7 +194,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
205
194
|
* @performance O(1) - Constant time, regardless of total row count
|
|
206
195
|
*/
|
|
207
196
|
getRowAtY(y: number): number {
|
|
208
|
-
return getRowAtY(y, this.rowHeight, this.scrollTop);
|
|
197
|
+
return getRowAtY(y, this.theme.rowHeight, this.scrollTop);
|
|
209
198
|
}
|
|
210
199
|
|
|
211
200
|
/**
|
|
@@ -277,6 +266,10 @@ export class CanvasRenderer<TData = any> {
|
|
|
277
266
|
|
|
278
267
|
this.canvas.addEventListener('mousedown', this.mousedownListener);
|
|
279
268
|
this.canvas.addEventListener('mousemove', this.mousemoveListener);
|
|
269
|
+
this.mouseleaveListener = () => {
|
|
270
|
+
this.canvas.style.cursor = '';
|
|
271
|
+
};
|
|
272
|
+
this.canvas.addEventListener('mouseleave', this.mouseleaveListener);
|
|
280
273
|
this.canvas.addEventListener('click', this.clickListener);
|
|
281
274
|
this.canvas.addEventListener('dblclick', this.dblclickListener);
|
|
282
275
|
this.canvas.addEventListener('mouseup', this.mouseupListener);
|
|
@@ -293,38 +286,23 @@ export class CanvasRenderer<TData = any> {
|
|
|
293
286
|
const container = this.canvas.parentElement;
|
|
294
287
|
if (!container) return;
|
|
295
288
|
|
|
296
|
-
const _oldScrollTop = this.scrollTop;
|
|
297
|
-
const _oldScrollLeft = this.scrollLeft;
|
|
298
|
-
|
|
299
289
|
this.scrollTop = container.scrollTop;
|
|
300
290
|
this.scrollLeft = container.scrollLeft;
|
|
301
|
-
|
|
302
|
-
// Update blit state
|
|
303
|
-
const lastScroll = this.blitState.updateScroll(this.scrollLeft, this.scrollTop);
|
|
304
|
-
|
|
305
|
-
// Check if we should blit
|
|
306
|
-
const { left, right } = getPinnedWidths(this.getVisibleColumns());
|
|
307
|
-
const blitResult = calculateBlit(
|
|
308
|
-
{ x: this.scrollLeft, y: this.scrollTop },
|
|
309
|
-
lastScroll,
|
|
310
|
-
{ width: this.viewportWidth, height: this.viewportHeight },
|
|
311
|
-
{ left, right }
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
if (blitResult.canBlit && this.blitState.hasLastFrame()) {
|
|
315
|
-
// Blitting is possible - the render will copy from last frame
|
|
316
|
-
this.damageTracker.markAllDirty(); // For now, still do full redraw but with blit
|
|
317
|
-
} else {
|
|
318
|
-
// Full redraw needed
|
|
319
|
-
this.damageTracker.markAllDirty();
|
|
320
|
-
}
|
|
321
|
-
|
|
291
|
+
this.damageTracker.markAllDirty();
|
|
322
292
|
this.scheduleRender();
|
|
323
293
|
}
|
|
324
294
|
|
|
325
|
-
setViewportDimensions(width: number, height: number): void {
|
|
295
|
+
setViewportDimensions(width: number, height: number, scrollbarWidth: number = 0): void {
|
|
296
|
+
if (
|
|
297
|
+
Math.abs(this.viewportWidth - width) < 1 &&
|
|
298
|
+
Math.abs(this.viewportHeight - height) < 1 &&
|
|
299
|
+
this.scrollbarWidth === scrollbarWidth
|
|
300
|
+
) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
326
303
|
this.viewportWidth = width;
|
|
327
304
|
this.viewportHeight = height;
|
|
305
|
+
this.scrollbarWidth = scrollbarWidth;
|
|
328
306
|
this.damageTracker.markAllDirty();
|
|
329
307
|
this.updateCanvasSize();
|
|
330
308
|
}
|
|
@@ -335,21 +313,13 @@ export class CanvasRenderer<TData = any> {
|
|
|
335
313
|
const width = this.viewportWidth || this.canvas.clientWidth;
|
|
336
314
|
const height = this.viewportHeight || this.canvas.clientHeight || 600;
|
|
337
315
|
|
|
316
|
+
// Set pixel buffer dimensions only. CSS sizing is handled by the stylesheet
|
|
317
|
+
// (width: 100%; height: 100%) so we never touch canvas.style.width/height here.
|
|
318
|
+
// Modifying canvas style dimensions would change layout, re-fire the
|
|
319
|
+
// ResizeObserver and create an infinite grow loop.
|
|
338
320
|
this.canvas.width = width * dpr;
|
|
339
321
|
this.canvas.height = height * dpr;
|
|
340
|
-
this.
|
|
341
|
-
this.canvas.style.height = `${height}px`;
|
|
342
|
-
|
|
343
|
-
if (this.ctx) {
|
|
344
|
-
if (typeof this.ctx.setTransform === 'function') {
|
|
345
|
-
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
346
|
-
} else {
|
|
347
|
-
(this.ctx as any).scale(dpr, dpr);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Reset blit state on resize
|
|
352
|
-
this.blitState.reset();
|
|
322
|
+
this.ctx?.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
353
323
|
this.scheduleRender();
|
|
354
324
|
}
|
|
355
325
|
|
|
@@ -358,10 +328,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
358
328
|
if (!container) return;
|
|
359
329
|
|
|
360
330
|
const rect = container.getBoundingClientRect();
|
|
361
|
-
this.
|
|
362
|
-
this.viewportHeight = rect.height;
|
|
363
|
-
|
|
364
|
-
this.updateCanvasSize();
|
|
331
|
+
this.setViewportDimensions(rect.width, rect.height);
|
|
365
332
|
}
|
|
366
333
|
|
|
367
334
|
render(): void {
|
|
@@ -369,14 +336,28 @@ export class CanvasRenderer<TData = any> {
|
|
|
369
336
|
this.scheduleRender();
|
|
370
337
|
}
|
|
371
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Schedule a render on the next animation frame.
|
|
341
|
+
* Coalesces: if a frame is already in-flight, marks a follow-up so the next
|
|
342
|
+
* frame fires immediately after (no renders dropped, no pile-up).
|
|
343
|
+
* No-op if nothing is dirty.
|
|
344
|
+
*/
|
|
372
345
|
private scheduleRender(): void {
|
|
373
|
-
if (this.
|
|
346
|
+
if (!this.damageTracker.hasDamage()) return;
|
|
347
|
+
|
|
348
|
+
if (this.animationFrameId !== null) {
|
|
349
|
+
this.nextRenderPending = true;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
374
352
|
|
|
375
|
-
this.renderPending = true;
|
|
376
353
|
this.animationFrameId = requestAnimationFrame(() => {
|
|
377
354
|
this.doRender();
|
|
378
|
-
this.renderPending = false;
|
|
379
355
|
this.animationFrameId = null;
|
|
356
|
+
|
|
357
|
+
if (this.nextRenderPending) {
|
|
358
|
+
this.nextRenderPending = false;
|
|
359
|
+
this.scheduleRender();
|
|
360
|
+
}
|
|
380
361
|
});
|
|
381
362
|
}
|
|
382
363
|
|
|
@@ -385,41 +366,28 @@ export class CanvasRenderer<TData = any> {
|
|
|
385
366
|
}
|
|
386
367
|
|
|
387
368
|
private getVisibleColumns(): Column[] {
|
|
388
|
-
return this.gridApi.getAllColumns().filter((col) => col
|
|
369
|
+
return this.gridApi.getAllColumns().filter((col) => isColumnVisible(col));
|
|
389
370
|
}
|
|
390
371
|
|
|
391
|
-
/**
|
|
392
|
-
* Prepare columns for rendering
|
|
393
|
-
*
|
|
394
|
-
* Caches column definitions and X positions for efficient cell rendering.
|
|
395
|
-
* This is called once per render frame before rendering visible rows.
|
|
396
|
-
*
|
|
397
|
-
* Performance optimizations:
|
|
398
|
-
* 1. Column definition caching - Avoids repeated getColumnDef() calls
|
|
399
|
-
* 2. Column position caching - Enables O(1) column X lookup instead of O(n)
|
|
400
|
-
*
|
|
401
|
-
* @see columnPreps - Cached column definitions
|
|
402
|
-
* @see columnPositions - Cached column X positions
|
|
403
|
-
*/
|
|
372
|
+
/** Build the per-column prep cache once per frame before rendering visible rows. */
|
|
404
373
|
private prepareColumns(): void {
|
|
405
|
-
const columns = this.getVisibleColumns();
|
|
406
374
|
this.columnPreps.clear();
|
|
407
|
-
this.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const colDef = getColumnDef(column, this.gridApi);
|
|
413
|
-
this.columnPreps.set(column.colId, prepColumn(this.ctx, column, colDef, this.theme));
|
|
414
|
-
this.columnPositions.set(column.colId, x);
|
|
415
|
-
x += Math.floor(column.width);
|
|
375
|
+
for (const column of this.getVisibleColumns()) {
|
|
376
|
+
this.columnPreps.set(
|
|
377
|
+
column.colId,
|
|
378
|
+
prepColumn(this.ctx, column, getColumnDef(column, this.gridApi), this.theme)
|
|
379
|
+
);
|
|
416
380
|
}
|
|
417
381
|
}
|
|
418
382
|
|
|
419
383
|
private doRender(): void {
|
|
384
|
+
// Skip paint entirely if nothing has been marked dirty.
|
|
385
|
+
if (!this.damageTracker.hasDamage()) return;
|
|
386
|
+
|
|
420
387
|
const startTime = performance.now();
|
|
421
388
|
const width = this.viewportWidth || this.canvas.clientWidth;
|
|
422
389
|
const height = this.viewportHeight || this.canvas.clientHeight;
|
|
390
|
+
const availableWidth = width - this.scrollbarWidth;
|
|
423
391
|
|
|
424
392
|
// Clear canvas
|
|
425
393
|
this.ctx.clearRect(0, 0, width, height);
|
|
@@ -440,12 +408,22 @@ export class CanvasRenderer<TData = any> {
|
|
|
440
408
|
const { startRow, endRow } = getVisibleRowRange(
|
|
441
409
|
this.scrollTop,
|
|
442
410
|
height,
|
|
443
|
-
this.rowHeight,
|
|
411
|
+
this.theme.rowHeight,
|
|
444
412
|
totalRows,
|
|
445
413
|
this.rowBuffer,
|
|
446
414
|
this.gridApi
|
|
447
415
|
);
|
|
448
416
|
|
|
417
|
+
// Log state periodically (not every frame to avoid flood)
|
|
418
|
+
if (Math.random() < 0.01) {
|
|
419
|
+
console.log('[ArgentGrid] doRender', {
|
|
420
|
+
viewport: { width, height },
|
|
421
|
+
rows: { total: totalRows, start: startRow, end: endRow },
|
|
422
|
+
scroll: { top: this.scrollTop, left: this.scrollLeft },
|
|
423
|
+
columns: allVisibleColumns.length,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
449
427
|
// Prepare columns (sets font, caches colDef)
|
|
450
428
|
this.prepareColumns();
|
|
451
429
|
|
|
@@ -453,28 +431,34 @@ export class CanvasRenderer<TData = any> {
|
|
|
453
431
|
this.ctx.font = getFontFromTheme(this.theme);
|
|
454
432
|
this.ctx.textBaseline = 'middle';
|
|
455
433
|
|
|
434
|
+
const positionedColumns = getPositionedColumns(
|
|
435
|
+
allVisibleColumns,
|
|
436
|
+
this.scrollLeft,
|
|
437
|
+
width,
|
|
438
|
+
leftWidth,
|
|
439
|
+
rightWidth,
|
|
440
|
+
availableWidth
|
|
441
|
+
);
|
|
442
|
+
|
|
456
443
|
// Render all visible rows
|
|
457
444
|
walkRows(
|
|
458
445
|
startRow,
|
|
459
446
|
endRow,
|
|
460
447
|
this.scrollTop,
|
|
461
|
-
this.rowHeight,
|
|
448
|
+
this.theme.rowHeight,
|
|
462
449
|
(rowIndex) => this.gridApi.getDisplayedRowAtIndex(rowIndex),
|
|
463
450
|
(rowIndex, y, _rowHeight, rowNode) => {
|
|
464
451
|
if (!rowNode) return;
|
|
465
|
-
this.renderRow(rowIndex, y, rowNode,
|
|
452
|
+
this.renderRow(rowIndex, y, rowNode, positionedColumns);
|
|
466
453
|
},
|
|
467
454
|
this.gridApi
|
|
468
455
|
);
|
|
469
456
|
|
|
470
457
|
// Draw grid lines
|
|
471
|
-
this.drawGridLines(
|
|
458
|
+
this.drawGridLines(positionedColumns, startRow, endRow, width, height, leftWidth, rightWidth);
|
|
472
459
|
|
|
473
460
|
// Draw range selections
|
|
474
|
-
this.drawRangeSelections(
|
|
475
|
-
|
|
476
|
-
// Store current frame for blitting
|
|
477
|
-
this.blitState.setLastCanvas(this.canvas);
|
|
461
|
+
this.drawRangeSelections(positionedColumns, leftWidth, rightWidth, width);
|
|
478
462
|
|
|
479
463
|
// Clear damage
|
|
480
464
|
this.damageTracker.clear();
|
|
@@ -483,7 +467,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
483
467
|
}
|
|
484
468
|
|
|
485
469
|
private drawRangeSelections(
|
|
486
|
-
|
|
470
|
+
positionedColumns: PositionedColumn[],
|
|
487
471
|
leftPinnedWidth: number,
|
|
488
472
|
rightPinnedWidth: number,
|
|
489
473
|
viewportWidth: number
|
|
@@ -493,30 +477,19 @@ export class CanvasRenderer<TData = any> {
|
|
|
493
477
|
|
|
494
478
|
for (const range of ranges) {
|
|
495
479
|
// Calculate Y boundaries
|
|
496
|
-
const startY = range.startRow * this.rowHeight - this.scrollTop;
|
|
497
|
-
const endY = (range.endRow + 1) * this.rowHeight - this.scrollTop;
|
|
498
|
-
|
|
499
|
-
// Calculate X boundaries
|
|
500
|
-
const startColIdx = allVisibleColumns.findIndex((c) => c.colId === range.startColumn);
|
|
501
|
-
const endColIdx = allVisibleColumns.findIndex((c) => c.colId === range.endColumn);
|
|
502
|
-
|
|
503
|
-
if (startColIdx === -1 || endColIdx === -1) continue;
|
|
480
|
+
const startY = range.startRow * this.theme.rowHeight - this.scrollTop;
|
|
481
|
+
const endY = (range.endRow + 1) * this.theme.rowHeight - this.scrollTop;
|
|
504
482
|
|
|
505
483
|
let minX = Infinity;
|
|
506
484
|
let maxX = -Infinity;
|
|
507
485
|
|
|
508
486
|
// Calculate the total bounding box of all columns in the range
|
|
509
487
|
range.columns.forEach((col) => {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
rightPinnedWidth,
|
|
516
|
-
viewportWidth
|
|
517
|
-
);
|
|
518
|
-
minX = Math.min(minX, xPos);
|
|
519
|
-
maxX = Math.max(maxX, xPos + col.width);
|
|
488
|
+
const pc = positionedColumns.find((p) => p.column.colId === col.colId);
|
|
489
|
+
if (pc) {
|
|
490
|
+
minX = Math.min(minX, pc.x);
|
|
491
|
+
maxX = Math.max(maxX, pc.x + pc.width);
|
|
492
|
+
}
|
|
520
493
|
});
|
|
521
494
|
|
|
522
495
|
if (minX === Infinity) continue;
|
|
@@ -542,18 +515,15 @@ export class CanvasRenderer<TData = any> {
|
|
|
542
515
|
rowIndex: number,
|
|
543
516
|
y: number,
|
|
544
517
|
rowNode: IRowNode<TData>,
|
|
545
|
-
|
|
546
|
-
viewportWidth: number,
|
|
547
|
-
leftWidth: number,
|
|
548
|
-
rightWidth: number
|
|
518
|
+
positionedColumns: PositionedColumn[]
|
|
549
519
|
): void {
|
|
550
520
|
if (rowNode.detail) {
|
|
551
|
-
this.renderDetailRow(rowIndex, y, rowNode, viewportWidth);
|
|
521
|
+
this.renderDetailRow(rowIndex, y, rowNode, this.viewportWidth);
|
|
552
522
|
return;
|
|
553
523
|
}
|
|
554
524
|
|
|
555
525
|
const isEvenRow = rowIndex % 2 === 0;
|
|
556
|
-
const rowHeight = rowNode.rowHeight || this.rowHeight;
|
|
526
|
+
const rowHeight = rowNode.rowHeight || this.theme.rowHeight;
|
|
557
527
|
|
|
558
528
|
// Draw row background
|
|
559
529
|
let bgColor = isEvenRow ? this.theme.bgCellEven : this.theme.bgCell;
|
|
@@ -562,61 +532,13 @@ export class CanvasRenderer<TData = any> {
|
|
|
562
532
|
}
|
|
563
533
|
|
|
564
534
|
this.ctx.fillStyle = bgColor;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// Render left pinned columns
|
|
568
|
-
const leftPinned = allVisibleColumns.filter((c) => c.pinned === 'left');
|
|
569
|
-
this.renderColumns(
|
|
570
|
-
leftPinned,
|
|
571
|
-
0,
|
|
572
|
-
false,
|
|
573
|
-
rowNode,
|
|
574
|
-
y,
|
|
575
|
-
viewportWidth,
|
|
576
|
-
leftWidth,
|
|
577
|
-
rightWidth,
|
|
578
|
-
allVisibleColumns
|
|
579
|
-
);
|
|
535
|
+
// Fill background for the entire available width
|
|
536
|
+
this.ctx.fillRect(0, Math.floor(y), this.viewportWidth - this.scrollbarWidth, rowHeight);
|
|
580
537
|
|
|
581
|
-
// Render
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
this.ctx.save();
|
|
585
|
-
this.ctx.beginPath();
|
|
586
|
-
this.ctx.rect(
|
|
587
|
-
Math.floor(leftWidth),
|
|
588
|
-
Math.floor(y),
|
|
589
|
-
Math.floor(viewportWidth - leftWidth - rightWidth),
|
|
590
|
-
rowHeight
|
|
591
|
-
);
|
|
592
|
-
this.ctx.clip();
|
|
593
|
-
this.renderColumns(
|
|
594
|
-
centerColumns,
|
|
595
|
-
leftWidth,
|
|
596
|
-
true,
|
|
597
|
-
rowNode,
|
|
598
|
-
y,
|
|
599
|
-
viewportWidth,
|
|
600
|
-
leftWidth,
|
|
601
|
-
rightWidth,
|
|
602
|
-
allVisibleColumns
|
|
603
|
-
);
|
|
604
|
-
this.ctx.restore();
|
|
538
|
+
// Render columns using pre-calculated positions
|
|
539
|
+
for (const pc of positionedColumns) {
|
|
540
|
+
this.renderCell(pc.column, pc.x, y, pc.width, rowNode, positionedColumns);
|
|
605
541
|
}
|
|
606
|
-
|
|
607
|
-
// Render right pinned columns
|
|
608
|
-
const rightPinned = allVisibleColumns.filter((c) => c.pinned === 'right');
|
|
609
|
-
this.renderColumns(
|
|
610
|
-
rightPinned,
|
|
611
|
-
viewportWidth - rightWidth,
|
|
612
|
-
false,
|
|
613
|
-
rowNode,
|
|
614
|
-
y,
|
|
615
|
-
viewportWidth,
|
|
616
|
-
leftWidth,
|
|
617
|
-
rightWidth,
|
|
618
|
-
allVisibleColumns
|
|
619
|
-
);
|
|
620
542
|
}
|
|
621
543
|
|
|
622
544
|
private renderDetailRow(
|
|
@@ -644,112 +566,48 @@ export class CanvasRenderer<TData = any> {
|
|
|
644
566
|
this.ctx.font = getFontFromTheme(this.theme);
|
|
645
567
|
}
|
|
646
568
|
|
|
647
|
-
private renderColumns(
|
|
648
|
-
columns: Column[],
|
|
649
|
-
startX: number,
|
|
650
|
-
isScrollable: boolean,
|
|
651
|
-
rowNode: IRowNode<TData>,
|
|
652
|
-
y: number,
|
|
653
|
-
viewportWidth: number,
|
|
654
|
-
leftWidth: number,
|
|
655
|
-
rightWidth: number,
|
|
656
|
-
allVisibleColumns: Column[]
|
|
657
|
-
): void {
|
|
658
|
-
let x = startX;
|
|
659
|
-
|
|
660
|
-
for (const col of columns) {
|
|
661
|
-
const cellX = isScrollable ? x - this.scrollLeft : x;
|
|
662
|
-
const cellWidth = col.width;
|
|
663
|
-
|
|
664
|
-
// Skip if outside viewport (for center columns)
|
|
665
|
-
if (isScrollable && (cellX + cellWidth < leftWidth || cellX > viewportWidth - rightWidth)) {
|
|
666
|
-
x += cellWidth;
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
this.renderCell(col, cellX, y, cellWidth, rowNode, allVisibleColumns);
|
|
671
|
-
x += cellWidth;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
569
|
private renderCell(
|
|
676
570
|
column: Column,
|
|
677
571
|
x: number,
|
|
678
572
|
y: number,
|
|
679
573
|
width: number,
|
|
680
574
|
rowNode: IRowNode<TData>,
|
|
681
|
-
|
|
575
|
+
positionedColumns: PositionedColumn[]
|
|
682
576
|
): void {
|
|
683
577
|
const prep = this.columnPreps.get(column.colId);
|
|
684
578
|
if (!prep) return;
|
|
685
579
|
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
let textX = x + this.theme.cellPadding;
|
|
689
|
-
|
|
690
|
-
const isSelectionColumn = column.colId === 'ag-Grid-SelectionColumn';
|
|
691
|
-
|
|
692
|
-
// Check for checkbox selection
|
|
693
|
-
if (isSelectionColumn) {
|
|
694
|
-
const checkboxSize = 14;
|
|
695
|
-
const checkboxY = Math.floor(y + (this.rowHeight - checkboxSize) / 2);
|
|
696
|
-
const checkboxX = Math.floor(x + (width - checkboxSize) / 2);
|
|
697
|
-
|
|
698
|
-
drawCheckbox(this.ctx, checkboxX, checkboxY, checkboxSize, rowNode.selected, this.theme);
|
|
699
|
-
return; // Dedicated column only shows checkbox
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Check for sparkline
|
|
703
|
-
if (prep.colDef?.sparklineOptions) {
|
|
704
|
-
drawSparkline(this.ctx, cellValue, x, y, width, this.rowHeight, prep.colDef.sparklineOptions);
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
|
|
580
|
+
const value = getCellValue(column, prep.colDef, rowNode, this.gridApi);
|
|
708
581
|
const formattedValue = getFormattedValue(
|
|
709
|
-
|
|
582
|
+
value,
|
|
710
583
|
prep.colDef,
|
|
711
584
|
rowNode.data,
|
|
712
585
|
rowNode,
|
|
713
586
|
this.gridApi
|
|
714
587
|
);
|
|
715
588
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
(rowNode.group || rowNode.master || rowNode.level > 0)
|
|
729
|
-
) {
|
|
730
|
-
const indent = rowNode.level * this.theme.groupIndentWidth;
|
|
731
|
-
textX += indent;
|
|
732
|
-
|
|
733
|
-
// Draw expand/collapse indicator
|
|
734
|
-
if (rowNode.group || rowNode.master) {
|
|
735
|
-
drawGroupIndicator(this.ctx, textX, y, this.rowHeight, rowNode.expanded, this.theme);
|
|
736
|
-
textX += this.theme.groupIndicatorSize + 3;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const truncatedText = truncateText(
|
|
741
|
-
this.ctx,
|
|
589
|
+
drawCell(this.ctx, prep, {
|
|
590
|
+
ctx: this.ctx,
|
|
591
|
+
theme: this.theme,
|
|
592
|
+
column,
|
|
593
|
+
colDef: prep.colDef,
|
|
594
|
+
rowNode,
|
|
595
|
+
rowIndex: rowNode.displayedRowIndex,
|
|
596
|
+
x,
|
|
597
|
+
y,
|
|
598
|
+
width,
|
|
599
|
+
height: rowNode.rowHeight || this.theme.rowHeight,
|
|
600
|
+
value,
|
|
742
601
|
formattedValue,
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
602
|
+
isSelected: rowNode.selected,
|
|
603
|
+
isHovered: false, // TODO: Implement hover
|
|
604
|
+
isEvenRow: rowNode.displayedRowIndex % 2 === 0,
|
|
605
|
+
api: this.gridApi,
|
|
606
|
+
});
|
|
749
607
|
}
|
|
750
608
|
|
|
751
609
|
private drawGridLines(
|
|
752
|
-
|
|
610
|
+
positionedColumns: PositionedColumn[],
|
|
753
611
|
startRow: number,
|
|
754
612
|
endRow: number,
|
|
755
613
|
viewportWidth: number,
|
|
@@ -762,9 +620,9 @@ export class CanvasRenderer<TData = any> {
|
|
|
762
620
|
this.ctx,
|
|
763
621
|
startRow,
|
|
764
622
|
endRow,
|
|
765
|
-
this.rowHeight,
|
|
623
|
+
this.theme.rowHeight,
|
|
766
624
|
this.scrollTop,
|
|
767
|
-
viewportWidth,
|
|
625
|
+
viewportWidth - this.scrollbarWidth,
|
|
768
626
|
this.theme,
|
|
769
627
|
this.gridApi
|
|
770
628
|
);
|
|
@@ -772,7 +630,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
772
630
|
// Draw vertical column lines
|
|
773
631
|
drawColumnLines(
|
|
774
632
|
this.ctx,
|
|
775
|
-
|
|
633
|
+
this.getVisibleColumns(),
|
|
776
634
|
this.scrollLeft,
|
|
777
635
|
this.scrollTop,
|
|
778
636
|
viewportWidth,
|
|
@@ -782,8 +640,9 @@ export class CanvasRenderer<TData = any> {
|
|
|
782
640
|
this.theme,
|
|
783
641
|
startRow,
|
|
784
642
|
endRow,
|
|
785
|
-
this.rowHeight,
|
|
786
|
-
this.gridApi
|
|
643
|
+
this.theme.rowHeight,
|
|
644
|
+
this.gridApi,
|
|
645
|
+
viewportWidth - this.scrollbarWidth
|
|
787
646
|
);
|
|
788
647
|
}
|
|
789
648
|
|
|
@@ -796,11 +655,12 @@ export class CanvasRenderer<TData = any> {
|
|
|
796
655
|
const { rowIndex, columnIndex } = performHitTest(
|
|
797
656
|
event.clientX - rect.left,
|
|
798
657
|
event.clientY - rect.top,
|
|
799
|
-
this.rowHeight,
|
|
658
|
+
this.theme.rowHeight,
|
|
800
659
|
this.scrollTop,
|
|
801
660
|
this.scrollLeft,
|
|
802
661
|
this.viewportWidth,
|
|
803
|
-
this.getVisibleColumns()
|
|
662
|
+
this.getVisibleColumns(),
|
|
663
|
+
this.viewportWidth - this.scrollbarWidth
|
|
804
664
|
);
|
|
805
665
|
const columns = this.getVisibleColumns();
|
|
806
666
|
const colId = columnIndex !== -1 ? columns[columnIndex].colId : null;
|
|
@@ -819,15 +679,20 @@ export class CanvasRenderer<TData = any> {
|
|
|
819
679
|
const { rowIndex, columnIndex } = performHitTest(
|
|
820
680
|
event.clientX - rect.left,
|
|
821
681
|
event.clientY - rect.top,
|
|
822
|
-
this.rowHeight,
|
|
682
|
+
this.theme.rowHeight,
|
|
823
683
|
this.scrollTop,
|
|
824
684
|
this.scrollLeft,
|
|
825
685
|
this.viewportWidth,
|
|
826
|
-
this.getVisibleColumns()
|
|
686
|
+
this.getVisibleColumns(),
|
|
687
|
+
this.viewportWidth - this.scrollbarWidth
|
|
827
688
|
);
|
|
828
689
|
const columns = this.getVisibleColumns();
|
|
829
690
|
const colId = columnIndex !== -1 ? columns[columnIndex].colId : null;
|
|
830
691
|
|
|
692
|
+
// Update cursor: pointer for button cells, default otherwise
|
|
693
|
+
const hoveredColDef = colId ? this.columnPreps.get(colId)?.colDef : null;
|
|
694
|
+
this.canvas.style.cursor = hoveredColDef?.buttonOptions ? 'pointer' : '';
|
|
695
|
+
|
|
831
696
|
if (this.onMouseMove) {
|
|
832
697
|
this.onMouseMove(event, rowIndex, colId);
|
|
833
698
|
}
|
|
@@ -839,11 +704,12 @@ export class CanvasRenderer<TData = any> {
|
|
|
839
704
|
const { rowIndex, columnIndex } = performHitTest(
|
|
840
705
|
event.clientX - rect.left,
|
|
841
706
|
event.clientY - rect.top,
|
|
842
|
-
this.rowHeight,
|
|
707
|
+
this.theme.rowHeight,
|
|
843
708
|
this.scrollTop,
|
|
844
709
|
this.scrollLeft,
|
|
845
710
|
this.viewportWidth,
|
|
846
|
-
this.getVisibleColumns()
|
|
711
|
+
this.getVisibleColumns(),
|
|
712
|
+
this.viewportWidth - this.scrollbarWidth
|
|
847
713
|
);
|
|
848
714
|
const columns = this.getVisibleColumns();
|
|
849
715
|
const colId = columnIndex !== -1 ? columns[columnIndex].colId : null;
|
|
@@ -858,23 +724,45 @@ export class CanvasRenderer<TData = any> {
|
|
|
858
724
|
const { rowIndex, columnIndex } = performHitTest(
|
|
859
725
|
event.clientX - rect.left,
|
|
860
726
|
event.clientY - rect.top,
|
|
861
|
-
this.rowHeight,
|
|
727
|
+
this.theme.rowHeight,
|
|
862
728
|
this.scrollTop,
|
|
863
729
|
this.scrollLeft,
|
|
864
730
|
this.viewportWidth,
|
|
865
|
-
this.getVisibleColumns()
|
|
731
|
+
this.getVisibleColumns(),
|
|
732
|
+
this.viewportWidth - this.scrollbarWidth
|
|
866
733
|
);
|
|
867
734
|
const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
|
|
868
735
|
if (!rowNode) return;
|
|
869
736
|
|
|
870
|
-
// Handle selection column
|
|
737
|
+
// Handle selection column or explicit checkbox renderer
|
|
871
738
|
const columns = this.getVisibleColumns();
|
|
872
739
|
const clickedCol = columnIndex !== -1 ? columns[columnIndex] : null;
|
|
873
|
-
|
|
740
|
+
const clickedColDef = clickedCol ? this.columnPreps.get(clickedCol.colId)?.colDef : null;
|
|
741
|
+
|
|
742
|
+
if (
|
|
743
|
+
clickedCol?.colId === 'ag-Grid-SelectionColumn' ||
|
|
744
|
+
clickedColDef?.cellRenderer === 'checkbox'
|
|
745
|
+
) {
|
|
874
746
|
rowNode.setSelected(!rowNode.selected);
|
|
875
747
|
return;
|
|
876
748
|
}
|
|
877
749
|
|
|
750
|
+
// Handle button cell — fire onClick and stop propagation to row click
|
|
751
|
+
if (clickedCol) {
|
|
752
|
+
const colDef = this.columnPreps.get(clickedCol.colId)?.colDef;
|
|
753
|
+
if (colDef?.buttonOptions?.onClick) {
|
|
754
|
+
colDef.buttonOptions.onClick({
|
|
755
|
+
value: clickedCol.field ? getValueByPath(rowNode.data, clickedCol.field) : undefined,
|
|
756
|
+
data: rowNode.data,
|
|
757
|
+
node: rowNode,
|
|
758
|
+
colDef,
|
|
759
|
+
api: this.gridApi,
|
|
760
|
+
event,
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
878
766
|
// Handle expand/collapse
|
|
879
767
|
if ((rowNode.group || rowNode.master) && columnIndex !== -1) {
|
|
880
768
|
const columns = this.getVisibleColumns();
|
|
@@ -930,11 +818,12 @@ export class CanvasRenderer<TData = any> {
|
|
|
930
818
|
const { rowIndex, columnIndex } = performHitTest(
|
|
931
819
|
event.clientX - rect.left,
|
|
932
820
|
event.clientY - rect.top,
|
|
933
|
-
this.rowHeight,
|
|
821
|
+
this.theme.rowHeight,
|
|
934
822
|
this.scrollTop,
|
|
935
823
|
this.scrollLeft,
|
|
936
824
|
this.viewportWidth,
|
|
937
|
-
this.getVisibleColumns()
|
|
825
|
+
this.getVisibleColumns(),
|
|
826
|
+
this.viewportWidth - this.scrollbarWidth
|
|
938
827
|
);
|
|
939
828
|
if (columnIndex === -1) return;
|
|
940
829
|
|
|
@@ -954,7 +843,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
954
843
|
return performHitTest(
|
|
955
844
|
event.clientX - rect.left,
|
|
956
845
|
event.clientY - rect.top,
|
|
957
|
-
this.rowHeight,
|
|
846
|
+
this.theme.rowHeight,
|
|
958
847
|
this.scrollTop,
|
|
959
848
|
this.scrollLeft,
|
|
960
849
|
this.viewportWidth,
|
|
@@ -970,7 +859,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
970
859
|
const container = this.canvas.parentElement;
|
|
971
860
|
if (!container) return;
|
|
972
861
|
|
|
973
|
-
const targetPosition = rowIndex * this.rowHeight;
|
|
862
|
+
const targetPosition = rowIndex * this.theme.rowHeight;
|
|
974
863
|
container.scrollTop = targetPosition;
|
|
975
864
|
this.scrollTop = targetPosition;
|
|
976
865
|
this.damageTracker.markAllDirty();
|
|
@@ -1042,7 +931,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
1042
931
|
getRowAtPosition(y: number): number {
|
|
1043
932
|
const scrollTop = this.scrollTop || 0;
|
|
1044
933
|
const rowY = y + scrollTop;
|
|
1045
|
-
return Math.floor(rowY / this.rowHeight);
|
|
934
|
+
return Math.floor(rowY / this.theme.rowHeight);
|
|
1046
935
|
}
|
|
1047
936
|
|
|
1048
937
|
destroy(): void {
|
|
@@ -1060,6 +949,8 @@ export class CanvasRenderer<TData = any> {
|
|
|
1060
949
|
this.canvas.removeEventListener('mousedown', this.mousedownListener);
|
|
1061
950
|
if (this.mousemoveListener)
|
|
1062
951
|
this.canvas.removeEventListener('mousemove', this.mousemoveListener);
|
|
952
|
+
if (this.mouseleaveListener)
|
|
953
|
+
this.canvas.removeEventListener('mouseleave', this.mouseleaveListener);
|
|
1063
954
|
if (this.clickListener) this.canvas.removeEventListener('click', this.clickListener);
|
|
1064
955
|
if (this.dblclickListener) this.canvas.removeEventListener('dblclick', this.dblclickListener);
|
|
1065
956
|
if (this.mouseupListener) this.canvas.removeEventListener('mouseup', this.mouseupListener);
|
|
@@ -1068,8 +959,8 @@ export class CanvasRenderer<TData = any> {
|
|
|
1068
959
|
window.removeEventListener('resize', this.resizeListener);
|
|
1069
960
|
}
|
|
1070
961
|
|
|
1071
|
-
this.
|
|
1072
|
-
this.
|
|
962
|
+
this.nextRenderPending = false;
|
|
963
|
+
this.animationFrameId = null;
|
|
1073
964
|
this.damageTracker.reset();
|
|
1074
965
|
}
|
|
1075
966
|
}
|