• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

glideapps / glide-data-grid / 16380845235

18 Jul 2025 09:41PM UTC coverage: 90.987% (+0.03%) from 90.954%
16380845235

Pull #1070

github

web-flow
Merge da42bdefd into e7d8fce6d
Pull Request #1070: feat: draw grid lines for selected rows cols

2955 of 3670 branches covered (80.52%)

109 of 112 new or added lines in 5 files covered. (97.32%)

489 existing lines in 5 files now uncovered.

18110 of 19904 relevant lines covered (90.99%)

3096.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.28
/packages/core/src/internal/data-grid/render/data-grid-render.ts
1
/* eslint-disable sonarjs/no-duplicate-string */
1✔
2
/* eslint-disable unicorn/no-for-loop */
1✔
3
import { type Rectangle } from "../data-grid-types.js";
1✔
4
import { CellSet } from "../cell-set.js";
1✔
5
import { getEffectiveColumns, type MappedGridColumn, rectBottomRight } from "./data-grid-lib.js";
1✔
6
import { blend } from "../color-parser.js";
1✔
7
import { assert } from "../../../common/support.js";
1✔
8
import type { DrawGridArg } from "./draw-grid-arg.js";
1✔
9
import { walkColumns, walkGroups, walkRowsInCol } from "./data-grid-render.walk.js";
1✔
10
import { drawCells } from "./data-grid-render.cells.js";
1✔
11
import { drawGridHeaders } from "./data-grid-render.header.js";
1✔
12
import { drawGridLines, overdrawStickyBoundaries, drawBlanks, drawExtraRowThemes } from "./data-grid-render.lines.js";
1✔
13
import { blitLastFrame, blitResizedCol, computeCanBlit } from "./data-grid-render.blit.js";
1✔
14
import { drawHighlightRings, drawFillHandle, drawColumnResizeOutline } from "./data-grid.render.rings.js";
1✔
15

1✔
16
// Future optimization opportunities
1✔
17
// - Create a cache of a buffer used to render the full view of a partially displayed column so that when
1✔
18
//   scrolling horizontally you can simply blit the pre-drawn column instead of continually paying the draw
1✔
19
//   cost as it slides into view.
1✔
20
// - The same as above but for partially displayed rows
1✔
21
// - Blit headers on horizontal scroll
1✔
22
// - Use webworker to load images, helpful with lots of large images
1✔
23
// - Retain mode for drawing cells. Instead of drawing cells as we come across them, first build a data
1✔
24
//   structure which contains all operations to perform, then sort them all by "prep" requirement, then do
1✔
25
//   all like operations at once.
1✔
26

1✔
27
function clipHeaderDamage(
26✔
28
    ctx: CanvasRenderingContext2D,
26✔
29
    effectiveColumns: readonly MappedGridColumn[],
26✔
30
    width: number,
26✔
31
    groupHeaderHeight: number,
26✔
32
    totalHeaderHeight: number,
26✔
33
    translateX: number,
26✔
34
    translateY: number,
26✔
35
    cellYOffset: number,
26✔
36
    damage: CellSet | undefined
26✔
37
): void {
26✔
38
    if (damage === undefined || damage.size === 0) return;
26!
39

26✔
40
    ctx.beginPath();
26✔
41

26✔
42
    walkGroups(effectiveColumns, width, translateX, groupHeaderHeight, (span, _group, x, y, w, h) => {
26✔
43
        const hasItemInSpan = damage.hasItemInRectangle({
71✔
44
            x: span[0],
71✔
45
            y: -2,
71✔
46
            width: span[1] - span[0] + 1,
71✔
47
            height: 1,
71✔
48
        });
71✔
49
        if (hasItemInSpan) {
71✔
50
            ctx.rect(x, y, w, h);
5✔
51
        }
5✔
52
    });
26✔
53

26✔
54
    walkColumns(
26✔
55
        effectiveColumns,
26✔
56
        cellYOffset,
26✔
57
        translateX,
26✔
58
        translateY,
26✔
59
        totalHeaderHeight,
26✔
60
        (c, drawX, _colDrawY, clipX) => {
26✔
61
            const diff = Math.max(0, clipX - drawX);
260✔
62

260✔
63
            const finalX = drawX + diff + 1;
260✔
64
            const finalWidth = c.width - diff - 1;
260✔
65
            if (damage.has([c.sourceIndex, -1])) {
260✔
66
                ctx.rect(finalX, groupHeaderHeight, finalWidth, totalHeaderHeight - groupHeaderHeight);
21✔
67
            }
21✔
68
        }
260✔
69
    );
26✔
70
    ctx.clip();
26✔
71
}
26✔
72

1✔
73
function getLastRow(
421✔
74
    effectiveColumns: readonly MappedGridColumn[],
421✔
75
    height: number,
421✔
76
    totalHeaderHeight: number,
421✔
77
    translateX: number,
421✔
78
    translateY: number,
421✔
79
    cellYOffset: number,
421✔
80
    rows: number,
421✔
81
    getRowHeight: (row: number) => number,
421✔
82
    freezeTrailingRows: number,
421✔
83
    hasAppendRow: boolean
421✔
84
): number {
421✔
85
    let result = 0;
421✔
86
    walkColumns(
421✔
87
        effectiveColumns,
421✔
88
        cellYOffset,
421✔
89
        translateX,
421✔
90
        translateY,
421✔
91
        totalHeaderHeight,
421✔
92
        (_c, __drawX, colDrawY, _clipX, startRow) => {
421✔
93
            walkRowsInCol(
421✔
94
                startRow,
421✔
95
                colDrawY,
421✔
96
                height,
421✔
97
                rows,
421✔
98
                getRowHeight,
421✔
99
                freezeTrailingRows,
421✔
100
                hasAppendRow,
421✔
101
                undefined,
421✔
102
                (_drawY, row, _rh, isSticky) => {
421✔
103
                    if (!isSticky) {
12,934✔
104
                        result = Math.max(row, result);
12,529✔
105
                    }
12,529✔
106
                }
12,934✔
107
            );
421✔
108

421✔
109
            return true;
421✔
110
        }
421✔
111
    );
421✔
112
    return result;
421✔
113
}
421✔
114

1✔
115
export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
1✔
116
    const {
631✔
117
        canvasCtx,
631✔
118
        headerCanvasCtx,
631✔
119
        width,
631✔
120
        height,
631✔
121
        cellXOffset,
631✔
122
        cellYOffset,
631✔
123
        translateX,
631✔
124
        translateY,
631✔
125
        mappedColumns,
631✔
126
        enableGroups,
631✔
127
        freezeColumns,
631✔
128
        dragAndDropState,
631✔
129
        theme,
631✔
130
        drawFocus,
631✔
131
        headerHeight,
631✔
132
        groupHeaderHeight,
631✔
133
        disabledRows,
631✔
134
        rowHeight,
631✔
135
        verticalBorder,
631✔
136
        overrideCursor,
631✔
137
        isResizing,
631✔
138
        selection,
631✔
139
        fillHandle,
631✔
140
        freezeTrailingRows,
631✔
141
        rows,
631✔
142
        getCellContent,
631✔
143
        getGroupDetails,
631✔
144
        getRowThemeOverride,
631✔
145
        isFocused,
631✔
146
        drawHeaderCallback,
631✔
147
        prelightCells,
631✔
148
        drawCellCallback,
631✔
149
        highlightRegions,
631✔
150
        resizeCol,
631✔
151
        imageLoader,
631✔
152
        lastBlitData,
631✔
153
        hoverValues,
631✔
154
        hyperWrapping,
631✔
155
        hoverInfo,
631✔
156
        spriteManager,
631✔
157
        maxScaleFactor,
631✔
158
        hasAppendRow,
631✔
159
        touchMode,
631✔
160
        enqueue,
631✔
161
        renderStateProvider,
631✔
162
        getCellRenderer,
631✔
163
        renderStrategy,
631✔
164
        bufferACtx,
631✔
165
        bufferBCtx,
631✔
166
        damage,
631✔
167
        minimumCellWidth,
631✔
168
        resizeIndicator,
631✔
169
    } = arg;
631✔
170
    if (width === 0 || height === 0) return;
631✔
171
    const doubleBuffer = renderStrategy === "double-buffer";
480✔
172
    const dpr = Math.min(maxScaleFactor, Math.ceil(window.devicePixelRatio ?? 1));
631!
173

631✔
174
    // if we are double buffering we need to make sure we can blit. If we can't we need to redraw the whole thing
631✔
175
    const canBlit = renderStrategy !== "direct" && computeCanBlit(arg, lastArg);
631✔
176

631✔
177
    const canvas = canvasCtx.canvas;
631✔
178

631✔
179
    if (canvas.width !== width * dpr || canvas.height !== height * dpr) {
631✔
180
        canvas.width = width * dpr;
163✔
181
        canvas.height = height * dpr;
163✔
182

163✔
183
        canvas.style.width = width + "px";
163✔
184
        canvas.style.height = height + "px";
163✔
185
    }
163✔
186

480✔
187
    const overlayCanvas = headerCanvasCtx.canvas;
480✔
188
    const totalHeaderHeight = enableGroups ? groupHeaderHeight + headerHeight : headerHeight;
631✔
189

631✔
190
    const overlayHeight = totalHeaderHeight + 1; // border
631✔
191
    if (overlayCanvas.width !== width * dpr || overlayCanvas.height !== overlayHeight * dpr) {
631✔
192
        overlayCanvas.width = width * dpr;
163✔
193
        overlayCanvas.height = overlayHeight * dpr;
163✔
194

163✔
195
        overlayCanvas.style.width = width + "px";
163✔
196
        overlayCanvas.style.height = overlayHeight + "px";
163✔
197
    }
163✔
198

480✔
199
    const bufferA = bufferACtx.canvas;
480✔
200
    const bufferB = bufferBCtx.canvas;
480✔
201

480✔
202
    if (doubleBuffer && (bufferA.width !== width * dpr || bufferA.height !== height * dpr)) {
631✔
203
        bufferA.width = width * dpr;
1✔
204
        bufferA.height = height * dpr;
1✔
205
        if (lastBlitData.current !== undefined) lastBlitData.current.aBufferScroll = undefined;
1!
206
    }
1✔
207

480✔
208
    if (doubleBuffer && (bufferB.width !== width * dpr || bufferB.height !== height * dpr)) {
631✔
209
        bufferB.width = width * dpr;
1✔
210
        bufferB.height = height * dpr;
1✔
211
        if (lastBlitData.current !== undefined) lastBlitData.current.bBufferScroll = undefined;
1!
212
    }
1✔
213

480✔
214
    const last = lastBlitData.current;
480✔
215
    if (
480✔
216
        canBlit === true &&
480✔
217
        cellXOffset === last?.cellXOffset &&
4✔
218
        cellYOffset === last?.cellYOffset &&
2!
219
        translateX === last?.translateX &&
×
220
        translateY === last?.translateY
×
221
    )
631✔
222
        return;
631!
223

480✔
224
    let mainCtx: CanvasRenderingContext2D | null = null;
480✔
225
    if (doubleBuffer) {
631✔
226
        mainCtx = canvasCtx;
3✔
227
    }
3✔
228
    const overlayCtx = headerCanvasCtx;
480✔
229
    let targetCtx: CanvasRenderingContext2D;
480✔
230
    if (!doubleBuffer) {
618✔
231
        targetCtx = canvasCtx;
477✔
232
    } else if (damage !== undefined) {
631!
233
        targetCtx = last?.lastBuffer === "b" ? bufferBCtx : bufferACtx;
×
234
    } else {
3✔
235
        targetCtx = last?.lastBuffer === "b" ? bufferACtx : bufferBCtx;
3✔
236
    }
3✔
237
    const targetBuffer = targetCtx.canvas;
480✔
238
    const blitSource = doubleBuffer ? (targetBuffer === bufferA ? bufferB : bufferA) : canvas;
631✔
239

631✔
240
    const getRowHeight = typeof rowHeight === "number" ? () => rowHeight : rowHeight;
631✔
241

631✔
242
    overlayCtx.save();
631✔
243
    targetCtx.save();
631✔
244

631✔
245
    overlayCtx.beginPath();
631✔
246
    targetCtx.beginPath();
631✔
247

631✔
248
    overlayCtx.textBaseline = "middle";
631✔
249
    targetCtx.textBaseline = "middle";
631✔
250

631✔
251
    if (dpr !== 1) {
631!
252
        overlayCtx.scale(dpr, dpr);
×
253
        targetCtx.scale(dpr, dpr);
×
254
    }
✔
255

480✔
256
    const effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, dragAndDropState, translateX);
480✔
257

480✔
258
    let drawRegions: Rectangle[] = [];
480✔
259

480✔
260
    const mustDrawFocusOnHeader = drawFocus && selection.current?.cell[1] === cellYOffset && translateY === 0;
631✔
261
    let mustDrawHighlightRingsOnHeader = false;
631✔
262
    if (highlightRegions !== undefined) {
631✔
263
        for (const r of highlightRegions) {
242✔
264
            if (r.style !== "no-outline" && r.range.y === cellYOffset && translateY === 0) {
278✔
265
                mustDrawHighlightRingsOnHeader = true;
45✔
266
                break;
45✔
267
            }
45✔
268
        }
278✔
269
    }
242✔
270
    const drawHeaderTexture = () => {
480✔
271
        drawGridHeaders(
445✔
272
            overlayCtx,
445✔
273
            effectiveCols,
445✔
274
            enableGroups,
445✔
275
            hoverInfo,
445✔
276
            width,
445✔
277
            translateX,
445✔
278
            headerHeight,
445✔
279
            groupHeaderHeight,
445✔
280
            dragAndDropState,
445✔
281
            isResizing,
445✔
282
            selection,
445✔
283
            theme,
445✔
284
            spriteManager,
445✔
285
            hoverValues,
445✔
286
            verticalBorder,
445✔
287
            getGroupDetails,
445✔
288
            damage,
445✔
289
            drawHeaderCallback,
445✔
290
            touchMode
445✔
291
        );
445✔
292

445✔
293
        drawGridLines(
445✔
294
            overlayCtx,
445✔
295
            effectiveCols,
445✔
296
            cellYOffset,
445✔
297
            translateX,
445✔
298
            translateY,
445✔
299
            width,
445✔
300
            height,
445✔
301
            undefined,
445✔
302
            undefined,
445✔
303
            groupHeaderHeight,
445✔
304
            totalHeaderHeight,
445✔
305
            getRowHeight,
445✔
306
            getRowThemeOverride,
445✔
307
            verticalBorder,
445✔
308
            freezeTrailingRows,
445✔
309
            rows,
445✔
310
            theme,
445✔
311
            true,
445✔
312
            arg.columnSelectionGridLines ? selection.columns : undefined,
445!
313
            false,
445✔
314
            arg.rowSelectionGridLines ? selection.rows : undefined
445!
315
        );
445✔
316

445✔
317
        overlayCtx.beginPath();
445✔
318
        overlayCtx.moveTo(0, overlayHeight - 0.5);
445✔
319
        overlayCtx.lineTo(width, overlayHeight - 0.5);
445✔
320
        overlayCtx.strokeStyle = blend(
445✔
321
            theme.headerBottomBorderColor ?? theme.horizontalBorderColor ?? theme.borderColor,
445✔
322
            theme.bgHeader
445✔
323
        );
445✔
324
        overlayCtx.stroke();
445✔
325

445✔
326
        // Overdraw the bottom border with the accent colour for each selected column so
445✔
327
        // the vertical accent grid lines visually connect with the header. We draw after
445✔
328
        // the default grey stroke so the accent sits on top.
445✔
329
        if (arg.columnSelectionGridLines && selection.columns.length > 0) {
445✔
330
            overlayCtx.beginPath();
33✔
331
            let xCursor = 0;
33✔
332
            for (let index = 0; index < effectiveCols.length; index++) {
33✔
333
                const col = effectiveCols[index];
331✔
334
                const isSelected = selection.columns.hasIndex(col.sourceIndex);
331✔
335

331✔
336
                // Check for left and right edge.
331✔
337

331✔
338
                const leftSelected = selection.columns?.hasIndex(col.sourceIndex) ?? false;
331!
339
                const rightSelected =
331✔
340
                    index < effectiveCols.length - 1
331✔
341
                        ? selection.columns?.hasIndex(effectiveCols[index + 1].sourceIndex) ?? false
298!
342
                        : false;
33✔
343

331✔
344
                console.log("col", index, isSelected, leftSelected, rightSelected);
331✔
345

331✔
346
                const offset = col.sourceIndex === 0 ? 0 : 1;
331✔
347
                const startX = xCursor + offset - (leftSelected ? 1 : 0);
331✔
348
                const endX = xCursor + col.width + 1;
331✔
349
                if (isSelected) {
331✔
350
                    overlayCtx.moveTo(startX, totalHeaderHeight + 0.5);
74✔
351
                    overlayCtx.lineTo(endX, totalHeaderHeight + 0.5);
74✔
352
                }
74✔
353
                xCursor += col.width;
331✔
354
            }
331✔
355
            overlayCtx.strokeStyle = theme.accentColor;
33✔
356
            overlayCtx.stroke();
33✔
357
        }
33✔
358

445✔
359
        if (mustDrawHighlightRingsOnHeader) {
445✔
360
            drawHighlightRings(
36✔
361
                overlayCtx,
36✔
362
                width,
36✔
363
                height,
36✔
364
                cellXOffset,
36✔
365
                cellYOffset,
36✔
366
                translateX,
36✔
367
                translateY,
36✔
368
                mappedColumns,
36✔
369
                freezeColumns,
36✔
370
                headerHeight,
36✔
371
                groupHeaderHeight,
36✔
372
                rowHeight,
36✔
373
                freezeTrailingRows,
36✔
374
                rows,
36✔
375
                highlightRegions,
36✔
376
                theme
36✔
377
            );
36✔
378
        }
36✔
379

445✔
380
        if (mustDrawFocusOnHeader) {
445✔
381
            drawFillHandle(
34✔
382
                overlayCtx,
34✔
383
                width,
34✔
384
                height,
34✔
385
                cellYOffset,
34✔
386
                translateX,
34✔
387
                translateY,
34✔
388
                effectiveCols,
34✔
389
                mappedColumns,
34✔
390
                theme,
34✔
391
                totalHeaderHeight,
34✔
392
                selection,
34✔
393
                getRowHeight,
34✔
394
                getCellContent,
34✔
395
                freezeTrailingRows,
34✔
396
                hasAppendRow,
34✔
397
                fillHandle,
34✔
398
                rows
34✔
399
            );
34✔
400
        }
34✔
401
    };
445✔
402

480✔
403
    // handle damage updates by directly drawing to the target to avoid large blits
480✔
404
    if (damage !== undefined) {
631✔
405
        const viewRegionWidth = effectiveCols[effectiveCols.length - 1].sourceIndex + 1;
59✔
406
        const damageInView = damage.hasItemInRegion([
59✔
407
            {
59✔
408
                x: cellXOffset,
59✔
409
                y: -2,
59✔
410
                width: viewRegionWidth,
59✔
411
                height: 2,
59✔
412
            },
59✔
413
            {
59✔
414
                x: cellXOffset,
59✔
415
                y: cellYOffset,
59✔
416
                width: viewRegionWidth,
59✔
417
                height: 300,
59✔
418
            },
59✔
419
            {
59✔
420
                x: 0,
59✔
421
                y: cellYOffset,
59✔
422
                width: freezeColumns,
59✔
423
                height: 300,
59✔
424
            },
59✔
425
            {
59✔
426
                x: 0,
59✔
427
                y: -2,
59✔
428
                width: freezeColumns,
59✔
429
                height: 2,
59✔
430
            },
59✔
431
            {
59✔
432
                x: cellXOffset,
59✔
433
                y: rows - freezeTrailingRows,
59✔
434
                width: viewRegionWidth,
59✔
435
                height: freezeTrailingRows,
59✔
436
                when: freezeTrailingRows > 0,
59✔
437
            },
59✔
438
        ]);
59✔
439

59✔
440
        const doDamage = (ctx: CanvasRenderingContext2D) => {
59✔
441
            drawCells(
55✔
442
                ctx,
55✔
443
                effectiveCols,
55✔
444
                mappedColumns,
55✔
445
                height,
55✔
446
                totalHeaderHeight,
55✔
447
                translateX,
55✔
448
                translateY,
55✔
449
                cellYOffset,
55✔
450
                rows,
55✔
451
                getRowHeight,
55✔
452
                getCellContent,
55✔
453
                getGroupDetails,
55✔
454
                getRowThemeOverride,
55✔
455
                disabledRows,
55✔
456
                isFocused,
55✔
457
                drawFocus,
55✔
458
                freezeTrailingRows,
55✔
459
                hasAppendRow,
55✔
460
                drawRegions,
55✔
461
                damage,
55✔
462
                selection,
55✔
463
                prelightCells,
55✔
464
                highlightRegions,
55✔
465
                imageLoader,
55✔
466
                spriteManager,
55✔
467
                hoverValues,
55✔
468
                hoverInfo,
55✔
469
                drawCellCallback,
55✔
470
                hyperWrapping,
55✔
471
                theme,
55✔
472
                enqueue,
55✔
473
                renderStateProvider,
55✔
474
                getCellRenderer,
55✔
475
                overrideCursor,
55✔
476
                minimumCellWidth
55✔
477
            );
55✔
478

55✔
479
            const selectionCurrent = selection.current;
55✔
480

55✔
481
            if (
55✔
482
                fillHandle &&
55✔
483
                drawFocus &&
5✔
484
                selectionCurrent !== undefined &&
5✔
485
                damage.has(rectBottomRight(selectionCurrent.range))
5✔
486
            ) {
55!
UNCOV
487
                drawFillHandle(
×
UNCOV
488
                    ctx,
×
UNCOV
489
                    width,
×
UNCOV
490
                    height,
×
UNCOV
491
                    cellYOffset,
×
UNCOV
492
                    translateX,
×
UNCOV
493
                    translateY,
×
UNCOV
494
                    effectiveCols,
×
UNCOV
495
                    mappedColumns,
×
UNCOV
496
                    theme,
×
UNCOV
497
                    totalHeaderHeight,
×
UNCOV
498
                    selection,
×
UNCOV
499
                    getRowHeight,
×
UNCOV
500
                    getCellContent,
×
UNCOV
501
                    freezeTrailingRows,
×
UNCOV
502
                    hasAppendRow,
×
UNCOV
503
                    fillHandle,
×
UNCOV
504
                    rows
×
UNCOV
505
                );
×
UNCOV
506
            }
×
507
        };
55✔
508

59✔
509
        if (damageInView) {
59✔
510
            doDamage(targetCtx);
55✔
511
            if (mainCtx !== null) {
55!
UNCOV
512
                mainCtx.save();
×
UNCOV
513
                mainCtx.scale(dpr, dpr);
×
UNCOV
514
                mainCtx.textBaseline = "middle";
×
UNCOV
515
                doDamage(mainCtx);
×
UNCOV
516
                mainCtx.restore();
×
UNCOV
517
            }
×
518

55✔
519
            const doHeaders = damage.hasHeader();
55✔
520
            if (doHeaders) {
55✔
521
                clipHeaderDamage(
26✔
522
                    overlayCtx,
26✔
523
                    effectiveCols,
26✔
524
                    width,
26✔
525
                    groupHeaderHeight,
26✔
526
                    totalHeaderHeight,
26✔
527
                    translateX,
26✔
528
                    translateY,
26✔
529
                    cellYOffset,
26✔
530
                    damage
26✔
531
                );
26✔
532
                drawHeaderTexture();
26✔
533
            }
26✔
534
        }
55✔
535

59✔
536
        targetCtx.restore();
59✔
537
        overlayCtx.restore();
59✔
538

59✔
539
        return;
59✔
540
    }
59✔
541

421✔
542
    if (
421✔
543
        canBlit !== true ||
421✔
544
        cellXOffset !== last?.cellXOffset ||
4✔
545
        translateX !== last?.translateX ||
2✔
546
        mustDrawFocusOnHeader !== last?.mustDrawFocusOnHeader ||
2✔
547
        mustDrawHighlightRingsOnHeader !== last?.mustDrawHighlightRingsOnHeader
2✔
548
    ) {
631✔
549
        drawHeaderTexture();
419✔
550
    }
419✔
551

421✔
552
    if (canBlit === true) {
631✔
553
        assert(blitSource !== undefined && last !== undefined);
4✔
554
        const { regions } = blitLastFrame(
4✔
555
            targetCtx,
4✔
556
            blitSource,
4✔
557
            blitSource === bufferA ? last.aBufferScroll : last.bBufferScroll,
4!
558
            blitSource === bufferA ? last.bBufferScroll : last.aBufferScroll,
4!
559
            last,
4✔
560
            cellXOffset,
4✔
561
            cellYOffset,
4✔
562
            translateX,
4✔
563
            translateY,
4✔
564
            freezeTrailingRows,
4✔
565
            width,
4✔
566
            height,
4✔
567
            rows,
4✔
568
            totalHeaderHeight,
4✔
569
            dpr,
4✔
570
            mappedColumns,
4✔
571
            effectiveCols,
4✔
572
            rowHeight,
4✔
573
            doubleBuffer
4✔
574
        );
4✔
575
        drawRegions = regions;
4✔
576
    } else if (canBlit !== false) {
631!
UNCOV
577
        assert(last !== undefined);
×
UNCOV
578
        const resizedCol = canBlit;
×
UNCOV
579
        drawRegions = blitResizedCol(
×
UNCOV
580
            last,
×
UNCOV
581
            cellXOffset,
×
UNCOV
582
            cellYOffset,
×
UNCOV
583
            translateX,
×
UNCOV
584
            translateY,
×
UNCOV
585
            width,
×
UNCOV
586
            height,
×
UNCOV
587
            totalHeaderHeight,
×
UNCOV
588
            effectiveCols,
×
UNCOV
589
            resizedCol
×
UNCOV
590
        );
×
UNCOV
591
    }
✔
592

421✔
593
    overdrawStickyBoundaries(
421✔
594
        targetCtx,
421✔
595
        effectiveCols,
421✔
596
        width,
421✔
597
        height,
421✔
598
        freezeTrailingRows,
421✔
599
        rows,
421✔
600
        verticalBorder,
421✔
601
        getRowHeight,
421✔
602
        theme
421✔
603
    );
421✔
604

421✔
605
    const highlightRedraw = drawHighlightRings(
421✔
606
        targetCtx,
421✔
607
        width,
421✔
608
        height,
421✔
609
        cellXOffset,
421✔
610
        cellYOffset,
421✔
611
        translateX,
421✔
612
        translateY,
421✔
613
        mappedColumns,
421✔
614
        freezeColumns,
421✔
615
        headerHeight,
421✔
616
        groupHeaderHeight,
421✔
617
        rowHeight,
421✔
618
        freezeTrailingRows,
421✔
619
        rows,
421✔
620
        highlightRegions,
421✔
621
        theme
421✔
622
    );
421✔
623

421✔
624
    // the overdraw may have nuked out our focus ring right edge.
421✔
625
    const focusRedraw = drawFocus
421✔
626
        ? drawFillHandle(
421✔
627
              targetCtx,
421✔
628
              width,
421✔
629
              height,
421✔
630
              cellYOffset,
421✔
631
              translateX,
421✔
632
              translateY,
421✔
633
              effectiveCols,
421✔
634
              mappedColumns,
421✔
635
              theme,
421✔
636
              totalHeaderHeight,
421✔
637
              selection,
421✔
638
              getRowHeight,
421✔
639
              getCellContent,
421✔
640
              freezeTrailingRows,
421✔
641
              hasAppendRow,
421✔
642
              fillHandle,
421✔
643
              rows
421✔
644
          )
421!
UNCOV
645
        : undefined;
×
646

631✔
647
    targetCtx.fillStyle = theme.bgCell;
631✔
648
    if (drawRegions.length > 0) {
631✔
649
        targetCtx.beginPath();
4✔
650
        for (const r of drawRegions) {
4✔
651
            targetCtx.rect(r.x, r.y, r.width, r.height);
4✔
652
        }
4✔
653
        targetCtx.clip();
4✔
654
        targetCtx.fill();
4✔
655
        targetCtx.beginPath();
4✔
656
    } else {
631✔
657
        targetCtx.fillRect(0, 0, width, height);
417✔
658
    }
417✔
659

421✔
660
    const spans = drawCells(
421✔
661
        targetCtx,
421✔
662
        effectiveCols,
421✔
663
        mappedColumns,
421✔
664
        height,
421✔
665
        totalHeaderHeight,
421✔
666
        translateX,
421✔
667
        translateY,
421✔
668
        cellYOffset,
421✔
669
        rows,
421✔
670
        getRowHeight,
421✔
671
        getCellContent,
421✔
672
        getGroupDetails,
421✔
673
        getRowThemeOverride,
421✔
674
        disabledRows,
421✔
675
        isFocused,
421✔
676
        drawFocus,
421✔
677
        freezeTrailingRows,
421✔
678
        hasAppendRow,
421✔
679
        drawRegions,
421✔
680
        damage,
421✔
681
        selection,
421✔
682
        prelightCells,
421✔
683
        highlightRegions,
421✔
684
        imageLoader,
421✔
685
        spriteManager,
421✔
686
        hoverValues,
421✔
687
        hoverInfo,
421✔
688
        drawCellCallback,
421✔
689
        hyperWrapping,
421✔
690
        theme,
421✔
691
        enqueue,
421✔
692
        renderStateProvider,
421✔
693
        getCellRenderer,
421✔
694
        overrideCursor,
421✔
695
        minimumCellWidth
421✔
696
    );
421✔
697

421✔
698
    drawBlanks(
421✔
699
        targetCtx,
421✔
700
        effectiveCols,
421✔
701
        mappedColumns,
421✔
702
        width,
421✔
703
        height,
421✔
704
        totalHeaderHeight,
421✔
705
        translateX,
421✔
706
        translateY,
421✔
707
        cellYOffset,
421✔
708
        rows,
421✔
709
        getRowHeight,
421✔
710
        getRowThemeOverride,
421✔
711
        selection.rows,
421✔
712
        disabledRows,
421✔
713
        freezeTrailingRows,
421✔
714
        hasAppendRow,
421✔
715
        drawRegions,
421✔
716
        damage,
421✔
717
        theme
421✔
718
    );
421✔
719

421✔
720
    drawExtraRowThemes(
421✔
721
        targetCtx,
421✔
722
        effectiveCols,
421✔
723
        cellYOffset,
421✔
724
        translateX,
421✔
725
        translateY,
421✔
726
        width,
421✔
727
        height,
421✔
728
        drawRegions,
421✔
729
        totalHeaderHeight,
421✔
730
        getRowHeight,
421✔
731
        getRowThemeOverride,
421✔
732
        verticalBorder,
421✔
733
        freezeTrailingRows,
421✔
734
        rows,
421✔
735
        theme
421✔
736
    );
421✔
737

421✔
738
    console.log("selection", selection);
421✔
739

421✔
740
    drawGridLines(
421✔
741
        targetCtx,
421✔
742
        effectiveCols,
421✔
743
        cellYOffset,
421✔
744
        translateX,
421✔
745
        translateY,
421✔
746
        width,
421✔
747
        height,
421✔
748
        drawRegions,
421✔
749
        spans,
421✔
750
        groupHeaderHeight,
421✔
751
        totalHeaderHeight,
421✔
752
        getRowHeight,
421✔
753
        getRowThemeOverride,
421✔
754
        verticalBorder,
421✔
755
        freezeTrailingRows,
421✔
756
        rows,
421✔
757
        theme,
421✔
758
        false,
421✔
759
        (arg.columnSelectionGridLines ? selection.columns : undefined),
631!
760
        false,
631✔
761
        (arg.rowSelectionGridLines ? selection.rows : undefined)
631!
762
    );
631✔
763

631✔
764
    highlightRedraw?.();
631✔
765
    focusRedraw?.();
631✔
766

631✔
767
    if (isResizing && resizeIndicator !== "none") {
631✔
768
        walkColumns(effectiveCols, 0, translateX, 0, totalHeaderHeight, (c, x) => {
7✔
769
            if (c.sourceIndex === resizeCol) {
14✔
770
                drawColumnResizeOutline(
7✔
771
                    overlayCtx,
7✔
772
                    x + c.width,
7✔
773
                    0,
7✔
774
                    totalHeaderHeight + 1,
7✔
775
                    blend(theme.resizeIndicatorColor ?? theme.accentLight, theme.bgHeader)
7✔
776
                );
7✔
777
                if (resizeIndicator === "full") {
7✔
778
                    drawColumnResizeOutline(
7✔
779
                        targetCtx,
7✔
780
                        x + c.width,
7✔
781
                        totalHeaderHeight,
7✔
782
                        height,
7✔
783
                        blend(theme.resizeIndicatorColor ?? theme.accentLight, theme.bgCell)
7✔
784
                    );
7✔
785
                }
7✔
786
                return true;
7✔
787
            }
7✔
788
            return false;
7✔
789
        });
7✔
790
    }
7✔
791

421✔
792
    if (mainCtx !== null) {
631✔
793
        mainCtx.fillStyle = theme.bgCell;
3✔
794
        mainCtx.fillRect(0, 0, width, height);
3✔
795
        mainCtx.drawImage(targetCtx.canvas, 0, 0);
3✔
796
    }
3✔
797

421✔
798
    const lastRowDrawn = getLastRow(
421✔
799
        effectiveCols,
421✔
800
        height,
421✔
801
        totalHeaderHeight,
421✔
802
        translateX,
421✔
803
        translateY,
421✔
804
        cellYOffset,
421✔
805
        rows,
421✔
806
        getRowHeight,
421✔
807
        freezeTrailingRows,
421✔
808
        hasAppendRow
421✔
809
    );
421✔
810

421✔
811
    imageLoader?.setWindow(
421✔
812
        {
631✔
813
            x: cellXOffset,
631✔
814
            y: cellYOffset,
631✔
815
            width: effectiveCols.length,
631✔
816
            height: lastRowDrawn - cellYOffset,
631✔
817
        },
631✔
818
        freezeColumns,
631✔
819
        Array.from({ length: freezeTrailingRows }, (_, i) => rows - 1 - i)
631✔
820
    );
631✔
821

631✔
822
    const scrollX = last !== undefined && (cellXOffset !== last.cellXOffset || translateX !== last.translateX);
631✔
823
    const scrollY = last !== undefined && (cellYOffset !== last.cellYOffset || translateY !== last.translateY);
631✔
824

631✔
825
    lastBlitData.current = {
631✔
826
        cellXOffset,
631✔
827
        cellYOffset,
631✔
828
        translateX,
631✔
829
        translateY,
631✔
830
        mustDrawFocusOnHeader,
631✔
831
        mustDrawHighlightRingsOnHeader,
631✔
832
        lastBuffer: doubleBuffer ? (targetBuffer === bufferA ? "a" : "b") : undefined,
631✔
833
        aBufferScroll: targetBuffer === bufferA ? [scrollX, scrollY] : last?.aBufferScroll,
631✔
834
        bBufferScroll: targetBuffer === bufferB ? [scrollX, scrollY] : last?.bBufferScroll,
631✔
835
    };
631✔
836

631✔
837
    targetCtx.restore();
631✔
838
    overlayCtx.restore();
631✔
839
}
631✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc