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

glideapps / glide-data-grid / 26291402391

22 May 2026 01:44PM UTC coverage: 91.249% (+0.4%) from 90.85%
26291402391

Pull #1188

github

web-flow
Merge 3ba391255 into 0875d78cc
Pull Request #1188: Sections - visually grouping rows with a section header

3227 of 3979 branches covered (81.1%)

1138 of 1226 new or added lines in 11 files covered. (92.82%)

1 existing line in 1 file now uncovered.

19239 of 21084 relevant lines covered (91.25%)

3177.01 hits per line

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

93.59
/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 { InnerGridCellKind, 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(
459✔
74
    effectiveColumns: readonly MappedGridColumn[],
459✔
75
    height: number,
459✔
76
    totalHeaderHeight: number,
459✔
77
    translateX: number,
459✔
78
    translateY: number,
459✔
79
    cellYOffset: number,
459✔
80
    rows: number,
459✔
81
    getRowHeight: (row: number) => number,
459✔
82
    freezeTrailingRows: number,
459✔
83
    hasAppendRow: boolean
459✔
84
): number {
459✔
85
    let result = 0;
459✔
86
    walkColumns(
459✔
87
        effectiveColumns,
459✔
88
        cellYOffset,
459✔
89
        translateX,
459✔
90
        translateY,
459✔
91
        totalHeaderHeight,
459✔
92
        (_c, __drawX, colDrawY, _clipX, startRow) => {
459✔
93
            walkRowsInCol(
459✔
94
                startRow,
459✔
95
                colDrawY,
459✔
96
                height,
459✔
97
                rows,
459✔
98
                getRowHeight,
459✔
99
                freezeTrailingRows,
459✔
100
                hasAppendRow,
459✔
101
                undefined,
459✔
102
                (_drawY, row, _rh, isSticky) => {
459✔
103
                    if (!isSticky) {
13,637✔
104
                        result = Math.max(row, result);
13,197✔
105
                    }
13,197✔
106
                }
13,637✔
107
            );
459✔
108

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

1✔
115
function addVisibleSectionRowsToDrawRegions(
2✔
116
    drawRegions: Rectangle[],
2✔
117
    width: number,
2✔
118
    height: number,
2✔
119
    totalHeaderHeight: number,
2✔
120
    translateY: number,
2✔
121
    cellYOffset: number,
2✔
122
    rows: number,
2✔
123
    getRowHeight: (row: number) => number,
2✔
124
    freezeTrailingRows: number,
2✔
125
    hasAppendRow: boolean,
2✔
126
    getCellContent: DrawGridArg["getCellContent"]
2✔
127
): void {
2✔
128
    const cell: [number, number] = [0, 0];
2✔
129
    walkRowsInCol(
2✔
130
        cellYOffset,
2✔
131
        totalHeaderHeight + translateY,
2✔
132
        height,
2✔
133
        rows,
2✔
134
        getRowHeight,
2✔
135
        freezeTrailingRows,
2✔
136
        hasAppendRow,
2✔
137
        undefined,
2✔
138
        (drawY, row, rh) => {
2✔
139
            if (row < 0) return;
64!
140

64✔
141
            cell[1] = row;
64✔
142
            if (getCellContent(cell).kind !== InnerGridCellKind.Section) return;
64!
NEW
143

×
NEW
144
            drawRegions.push({
×
NEW
145
                x: 0,
×
NEW
146
                y: drawY,
×
NEW
147
                width,
×
NEW
148
                height: rh,
×
NEW
149
            });
×
150
        }
64✔
151
    );
2✔
152
}
2✔
153

1✔
154
export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
1✔
155
    const {
687✔
156
        canvasCtx,
687✔
157
        headerCanvasCtx,
687✔
158
        width,
687✔
159
        height,
687✔
160
        cellXOffset,
687✔
161
        cellYOffset,
687✔
162
        translateX,
687✔
163
        translateY,
687✔
164
        mappedColumns,
687✔
165
        enableGroups,
687✔
166
        freezeColumns,
687✔
167
        dragAndDropState,
687✔
168
        theme,
687✔
169
        drawFocus,
687✔
170
        headerHeight,
687✔
171
        groupHeaderHeight,
687✔
172
        disabledRows,
687✔
173
        rowHeight,
687✔
174
        verticalBorder,
687✔
175
        overrideCursor,
687✔
176
        isResizing,
687✔
177
        selection,
687✔
178
        fillHandle,
687✔
179
        freezeTrailingRows,
687✔
180
        rows,
687✔
181
        getCellContent,
687✔
182
        getGroupDetails,
687✔
183
        getRowThemeOverride,
687✔
184
        isFocused,
687✔
185
        drawHeaderCallback,
687✔
186
        prelightCells,
687✔
187
        drawCellCallback,
687✔
188
        highlightRegions,
687✔
189
        resizeCol,
687✔
190
        imageLoader,
687✔
191
        lastBlitData,
687✔
192
        hoverValues,
687✔
193
        hyperWrapping,
687✔
194
        hoverInfo,
687✔
195
        spriteManager,
687✔
196
        maxScaleFactor,
687✔
197
        hasAppendRow,
687✔
198
        touchMode,
687✔
199
        enqueue,
687✔
200
        renderStateProvider,
687✔
201
        getCellRenderer,
687✔
202
        renderStrategy,
687✔
203
        bufferACtx,
687✔
204
        bufferBCtx,
687✔
205
        damage,
687✔
206
        minimumCellWidth,
687✔
207
        resizeIndicator,
687✔
208
    } = arg;
687✔
209
    if (width === 0 || height === 0) return;
687✔
210
    const doubleBuffer = renderStrategy === "double-buffer";
519✔
211
    const dpr = Math.min(maxScaleFactor, Math.ceil(window.devicePixelRatio ?? 1));
687!
212

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

687✔
216
    const canvas = canvasCtx.canvas;
687✔
217

687✔
218
    if (canvas.width !== width * dpr || canvas.height !== height * dpr) {
687✔
219
        canvas.width = width * dpr;
181✔
220
        canvas.height = height * dpr;
181✔
221

181✔
222
        canvas.style.width = width + "px";
181✔
223
        canvas.style.height = height + "px";
181✔
224
    }
181✔
225

519✔
226
    const overlayCanvas = headerCanvasCtx.canvas;
519✔
227
    const totalHeaderHeight = enableGroups ? groupHeaderHeight + headerHeight : headerHeight;
687✔
228

687✔
229
    const overlayHeight = totalHeaderHeight + 1; // border
687✔
230
    if (overlayCanvas.width !== width * dpr || overlayCanvas.height !== overlayHeight * dpr) {
687✔
231
        overlayCanvas.width = width * dpr;
181✔
232
        overlayCanvas.height = overlayHeight * dpr;
181✔
233

181✔
234
        overlayCanvas.style.width = width + "px";
181✔
235
        overlayCanvas.style.height = overlayHeight + "px";
181✔
236
    }
181✔
237

519✔
238
    const bufferA = bufferACtx.canvas;
519✔
239
    const bufferB = bufferBCtx.canvas;
519✔
240

519✔
241
    if (doubleBuffer && (bufferA.width !== width * dpr || bufferA.height !== height * dpr)) {
687✔
242
        bufferA.width = width * dpr;
1✔
243
        bufferA.height = height * dpr;
1✔
244
        if (lastBlitData.current !== undefined) lastBlitData.current.aBufferScroll = undefined;
1!
245
    }
1✔
246

519✔
247
    if (doubleBuffer && (bufferB.width !== width * dpr || bufferB.height !== height * dpr)) {
687✔
248
        bufferB.width = width * dpr;
1✔
249
        bufferB.height = height * dpr;
1✔
250
        if (lastBlitData.current !== undefined) lastBlitData.current.bBufferScroll = undefined;
1!
251
    }
1✔
252

519✔
253
    const last = lastBlitData.current;
519✔
254
    if (
519✔
255
        canBlit === true &&
519✔
256
        cellXOffset === last?.cellXOffset &&
6✔
257
        cellYOffset === last?.cellYOffset &&
4!
258
        translateX === last?.translateX &&
×
259
        translateY === last?.translateY
×
260
    )
687✔
261
        return;
687!
262

519✔
263
    let mainCtx: CanvasRenderingContext2D | null = null;
519✔
264
    if (doubleBuffer) {
687✔
265
        mainCtx = canvasCtx;
3✔
266
    }
3✔
267
    const overlayCtx = headerCanvasCtx;
519✔
268
    let targetCtx: CanvasRenderingContext2D;
519✔
269
    if (!doubleBuffer) {
672✔
270
        targetCtx = canvasCtx;
516✔
271
    } else if (damage !== undefined) {
687!
272
        targetCtx = last?.lastBuffer === "b" ? bufferBCtx : bufferACtx;
×
273
    } else {
3✔
274
        targetCtx = last?.lastBuffer === "b" ? bufferACtx : bufferBCtx;
3✔
275
    }
3✔
276
    const targetBuffer = targetCtx.canvas;
519✔
277
    const blitSource = doubleBuffer ? (targetBuffer === bufferA ? bufferB : bufferA) : canvas;
687✔
278

687✔
279
    const getRowHeight = typeof rowHeight === "number" ? () => rowHeight : rowHeight;
687✔
280

687✔
281
    overlayCtx.save();
687✔
282
    targetCtx.save();
687✔
283

687✔
284
    overlayCtx.beginPath();
687✔
285
    targetCtx.beginPath();
687✔
286

687✔
287
    overlayCtx.textBaseline = "middle";
687✔
288
    targetCtx.textBaseline = "middle";
687✔
289

687✔
290
    if (dpr !== 1) {
687!
291
        overlayCtx.scale(dpr, dpr);
×
292
        targetCtx.scale(dpr, dpr);
×
293
    }
✔
294

519✔
295
    const effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, dragAndDropState, translateX);
519✔
296

519✔
297
    let drawRegions: Rectangle[] = [];
519✔
298

519✔
299
    const mustDrawFocusOnHeader = drawFocus && selection.current?.cell[1] === cellYOffset && translateY === 0;
687✔
300
    let mustDrawHighlightRingsOnHeader = false;
687✔
301
    if (highlightRegions !== undefined) {
687✔
302
        for (const r of highlightRegions) {
263✔
303
            if (r.style !== "no-outline" && r.range.y === cellYOffset && translateY === 0) {
300✔
304
                mustDrawHighlightRingsOnHeader = true;
46✔
305
                break;
46✔
306
            }
46✔
307
        }
300✔
308
    }
263✔
309
    const drawHeaderTexture = () => {
519✔
310
        drawGridHeaders(
481✔
311
            overlayCtx,
481✔
312
            effectiveCols,
481✔
313
            enableGroups,
481✔
314
            hoverInfo,
481✔
315
            width,
481✔
316
            translateX,
481✔
317
            headerHeight,
481✔
318
            groupHeaderHeight,
481✔
319
            dragAndDropState,
481✔
320
            isResizing,
481✔
321
            selection,
481✔
322
            theme,
481✔
323
            spriteManager,
481✔
324
            hoverValues,
481✔
325
            verticalBorder,
481✔
326
            getGroupDetails,
481✔
327
            damage,
481✔
328
            drawHeaderCallback,
481✔
329
            touchMode
481✔
330
        );
481✔
331

481✔
332
        drawGridLines(
481✔
333
            overlayCtx,
481✔
334
            effectiveCols,
481✔
335
            cellYOffset,
481✔
336
            translateX,
481✔
337
            translateY,
481✔
338
            width,
481✔
339
            height,
481✔
340
            undefined,
481✔
341
            undefined,
481✔
342
            groupHeaderHeight,
481✔
343
            totalHeaderHeight,
481✔
344
            getRowHeight,
481✔
345
            getRowThemeOverride,
481✔
346
            verticalBorder,
481✔
347
            freezeTrailingRows,
481✔
348
            rows,
481✔
349
            theme,
481✔
350
            true
481✔
351
        );
481✔
352

481✔
353
        overlayCtx.beginPath();
481✔
354
        overlayCtx.moveTo(0, overlayHeight - 0.5);
481✔
355
        overlayCtx.lineTo(width, overlayHeight - 0.5);
481✔
356
        overlayCtx.strokeStyle = blend(
481✔
357
            theme.headerBottomBorderColor ?? theme.horizontalBorderColor ?? theme.borderColor,
481✔
358
            theme.bgHeader
481✔
359
        );
481✔
360
        overlayCtx.stroke();
481✔
361

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

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

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

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

56✔
482
            const selectionCurrent = selection.current;
56✔
483

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

60✔
513
        if (damageInView) {
60✔
514
            doDamage(targetCtx);
56✔
515
            if (mainCtx !== null) {
56!
516
                mainCtx.save();
×
517
                mainCtx.scale(dpr, dpr);
×
518
                mainCtx.textBaseline = "middle";
×
519
                doDamage(mainCtx);
×
520
                mainCtx.restore();
×
521
            }
×
522

56✔
523
            const doHeaders = damage.hasHeader();
56✔
524
            if (doHeaders) {
56✔
525
                clipHeaderDamage(
26✔
526
                    overlayCtx,
26✔
527
                    effectiveCols,
26✔
528
                    width,
26✔
529
                    groupHeaderHeight,
26✔
530
                    totalHeaderHeight,
26✔
531
                    translateX,
26✔
532
                    translateY,
26✔
533
                    cellYOffset,
26✔
534
                    damage
26✔
535
                );
26✔
536
                drawHeaderTexture();
26✔
537
            }
26✔
538
        }
56✔
539

60✔
540
        targetCtx.restore();
60✔
541
        overlayCtx.restore();
60✔
542

60✔
543
        return;
60✔
544
    }
60✔
545

459✔
546
    if (
459✔
547
        canBlit !== true ||
459✔
548
        cellXOffset !== last?.cellXOffset ||
6✔
549
        translateX !== last?.translateX ||
4✔
550
        mustDrawFocusOnHeader !== last?.mustDrawFocusOnHeader ||
4✔
551
        mustDrawHighlightRingsOnHeader !== last?.mustDrawHighlightRingsOnHeader
4✔
552
    ) {
687✔
553
        drawHeaderTexture();
455✔
554
    }
455✔
555

459✔
556
    if (canBlit === true) {
687✔
557
        assert(blitSource !== undefined && last !== undefined);
6✔
558
        const horizontalScroll = cellXOffset !== last.cellXOffset || translateX !== last.translateX;
6✔
559
        const { regions } = blitLastFrame(
6✔
560
            targetCtx,
6✔
561
            blitSource,
6✔
562
            blitSource === bufferA ? last.aBufferScroll : last.bBufferScroll,
6!
563
            blitSource === bufferA ? last.bBufferScroll : last.aBufferScroll,
6!
564
            last,
6✔
565
            cellXOffset,
6✔
566
            cellYOffset,
6✔
567
            translateX,
6✔
568
            translateY,
6✔
569
            freezeTrailingRows,
6✔
570
            width,
6✔
571
            height,
6✔
572
            rows,
6✔
573
            totalHeaderHeight,
6✔
574
            dpr,
6✔
575
            mappedColumns,
6✔
576
            effectiveCols,
6✔
577
            rowHeight,
6✔
578
            doubleBuffer
6✔
579
        );
6✔
580
        drawRegions = regions;
6✔
581
        if (horizontalScroll && drawRegions.length > 0) {
6✔
582
            // Section titles can be screen-anchored across frozen splits, so shifted pixels must be cleared.
2✔
583
            addVisibleSectionRowsToDrawRegions(
2✔
584
                drawRegions,
2✔
585
                width,
2✔
586
                height,
2✔
587
                totalHeaderHeight,
2✔
588
                translateY,
2✔
589
                cellYOffset,
2✔
590
                rows,
2✔
591
                getRowHeight,
2✔
592
                freezeTrailingRows,
2✔
593
                hasAppendRow,
2✔
594
                getCellContent
2✔
595
            );
2✔
596
        }
2✔
597
    } else if (canBlit !== false) {
687!
598
        assert(last !== undefined);
×
599
        const resizedCol = canBlit;
×
600
        drawRegions = blitResizedCol(
×
601
            last,
×
602
            cellXOffset,
×
603
            cellYOffset,
×
604
            translateX,
×
605
            translateY,
×
606
            width,
×
607
            height,
×
608
            totalHeaderHeight,
×
609
            effectiveCols,
×
610
            resizedCol
×
611
        );
×
612
    }
✔
613

459✔
614
    overdrawStickyBoundaries(
459✔
615
        targetCtx,
459✔
616
        effectiveCols,
459✔
617
        width,
459✔
618
        height,
459✔
619
        freezeTrailingRows,
459✔
620
        rows,
459✔
621
        totalHeaderHeight,
459✔
622
        translateY,
459✔
623
        cellYOffset,
459✔
624
        verticalBorder,
459✔
625
        getRowHeight,
459✔
626
        getCellContent,
459✔
627
        theme
459✔
628
    );
459✔
629

459✔
630
    const highlightRedraw = drawHighlightRings(
459✔
631
        targetCtx,
459✔
632
        width,
459✔
633
        height,
459✔
634
        cellXOffset,
459✔
635
        cellYOffset,
459✔
636
        translateX,
459✔
637
        translateY,
459✔
638
        mappedColumns,
459✔
639
        freezeColumns,
459✔
640
        headerHeight,
459✔
641
        groupHeaderHeight,
459✔
642
        rowHeight,
459✔
643
        freezeTrailingRows,
459✔
644
        rows,
459✔
645
        highlightRegions,
459✔
646
        theme
459✔
647
    );
459✔
648

459✔
649
    // the overdraw may have nuked out our focus ring right edge.
459✔
650
    const focusRedraw = drawFocus
459✔
651
        ? drawFillHandle(
459✔
652
              targetCtx,
459✔
653
              width,
459✔
654
              height,
459✔
655
              cellYOffset,
459✔
656
              translateX,
459✔
657
              translateY,
459✔
658
              effectiveCols,
459✔
659
              mappedColumns,
459✔
660
              theme,
459✔
661
              totalHeaderHeight,
459✔
662
              selection,
459✔
663
              getRowHeight,
459✔
664
              getCellContent,
459✔
665
              freezeTrailingRows,
459✔
666
              hasAppendRow,
459✔
667
              fillHandle,
459✔
668
              rows
459✔
669
          )
459!
670
        : undefined;
×
671

687✔
672
    targetCtx.fillStyle = theme.bgCell;
687✔
673
    if (drawRegions.length > 0) {
687✔
674
        targetCtx.beginPath();
6✔
675
        for (const r of drawRegions) {
6✔
676
            targetCtx.rect(r.x, r.y, r.width, r.height);
6✔
677
        }
6✔
678
        targetCtx.clip();
6✔
679
        targetCtx.fill();
6✔
680
        targetCtx.beginPath();
6✔
681
    } else {
687✔
682
        targetCtx.fillRect(0, 0, width, height);
453✔
683
    }
453✔
684

459✔
685
    const spans = drawCells(
459✔
686
        targetCtx,
459✔
687
        effectiveCols,
459✔
688
        mappedColumns,
459✔
689
        height,
459✔
690
        totalHeaderHeight,
459✔
691
        translateX,
459✔
692
        translateY,
459✔
693
        cellYOffset,
459✔
694
        rows,
459✔
695
        getRowHeight,
459✔
696
        getCellContent,
459✔
697
        getGroupDetails,
459✔
698
        getRowThemeOverride,
459✔
699
        disabledRows,
459✔
700
        isFocused,
459✔
701
        drawFocus,
459✔
702
        freezeTrailingRows,
459✔
703
        hasAppendRow,
459✔
704
        drawRegions,
459✔
705
        damage,
459✔
706
        selection,
459✔
707
        prelightCells,
459✔
708
        highlightRegions,
459✔
709
        imageLoader,
459✔
710
        spriteManager,
459✔
711
        hoverValues,
459✔
712
        hoverInfo,
459✔
713
        drawCellCallback,
459✔
714
        hyperWrapping,
459✔
715
        theme,
459✔
716
        enqueue,
459✔
717
        renderStateProvider,
459✔
718
        getCellRenderer,
459✔
719
        overrideCursor,
459✔
720
        minimumCellWidth
459✔
721
    );
459✔
722

459✔
723
    drawBlanks(
459✔
724
        targetCtx,
459✔
725
        effectiveCols,
459✔
726
        mappedColumns,
459✔
727
        width,
459✔
728
        height,
459✔
729
        totalHeaderHeight,
459✔
730
        translateX,
459✔
731
        translateY,
459✔
732
        cellYOffset,
459✔
733
        rows,
459✔
734
        getRowHeight,
459✔
735
        getRowThemeOverride,
459✔
736
        selection.rows,
459✔
737
        disabledRows,
459✔
738
        freezeTrailingRows,
459✔
739
        hasAppendRow,
459✔
740
        drawRegions,
459✔
741
        damage,
459✔
742
        theme
459✔
743
    );
459✔
744

459✔
745
    drawExtraRowThemes(
459✔
746
        targetCtx,
459✔
747
        effectiveCols,
459✔
748
        cellYOffset,
459✔
749
        translateX,
459✔
750
        translateY,
459✔
751
        width,
459✔
752
        height,
459✔
753
        drawRegions,
459✔
754
        totalHeaderHeight,
459✔
755
        getRowHeight,
459✔
756
        getRowThemeOverride,
459✔
757
        verticalBorder,
459✔
758
        freezeTrailingRows,
459✔
759
        rows,
459✔
760
        theme
459✔
761
    );
459✔
762

459✔
763
    drawGridLines(
459✔
764
        targetCtx,
459✔
765
        effectiveCols,
459✔
766
        cellYOffset,
459✔
767
        translateX,
459✔
768
        translateY,
459✔
769
        width,
459✔
770
        height,
459✔
771
        drawRegions,
459✔
772
        spans,
459✔
773
        groupHeaderHeight,
459✔
774
        totalHeaderHeight,
459✔
775
        getRowHeight,
459✔
776
        getRowThemeOverride,
459✔
777
        verticalBorder,
459✔
778
        freezeTrailingRows,
459✔
779
        rows,
459✔
780
        theme
459✔
781
    );
459✔
782

459✔
783
    highlightRedraw?.();
687✔
784
    focusRedraw?.();
687✔
785

687✔
786
    if (isResizing && resizeIndicator !== "none") {
687✔
787
        walkColumns(effectiveCols, 0, translateX, 0, totalHeaderHeight, (c, x) => {
7✔
788
            if (c.sourceIndex === resizeCol) {
14✔
789
                drawColumnResizeOutline(
7✔
790
                    overlayCtx,
7✔
791
                    x + c.width,
7✔
792
                    0,
7✔
793
                    totalHeaderHeight + 1,
7✔
794
                    blend(theme.resizeIndicatorColor ?? theme.accentLight, theme.bgHeader)
7✔
795
                );
7✔
796
                if (resizeIndicator === "full") {
7✔
797
                    drawColumnResizeOutline(
7✔
798
                        targetCtx,
7✔
799
                        x + c.width,
7✔
800
                        totalHeaderHeight,
7✔
801
                        height,
7✔
802
                        blend(theme.resizeIndicatorColor ?? theme.accentLight, theme.bgCell)
7✔
803
                    );
7✔
804
                }
7✔
805
                return true;
7✔
806
            }
7✔
807
            return false;
7✔
808
        });
7✔
809
    }
7✔
810

459✔
811
    if (mainCtx !== null) {
687✔
812
        mainCtx.fillStyle = theme.bgCell;
3✔
813
        mainCtx.fillRect(0, 0, width, height);
3✔
814
        mainCtx.drawImage(targetCtx.canvas, 0, 0);
3✔
815
    }
3✔
816

459✔
817
    const lastRowDrawn = getLastRow(
459✔
818
        effectiveCols,
459✔
819
        height,
459✔
820
        totalHeaderHeight,
459✔
821
        translateX,
459✔
822
        translateY,
459✔
823
        cellYOffset,
459✔
824
        rows,
459✔
825
        getRowHeight,
459✔
826
        freezeTrailingRows,
459✔
827
        hasAppendRow
459✔
828
    );
459✔
829

459✔
830
    imageLoader?.setWindow(
459✔
831
        {
687✔
832
            x: cellXOffset,
687✔
833
            y: cellYOffset,
687✔
834
            width: effectiveCols.length,
687✔
835
            height: lastRowDrawn - cellYOffset,
687✔
836
        },
687✔
837
        freezeColumns,
687✔
838
        Array.from({ length: freezeTrailingRows }, (_, i) => rows - 1 - i)
687✔
839
    );
687✔
840

687✔
841
    const scrollX = last !== undefined && (cellXOffset !== last.cellXOffset || translateX !== last.translateX);
687✔
842
    const scrollY = last !== undefined && (cellYOffset !== last.cellYOffset || translateY !== last.translateY);
687✔
843

687✔
844
    lastBlitData.current = {
687✔
845
        cellXOffset,
687✔
846
        cellYOffset,
687✔
847
        translateX,
687✔
848
        translateY,
687✔
849
        mustDrawFocusOnHeader,
687✔
850
        mustDrawHighlightRingsOnHeader,
687✔
851
        lastBuffer: doubleBuffer ? (targetBuffer === bufferA ? "a" : "b") : undefined,
687✔
852
        aBufferScroll: targetBuffer === bufferA ? [scrollX, scrollY] : last?.aBufferScroll,
687✔
853
        bBufferScroll: targetBuffer === bufferB ? [scrollX, scrollY] : last?.bBufferScroll,
687✔
854
    };
687✔
855

687✔
856
    targetCtx.restore();
687✔
857
    overlayCtx.restore();
687✔
858
}
687✔
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