argent-grid 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 (53) hide show
  1. package/.github/workflows/pages.yml +68 -0
  2. package/AGENTS.md +179 -0
  3. package/README.md +222 -0
  4. package/demo-app/README.md +70 -0
  5. package/demo-app/angular.json +78 -0
  6. package/demo-app/e2e/benchmark.spec.ts +53 -0
  7. package/demo-app/e2e/demo-page.spec.ts +77 -0
  8. package/demo-app/e2e/grid-features.spec.ts +269 -0
  9. package/demo-app/package-lock.json +14023 -0
  10. package/demo-app/package.json +36 -0
  11. package/demo-app/playwright-test-menu.js +19 -0
  12. package/demo-app/playwright.config.ts +23 -0
  13. package/demo-app/src/app/app.component.ts +10 -0
  14. package/demo-app/src/app/app.config.ts +13 -0
  15. package/demo-app/src/app/app.routes.ts +7 -0
  16. package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
  17. package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
  18. package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
  19. package/demo-app/src/index.html +19 -0
  20. package/demo-app/src/main.ts +6 -0
  21. package/demo-app/tsconfig.json +31 -0
  22. package/ng-package.json +8 -0
  23. package/package.json +60 -0
  24. package/plan.md +131 -0
  25. package/setup-vitest.ts +18 -0
  26. package/src/lib/argent-grid.module.ts +21 -0
  27. package/src/lib/components/argent-grid.component.css +483 -0
  28. package/src/lib/components/argent-grid.component.html +320 -0
  29. package/src/lib/components/argent-grid.component.spec.ts +189 -0
  30. package/src/lib/components/argent-grid.component.ts +1188 -0
  31. package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
  32. package/src/lib/rendering/canvas-renderer.ts +962 -0
  33. package/src/lib/rendering/render/blit.spec.ts +453 -0
  34. package/src/lib/rendering/render/blit.ts +393 -0
  35. package/src/lib/rendering/render/cells.ts +369 -0
  36. package/src/lib/rendering/render/index.ts +105 -0
  37. package/src/lib/rendering/render/lines.ts +363 -0
  38. package/src/lib/rendering/render/theme.spec.ts +282 -0
  39. package/src/lib/rendering/render/theme.ts +201 -0
  40. package/src/lib/rendering/render/types.ts +279 -0
  41. package/src/lib/rendering/render/walk.spec.ts +360 -0
  42. package/src/lib/rendering/render/walk.ts +360 -0
  43. package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
  44. package/src/lib/rendering/utils/damage-tracker.ts +423 -0
  45. package/src/lib/rendering/utils/index.ts +7 -0
  46. package/src/lib/services/grid.service.spec.ts +1039 -0
  47. package/src/lib/services/grid.service.ts +1284 -0
  48. package/src/lib/types/ag-grid-types.ts +970 -0
  49. package/src/public-api.ts +22 -0
  50. package/tsconfig.json +32 -0
  51. package/tsconfig.lib.json +11 -0
  52. package/tsconfig.spec.json +8 -0
  53. package/vitest.config.ts +55 -0
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Cell Rendering for Canvas Renderer
3
+ *
4
+ * Handles drawing of individual cells with prep/draw cycle optimization.
5
+ */
6
+
7
+ import { Column, IRowNode, ColDef, GridApi } from '../../types/ag-grid-types';
8
+ import { CellDrawContext, ColumnPrepResult, GridTheme } from './types';
9
+ import { getFontFromTheme } from './theme';
10
+
11
+ /**
12
+ * Get value from object using path (e.g. 'pivotData.NY.salary')
13
+ */
14
+ export function getValueByPath(obj: any, path: string): any {
15
+ if (!path || !obj) return undefined;
16
+ if (!path.includes('.')) return obj[path];
17
+
18
+ return path.split('.').reduce((acc, part) => acc && acc[part], obj);
19
+ }
20
+
21
+ // ============================================================================
22
+ // CELL PREP PHASE
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Prepare column for rendering
27
+ * Called once per column before rendering all cells in that column
28
+ */
29
+ export function prepColumn<TData = any>(
30
+ ctx: CanvasRenderingContext2D,
31
+ column: Column,
32
+ colDef: ColDef<TData> | null,
33
+ theme: GridTheme
34
+ ): ColumnPrepResult<TData> {
35
+ // Set font once per column (expensive operation)
36
+ const font = getFontFromTheme(theme);
37
+ ctx.font = font;
38
+
39
+ return {
40
+ column,
41
+ colDef,
42
+ theme,
43
+ font,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Prepare multiple columns
49
+ */
50
+ export function prepColumns<TData = any>(
51
+ ctx: CanvasRenderingContext2D,
52
+ columns: Column[],
53
+ getColDef: (col: Column) => ColDef<TData> | null,
54
+ theme: GridTheme
55
+ ): Map<string, ColumnPrepResult<TData>> {
56
+ const results = new Map<string, ColumnPrepResult<TData>>();
57
+
58
+ for (const column of columns) {
59
+ const colDef = getColDef(column);
60
+ results.set(column.colId, prepColumn(ctx, column, colDef, theme));
61
+ }
62
+
63
+ return results;
64
+ }
65
+
66
+ // ============================================================================
67
+ // CELL DRAW PHASE
68
+ // ============================================================================
69
+
70
+ /**
71
+ * Draw a single cell
72
+ */
73
+ export function drawCell<TData = any>(
74
+ ctx: CanvasRenderingContext2D,
75
+ prep: ColumnPrepResult<TData>,
76
+ context: CellDrawContext<TData>
77
+ ): void {
78
+ const { x, y, width, height, value, formattedValue, column, rowNode } = context;
79
+
80
+ // Draw cell background
81
+ drawCellBackground(ctx, context);
82
+
83
+ // Draw cell content based on column type
84
+ drawCellContent(ctx, prep, context);
85
+
86
+ // Draw group indicators if needed
87
+ if (rowNode && (rowNode.group || rowNode.level > 0)) {
88
+ drawGroupIndicators(ctx, prep, context);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Draw cell background
94
+ */
95
+ export function drawCellBackground<TData = any>(
96
+ ctx: CanvasRenderingContext2D,
97
+ context: CellDrawContext<TData>
98
+ ): void {
99
+ const { x, y, width, height, isSelected, isHovered, isEvenRow } = context;
100
+ const { theme } = context;
101
+
102
+ // Determine background color
103
+ let bgColor = isEvenRow ? theme.bgCellEven : theme.bgCell;
104
+ if (isSelected) bgColor = theme.bgSelection;
105
+ if (isHovered) bgColor = theme.bgHover;
106
+
107
+ ctx.fillStyle = bgColor;
108
+ ctx.fillRect(Math.floor(x), Math.floor(y), Math.floor(width), Math.floor(height));
109
+ }
110
+
111
+ /**
112
+ * Draw cell content (text)
113
+ */
114
+ export function drawCellContent<TData = any>(
115
+ ctx: CanvasRenderingContext2D,
116
+ prep: ColumnPrepResult<TData>,
117
+ context: CellDrawContext<TData>
118
+ ): void {
119
+ const { x, y, width, height, formattedValue, theme } = context;
120
+
121
+ if (!formattedValue) return;
122
+
123
+ // Calculate text position with padding
124
+ const textX = x + theme.cellPadding;
125
+ const textY = y + height / 2; // Centered vertically
126
+
127
+ // Set text properties
128
+ ctx.fillStyle = theme.textCell;
129
+ ctx.textBaseline = 'middle';
130
+
131
+ // Truncate text if needed
132
+ const maxWidth = width - theme.cellPadding * 2;
133
+ const truncatedText = truncateText(ctx, formattedValue, maxWidth);
134
+
135
+ if (truncatedText) {
136
+ ctx.fillText(truncatedText, Math.floor(textX), Math.floor(textY));
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Draw group/tree indicators
142
+ */
143
+ export function drawGroupIndicators<TData = any>(
144
+ ctx: CanvasRenderingContext2D,
145
+ prep: ColumnPrepResult<TData>,
146
+ context: CellDrawContext<TData>
147
+ ): void {
148
+ const { x, y, width, height, column, rowNode, theme, isEvenRow } = context;
149
+
150
+ if (!rowNode) return;
151
+
152
+ // Only draw on first visible column or auto-group column
153
+ const isAutoGroupCol = column.colId === 'ag-Grid-AutoColumn';
154
+
155
+ if (!isAutoGroupCol) return;
156
+
157
+ // Calculate indent
158
+ const indent = rowNode.level * theme.groupIndentWidth;
159
+ const indicatorX = x + theme.cellPadding + indent;
160
+ const indicatorY = y + height / 2;
161
+
162
+ // Draw expand/collapse indicator for groups
163
+ if (rowNode.group) {
164
+ ctx.fillStyle = theme.textCell;
165
+ ctx.beginPath();
166
+
167
+ if (rowNode.expanded) {
168
+ // Expanded: horizontal line (minus sign)
169
+ const lineY = Math.floor(indicatorY);
170
+ ctx.moveTo(Math.floor(indicatorX), lineY);
171
+ ctx.lineTo(Math.floor(indicatorX + theme.groupIndicatorSize), lineY);
172
+ } else {
173
+ // Collapsed: plus sign
174
+ const size = theme.groupIndicatorSize;
175
+ const centerX = Math.floor(indicatorX + size / 2);
176
+ const centerY = Math.floor(indicatorY);
177
+
178
+ // Horizontal line
179
+ ctx.moveTo(Math.floor(indicatorX), centerY);
180
+ ctx.lineTo(Math.floor(indicatorX + size), centerY);
181
+ // Vertical line
182
+ ctx.moveTo(centerX, centerY - size / 2);
183
+ ctx.lineTo(centerX, centerY + size / 2);
184
+ }
185
+
186
+ ctx.stroke();
187
+ }
188
+ }
189
+
190
+ // ============================================================================
191
+ // TEXT UTILITIES
192
+ // ============================================================================
193
+
194
+ /**
195
+ * Truncate text to fit within max width
196
+ */
197
+ export function truncateText(
198
+ ctx: CanvasRenderingContext2D,
199
+ text: string,
200
+ maxWidth: number
201
+ ): string {
202
+ if (maxWidth <= 0) return '';
203
+
204
+ const metrics = ctx.measureText(text);
205
+ if (metrics.width <= maxWidth) {
206
+ return text;
207
+ }
208
+
209
+ // Binary search for optimal truncation point
210
+ let start = 0;
211
+ let end = text.length;
212
+
213
+ while (start < end) {
214
+ const mid = Math.floor((start + end) / 2);
215
+ const truncated = text.slice(0, mid) + '...';
216
+
217
+ if (ctx.measureText(truncated).width <= maxWidth) {
218
+ start = mid + 1;
219
+ } else {
220
+ end = mid;
221
+ }
222
+ }
223
+
224
+ return text.slice(0, Math.max(0, start - 1)) + '...';
225
+ }
226
+
227
+ /**
228
+ * Measure text width
229
+ */
230
+ export function measureText(
231
+ ctx: CanvasRenderingContext2D,
232
+ text: string
233
+ ): number {
234
+ return ctx.measureText(text).width;
235
+ }
236
+
237
+ /**
238
+ * Calculate optimal column width based on content
239
+ */
240
+ export function calculateColumnWidth<TData = any>(
241
+ ctx: CanvasRenderingContext2D,
242
+ column: Column,
243
+ colDef: ColDef<TData> | null,
244
+ theme: GridTheme,
245
+ sampleData: any[],
246
+ maxRows: number = 100
247
+ ): number {
248
+ const font = getFontFromTheme(theme);
249
+ ctx.font = font;
250
+
251
+ let maxWidth = 0;
252
+
253
+ // Check header width
254
+ const headerText = column.headerName || column.field || '';
255
+ maxWidth = Math.max(maxWidth, ctx.measureText(headerText).width);
256
+
257
+ // Check sample data widths
258
+ const field = column.field;
259
+ if (field) {
260
+ const rowsToCheck = Math.min(sampleData.length, maxRows);
261
+ for (let i = 0; i < rowsToCheck; i++) {
262
+ const value = sampleData[i]?.[field];
263
+ if (value != null) {
264
+ const text = String(value);
265
+ maxWidth = Math.max(maxWidth, ctx.measureText(text).width);
266
+ }
267
+ }
268
+ }
269
+
270
+ // Add padding
271
+ return Math.ceil(maxWidth + theme.cellPadding * 2);
272
+ }
273
+
274
+ // ============================================================================
275
+ // CELL VALUE FORMATTING
276
+ // ============================================================================
277
+
278
+ /**
279
+ * Get formatted cell value
280
+ */
281
+ export function getFormattedValue<TData = any>(
282
+ value: any,
283
+ colDef: ColDef<TData> | null,
284
+ data: TData,
285
+ rowNode: IRowNode<TData>,
286
+ api: GridApi<TData>
287
+ ): string {
288
+ if (value === null || value === undefined) {
289
+ return '';
290
+ }
291
+
292
+ // Use custom formatter if provided
293
+ if (colDef && typeof colDef.valueFormatter === 'function') {
294
+ try {
295
+ return colDef.valueFormatter({
296
+ value,
297
+ data,
298
+ node: rowNode,
299
+ colDef,
300
+ api,
301
+ });
302
+ } catch (e) {
303
+ console.warn('Value formatter error:', e);
304
+ }
305
+ }
306
+
307
+ return String(value);
308
+ }
309
+
310
+ // ============================================================================
311
+ // BATCH CELL RENDERING
312
+ // ============================================================================
313
+
314
+ /**
315
+ * Render all cells in a row
316
+ */
317
+ export function renderRow<TData = any>(
318
+ ctx: CanvasRenderingContext2D,
319
+ columns: Column[],
320
+ colPreps: Map<string, ColumnPrepResult<TData>>,
321
+ rowNode: IRowNode<TData>,
322
+ rowIndex: number,
323
+ y: number,
324
+ height: number,
325
+ getCellX: (column: Column) => number,
326
+ api: GridApi<TData>,
327
+ theme: GridTheme,
328
+ options: {
329
+ isSelected?: boolean;
330
+ isHovered?: boolean;
331
+ } = {}
332
+ ): void {
333
+ const isEvenRow = rowIndex % 2 === 0;
334
+
335
+ for (const column of columns) {
336
+ const prep = colPreps.get(column.colId);
337
+ if (!prep) continue;
338
+
339
+ const x = getCellX(column);
340
+ const value = column.field ? getValueByPath(rowNode.data, column.field) : undefined;
341
+ const formattedValue = getFormattedValue(
342
+ value,
343
+ prep.colDef,
344
+ rowNode.data,
345
+ rowNode,
346
+ api
347
+ );
348
+
349
+ const context: CellDrawContext<TData> = {
350
+ ctx,
351
+ theme,
352
+ column,
353
+ colDef: prep.colDef,
354
+ rowNode,
355
+ rowIndex,
356
+ x,
357
+ y,
358
+ width: column.width,
359
+ height,
360
+ value,
361
+ formattedValue,
362
+ isSelected: options.isSelected || rowNode.selected,
363
+ isHovered: options.isHovered || false,
364
+ isEvenRow,
365
+ };
366
+
367
+ drawCell(ctx, prep, context);
368
+ }
369
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Rendering Module Index
3
+ *
4
+ * Exports all rendering-related modules.
5
+ */
6
+
7
+ // Types (base definitions)
8
+ export {
9
+ Rectangle,
10
+ Point,
11
+ Size,
12
+ ScrollPosition,
13
+ PositionedColumn,
14
+ ColumnWalkCallback,
15
+ RowWalkCallback,
16
+ CellWalkCallback,
17
+ CellDrawContext,
18
+ ColumnPrepResult,
19
+ BlitResult,
20
+ BufferPair,
21
+ DamageType,
22
+ DirtyRegions,
23
+ GridTheme,
24
+ PartialTheme,
25
+ RenderState,
26
+ VisibleRange,
27
+ HitTestResult,
28
+ GridMouseEvent,
29
+ } from './types';
30
+
31
+ // Theme (re-export the DEFAULT_THEME and utilities)
32
+ export {
33
+ DEFAULT_THEME,
34
+ DARK_THEME,
35
+ THEME_PRESETS,
36
+ mergeTheme,
37
+ getFontFromTheme,
38
+ getRowTheme,
39
+ getCellBackgroundColor,
40
+ getThemePreset,
41
+ createTheme,
42
+ } from './theme';
43
+
44
+ // Walker functions
45
+ export {
46
+ walkColumns,
47
+ getPositionedColumns,
48
+ getPinnedWidths,
49
+ walkRows,
50
+ getVisibleRowRange,
51
+ getRowY,
52
+ walkCells,
53
+ getColumnAtX,
54
+ getColumnIndex,
55
+ getTotalColumnWidth,
56
+ getRowAtY,
57
+ isRowVisible,
58
+ calculateVisibleRange,
59
+ } from './walk';
60
+
61
+ // Blitting optimization
62
+ export {
63
+ MIN_BLIT_DELTA,
64
+ MAX_BLIT_DELTA_RATIO,
65
+ shouldBlit,
66
+ calculateBlit,
67
+ blitLastFrame,
68
+ createBufferPair,
69
+ swapBuffers,
70
+ displayBuffer,
71
+ resizeBufferPair,
72
+ BlitState,
73
+ } from './blit';
74
+
75
+ // Cell rendering (explicit exports to avoid conflicts)
76
+ export {
77
+ prepColumn,
78
+ prepColumns,
79
+ drawCell,
80
+ drawCellBackground,
81
+ drawCellContent,
82
+ drawGroupIndicators,
83
+ truncateText,
84
+ measureText,
85
+ calculateColumnWidth,
86
+ getFormattedValue,
87
+ getValueByPath,
88
+ renderRow,
89
+ } from './cells';
90
+
91
+ // Grid lines
92
+ export {
93
+ drawCrispLine,
94
+ drawHorizontalLine,
95
+ drawVerticalLine,
96
+ drawRowLines,
97
+ drawColumnLines,
98
+ getColumnBorderPositions,
99
+ drawGridLines,
100
+ drawBorder,
101
+ drawCellSelectionBorder,
102
+ drawRangeSelectionBorder,
103
+ drawPinnedRegionBorders,
104
+ drawPinnedRegionShadows,
105
+ } from './lines';