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

glideapps / glide-data-grid / 4060821762

pending completion
4060821762

push

github

Jason Smith
Make sure to support RTL in column headers

3594 of 4919 branches covered (73.06%)

18 of 18 new or added lines in 2 files covered. (100.0%)

4568 of 5353 relevant lines covered (85.34%)

3443.69 hits per line

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

83.92
/packages/core/src/data-grid/data-grid-render.tsx
1
/* eslint-disable unicorn/no-for-loop */
2
import {
8✔
3
    GridSelection,
4
    DrawHeaderCallback,
5
    InnerGridCell,
6
    Rectangle,
7
    CompactSelection,
8
    DrawCustomCellCallback,
9
    GridColumnIcon,
10
    Item,
11
    CellList,
12
    GridMouseGroupHeaderEventArgs,
13
    headerCellCheckboxPrefix,
14
    GridCellKind,
15
    isInnerOnlyCell,
16
    BooleanIndeterminate,
17
    headerCellCheckedMarker,
18
    headerCellUnheckedMarker,
19
    TrailingRowType,
20
    ImageWindowLoader,
21
    GridCell,
22
} from "./data-grid-types";
23
import groupBy from "lodash/groupBy.js";
8✔
24
import type { HoverValues } from "./animation-manager";
25
import {
8✔
26
    getEffectiveColumns,
27
    getStickyWidth,
28
    MappedGridColumn,
29
    roundedPoly,
30
    drawWithLastUpdate,
31
    isGroupEqual,
32
    cellIsSelected,
33
    cellIsInRange,
34
    computeBounds,
35
    getMiddleCenterBias,
36
    drawCheckbox,
37
} from "./data-grid-lib";
38
import type { SpriteManager, SpriteVariant } from "./data-grid-sprites";
39
import type { Theme } from "../common/styles";
40
import { blend, withAlpha } from "./color-parser";
8✔
41
import type { DrawArgs, GetCellRendererCallback, PrepResult } from "./cells/cell-types";
42
import { assert, deepEqual } from "../common/support";
8✔
43
import { direction } from "../common/utils";
8✔
44

45
// Future optimization opportunities
46
// - Create a cache of a buffer used to render the full view of a partially displayed column so that when
47
//   scrolling horizontally you can simply blit the pre-drawn column instead of continually paying the draw
48
//   cost as it slides into view.
49
// - Blit headers on horizontal scroll
50
// - Use webworker to load images, helpful with lots of large images
51
// - Sprite map currently wastes a lot of canvas texture space
52
// - Retain mode for drawing cells. Instead of drawing cells as we come across them, first build a data
53
//   structure which contains all operations to perform, then sort them all by "prep" requirement, then do
54
//   all like operations at once.
55

56
type HoverInfo = readonly [Item, Item];
57

58
export interface Highlight {
59
    readonly color: string;
60
    readonly range: Rectangle;
61
    readonly style?: "dashed" | "solid" | "no-outline";
62
}
63

64
interface GroupDetails {
65
    readonly name: string;
66
    readonly icon?: string;
67
    readonly overrideTheme?: Partial<Theme>;
68
    readonly actions?: readonly {
69
        readonly title: string;
70
        readonly onClick: (e: GridMouseGroupHeaderEventArgs) => void;
71
        readonly icon: GridColumnIcon | string;
72
    }[];
73
}
74

75
export type GroupDetailsCallback = (groupName: string) => GroupDetails;
76
export type GetRowThemeCallback = (row: number) => Partial<Theme> | undefined;
77

78
const loadingCell: InnerGridCell = {
8✔
79
    kind: GridCellKind.Loading,
80
    allowOverlay: false,
81
};
82

83
export interface BlitData {
84
    readonly cellXOffset: number;
85
    readonly cellYOffset: number;
86
    readonly translateX: number;
87
    readonly translateY: number;
88
    readonly mustDrawFocusOnHeader: boolean;
89
    readonly lastBuffer: "a" | "b" | undefined;
90
}
91

92
interface DragAndDropState {
93
    src: number;
94
    dest: number;
95
}
96

97
export function drawCell(
8✔
98
    ctx: CanvasRenderingContext2D,
99
    row: number,
100
    cell: InnerGridCell,
101
    col: number,
102
    x: number,
103
    y: number,
104
    w: number,
105
    h: number,
106
    highlighted: boolean,
107
    theme: Theme,
108
    drawCustomCell: DrawCustomCellCallback | undefined,
109
    imageLoader: ImageWindowLoader,
110
    spriteManager: SpriteManager,
111
    hoverAmount: number,
112
    hoverInfo: HoverInfo | undefined,
113
    hyperWrapping: boolean,
114
    frameTime: number,
115
    lastPrep: PrepResult | undefined,
116
    enqueue: ((item: Item) => void) | undefined,
117
    getCellRenderer: GetCellRendererCallback
118
): PrepResult | undefined {
119
    let hoverX: number | undefined;
120
    let hoverY: number | undefined;
121
    if (hoverInfo !== undefined && hoverInfo[0][0] === col && hoverInfo[0][1] === row) {
103,818✔
122
        hoverX = hoverInfo[1][0];
10✔
123
        hoverY = hoverInfo[1][1];
10✔
124
    }
125
    let result: PrepResult | undefined = undefined;
103,818✔
126
    const args: DrawArgs<typeof cell> = {
103,818✔
127
        ctx,
128
        theme,
129
        col,
130
        row,
131
        cell,
132
        rect: { x, y, width: w, height: h },
133
        highlighted,
134
        hoverAmount,
135
        hoverX,
136
        hoverY,
137
        imageLoader,
138
        spriteManager,
139
        hyperWrapping,
140
        requestAnimationFrame: () => {
141
            forceAnim = true;
×
142
        },
143
    };
144
    let forceAnim = false;
103,818✔
145
    const needsAnim = drawWithLastUpdate(args, cell.lastUpdated, frameTime, lastPrep, () => {
103,818✔
146
        const drawn = isInnerOnlyCell(cell) ? false : drawCustomCell?.(args as DrawArgs<GridCell>) === true;
103,818!
147
        if (!drawn) {
103,818✔
148
            const r = getCellRenderer(cell);
103,818✔
149
            if (r !== undefined) {
103,818✔
150
                if (lastPrep?.renderer !== r) {
103,818✔
151
                    lastPrep?.deprep?.(args);
6,790✔
152
                    lastPrep = undefined;
6,790✔
153
                }
154
                const partialPrepResult = r.drawPrep?.(args, lastPrep);
103,818✔
155
                r.draw(args, cell);
103,818✔
156
                result = {
103,818✔
157
                    deprep: partialPrepResult?.deprep,
311,454✔
158
                    fillStyle: partialPrepResult?.fillStyle,
311,454✔
159
                    font: partialPrepResult?.font,
311,454✔
160
                    renderer: r,
161
                };
162
            }
163
        }
164
    });
165
    if (needsAnim || forceAnim) enqueue?.([col, row]);
103,818!
166
    return result;
103,818✔
167
}
168

169
function blitLastFrame(
170
    ctx: CanvasRenderingContext2D,
171
    canvas: HTMLCanvasElement,
172
    last: BlitData,
173
    cellXOffset: number,
174
    cellYOffset: number,
175
    translateX: number,
176
    translateY: number,
177
    lastRowSticky: boolean,
178
    width: number,
179
    height: number,
180
    rows: number,
181
    totalHeaderHeight: number,
182
    dpr: number,
183
    mappedColumns: readonly MappedGridColumn[],
184
    effectiveCols: readonly MappedGridColumn[],
185
    getRowHeight: number | ((r: number) => number),
186
    doubleBuffer: boolean
187
) {
188
    const drawRegions: Rectangle[] = [];
4✔
189
    let blittedYOnly = false;
4✔
190

191
    ctx.imageSmoothingEnabled = false;
4✔
192
    const minY = Math.min(last.cellYOffset, cellYOffset);
4✔
193
    const maxY = Math.max(last.cellYOffset, cellYOffset);
4✔
194
    let deltaY = 0;
4✔
195
    if (typeof getRowHeight === "number") {
4!
196
        deltaY += (maxY - minY) * getRowHeight;
4✔
197
    } else {
198
        for (let i = minY; i < maxY; i++) {
×
199
            deltaY += getRowHeight(i);
×
200
        }
201
    }
202
    if (cellYOffset > last.cellYOffset) {
4✔
203
        deltaY = -deltaY;
1✔
204
    }
205
    deltaY += translateY - last.translateY;
4✔
206

207
    const minX = Math.min(last.cellXOffset, cellXOffset);
4✔
208
    const maxX = Math.max(last.cellXOffset, cellXOffset);
4✔
209
    let deltaX = 0;
4✔
210
    for (let i = minX; i < maxX; i++) {
4✔
211
        deltaX += mappedColumns[i].width;
2✔
212
    }
213
    if (cellXOffset > last.cellXOffset) {
4✔
214
        deltaX = -deltaX;
1✔
215
    }
216
    deltaX += translateX - last.translateX;
4✔
217

218
    let stickyWidth = getStickyWidth(effectiveCols);
4✔
219
    if (stickyWidth > 0) stickyWidth++;
4!
220

221
    if (deltaX !== 0 && deltaY !== 0) {
4!
222
        return {
×
223
            regions: [],
224
            yOnly: false,
225
        };
226
    }
227

228
    const stickyRowHeight = lastRowSticky
4!
229
        ? typeof getRowHeight === "number"
4!
230
            ? getRowHeight
231
            : getRowHeight(rows - 1)
232
        : 0;
233

234
    const blitWidth = width - stickyWidth - Math.abs(deltaX);
4✔
235
    const blitHeight = height - totalHeaderHeight - stickyRowHeight - Math.abs(deltaY) - 1;
4✔
236

237
    if (blitWidth > 150 && blitHeight > 150) {
4✔
238
        blittedYOnly = deltaX === 0;
4✔
239

240
        const args = {
4✔
241
            sx: 0,
242
            sy: 0,
243
            sw: width * dpr,
244
            sh: height * dpr,
245
            dx: 0,
246
            dy: 0,
247
            dw: width * dpr,
248
            dh: height * dpr,
249
        };
250

251
        // blit Y
252
        if (deltaY > 0) {
4✔
253
            // scrolling up
254
            args.sy = (totalHeaderHeight + 1) * dpr;
1✔
255
            args.sh = blitHeight * dpr;
1✔
256
            args.dy = (deltaY + totalHeaderHeight + 1) * dpr;
1✔
257
            args.dh = blitHeight * dpr;
1✔
258

259
            drawRegions.push({
1✔
260
                x: 0,
261
                y: totalHeaderHeight,
262
                width: width,
263
                height: deltaY + 1,
264
            });
265
        } else if (deltaY < 0) {
3✔
266
            // scrolling down
267
            args.sy = (-deltaY + totalHeaderHeight + 1) * dpr;
1✔
268
            args.sh = blitHeight * dpr;
1✔
269
            args.dy = (totalHeaderHeight + 1) * dpr;
1✔
270
            args.dh = blitHeight * dpr;
1✔
271

272
            drawRegions.push({
1✔
273
                x: 0,
274
                y: height + deltaY - stickyRowHeight,
275
                width: width,
276
                height: -deltaY + stickyRowHeight,
277
            });
278
        }
279

280
        // blit X
281
        if (deltaX > 0) {
4✔
282
            // pixels moving right
283
            args.sx = stickyWidth * dpr;
1✔
284
            args.sw = blitWidth * dpr;
1✔
285
            args.dx = (deltaX + stickyWidth) * dpr;
1✔
286
            args.dw = blitWidth * dpr;
1✔
287

288
            drawRegions.push({
1✔
289
                x: stickyWidth - 1,
290
                y: 0,
291
                width: deltaX + 2, // extra width to account for first col not drawing a left side border
292
                height: height,
293
            });
294
        } else if (deltaX < 0) {
3✔
295
            // pixels moving left
296
            args.sx = (stickyWidth - deltaX) * dpr;
1✔
297
            args.sw = blitWidth * dpr;
1✔
298
            args.dx = stickyWidth * dpr;
1✔
299
            args.dw = blitWidth * dpr;
1✔
300

301
            drawRegions.push({
1✔
302
                x: width + deltaX,
303
                y: 0,
304
                width: -deltaX,
305
                height: height,
306
            });
307
        }
308

309
        ctx.setTransform(1, 0, 0, 1, 0, 0);
4✔
310
        if (stickyWidth > 0 && deltaX !== 0 && deltaY === 0 && doubleBuffer) {
4!
311
            // When double buffering the freeze columns can be offset by a couple pixels vertically between the two
312
            // buffers. We don't want to redraw them so we need to make sure to copy them.
313
            ctx.drawImage(canvas, 0, 0, stickyWidth * dpr, height * dpr, 0, 0, stickyWidth * dpr, height * dpr);
×
314
        }
315
        ctx.drawImage(canvas, args.sx, args.sy, args.sw, args.sh, args.dx, args.dy, args.dw, args.dh);
4✔
316
        ctx.scale(dpr, dpr);
4✔
317
    }
318
    ctx.imageSmoothingEnabled = true;
4✔
319

320
    return {
4✔
321
        regions: drawRegions,
322
        yOnly: blittedYOnly,
323
    };
324
}
325

326
function blitResizedCol(
327
    // ctx: CanvasRenderingContext2D,
328
    // canvas: HTMLCanvasElement,
329
    last: BlitData,
330
    cellXOffset: number,
331
    cellYOffset: number,
332
    translateX: number,
333
    translateY: number,
334
    width: number,
335
    height: number,
336
    totalHeaderHeight: number,
337
    // dpr: number,
338
    effectiveCols: readonly MappedGridColumn[],
339
    resizedIndex: number
340
) {
341
    const drawRegions: Rectangle[] = [];
×
342

343
    // ctx.imageSmoothingEnabled = false;
344

345
    if (
×
346
        cellXOffset !== last.cellXOffset ||
×
347
        cellYOffset !== last.cellYOffset ||
348
        translateX !== last.translateX ||
349
        translateY !== last.translateY
350
    ) {
351
        return drawRegions;
×
352
    }
353

354
    walkColumns(effectiveCols, cellYOffset, translateX, translateY, totalHeaderHeight, (c, drawX, _drawY, clipX) => {
×
355
        if (c.sourceIndex === resizedIndex) {
×
356
            const x = Math.max(drawX, clipX) + 1;
×
357
            drawRegions.push({
×
358
                x,
359
                y: 0,
360
                width: width - x,
361
                height,
362
            });
363
            return true;
×
364
        }
365
    });
366
    return drawRegions;
×
367
}
368

369
// lines are effectively drawn on the top left edge of a cell.
370
function drawGridLines(
371
    ctx: CanvasRenderingContext2D,
372
    effectiveCols: readonly MappedGridColumn[],
373
    cellYOffset: number,
374
    translateX: number,
375
    translateY: number,
376
    width: number,
377
    height: number,
378
    drawRegions: Rectangle[] | undefined,
379
    spans: Rectangle[] | undefined,
380
    groupHeaderHeight: number,
381
    totalHeaderHeight: number,
382
    getRowHeight: (row: number) => number,
383
    getRowThemeOverride: GetRowThemeCallback | undefined,
384
    verticalBorder: (col: number) => boolean,
385
    trailingRowType: TrailingRowType,
386
    rows: number,
387
    theme: Theme,
388
    verticalOnly: boolean = false
456✔
389
) {
390
    if (spans !== undefined) {
925✔
391
        ctx.beginPath();
4✔
392
        ctx.save();
4✔
393
        ctx.rect(0, 0, width, height);
4✔
394
        for (const span of spans) {
4✔
395
            ctx.rect(span.x + 1, span.y + 1, span.width - 1, span.height - 1);
4✔
396
        }
397
        ctx.clip("evenodd");
4✔
398
    }
399
    const hColor = theme.horizontalBorderColor ?? theme.borderColor;
925!
400
    const vColor = theme.borderColor;
925✔
401

402
    let minX = 0;
925✔
403
    let maxX = width;
925✔
404
    let minY = 0;
925✔
405
    let maxY = height;
925✔
406

407
    if (drawRegions !== undefined && drawRegions.length > 0) {
925✔
408
        minX = Number.MAX_SAFE_INTEGER;
4✔
409
        minY = Number.MAX_SAFE_INTEGER;
4✔
410
        maxX = Number.MIN_SAFE_INTEGER;
4✔
411
        maxY = Number.MIN_SAFE_INTEGER;
4✔
412
        for (const r of drawRegions) {
4✔
413
            minX = Math.min(minX, r.x - 1);
4✔
414
            maxX = Math.max(maxX, r.x + r.width + 1);
4✔
415
            minY = Math.min(minY, r.y - 1);
4✔
416
            maxY = Math.max(maxY, r.y + r.height + 1);
4✔
417
        }
418
    }
419

420
    const toDraw: { x1: number; y1: number; x2: number; y2: number; color: string }[] = [];
925✔
421

422
    ctx.beginPath();
925✔
423

424
    // vertical lines
425
    let x = 0.5;
925✔
426
    for (let index = 0; index < effectiveCols.length; index++) {
925✔
427
        const c = effectiveCols[index];
6,940✔
428
        if (c.width === 0) continue;
6,940!
429
        x += c.width;
6,940✔
430
        const tx = c.sticky ? x : x + translateX;
6,940✔
431
        if (tx >= minX && tx <= maxX && verticalBorder(index + 1)) {
6,940✔
432
            toDraw.push({
6,044✔
433
                x1: tx,
434
                y1: Math.max(groupHeaderHeight, minY),
435
                x2: tx,
436
                y2: Math.min(height, maxY),
437
                color: vColor,
438
            });
439
        }
440
    }
441

442
    const stickyHeight = getRowHeight(rows - 1);
925✔
443
    const stickyRowY = height - stickyHeight + 0.5;
925✔
444
    const lastRowSticky = trailingRowType === "sticky";
925✔
445
    if (lastRowSticky) {
925✔
446
        toDraw.push({ x1: minX, y1: stickyRowY, x2: maxX, y2: stickyRowY, color: hColor });
903✔
447
    }
448

449
    if (verticalOnly !== true) {
925✔
450
        // horizontal lines
451
        let y = totalHeaderHeight + 0.5;
456✔
452
        let row = cellYOffset;
456✔
453
        const target = lastRowSticky ? height - stickyHeight : height;
456✔
454
        while (y + translateY <= target) {
456✔
455
            const ty = y + translateY;
10,089✔
456
            // This shouldn't be needed it seems like... yet it is. We're not sure why.
457
            if (ty >= minY && ty <= maxY - 1 && (!lastRowSticky || row !== rows - 1 || Math.abs(ty - stickyRowY) > 1)) {
10,089✔
458
                const rowTheme = getRowThemeOverride?.(row);
10,034!
459
                toDraw.push({
10,034✔
460
                    x1: minX,
461
                    y1: ty,
462
                    x2: maxX,
463
                    y2: ty,
464
                    color: rowTheme?.horizontalBorderColor ?? rowTheme?.borderColor ?? hColor,
120,408!
465
                });
466
            }
467

468
            y += getRowHeight(row);
10,089✔
469
            row++;
10,089✔
470
        }
471
    }
472

473
    const groups = groupBy(toDraw, line => line.color);
16,981✔
474
    for (const g of Object.keys(groups)) {
925✔
475
        ctx.strokeStyle = g;
925✔
476
        for (const line of groups[g]) {
925✔
477
            ctx.moveTo(line.x1, line.y1);
16,981✔
478
            ctx.lineTo(line.x2, line.y2);
16,981✔
479
        }
480
        ctx.stroke();
925✔
481
        ctx.beginPath();
925✔
482
    }
483

484
    if (spans !== undefined) {
925✔
485
        ctx.restore();
4✔
486
    }
487
}
488

489
export function getActionBoundsForGroup(
8✔
490
    box: Rectangle,
491
    actions: NonNullable<GroupDetails["actions"]>
492
): readonly Rectangle[] {
493
    const result: Rectangle[] = [];
4✔
494
    let x = box.x + box.width - 26 * actions.length;
4✔
495
    const y = box.y + box.height / 2 - 13;
4✔
496
    const height = 26;
4✔
497
    const width = 26;
4✔
498
    for (let i = 0; i < actions.length; i++) {
4✔
499
        result.push({
4✔
500
            x,
501
            y,
502
            width,
503
            height,
504
        });
505
        x += 26;
4✔
506
    }
507
    return result;
4✔
508
}
509

510
export function pointInRect(rect: Rectangle, x: number, y: number): boolean {
8✔
511
    return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
4✔
512
}
513

514
function drawGroups(
515
    ctx: CanvasRenderingContext2D,
516
    effectiveCols: readonly MappedGridColumn[],
517
    width: number,
518
    translateX: number,
519
    groupHeaderHeight: number,
520
    hovered: HoverInfo | undefined,
521
    theme: Theme,
522
    spriteManager: SpriteManager,
523
    _hoverValues: HoverValues,
524
    verticalBorder: (col: number) => boolean,
525
    getGroupDetails: GroupDetailsCallback,
526
    damage: CellList | undefined
527
) {
528
    const xPad = 8;
16✔
529
    const [hCol, hRow] = hovered?.[0] ?? [];
16✔
530

531
    let finalX = 0;
16✔
532
    walkGroups(effectiveCols, width, translateX, groupHeaderHeight, (span, groupName, x, y, w, h) => {
16✔
533
        if (damage !== undefined && !damage.some(d => d[1] === -2 && d[0] >= span[0] && d[0] <= span[1])) return;
37✔
534
        ctx.save();
28✔
535
        ctx.beginPath();
28✔
536
        ctx.rect(x, y, w, h);
28✔
537
        ctx.clip();
28✔
538

539
        const group = getGroupDetails(groupName);
28✔
540
        const groupTheme = group?.overrideTheme === undefined ? theme : { ...theme, ...group.overrideTheme };
28!
541
        const isHovered = hRow === -2 && hCol !== undefined && hCol >= span[0] && hCol <= span[1];
28✔
542

543
        const fillColor = isHovered ? groupTheme.bgHeaderHovered : groupTheme.bgHeader;
28✔
544
        if (fillColor !== theme.bgHeader) {
28✔
545
            ctx.fillStyle = fillColor;
1✔
546
            ctx.fill();
1✔
547
        }
548

549
        ctx.fillStyle = groupTheme.textGroupHeader ?? groupTheme.textHeader;
28!
550
        if (group !== undefined) {
28✔
551
            let drawX = x;
28✔
552
            if (group.icon !== undefined) {
28✔
553
                spriteManager.drawSprite(
5✔
554
                    group.icon,
555
                    "normal",
556
                    ctx,
557
                    drawX + xPad,
558
                    (groupHeaderHeight - 20) / 2,
559
                    20,
560
                    groupTheme
561
                );
562
                drawX += 26;
5✔
563
            }
564
            ctx.fillText(
28✔
565
                group.name,
566
                drawX + xPad,
567
                groupHeaderHeight / 2 + getMiddleCenterBias(ctx, `${theme.headerFontStyle} ${theme.fontFamily}`)
568
            );
569

570
            if (group.actions !== undefined && isHovered) {
28✔
571
                const actionBoxes = getActionBoundsForGroup({ x, y, width: w, height: h }, group.actions);
1✔
572

573
                ctx.beginPath();
1✔
574
                const fadeStartX = actionBoxes[0].x - 10;
1✔
575
                const fadeWidth = x + w - fadeStartX;
1✔
576
                ctx.rect(fadeStartX, 0, fadeWidth, groupHeaderHeight);
1✔
577
                const grad = ctx.createLinearGradient(fadeStartX, 0, fadeStartX + fadeWidth, 0);
1✔
578
                const trans = withAlpha(fillColor, 0);
1✔
579
                grad.addColorStop(0, trans);
1✔
580
                grad.addColorStop(10 / fadeWidth, fillColor);
1✔
581
                grad.addColorStop(1, fillColor);
1✔
582
                ctx.fillStyle = grad;
1✔
583

584
                ctx.fill();
1✔
585

586
                ctx.globalAlpha = 0.6;
1✔
587

588
                // eslint-disable-next-line prefer-const
589
                const [mouseX, mouseY] = hovered?.[1] ?? [-1, -1];
1!
590
                for (let i = 0; i < group.actions.length; i++) {
1✔
591
                    const action = group.actions[i];
1✔
592
                    const box = actionBoxes[i];
1✔
593
                    const actionHovered = pointInRect(box, mouseX + x, mouseY);
1✔
594
                    if (actionHovered) {
1✔
595
                        ctx.globalAlpha = 1;
1✔
596
                    }
597
                    spriteManager.drawSprite(
1✔
598
                        action.icon,
599
                        "normal",
600
                        ctx,
601
                        box.x + box.width / 2 - 10,
602
                        box.y + box.height / 2 - 10,
603
                        20,
604
                        groupTheme
605
                    );
606
                    if (actionHovered) {
1✔
607
                        ctx.globalAlpha = 0.6;
1✔
608
                    }
609
                }
610

611
                ctx.globalAlpha = 1;
1✔
612
            }
613
        }
614

615
        if (x !== 0 && verticalBorder(span[0])) {
28✔
616
            ctx.beginPath();
13✔
617
            ctx.moveTo(x + 0.5, 0);
13✔
618
            ctx.lineTo(x + 0.5, groupHeaderHeight);
13✔
619
            ctx.strokeStyle = theme.borderColor;
13✔
620
            ctx.lineWidth = 1;
13✔
621
            ctx.stroke();
13✔
622
        }
623

624
        ctx.restore();
28✔
625

626
        finalX = x + w;
28✔
627
    });
628

629
    ctx.beginPath();
16✔
630
    ctx.moveTo(finalX + 0.5, 0);
16✔
631
    ctx.lineTo(finalX + 0.5, groupHeaderHeight);
16✔
632

633
    ctx.moveTo(0, groupHeaderHeight + 0.5);
16✔
634
    ctx.lineTo(width, groupHeaderHeight + 0.5);
16✔
635
    ctx.strokeStyle = theme.borderColor;
16✔
636
    ctx.lineWidth = 1;
16✔
637
    ctx.stroke();
16✔
638
}
639

640
const menuButtonSize = 30;
8✔
641
export function getHeaderMenuBounds(x: number, y: number, width: number, height: number, isRtl: boolean): Rectangle {
8✔
642
    if (isRtl) return { x, y, width: menuButtonSize, height: Math.min(menuButtonSize, height) };
3,405!
643
    return {
3,405✔
644
        x: x + width - menuButtonSize, // right align
645
        y: Math.max(y, y + height / 2 - menuButtonSize / 2), // center vertically
646
        width: menuButtonSize,
647
        height: Math.min(menuButtonSize, height),
648
    };
649
}
650

651
export function drawHeader(
8✔
652
    ctx: CanvasRenderingContext2D,
653
    x: number,
654
    y: number,
655
    width: number,
656
    height: number,
657
    c: MappedGridColumn,
658
    selected: boolean,
659
    theme: Theme,
660
    isHovered: boolean,
661
    hasSelectedCell: boolean,
662
    hoverAmount: number,
663
    spriteManager: SpriteManager,
664
    drawHeaderCallback: DrawHeaderCallback | undefined,
665
    touchMode: boolean
666
) {
667
    const isCheckboxHeader = c.title.startsWith(headerCellCheckboxPrefix);
3,399✔
668
    const isRtl = direction(c.title) === "rtl";
3,399✔
669
    const menuBounds = getHeaderMenuBounds(x, y, width, height, isRtl);
3,399✔
670

671
    if (drawHeaderCallback !== undefined) {
3,399!
672
        let passCol = c;
×
673
        if (isCheckboxHeader) {
×
674
            passCol = {
×
675
                ...c,
676
                title: "",
677
            };
678
        }
679
        if (
×
680
            drawHeaderCallback({
681
                ctx,
682
                theme,
683
                rect: { x, y, width, height },
684
                column: passCol,
685
                columnIndex: passCol.sourceIndex,
686
                isSelected: selected,
687
                hoverAmount,
688
                isHovered,
689
                hasSelectedCell,
690
                spriteManager,
691
                menuBounds,
692
            })
693
        ) {
694
            return;
×
695
        }
696
    }
697

698
    if (isCheckboxHeader) {
3,399✔
699
        let checked: boolean | BooleanIndeterminate = undefined;
98✔
700
        if (c.title === headerCellCheckedMarker) checked = true;
98✔
701
        if (c.title === headerCellUnheckedMarker) checked = false;
98✔
702
        if (checked !== true) {
98✔
703
            ctx.globalAlpha = hoverAmount;
95✔
704
        }
705
        drawCheckbox(ctx, theme, checked, x, y, width, height, false, undefined, undefined, 18);
98✔
706
        if (checked !== true) {
98✔
707
            ctx.globalAlpha = 1;
95✔
708
        }
709
        return;
98✔
710
    }
711

712
    const xPad = theme.cellHorizontalPadding;
3,301✔
713
    const fillStyle = selected ? theme.textHeaderSelected : theme.textHeader;
3,301✔
714

715
    const shouldDrawMenu = c.hasMenu === true && (isHovered || (touchMode && selected));
3,301!
716

717
    const dirScalar = isRtl ? -1 : 1;
3,301!
718

719
    let drawX = isRtl ? x + width - xPad : x + xPad;
3,301!
720
    if (c.icon !== undefined) {
3,301✔
721
        let variant: SpriteVariant = selected ? "selected" : "normal";
3,248✔
722
        if (c.style === "highlight") {
3,248!
723
            variant = selected ? "selected" : "special";
×
724
        }
725
        const headerSize = theme.headerIconSize;
3,248✔
726
        spriteManager.drawSprite(
3,248✔
727
            c.icon,
728
            variant,
729
            ctx,
730
            isRtl ? drawX - headerSize : drawX,
3,248!
731
            y + (height - headerSize) / 2,
732
            headerSize,
733
            theme
734
        );
735

736
        if (c.overlayIcon !== undefined) {
3,248!
737
            spriteManager.drawSprite(
×
738
                c.overlayIcon,
739
                selected ? "selected" : "special",
×
740
                ctx,
741
                isRtl ? drawX - headerSize + 9 : drawX + 9,
×
742
                y + ((height - 18) / 2 + 6),
743
                18,
744
                theme
745
            );
746
        }
747

748
        drawX += Math.ceil(headerSize * 1.3) * dirScalar;
3,248✔
749
    }
750

751
    if (shouldDrawMenu && c.hasMenu === true && width > 35) {
3,301✔
752
        const fadeWidth = 35;
5✔
753
        const fadeStart = isRtl ? fadeWidth : width - fadeWidth;
5!
754
        const fadeEnd = isRtl ? fadeWidth * 0.7 : width - fadeWidth * 0.7;
5!
755

756
        const fadeStartPercent = fadeStart / width;
5✔
757
        const fadeEndPercent = fadeEnd / width;
5✔
758

759
        const grad = ctx.createLinearGradient(x, 0, x + width, 0);
5✔
760
        const trans = withAlpha(fillStyle, 0);
5✔
761

762
        grad.addColorStop(isRtl ? 1 : 0, fillStyle);
5!
763
        grad.addColorStop(fadeStartPercent, fillStyle);
5✔
764
        grad.addColorStop(fadeEndPercent, trans);
5✔
765
        grad.addColorStop(isRtl ? 0 : 1, trans);
5!
766
        ctx.fillStyle = grad;
5✔
767
    } else {
768
        ctx.fillStyle = fillStyle;
3,296✔
769
    }
770

771
    if (isRtl) {
3,301!
772
        ctx.textAlign = "right";
×
773
    }
774
    ctx.fillText(
3,301✔
775
        c.title,
776
        drawX,
777
        y + height / 2 + getMiddleCenterBias(ctx, `${theme.headerFontStyle} ${theme.fontFamily}`)
778
    );
779
    if (isRtl) {
3,301!
780
        ctx.textAlign = "left";
×
781
    }
782

783
    if (shouldDrawMenu && c.hasMenu === true) {
3,301✔
784
        ctx.beginPath();
5✔
785
        const triangleX = menuBounds.x + menuBounds.width / 2 - 5.5;
5✔
786
        const triangleY = menuBounds.y + menuBounds.height / 2 - 3;
5✔
787
        roundedPoly(
5✔
788
            ctx,
789
            [
790
                {
791
                    x: triangleX,
792
                    y: triangleY,
793
                },
794
                {
795
                    x: triangleX + 11,
796
                    y: triangleY,
797
                },
798
                {
799
                    x: triangleX + 5.5,
800
                    y: triangleY + 6,
801
                },
802
            ],
803
            1
804
        );
805

806
        ctx.fillStyle = fillStyle;
5✔
807
        ctx.fill();
5✔
808
    }
809
}
810

811
function drawGridHeaders(
812
    ctx: CanvasRenderingContext2D,
813
    effectiveCols: readonly MappedGridColumn[],
814
    enableGroups: boolean,
815
    hovered: HoverInfo | undefined,
816
    width: number,
817
    translateX: number,
818
    headerHeight: number,
819
    groupHeaderHeight: number,
820
    dragAndDropState: DragAndDropState | undefined,
821
    isResizing: boolean,
822
    selection: GridSelection,
823
    outerTheme: Theme,
824
    spriteManager: SpriteManager,
825
    hoverValues: HoverValues,
826
    verticalBorder: (col: number) => boolean,
827
    getGroupDetails: GroupDetailsCallback,
828
    damage: CellList | undefined,
829
    drawHeaderCallback: DrawHeaderCallback | undefined,
830
    touchMode: boolean
831
) {
832
    const totalHeaderHeight = headerHeight + groupHeaderHeight;
469✔
833
    if (totalHeaderHeight <= 0) return;
469!
834

835
    ctx.fillStyle = outerTheme.bgHeader;
469✔
836
    ctx.fillRect(0, 0, width, totalHeaderHeight);
469✔
837

838
    const [hCol, hRow] = hovered?.[0] ?? [];
469✔
839

840
    const font = `${outerTheme.headerFontStyle} ${outerTheme.fontFamily}`;
469✔
841
    // Assinging the context font too much can be expensive, it can be worth it to minimze this
842
    ctx.font = font;
469✔
843
    walkColumns(effectiveCols, 0, translateX, 0, totalHeaderHeight, (c, x, _y, clipX) => {
469✔
844
        if (damage !== undefined && !damage.some(d => d[1] === -1 && d[0] === c.sourceIndex)) return;
3,535✔
845
        const diff = Math.max(0, clipX - x);
3,399✔
846
        ctx.save();
3,399✔
847
        ctx.beginPath();
3,399✔
848
        ctx.rect(x + diff, groupHeaderHeight, c.width - diff, headerHeight);
3,399✔
849
        ctx.clip();
3,399✔
850

851
        const groupTheme = getGroupDetails(c.group ?? "").overrideTheme;
3,399✔
852
        const theme =
853
            c.themeOverride === undefined && groupTheme === undefined
3,399!
854
                ? outerTheme
855
                : { ...outerTheme, ...groupTheme, ...c.themeOverride };
856

857
        if (theme.bgHeader !== outerTheme.bgHeader) {
3,399!
858
            ctx.fillStyle = theme.bgHeader;
×
859
            ctx.fill();
×
860
        }
861

862
        const f = `${theme.headerFontStyle} ${theme.fontFamily}`;
3,399✔
863
        if (font !== f) {
3,399!
864
            ctx.font = f;
×
865
        }
866
        const selected = selection.columns.hasIndex(c.sourceIndex);
3,399✔
867
        const noHover = dragAndDropState !== undefined || isResizing;
3,399✔
868
        const hoveredBoolean = !noHover && hRow === -1 && hCol === c.sourceIndex;
3,399✔
869
        const hover = noHover
3,399✔
870
            ? 0
871
            : hoverValues.find(s => s.item[0] === c.sourceIndex && s.item[1] === -1)?.hoverAmount ?? 0;
34✔
872

873
        const hasSelectedCell = selection?.current !== undefined && selection.current.cell[0] === c.sourceIndex;
3,399!
874

875
        const bgFillStyle = selected ? theme.accentColor : hasSelectedCell ? theme.bgHeaderHasFocus : theme.bgHeader;
3,399✔
876

877
        const y = enableGroups ? groupHeaderHeight : 0;
3,399✔
878
        const xOffset = c.sourceIndex === 0 ? 0 : 1;
3,399✔
879

880
        if (selected) {
3,399✔
881
            ctx.fillStyle = bgFillStyle;
57✔
882
            ctx.fillRect(x + xOffset, y, c.width - xOffset, headerHeight);
57✔
883
        } else if (hasSelectedCell || hover > 0) {
3,342✔
884
            ctx.beginPath();
166✔
885
            ctx.rect(x + xOffset, y, c.width - xOffset, headerHeight);
166✔
886
            if (hasSelectedCell) {
166✔
887
                ctx.fillStyle = theme.bgHeaderHasFocus;
150✔
888
                ctx.fill();
150✔
889
            }
890
            if (hover > 0) {
166✔
891
                ctx.globalAlpha = hover;
16✔
892
                ctx.fillStyle = theme.bgHeaderHovered;
16✔
893
                ctx.fill();
16✔
894
                ctx.globalAlpha = 1;
16✔
895
            }
896
        }
897

898
        drawHeader(
3,399✔
899
            ctx,
900
            x,
901
            y,
902
            c.width,
903
            headerHeight,
904
            c,
905
            selected,
906
            theme,
907
            hoveredBoolean,
908
            hasSelectedCell,
909
            hover,
910
            spriteManager,
911
            drawHeaderCallback,
912
            touchMode
913
        );
914
        ctx.restore();
3,399✔
915
    });
916

917
    if (enableGroups) {
469✔
918
        drawGroups(
16✔
919
            ctx,
920
            effectiveCols,
921
            width,
922
            translateX,
923
            groupHeaderHeight,
924
            hovered,
925
            outerTheme,
926
            spriteManager,
927
            hoverValues,
928
            verticalBorder,
929
            getGroupDetails,
930
            damage
931
        );
932
    }
933
}
934

935
function intersectRect(x1: number, y1: number, w1: number, h1: number, x2: number, y2: number, w2: number, h2: number) {
936
    return x1 <= x2 + w2 && x2 <= x1 + w1 && y1 <= y2 + h2 && y2 <= y1 + h1;
3,315✔
937
}
938

939
function clipDamage(
940
    ctx: CanvasRenderingContext2D,
941
    effectiveColumns: readonly MappedGridColumn[],
942
    width: number,
943
    height: number,
944
    groupHeaderHeight: number,
945
    totalHeaderHeight: number,
946
    translateX: number,
947
    translateY: number,
948
    cellYOffset: number,
949
    rows: number,
950
    getRowHeight: (row: number) => number,
951
    trailingRowType: TrailingRowType,
952
    damage: CellList | undefined,
953
    includeCells: boolean
954
): void {
955
    if (damage === undefined || damage.length === 0) return;
55!
956

957
    const stickyRowHeight = trailingRowType === "sticky" ? getRowHeight(rows - 1) : 0;
55✔
958

959
    ctx.beginPath();
55✔
960

961
    walkGroups(effectiveColumns, width, translateX, groupHeaderHeight, (span, _group, x, y, w, h) => {
55✔
962
        for (let i = 0; i < damage.length; i++) {
78✔
963
            const d = damage[i];
478✔
964
            if (d[1] === -2 && d[0] >= span[0] && d[0] <= span[1]) {
478✔
965
                ctx.rect(x, y, w, h);
2✔
966
                break;
2✔
967
            }
968
        }
969
    });
970

971
    walkColumns(
55✔
972
        effectiveColumns,
973
        cellYOffset,
974
        translateX,
975
        translateY,
976
        totalHeaderHeight,
977
        (c, drawX, colDrawY, clipX, startRow) => {
978
            const diff = Math.max(0, clipX - drawX);
550✔
979

980
            const finalX = drawX + diff + 1;
550✔
981
            const finalWidth = c.width - diff - 1;
550✔
982
            for (let i = 0; i < damage.length; i++) {
550✔
983
                const d = damage[i];
4,325✔
984
                if (d[0] === c.sourceIndex && (d[1] === -1 || d[1] === undefined)) {
4,325✔
985
                    ctx.rect(finalX, groupHeaderHeight, finalWidth, totalHeaderHeight - groupHeaderHeight);
28✔
986
                    break;
28✔
987
                }
988
            }
989

990
            if (!includeCells) return;
550✔
991

992
            walkRowsInCol(
400✔
993
                startRow,
994
                colDrawY,
995
                height,
996
                rows,
997
                getRowHeight,
998
                trailingRowType,
999
                (drawY, row, rh, isSticky) => {
1000
                    let isDamaged = false;
12,760✔
1001
                    for (let i = 0; i < damage.length; i++) {
12,760✔
1002
                        const d = damage[i];
124,195✔
1003
                        if (d[0] === c.sourceIndex && d[1] === row) {
124,195✔
1004
                            isDamaged = true;
128✔
1005
                            break;
128✔
1006
                        }
1007
                    }
1008
                    if (isDamaged) {
12,760✔
1009
                        const top = drawY + 1;
128✔
1010
                        const bottom = isSticky ? top + rh - 1 : Math.min(top + rh - 1, height - stickyRowHeight);
128!
1011
                        const h = bottom - top;
128✔
1012

1013
                        if (h > 0) {
128✔
1014
                            ctx.rect(finalX, top, finalWidth, h);
127✔
1015
                        }
1016
                    }
1017
                }
1018
            );
1019
        }
1020
    );
1021
    ctx.clip();
55✔
1022
}
1023

1024
function getSpanBounds(
1025
    span: Item,
1026
    cellX: number,
1027
    cellY: number,
1028
    cellW: number,
1029
    cellH: number,
1030
    column: MappedGridColumn,
1031
    allColumns: readonly MappedGridColumn[]
1032
): [Rectangle | undefined, Rectangle | undefined] {
1033
    const [startCol, endCol] = span;
5✔
1034

1035
    let frozenRect: Rectangle | undefined;
1036
    let contentRect: Rectangle | undefined;
1037

1038
    const firstNonSticky = allColumns.find(x => !x.sticky)?.sourceIndex ?? 0;
5!
1039
    if (endCol > firstNonSticky) {
5✔
1040
        const renderFromCol = Math.max(startCol, firstNonSticky);
5✔
1041
        let tempX = cellX;
5✔
1042
        let tempW = cellW;
5✔
1043
        for (let x = column.sourceIndex - 1; x >= renderFromCol; x--) {
5✔
1044
            tempX -= allColumns[x].width;
×
1045
            tempW += allColumns[x].width;
×
1046
        }
1047
        for (let x = column.sourceIndex + 1; x <= endCol; x++) {
5✔
1048
            tempW += allColumns[x].width;
5✔
1049
        }
1050
        contentRect = {
5✔
1051
            x: tempX,
1052
            y: cellY,
1053
            width: tempW,
1054
            height: cellH,
1055
        };
1056
    }
1057

1058
    if (firstNonSticky > startCol) {
5!
1059
        const renderToCol = Math.min(endCol, firstNonSticky - 1);
×
1060
        let tempX = cellX;
×
1061
        let tempW = cellW;
×
1062
        for (let x = column.sourceIndex - 1; x >= startCol; x--) {
×
1063
            tempX -= allColumns[x].width;
×
1064
            tempW += allColumns[x].width;
×
1065
        }
1066
        for (let x = column.sourceIndex + 1; x <= renderToCol; x++) {
×
1067
            tempW += allColumns[x].width;
×
1068
        }
1069
        frozenRect = {
×
1070
            x: tempX,
1071
            y: cellY,
1072
            width: tempW,
1073
            height: cellH,
1074
        };
1075
    }
1076

1077
    return [frozenRect, contentRect];
5✔
1078
}
1079

1080
// preppable items:
1081
// - font
1082
// - fillStyle
1083

1084
// Column draw loop prep cycle
1085
// - Prep item
1086
// - Prep sets props
1087
// - Prep returns list of cared about props
1088
// - Draw item
1089
// - Loop may set some items, if present in args list, set undefined
1090
// - Prep next item, giving previous result
1091
// - If next item type is different, de-prep
1092
// - Result per column
1093
function drawCells(
1094
    ctx: CanvasRenderingContext2D,
1095
    effectiveColumns: readonly MappedGridColumn[],
1096
    allColumns: readonly MappedGridColumn[],
1097
    height: number,
1098
    totalHeaderHeight: number,
1099
    translateX: number,
1100
    translateY: number,
1101
    cellYOffset: number,
1102
    rows: number,
1103
    getRowHeight: (row: number) => number,
1104
    getCellContent: (cell: Item) => InnerGridCell,
1105
    getGroupDetails: GroupDetailsCallback,
1106
    getRowThemeOverride: GetRowThemeCallback | undefined,
1107
    disabledRows: CompactSelection,
1108
    isFocused: boolean,
1109
    drawFocus: boolean,
1110
    trailingRowType: TrailingRowType,
1111
    drawRegions: readonly Rectangle[],
1112
    damage: CellList | undefined,
1113
    selection: GridSelection,
1114
    prelightCells: CellList | undefined,
1115
    highlightRegions: readonly Highlight[] | undefined,
1116
    drawCustomCell: DrawCustomCellCallback | undefined,
1117
    imageLoader: ImageWindowLoader,
1118
    spriteManager: SpriteManager,
1119
    hoverValues: HoverValues,
1120
    hoverInfo: HoverInfo | undefined,
1121
    hyperWrapping: boolean,
1122
    outerTheme: Theme,
1123
    enqueue: (item: Item) => void,
1124
    getCellRenderer: GetCellRendererCallback
1125
): Rectangle[] | undefined {
1126
    let toDraw = damage?.length ?? Number.MAX_SAFE_INTEGER;
496✔
1127
    const frameTime = performance.now();
496✔
1128
    let font = `${outerTheme.baseFontStyle} ${outerTheme.fontFamily}`;
496✔
1129
    ctx.font = font;
496✔
1130
    let result: Rectangle[] | undefined;
1131
    const handledSpans = new Set<string>();
496✔
1132
    walkColumns(
496✔
1133
        effectiveColumns,
1134
        cellYOffset,
1135
        translateX,
1136
        translateY,
1137
        totalHeaderHeight,
1138
        (c, drawX, colDrawStartY, clipX, startRow) => {
1139
            const diff = Math.max(0, clipX - drawX);
3,658✔
1140

1141
            const colDrawX = drawX + diff;
3,658✔
1142
            const colDrawY = totalHeaderHeight + 1;
3,658✔
1143
            const colWidth = c.width - diff;
3,658✔
1144
            const colHeight = height - totalHeaderHeight - 1;
3,658✔
1145
            if (drawRegions.length > 0) {
3,658✔
1146
                let found = false;
40✔
1147
                for (let i = 0; i < drawRegions.length; i++) {
40✔
1148
                    const dr = drawRegions[i];
40✔
1149
                    if (intersectRect(colDrawX, colDrawY, colWidth, colHeight, dr.x, dr.y, dr.width, dr.height)) {
40✔
1150
                        found = true;
24✔
1151
                        break;
24✔
1152
                    }
1153
                }
1154
                if (!found) return;
40✔
1155
            }
1156

1157
            const reclip = () => {
3,642✔
1158
                ctx.save();
3,646✔
1159
                ctx.beginPath();
3,646✔
1160
                ctx.rect(colDrawX, colDrawY, colWidth, colHeight);
3,646✔
1161
                ctx.clip();
3,646✔
1162
            };
1163

1164
            const colSelected = selection.columns.hasIndex(c.sourceIndex);
3,642✔
1165

1166
            const groupTheme = getGroupDetails(c.group ?? "").overrideTheme;
3,642✔
1167
            const colTheme =
1168
                c.themeOverride === undefined && groupTheme === undefined
3,642!
1169
                    ? outerTheme
1170
                    : { ...outerTheme, ...groupTheme, ...c.themeOverride };
1171
            const colFont = `${colTheme.baseFontStyle} ${colTheme.fontFamily}`;
3,642✔
1172
            if (colFont !== font) {
3,642!
1173
                font = colFont;
×
1174
                ctx.font = colFont;
×
1175
            }
1176
            reclip();
3,642✔
1177
            let prepResult: PrepResult | undefined = undefined;
3,642✔
1178

1179
            walkRowsInCol(
3,642✔
1180
                startRow,
1181
                colDrawStartY,
1182
                height,
1183
                rows,
1184
                getRowHeight,
1185
                trailingRowType,
1186
                (drawY, row, rh, isSticky, isTrailingRow) => {
1187
                    if (row < 0) return;
111,692!
1188
                    // if (damage !== undefined && !damage.some(d => d[0] === c.sourceIndex && d[1] === row)) {
1189
                    //     return;
1190
                    // }
1191
                    // if (
1192
                    //     drawRegions.length > 0 &&
1193
                    //     !drawRegions.some(dr => intersectRect(drawX, drawY, c.width, rh, dr.x, dr.y, dr.width, dr.height))
1194
                    // ) {
1195
                    //     return;
1196
                    // }
1197

1198
                    // These are dumb versions of the above. I cannot for the life of believe that this matters but this is
1199
                    // the tightest part of the draw loop and the allocations above actually has a very measurable impact
1200
                    // on performance. For the love of all that is unholy please keep checking this again in the future.
1201
                    // As soon as this doesn't have any impact of note go back to the saner looking code. The smoke test
1202
                    // here is to scroll to the bottom of a test case first, then scroll back up while profiling and see
1203
                    // how many major GC collections you get. These allocate a lot of objects.
1204
                    if (damage !== undefined) {
111,692✔
1205
                        let found = false;
7,439✔
1206
                        for (let i = 0; i < damage.length; i++) {
7,439✔
1207
                            const d = damage[i];
102,633✔
1208
                            if (d[0] === c.sourceIndex && d[1] === row) {
102,633✔
1209
                                found = true;
128✔
1210
                                break;
128✔
1211
                            }
1212
                        }
1213
                        if (!found) return;
7,439✔
1214
                    }
1215
                    if (drawRegions.length > 0) {
104,381✔
1216
                        let found = false;
768✔
1217
                        for (let i = 0; i < drawRegions.length; i++) {
768✔
1218
                            const dr = drawRegions[i];
768✔
1219
                            if (intersectRect(drawX, drawY, c.width, rh, dr.x, dr.y, dr.width, dr.height)) {
768✔
1220
                                found = true;
208✔
1221
                                break;
208✔
1222
                            }
1223
                        }
1224
                        if (!found) return;
768✔
1225
                    }
1226

1227
                    const rowSelected = selection.rows.hasIndex(row);
103,821✔
1228
                    const rowDisabled = disabledRows.hasIndex(row);
103,821✔
1229

1230
                    const cell: InnerGridCell = row < rows ? getCellContent([c.sourceIndex, row]) : loadingCell;
103,821✔
1231

1232
                    let cellX = drawX;
103,821✔
1233
                    let cellWidth = c.width;
103,821✔
1234
                    let drawingSpan = false;
103,821✔
1235
                    let skipContents = false;
103,821✔
1236
                    if (cell.span !== undefined) {
103,821✔
1237
                        const [startCol, endCol] = cell.span;
8✔
1238
                        const spanKey = `${row},${startCol},${endCol},${c.sticky}`;
8✔
1239
                        if (!handledSpans.has(spanKey)) {
8✔
1240
                            const areas = getSpanBounds(cell.span, drawX, drawY, c.width, rh, c, allColumns);
4✔
1241
                            const area = c.sticky ? areas[0] : areas[1];
4!
1242
                            if (!c.sticky && areas[0] !== undefined) {
4!
1243
                                skipContents = true;
×
1244
                            }
1245
                            if (area !== undefined) {
4✔
1246
                                cellX = area.x;
4✔
1247
                                cellWidth = area.width;
4✔
1248
                                handledSpans.add(spanKey);
4✔
1249
                                ctx.restore();
4✔
1250
                                prepResult = undefined;
4✔
1251
                                ctx.save();
4✔
1252
                                ctx.beginPath();
4✔
1253
                                const d = Math.max(0, clipX - area.x);
4✔
1254
                                ctx.rect(area.x + d, drawY, area.width - d, rh);
4✔
1255
                                if (result === undefined) {
4✔
1256
                                    result = [];
4✔
1257
                                }
1258
                                result.push({
4✔
1259
                                    x: area.x + d,
1260
                                    y: drawY,
1261
                                    width: area.width - d,
1262
                                    height: rh,
1263
                                });
1264
                                ctx.clip();
4✔
1265
                                drawingSpan = true;
4✔
1266
                            }
1267
                        } else {
1268
                            toDraw--;
4✔
1269
                            return;
4✔
1270
                        }
1271
                    }
1272

1273
                    const rowTheme = getRowThemeOverride?.(row);
103,817!
1274
                    const trailingTheme =
1275
                        isTrailingRow && c.trailingRowOptions?.themeOverride !== undefined
103,817!
1276
                            ? c.trailingRowOptions?.themeOverride
×
1277
                            : undefined;
1278
                    const theme =
1279
                        cell.themeOverride === undefined && rowTheme === undefined && trailingTheme === undefined
103,817!
1280
                            ? colTheme
1281
                            : { ...colTheme, ...rowTheme, ...trailingTheme, ...cell.themeOverride };
1282

1283
                    ctx.beginPath();
103,817✔
1284

1285
                    const cellIndex = [c.sourceIndex, row] as const;
103,817✔
1286
                    const isSelected = cellIsSelected(cellIndex, cell, selection);
103,817✔
1287
                    let accentCount = cellIsInRange(cellIndex, cell, selection);
103,817✔
1288
                    const spanIsHighlighted =
1289
                        cell.span !== undefined &&
103,817✔
1290
                        selection.columns.some(
1291
                            index => cell.span !== undefined && index >= cell.span[0] && index <= cell.span[1]
×
1292
                        );
1293
                    if (isSelected && !isFocused && drawFocus) {
103,817✔
1294
                        accentCount = 0;
117✔
1295
                    } else if (isSelected) {
103,700✔
1296
                        accentCount = Math.max(accentCount, 1);
43✔
1297
                    }
1298
                    if (spanIsHighlighted) {
103,817!
1299
                        accentCount++;
×
1300
                    }
1301
                    if (!isSelected) {
103,817✔
1302
                        if (rowSelected) accentCount++;
103,657✔
1303
                        if (colSelected && !isSticky) accentCount++;
103,657✔
1304
                    }
1305

1306
                    const bgCell = cell.kind === GridCellKind.Protected ? theme.bgCellMedium : theme.bgCell;
103,817✔
1307
                    let fill: string | undefined;
1308
                    if (isSticky || bgCell !== outerTheme.bgCell) {
103,817✔
1309
                        fill = blend(bgCell, fill);
12,539✔
1310
                    }
1311

1312
                    if (accentCount > 0 || rowDisabled) {
103,817✔
1313
                        if (rowDisabled) {
7,302✔
1314
                            fill = blend(theme.bgHeader, fill);
3,264✔
1315
                        }
1316
                        for (let i = 0; i < accentCount; i++) {
7,302✔
1317
                            fill = blend(theme.accentLight, fill);
4,041✔
1318
                        }
1319
                    } else {
1320
                        if (prelightCells?.some(pre => pre[0] === c.sourceIndex && pre[1] === row) === true) {
99,765✔
1321
                            fill = blend(theme.bgSearchResult, fill);
33✔
1322
                        }
1323
                    }
1324

1325
                    if (highlightRegions !== undefined) {
103,817✔
1326
                        for (const region of highlightRegions) {
449✔
1327
                            const r = region.range;
449✔
1328
                            if (
449✔
1329
                                r.x <= c.sourceIndex &&
955✔
1330
                                c.sourceIndex < r.x + r.width &&
1331
                                r.y <= row &&
1332
                                row < r.y + r.height
1333
                            ) {
1334
                                fill = blend(region.color, fill);
30✔
1335
                            }
1336
                        }
1337
                    }
1338

1339
                    if (fill !== undefined) {
103,817✔
1340
                        ctx.fillStyle = fill;
16,355✔
1341
                        if (prepResult !== undefined) {
16,355✔
1342
                            prepResult.fillStyle = fill;
15,794✔
1343
                        }
1344
                        ctx.fillRect(cellX, drawY, cellWidth, rh);
16,355✔
1345
                    }
1346

1347
                    if (cell.style === "faded") {
103,817!
1348
                        ctx.globalAlpha = 0.6;
×
1349
                    }
1350

1351
                    const hoverValue = hoverValues.find(hv => hv.item[0] === c.sourceIndex && hv.item[1] === row);
103,817✔
1352

1353
                    if (cellWidth > 10 && !skipContents) {
103,817✔
1354
                        const cellFont = `${theme.baseFontStyle} ${theme.fontFamily}`;
103,817✔
1355
                        if (cellFont !== font) {
103,817!
1356
                            ctx.font = cellFont;
×
1357
                            font = cellFont;
×
1358
                        }
1359
                        prepResult = drawCell(
103,817✔
1360
                            ctx,
1361
                            row,
1362
                            cell,
1363
                            c.sourceIndex,
1364
                            cellX,
1365
                            drawY,
1366
                            cellWidth,
1367
                            rh,
1368
                            accentCount > 0,
1369
                            theme,
1370
                            drawCustomCell,
1371
                            imageLoader,
1372
                            spriteManager,
1373
                            hoverValue?.hoverAmount ?? 0,
622,902!
1374
                            hoverInfo,
1375
                            hyperWrapping,
1376
                            frameTime,
1377
                            prepResult,
1378
                            enqueue,
1379
                            getCellRenderer
1380
                        );
1381
                    }
1382

1383
                    if (cell.style === "faded") {
103,817!
1384
                        ctx.globalAlpha = 1;
×
1385
                    }
1386
                    toDraw--;
103,817✔
1387
                    if (drawingSpan) {
103,817✔
1388
                        ctx.restore();
4✔
1389
                        prepResult?.deprep?.({ ctx });
4!
1390
                        prepResult = undefined;
4✔
1391
                        reclip();
4✔
1392
                        font = colFont;
4✔
1393
                        ctx.font = colFont;
4✔
1394
                    }
1395
                    return toDraw <= 0;
103,817✔
1396
                }
1397
            );
1398

1399
            ctx.restore();
3,642✔
1400
            return toDraw <= 0;
3,642✔
1401
        }
1402
    );
1403
    return result;
496✔
1404
}
1405

1406
function drawBlanks(
1407
    ctx: CanvasRenderingContext2D,
1408
    effectiveColumns: readonly MappedGridColumn[],
1409
    allColumns: readonly MappedGridColumn[],
1410
    width: number,
1411
    height: number,
1412
    totalHeaderHeight: number,
1413
    translateX: number,
1414
    translateY: number,
1415
    cellYOffset: number,
1416
    rows: number,
1417
    getRowHeight: (row: number) => number,
1418
    getRowTheme: GetRowThemeCallback | undefined,
1419
    selectedRows: CompactSelection,
1420
    disabledRows: CompactSelection,
1421
    trailingRowType: TrailingRowType,
1422
    drawRegions: readonly Rectangle[],
1423
    damage: CellList | undefined,
1424
    theme: Theme
1425
): void {
1426
    if (
456✔
1427
        damage !== undefined ||
912✔
1428
        effectiveColumns[effectiveColumns.length - 1] !== allColumns[effectiveColumns.length - 1]
1429
    )
1430
        return;
3✔
1431
    walkColumns(
453✔
1432
        effectiveColumns,
1433
        cellYOffset,
1434
        translateX,
1435
        translateY,
1436
        totalHeaderHeight,
1437
        (c, drawX, colDrawY, clipX, startRow) => {
1438
            if (c !== effectiveColumns[effectiveColumns.length - 1]) return;
3,382✔
1439
            drawX += c.width;
453✔
1440
            const x = Math.max(drawX, clipX);
453✔
1441
            if (x > width) return;
453✔
1442
            ctx.save();
20✔
1443
            ctx.beginPath();
20✔
1444
            ctx.rect(x, totalHeaderHeight + 1, 10_000, height - totalHeaderHeight - 1);
20✔
1445
            ctx.clip();
20✔
1446

1447
            walkRowsInCol(
20✔
1448
                startRow,
1449
                colDrawY,
1450
                height,
1451
                rows,
1452
                getRowHeight,
1453
                trailingRowType,
1454
                (drawY, row, rh, isSticky) => {
1455
                    if (
626!
1456
                        !isSticky &&
1,242!
1457
                        drawRegions.length > 0 &&
1458
                        !drawRegions.some(dr =>
1459
                            intersectRect(drawX, drawY, 10_000, rh, dr.x, dr.y, dr.width, dr.height)
×
1460
                        )
1461
                    ) {
1462
                        return;
×
1463
                    }
1464

1465
                    const rowSelected = selectedRows.hasIndex(row);
626✔
1466
                    const rowDisabled = disabledRows.hasIndex(row);
626✔
1467

1468
                    ctx.beginPath();
626✔
1469

1470
                    const rowTheme = getRowTheme?.(row);
626!
1471

1472
                    const blankTheme = rowTheme === undefined ? theme : { ...theme, ...rowTheme };
626!
1473

1474
                    if (blankTheme.bgCell !== theme.bgCell) {
626!
1475
                        ctx.fillStyle = blankTheme.bgCell;
×
1476
                        ctx.fillRect(drawX, drawY, 10_000, rh);
×
1477
                    }
1478
                    if (rowDisabled) {
626✔
1479
                        ctx.fillStyle = blankTheme.bgHeader;
10✔
1480
                        ctx.fillRect(drawX, drawY, 10_000, rh);
10✔
1481
                    }
1482
                    if (rowSelected) {
626!
1483
                        ctx.fillStyle = blankTheme.accentLight;
×
1484
                        ctx.fillRect(drawX, drawY, 10_000, rh);
×
1485
                    }
1486
                }
1487
            );
1488

1489
            ctx.restore();
20✔
1490
        }
1491
    );
1492
}
1493

1494
function overdrawStickyBoundaries(
1495
    ctx: CanvasRenderingContext2D,
1496
    effectiveCols: readonly MappedGridColumn[],
1497
    width: number,
1498
    height: number,
1499
    lastRowSticky: boolean,
1500
    rows: number,
1501
    verticalBorder: (col: number) => boolean,
1502
    getRowHeight: (row: number) => number,
1503
    theme: Theme
1504
) {
1505
    let drawFreezeBorder = false;
456✔
1506
    for (const c of effectiveCols) {
456✔
1507
        if (c.sticky) continue;
523✔
1508
        drawFreezeBorder = verticalBorder(c.sourceIndex);
424✔
1509
        break;
424✔
1510
    }
1511
    const hColor = theme.horizontalBorderColor ?? theme.borderColor;
456!
1512
    const vColor = theme.borderColor;
456✔
1513
    const drawX = drawFreezeBorder ? getStickyWidth(effectiveCols) : 0;
456✔
1514

1515
    if (drawX !== 0) {
456✔
1516
        ctx.beginPath();
67✔
1517
        ctx.moveTo(drawX + 0.5, 0);
67✔
1518
        ctx.lineTo(drawX + 0.5, height);
67✔
1519
        ctx.strokeStyle = blend(vColor, theme.bgCell);
67✔
1520
        ctx.stroke();
67✔
1521
    }
1522

1523
    if (lastRowSticky) {
456✔
1524
        const h = getRowHeight(rows - 1);
445✔
1525
        ctx.beginPath();
445✔
1526
        ctx.moveTo(0, height - h + 0.5);
445✔
1527
        ctx.lineTo(width, height - h + 0.5);
445✔
1528
        ctx.strokeStyle = blend(hColor, theme.bgCell);
445✔
1529
        ctx.stroke();
445✔
1530
    }
1531
}
1532

1533
function drawHighlightRings(
1534
    ctx: CanvasRenderingContext2D,
1535
    width: number,
1536
    height: number,
1537
    cellXOffset: number,
1538
    cellYOffset: number,
1539
    translateX: number,
1540
    translateY: number,
1541
    mappedColumns: readonly MappedGridColumn[],
1542
    freezeColumns: number,
1543
    headerHeight: number,
1544
    groupHeaderHeight: number,
1545
    rowHeight: number | ((index: number) => number),
1546
    lastRowSticky: boolean,
1547
    rows: number,
1548
    allHighlightRegions: readonly Highlight[] | undefined
1549
): (() => void) | undefined {
1550
    const highlightRegions = allHighlightRegions?.filter(x => x.style !== "no-outline");
456✔
1551
    if (highlightRegions === undefined || highlightRegions.length === 0) return undefined;
456✔
1552
    const drawRects = highlightRegions.map(h => {
4✔
1553
        const r = h.range;
4✔
1554
        const topLeftBounds = computeBounds(
4✔
1555
            r.x,
1556
            r.y,
1557
            width,
1558
            height,
1559
            groupHeaderHeight,
1560
            headerHeight + groupHeaderHeight,
1561
            cellXOffset,
1562
            cellYOffset,
1563
            translateX,
1564
            translateY,
1565
            rows,
1566
            freezeColumns,
1567
            lastRowSticky,
1568
            mappedColumns,
1569
            rowHeight
1570
        );
1571
        if (r.width === 1 && r.height === 1) {
4!
1572
            if (r.x < freezeColumns) {
×
1573
                return [{ color: h.color, style: h.style ?? "dashed", rect: topLeftBounds }, undefined];
×
1574
            }
1575
            return [undefined, { color: h.color, style: h.style ?? "dashed", rect: topLeftBounds }];
×
1576
        }
1577

1578
        const bottomRightBounds = computeBounds(
4✔
1579
            r.x + r.width - 1,
1580
            r.y + r.height - 1,
1581
            width,
1582
            height,
1583
            groupHeaderHeight,
1584
            headerHeight + groupHeaderHeight,
1585
            cellXOffset,
1586
            cellYOffset,
1587
            translateX,
1588
            translateY,
1589
            rows,
1590
            freezeColumns,
1591
            lastRowSticky,
1592
            mappedColumns,
1593
            rowHeight
1594
        );
1595
        if (r.x < freezeColumns && r.x + r.width >= freezeColumns) {
4!
1596
            const freezeSectionRightBounds = computeBounds(
×
1597
                freezeColumns - 1,
1598
                r.y + r.height - 1,
1599
                width,
1600
                height,
1601
                groupHeaderHeight,
1602
                headerHeight + groupHeaderHeight,
1603
                cellXOffset,
1604
                cellYOffset,
1605
                translateX,
1606
                translateY,
1607
                rows,
1608
                freezeColumns,
1609
                lastRowSticky,
1610
                mappedColumns,
1611
                rowHeight
1612
            );
1613
            const unfreezeSectionleftBounds = computeBounds(
×
1614
                freezeColumns,
1615
                r.y + r.height - 1,
1616
                width,
1617
                height,
1618
                groupHeaderHeight,
1619
                headerHeight + groupHeaderHeight,
1620
                cellXOffset,
1621
                cellYOffset,
1622
                translateX,
1623
                translateY,
1624
                rows,
1625
                freezeColumns,
1626
                lastRowSticky,
1627
                mappedColumns,
1628
                rowHeight
1629
            );
1630

1631
            return [
×
1632
                {
1633
                    color: h.color,
1634
                    style: h.style ?? "dashed",
×
1635
                    rect: {
1636
                        x: topLeftBounds.x,
1637
                        y: topLeftBounds.y,
1638
                        width: freezeSectionRightBounds.x + freezeSectionRightBounds.width - topLeftBounds.x,
1639
                        height: freezeSectionRightBounds.y + freezeSectionRightBounds.height - topLeftBounds.y,
1640
                    } as Rectangle,
1641
                },
1642
                {
1643
                    color: h.color,
1644
                    style: h.style ?? "dashed",
×
1645
                    rect: {
1646
                        x: unfreezeSectionleftBounds.x,
1647
                        y: unfreezeSectionleftBounds.y,
1648
                        width: bottomRightBounds.x + bottomRightBounds.width - unfreezeSectionleftBounds.x,
1649
                        height: bottomRightBounds.y + bottomRightBounds.height - unfreezeSectionleftBounds.y,
1650
                    } as Rectangle,
1651
                },
1652
            ];
1653
        } else {
1654
            return [
4✔
1655
                undefined,
1656
                {
1657
                    color: h.color,
1658
                    style: h.style ?? "dashed",
12!
1659
                    rect: {
1660
                        x: topLeftBounds.x,
1661
                        y: topLeftBounds.y,
1662
                        width: bottomRightBounds.x + bottomRightBounds.width - topLeftBounds.x,
1663
                        height: bottomRightBounds.y + bottomRightBounds.height - topLeftBounds.y,
1664
                    } as Rectangle,
1665
                },
1666
            ];
1667
        }
1668
    });
1669

1670
    const stickyWidth = getStickyWidth(mappedColumns);
4✔
1671

1672
    const drawCb = () => {
4✔
1673
        ctx.beginPath();
8✔
1674
        ctx.save();
8✔
1675
        let dashed = false;
8✔
1676
        const setDashed = (dash: boolean) => {
8✔
1677
            if (dashed === dash) return;
6!
1678
            ctx.setLineDash(dash ? [5, 3] : []);
6!
1679
            dashed = dash;
6✔
1680
        };
1681

1682
        ctx.lineWidth = 1;
8✔
1683
        for (const dr of drawRects) {
8✔
1684
            const [s] = dr;
8✔
1685
            if (
8!
1686
                s !== undefined &&
8!
1687
                intersectRect(0, 0, width, height, s.rect.x, s.rect.y, s.rect.width, s.rect.height)
1688
            ) {
1689
                setDashed(s.style === "dashed");
×
1690
                ctx.strokeStyle = withAlpha(s.color, 1);
×
1691
                ctx.strokeRect(s.rect.x + 1, s.rect.y + 1, s.rect.width - 2, s.rect.height - 2);
×
1692
            }
1693
        }
1694
        let clipped = false;
8✔
1695
        for (const dr of drawRects) {
8✔
1696
            const [, s] = dr;
8✔
1697
            if (
8✔
1698
                s !== undefined &&
16✔
1699
                intersectRect(0, 0, width, height, s.rect.x, s.rect.y, s.rect.width, s.rect.height)
1700
            ) {
1701
                setDashed(s.style === "dashed");
6✔
1702
                if (!clipped && s.rect.x < stickyWidth) {
6!
1703
                    ctx.rect(stickyWidth, 0, width, height);
×
1704
                    ctx.clip();
×
1705
                    clipped = true;
×
1706
                }
1707
                ctx.strokeStyle = withAlpha(s.color, 1);
6✔
1708
                ctx.strokeRect(s.rect.x + 1, s.rect.y + 1, s.rect.width - 2, s.rect.height - 2);
6✔
1709
            }
1710
        }
1711
        ctx.restore();
8✔
1712
    };
1713

1714
    drawCb();
4✔
1715
    return drawCb;
4✔
1716
}
1717

1718
function drawFocusRing(
1719
    ctx: CanvasRenderingContext2D,
1720
    width: number,
1721
    height: number,
1722
    cellYOffset: number,
1723
    translateX: number,
1724
    translateY: number,
1725
    effectiveCols: readonly MappedGridColumn[],
1726
    allColumns: readonly MappedGridColumn[],
1727
    theme: Theme,
1728
    totalHeaderHeight: number,
1729
    selectedCell: GridSelection,
1730
    getRowHeight: (row: number) => number,
1731
    getCellContent: (cell: Item) => InnerGridCell,
1732
    trailingRowType: TrailingRowType,
1733
    fillHandle: boolean,
1734
    rows: number
1735
): (() => void) | undefined {
1736
    if (selectedCell.current === undefined || !effectiveCols.some(c => c.sourceIndex === selectedCell.current?.cell[0]))
479!
1737
        return undefined;
304✔
1738
    const [targetCol, targetRow] = selectedCell.current.cell;
175✔
1739
    const cell = getCellContent(selectedCell.current.cell);
175✔
1740
    const targetColSpan = cell.span ?? [targetCol, targetCol];
175✔
1741

1742
    const isStickyRow = trailingRowType === "sticky" && targetRow === rows - 1;
175✔
1743
    const stickRowHeight = trailingRowType === "sticky" && !isStickyRow ? getRowHeight(rows - 1) - 1 : 0;
175✔
1744

1745
    let drawCb: (() => void) | undefined = undefined;
175✔
1746

1747
    walkColumns(
175✔
1748
        effectiveCols,
1749
        cellYOffset,
1750
        translateX,
1751
        translateY,
1752
        totalHeaderHeight,
1753
        (col, drawX, colDrawY, clipX, startRow) => {
1754
            if (col.sticky && targetCol > col.sourceIndex) return;
394✔
1755
            if (col.sourceIndex < targetColSpan[0] || col.sourceIndex > targetColSpan[1]) {
375✔
1756
                return;
200✔
1757
            }
1758

1759
            walkRowsInCol(startRow, colDrawY, height, rows, getRowHeight, trailingRowType, (drawY, row, rh) => {
175✔
1760
                if (row !== targetRow) return;
762✔
1761

1762
                let cellX = drawX;
168✔
1763
                let cellWidth = col.width;
168✔
1764

1765
                if (cell.span !== undefined) {
168✔
1766
                    const areas = getSpanBounds(cell.span, drawX, drawY, col.width, rh, col, allColumns);
1✔
1767
                    const area = col.sticky ? areas[0] : areas[1];
1!
1768

1769
                    if (area !== undefined) {
1✔
1770
                        cellX = area.x;
1✔
1771
                        cellWidth = area.width;
1✔
1772
                    }
1773
                }
1774

1775
                drawCb = () => {
168✔
1776
                    if (clipX > cellX && !col.sticky) {
315!
1777
                        ctx.beginPath();
×
1778
                        ctx.rect(clipX, 0, width - clipX, height);
×
1779
                        ctx.clip();
×
1780
                    }
1781
                    ctx.beginPath();
315✔
1782
                    ctx.rect(cellX + 0.5, drawY + 0.5, cellWidth, rh);
315✔
1783
                    ctx.strokeStyle = col.themeOverride?.accentColor ?? theme.accentColor;
315!
1784
                    ctx.lineWidth = 1;
315✔
1785
                    ctx.stroke();
315✔
1786

1787
                    if (fillHandle) {
315✔
1788
                        ctx.beginPath();
18✔
1789
                        ctx.rect(cellX + cellWidth - 4, drawY + rh - 4, 4, 4);
18✔
1790
                        ctx.fillStyle = col.themeOverride?.accentColor ?? theme.accentColor;
18!
1791
                        ctx.fill();
18✔
1792
                    }
1793
                };
1794
                return true;
168✔
1795
            });
1796

1797
            return true;
175✔
1798
        }
1799
    );
1800

1801
    if (drawCb === undefined) return undefined;
175✔
1802

1803
    const result = () => {
168✔
1804
        ctx.save();
315✔
1805
        ctx.beginPath();
315✔
1806
        ctx.rect(0, totalHeaderHeight, width, height - totalHeaderHeight - stickRowHeight);
315✔
1807
        ctx.clip();
315✔
1808

1809
        drawCb?.();
315!
1810

1811
        ctx.restore();
315✔
1812
    };
1813

1814
    result();
168✔
1815

1816
    return result;
168✔
1817
}
1818

1819
function getLastRow(
1820
    effectiveColumns: readonly MappedGridColumn[],
1821
    height: number,
1822
    totalHeaderHeight: number,
1823
    translateX: number,
1824
    translateY: number,
1825
    cellYOffset: number,
1826
    rows: number,
1827
    getRowHeight: (row: number) => number,
1828
    trailingRowType: TrailingRowType
1829
): number {
1830
    let result = 0;
456✔
1831
    walkColumns(
456✔
1832
        effectiveColumns,
1833
        cellYOffset,
1834
        translateX,
1835
        translateY,
1836
        totalHeaderHeight,
1837
        (_c, __drawX, colDrawY, _clipX, startRow) => {
1838
            walkRowsInCol(
456✔
1839
                startRow,
1840
                colDrawY,
1841
                height,
1842
                rows,
1843
                getRowHeight,
1844
                trailingRowType,
1845
                (_drawY, row, _rh, isSticky) => {
1846
                    if (!isSticky) {
10,845✔
1847
                        result = Math.max(row, result);
10,400✔
1848
                    }
1849
                }
1850
            );
1851

1852
            return true;
456✔
1853
        }
1854
    );
1855
    return result;
456✔
1856
}
1857

1858
export interface DrawGridArg {
1859
    readonly canvas: HTMLCanvasElement;
1860
    readonly headerCanvas: HTMLCanvasElement;
1861
    readonly bufferA: HTMLCanvasElement;
1862
    readonly bufferB: HTMLCanvasElement;
1863
    readonly width: number;
1864
    readonly height: number;
1865
    readonly cellXOffset: number;
1866
    readonly cellYOffset: number;
1867
    readonly translateX: number;
1868
    readonly translateY: number;
1869
    readonly mappedColumns: readonly MappedGridColumn[];
1870
    readonly enableGroups: boolean;
1871
    readonly freezeColumns: number;
1872
    readonly dragAndDropState: DragAndDropState | undefined;
1873
    readonly theme: Theme;
1874
    readonly headerHeight: number;
1875
    readonly groupHeaderHeight: number;
1876
    readonly disabledRows: CompactSelection;
1877
    readonly rowHeight: number | ((index: number) => number);
1878
    readonly verticalBorder: (col: number) => boolean;
1879
    readonly isResizing: boolean;
1880
    readonly isFocused: boolean;
1881
    readonly drawFocus: boolean;
1882
    readonly selection: GridSelection;
1883
    readonly fillHandle: boolean;
1884
    readonly lastRowSticky: TrailingRowType;
1885
    readonly hyperWrapping: boolean;
1886
    readonly rows: number;
1887
    readonly getCellContent: (cell: Item) => InnerGridCell;
1888
    readonly getGroupDetails: GroupDetailsCallback;
1889
    readonly getRowThemeOverride: GetRowThemeCallback | undefined;
1890
    readonly drawCustomCell: DrawCustomCellCallback | undefined;
1891
    readonly drawHeaderCallback: DrawHeaderCallback | undefined;
1892
    readonly prelightCells: CellList | undefined;
1893
    readonly highlightRegions: readonly Highlight[] | undefined;
1894
    readonly imageLoader: ImageWindowLoader;
1895
    readonly lastBlitData: React.MutableRefObject<BlitData | undefined>;
1896
    readonly damage: CellList | undefined;
1897
    readonly hoverValues: HoverValues;
1898
    readonly hoverInfo: HoverInfo | undefined;
1899
    readonly spriteManager: SpriteManager;
1900
    readonly scrolling: boolean;
1901
    readonly touchMode: boolean;
1902
    readonly renderStrategy: "single-buffer" | "double-buffer" | "direct";
1903
    readonly enqueue: (item: Item) => void;
1904
    readonly getCellRenderer: GetCellRendererCallback;
1905
}
1906

1907
function computeCanBlit(current: DrawGridArg, last: DrawGridArg | undefined): boolean | number {
1908
    if (last === undefined) return false;
498✔
1909
    if (
326✔
1910
        current.width !== last.width ||
2,602✔
1911
        current.height !== last.height ||
1912
        current.theme !== last.theme ||
1913
        current.headerHeight !== last.headerHeight ||
1914
        current.rowHeight !== last.rowHeight ||
1915
        current.rows !== last.rows ||
1916
        current.getRowThemeOverride !== last.getRowThemeOverride ||
1917
        current.isFocused !== last.isFocused ||
1918
        current.isResizing !== last.isResizing ||
1919
        current.verticalBorder !== last.verticalBorder ||
1920
        current.getCellContent !== last.getCellContent ||
1921
        current.highlightRegions !== last.highlightRegions ||
1922
        current.selection !== last.selection ||
1923
        current.dragAndDropState !== last.dragAndDropState ||
1924
        current.prelightCells !== last.prelightCells ||
1925
        current.touchMode !== last.touchMode ||
1926
        current.scrolling !== last.scrolling
1927
    ) {
1928
        return false;
322✔
1929
    }
1930
    if (current.mappedColumns !== last.mappedColumns) {
4!
1931
        if (current.mappedColumns.length > 100 || current.mappedColumns.length !== last.mappedColumns.length) {
×
1932
            // The array is big, let's just redraw the damned thing rather than check these all. Or the number of cols
1933
            // changed in which case I dont want to figure out what happened.
1934
            return false;
×
1935
        }
1936
        // We want to know if only one column has resized. If this is the case we can do a special left/right sliding
1937
        // blit. Or just not redraw shit on the left.
1938
        let resized: number | undefined;
1939
        for (let i = 0; i < current.mappedColumns.length; i++) {
×
1940
            const curCol = current.mappedColumns[i];
×
1941
            const lastCol = last.mappedColumns[i];
×
1942

1943
            if (deepEqual(curCol, lastCol)) continue;
×
1944

1945
            // two columns changed, abort
1946
            if (resized !== undefined) return false;
×
1947

1948
            if (curCol.width === lastCol.width) return false;
×
1949

1950
            const { width, ...curRest } = curCol;
×
1951
            const { width: lastWidth, ...lastRest } = lastCol;
×
1952

1953
            // more than width changed, abort
1954
            if (!deepEqual(curRest, lastRest)) return false;
×
1955
            resized = i;
×
1956
        }
1957
        if (resized === undefined) {
×
1958
            // we never found a changed column, cool, we can blit
1959
            return true;
×
1960
        }
1961
        return resized;
×
1962
    }
1963
    return true;
4✔
1964
}
1965

1966
export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
8✔
1967
    const {
1968
        canvas,
1969
        headerCanvas,
1970
        width,
1971
        height,
1972
        cellXOffset,
1973
        cellYOffset,
1974
        translateX,
1975
        translateY,
1976
        mappedColumns,
1977
        enableGroups,
1978
        freezeColumns,
1979
        dragAndDropState,
1980
        theme,
1981
        drawFocus,
1982
        headerHeight,
1983
        groupHeaderHeight,
1984
        disabledRows,
1985
        rowHeight,
1986
        verticalBorder,
1987
        isResizing,
1988
        selection,
1989
        fillHandle,
1990
        lastRowSticky: trailingRowType,
1991
        rows,
1992
        getCellContent,
1993
        getGroupDetails,
1994
        getRowThemeOverride,
1995
        isFocused,
1996
        drawCustomCell,
1997
        drawHeaderCallback,
1998
        prelightCells,
1999
        highlightRegions,
2000
        imageLoader,
2001
        lastBlitData,
2002
        hoverValues,
2003
        hyperWrapping,
2004
        hoverInfo,
2005
        spriteManager,
2006
        scrolling,
2007
        touchMode,
2008
        enqueue,
2009
        getCellRenderer,
2010
        renderStrategy,
2011
        bufferA,
2012
        bufferB,
2013
    } = arg;
498✔
2014
    let { damage } = arg;
498✔
2015
    if (width === 0 || height === 0) return;
498!
2016
    const doubleBuffer = renderStrategy === "double-buffer";
498✔
2017
    const dpr = scrolling ? 1 : Math.ceil(window.devicePixelRatio ?? 1);
498!
2018

2019
    const canBlit = renderStrategy !== "direct" && computeCanBlit(arg, lastArg);
498✔
2020

2021
    if (canvas.width !== width * dpr || canvas.height !== height * dpr) {
498✔
2022
        canvas.width = width * dpr;
249✔
2023
        canvas.height = height * dpr;
249✔
2024

2025
        canvas.style.width = width + "px";
249✔
2026
        canvas.style.height = height + "px";
249✔
2027
    }
2028

2029
    const overlayCanvas = headerCanvas;
498✔
2030
    const totalHeaderHeight = enableGroups ? groupHeaderHeight + headerHeight : headerHeight;
498✔
2031

2032
    const overlayHeight = totalHeaderHeight + 1; // border
498✔
2033
    if (overlayCanvas.width !== width * dpr || overlayCanvas.height !== overlayHeight * dpr) {
498✔
2034
        overlayCanvas.width = width * dpr;
249✔
2035
        overlayCanvas.height = overlayHeight * dpr;
249✔
2036

2037
        overlayCanvas.style.width = width + "px";
249✔
2038
        overlayCanvas.style.height = overlayHeight + "px";
249✔
2039
    }
2040

2041
    if (doubleBuffer && (bufferA.width !== width * dpr || bufferA.height !== height * dpr)) {
498✔
2042
        bufferA.width = width * dpr;
2✔
2043
        bufferA.height = height * dpr;
2✔
2044
    }
2045

2046
    if (doubleBuffer && (bufferB.width !== width * dpr || bufferB.height !== height * dpr)) {
498✔
2047
        bufferB.width = width * dpr;
2✔
2048
        bufferB.height = height * dpr;
2✔
2049
    }
2050

2051
    const last = lastBlitData.current;
498✔
2052
    if (
498!
2053
        canBlit === true &&
504!
2054
        cellXOffset === last?.cellXOffset &&
12!
2055
        cellYOffset === last?.cellYOffset &&
6!
2056
        translateX === last?.translateX &&
×
2057
        translateY === last?.translateY
×
2058
    )
2059
        return;
×
2060

2061
    let mainCtx: CanvasRenderingContext2D | null = null;
498✔
2062
    if (doubleBuffer) {
498✔
2063
        mainCtx = canvas.getContext("2d", {
4✔
2064
            alpha: false,
2065
        });
2066
    }
2067
    const overlayCtx = overlayCanvas.getContext("2d", {
498✔
2068
        alpha: false,
2069
    });
2070
    let targetBuffer: HTMLCanvasElement;
2071
    if (!doubleBuffer) {
498✔
2072
        targetBuffer = canvas;
494✔
2073
    } else if (damage !== undefined) {
4!
2074
        targetBuffer = last?.lastBuffer === "b" ? bufferB : bufferA;
×
2075
    } else {
2076
        targetBuffer = last?.lastBuffer === "b" ? bufferA : bufferB;
4✔
2077
    }
2078
    const targetCtx = targetBuffer.getContext("2d", {
498✔
2079
        alpha: false,
2080
    });
2081
    const blitSource = doubleBuffer ? (targetBuffer === bufferA ? bufferB : bufferA) : canvas;
498✔
2082

2083
    if (overlayCtx === null || targetCtx === null) return;
498!
2084

2085
    const getRowHeight = typeof rowHeight === "number" ? () => rowHeight : rowHeight;
148,023✔
2086

2087
    overlayCtx.save();
498✔
2088
    overlayCtx.beginPath();
498✔
2089
    targetCtx.save();
498✔
2090
    targetCtx.beginPath();
498✔
2091

2092
    overlayCtx.textBaseline = "middle";
498✔
2093
    targetCtx.textBaseline = "middle";
498✔
2094

2095
    if (dpr !== 1) {
498!
2096
        overlayCtx.scale(dpr, dpr);
×
2097
        targetCtx.scale(dpr, dpr);
×
2098
    }
2099

2100
    const effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, dragAndDropState, translateX);
498✔
2101

2102
    let drawRegions: Rectangle[] = [];
498✔
2103

2104
    const mustDrawFocusOnHeader = drawFocus && selection.current?.cell[1] === cellYOffset && translateY === 0;
498✔
2105
    const drawHeaderTexture = () => {
498✔
2106
        drawGridHeaders(
469✔
2107
            overlayCtx,
2108
            effectiveCols,
2109
            enableGroups,
2110
            hoverInfo,
2111
            width,
2112
            translateX,
2113
            headerHeight,
2114
            groupHeaderHeight,
2115
            dragAndDropState,
2116
            isResizing,
2117
            selection,
2118
            theme,
2119
            spriteManager,
2120
            hoverValues,
2121
            verticalBorder,
2122
            getGroupDetails,
2123
            damage,
2124
            drawHeaderCallback,
2125
            touchMode
2126
        );
2127

2128
        drawGridLines(
469✔
2129
            overlayCtx,
2130
            effectiveCols,
2131
            cellYOffset,
2132
            translateX,
2133
            translateY,
2134
            width,
2135
            height,
2136
            undefined,
2137
            undefined,
2138
            groupHeaderHeight,
2139
            totalHeaderHeight,
2140
            getRowHeight,
2141
            getRowThemeOverride,
2142
            verticalBorder,
2143
            trailingRowType,
2144
            rows,
2145
            theme,
2146
            true
2147
        );
2148

2149
        overlayCtx.beginPath();
469✔
2150
        overlayCtx.moveTo(0, overlayHeight - 0.5);
469✔
2151
        overlayCtx.lineTo(width, overlayHeight - 0.5);
469✔
2152
        overlayCtx.strokeStyle = blend(
469✔
2153
            theme.headerBottomBorderColor ?? theme.horizontalBorderColor ?? theme.borderColor,
2,814!
2154
            theme.bgHeader
2155
        );
2156
        overlayCtx.stroke();
469✔
2157

2158
        if (mustDrawFocusOnHeader) {
469✔
2159
            drawFocusRing(
23✔
2160
                overlayCtx,
2161
                width,
2162
                height,
2163
                cellYOffset,
2164
                translateX,
2165
                translateY,
2166
                effectiveCols,
2167
                mappedColumns,
2168
                theme,
2169
                totalHeaderHeight,
2170
                selection,
2171
                getRowHeight,
2172
                getCellContent,
2173
                trailingRowType,
2174
                fillHandle,
2175
                rows
2176
            );
2177
        }
2178
    };
2179

2180
    // handle damage updates by directly drawing to the target to avoid large blits
2181
    if (damage !== undefined) {
498✔
2182
        let doHeaders = false;
42✔
2183
        damage = damage.filter(x => {
42✔
2184
            doHeaders = doHeaders || x[1] < 0;
1,114✔
2185
            return (
1,114✔
2186
                x[1] < 0 ||
4,313✔
2187
                intersectRect(cellXOffset, cellYOffset, effectiveCols.length, 300, x[0], x[1], 1, 1) ||
2188
                intersectRect(0, cellYOffset, freezeColumns, 300, x[0], x[1], 1, 1) ||
2189
                (trailingRowType && intersectRect(cellXOffset, rows - 1, effectiveCols.length, 1, x[0], x[1], 1, 1))
2190
            );
2191
        });
2192

2193
        if (damage.length > 0) {
42✔
2194
            clipDamage(
40✔
2195
                targetCtx,
2196
                effectiveCols,
2197
                width,
2198
                height,
2199
                groupHeaderHeight,
2200
                totalHeaderHeight,
2201
                translateX,
2202
                translateY,
2203
                cellYOffset,
2204
                rows,
2205
                getRowHeight,
2206
                trailingRowType,
2207
                damage,
2208
                true
2209
            );
2210

2211
            targetCtx.fillStyle = theme.bgCell;
40✔
2212
            targetCtx.fillRect(0, totalHeaderHeight + 1, width, height - totalHeaderHeight - 1);
40✔
2213

2214
            drawCells(
40✔
2215
                targetCtx,
2216
                effectiveCols,
2217
                mappedColumns,
2218
                height,
2219
                totalHeaderHeight,
2220
                translateX,
2221
                translateY,
2222
                cellYOffset,
2223
                rows,
2224
                getRowHeight,
2225
                getCellContent,
2226
                getGroupDetails,
2227
                getRowThemeOverride,
2228
                disabledRows,
2229
                isFocused,
2230
                drawFocus,
2231
                trailingRowType,
2232
                drawRegions,
2233
                damage,
2234
                selection,
2235
                prelightCells,
2236
                highlightRegions,
2237
                drawCustomCell,
2238
                imageLoader,
2239
                spriteManager,
2240
                hoverValues,
2241
                hoverInfo,
2242
                hyperWrapping,
2243
                theme,
2244
                enqueue,
2245
                getCellRenderer
2246
            );
2247

2248
            if (
40!
2249
                fillHandle &&
49✔
2250
                drawFocus &&
2251
                selection.current !== undefined &&
2252
                damage.some(x => x[0] === selection.current?.cell[0] && x[1] === selection.current?.cell[1])
6!
2253
            ) {
2254
                drawFocusRing(
×
2255
                    targetCtx,
2256
                    width,
2257
                    height,
2258
                    cellYOffset,
2259
                    translateX,
2260
                    translateY,
2261
                    effectiveCols,
2262
                    mappedColumns,
2263
                    theme,
2264
                    totalHeaderHeight,
2265
                    selection,
2266
                    getRowHeight,
2267
                    getCellContent,
2268
                    trailingRowType,
2269
                    fillHandle,
2270
                    rows
2271
                );
2272
            }
2273
        }
2274

2275
        if (doHeaders) {
42✔
2276
            clipDamage(
15✔
2277
                overlayCtx,
2278
                effectiveCols,
2279
                width,
2280
                totalHeaderHeight,
2281
                groupHeaderHeight,
2282
                totalHeaderHeight,
2283
                translateX,
2284
                translateY,
2285
                cellYOffset,
2286
                rows,
2287
                getRowHeight,
2288
                trailingRowType,
2289
                damage,
2290
                false
2291
            );
2292
            drawHeaderTexture();
15✔
2293
        }
2294
        targetCtx.restore();
42✔
2295
        overlayCtx.restore();
42✔
2296

2297
        if (mainCtx !== null) {
42!
2298
            mainCtx.fillStyle = theme.bgCell;
×
2299
            mainCtx.fillRect(0, 0, width, height);
×
2300
            mainCtx.drawImage(targetCtx.canvas, 0, 0);
×
2301
        }
2302

2303
        return;
42✔
2304
    }
2305

2306
    if (
456✔
2307
        canBlit !== true ||
464✔
2308
        cellXOffset !== last?.cellXOffset ||
12!
2309
        translateX !== last?.translateX ||
6!
2310
        mustDrawFocusOnHeader !== last?.mustDrawFocusOnHeader
6!
2311
    ) {
2312
        drawHeaderTexture();
454✔
2313
    }
2314

2315
    if (canBlit === true) {
456✔
2316
        assert(blitSource !== undefined && last !== undefined);
4✔
2317
        const { regions } = blitLastFrame(
4✔
2318
            targetCtx,
2319
            blitSource,
2320
            last,
2321
            cellXOffset,
2322
            cellYOffset,
2323
            translateX,
2324
            translateY,
2325
            trailingRowType === "sticky",
2326
            width,
2327
            height,
2328
            rows,
2329
            totalHeaderHeight,
2330
            dpr,
2331
            mappedColumns,
2332
            effectiveCols,
2333
            rowHeight,
2334
            doubleBuffer
2335
        );
2336
        drawRegions = regions;
4✔
2337
    } else if (canBlit !== false) {
452!
2338
        assert(last !== undefined);
×
2339
        const resizedCol = canBlit;
×
2340
        drawRegions = blitResizedCol(
×
2341
            last,
2342
            cellXOffset,
2343
            cellYOffset,
2344
            translateX,
2345
            translateY,
2346
            width,
2347
            height,
2348
            totalHeaderHeight,
2349
            effectiveCols,
2350
            resizedCol
2351
        );
2352
    }
2353

2354
    overdrawStickyBoundaries(
456✔
2355
        targetCtx,
2356
        effectiveCols,
2357
        width,
2358
        height,
2359
        trailingRowType === "sticky",
2360
        rows,
2361
        verticalBorder,
2362
        getRowHeight,
2363
        theme
2364
    );
2365

2366
    // the overdraw may have nuked out our focus ring right edge.
2367
    const focusRedraw = drawFocus
456!
2368
        ? drawFocusRing(
2369
              targetCtx,
2370
              width,
2371
              height,
2372
              cellYOffset,
2373
              translateX,
2374
              translateY,
2375
              effectiveCols,
2376
              mappedColumns,
2377
              theme,
2378
              totalHeaderHeight,
2379
              selection,
2380
              getRowHeight,
2381
              getCellContent,
2382
              trailingRowType,
2383
              fillHandle,
2384
              rows
2385
          )
2386
        : undefined;
2387

2388
    const highlightRedraw = drawHighlightRings(
456✔
2389
        targetCtx,
2390
        width,
2391
        height,
2392
        cellXOffset,
2393
        cellYOffset,
2394
        translateX,
2395
        translateY,
2396
        mappedColumns,
2397
        freezeColumns,
2398
        headerHeight,
2399
        groupHeaderHeight,
2400
        rowHeight,
2401
        trailingRowType === "sticky",
2402
        rows,
2403
        highlightRegions
2404
    );
2405

2406
    targetCtx.fillStyle = theme.bgCell;
456✔
2407
    if (drawRegions.length > 0) {
456✔
2408
        targetCtx.beginPath();
4✔
2409
        for (const r of drawRegions) {
4✔
2410
            targetCtx.rect(r.x, r.y, r.width, r.height);
4✔
2411
        }
2412
        targetCtx.clip();
4✔
2413
        targetCtx.fill();
4✔
2414
        targetCtx.beginPath();
4✔
2415
    } else {
2416
        targetCtx.fillRect(0, 0, width, height);
452✔
2417
    }
2418

2419
    const spans = drawCells(
456✔
2420
        targetCtx,
2421
        effectiveCols,
2422
        mappedColumns,
2423
        height,
2424
        totalHeaderHeight,
2425
        translateX,
2426
        translateY,
2427
        cellYOffset,
2428
        rows,
2429
        getRowHeight,
2430
        getCellContent,
2431
        getGroupDetails,
2432
        getRowThemeOverride,
2433
        disabledRows,
2434
        isFocused,
2435
        drawFocus,
2436
        trailingRowType,
2437
        drawRegions,
2438
        damage,
2439
        selection,
2440
        prelightCells,
2441
        highlightRegions,
2442
        drawCustomCell,
2443
        imageLoader,
2444
        spriteManager,
2445
        hoverValues,
2446
        hoverInfo,
2447
        hyperWrapping,
2448
        theme,
2449
        enqueue,
2450
        getCellRenderer
2451
    );
2452

2453
    drawBlanks(
456✔
2454
        targetCtx,
2455
        effectiveCols,
2456
        mappedColumns,
2457
        width,
2458
        height,
2459
        totalHeaderHeight,
2460
        translateX,
2461
        translateY,
2462
        cellYOffset,
2463
        rows,
2464
        getRowHeight,
2465
        getRowThemeOverride,
2466
        selection.rows,
2467
        disabledRows,
2468
        trailingRowType,
2469
        drawRegions,
2470
        damage,
2471
        theme
2472
    );
2473

2474
    drawGridLines(
456✔
2475
        targetCtx,
2476
        effectiveCols,
2477
        cellYOffset,
2478
        translateX,
2479
        translateY,
2480
        width,
2481
        height,
2482
        drawRegions,
2483
        spans,
2484
        groupHeaderHeight,
2485
        totalHeaderHeight,
2486
        getRowHeight,
2487
        getRowThemeOverride,
2488
        verticalBorder,
2489
        trailingRowType,
2490
        rows,
2491
        theme
2492
    );
2493

2494
    focusRedraw?.();
456✔
2495
    highlightRedraw?.();
456✔
2496

2497
    if (mainCtx !== null) {
456✔
2498
        mainCtx.fillStyle = theme.bgCell;
4✔
2499
        mainCtx.fillRect(0, 0, width, height);
4✔
2500
        mainCtx.drawImage(targetCtx.canvas, 0, 0);
4✔
2501
    }
2502

2503
    const lastRowDrawn = getLastRow(
456✔
2504
        effectiveCols,
2505
        height,
2506
        totalHeaderHeight,
2507
        translateX,
2508
        translateY,
2509
        cellYOffset,
2510
        rows,
2511
        getRowHeight,
2512
        trailingRowType
2513
    );
2514

2515
    imageLoader?.setWindow(
456!
2516
        {
2517
            x: cellXOffset,
2518
            y: cellYOffset,
2519
            width: effectiveCols.length,
2520
            height: lastRowDrawn - cellYOffset,
2521
        },
2522
        freezeColumns
2523
    );
2524

2525
    lastBlitData.current = {
456✔
2526
        cellXOffset,
2527
        cellYOffset,
2528
        translateX,
2529
        translateY,
2530
        mustDrawFocusOnHeader,
2531
        lastBuffer: doubleBuffer ? (targetBuffer === bufferA ? "a" : "b") : undefined,
460✔
2532
    };
2533

2534
    targetCtx.restore();
456✔
2535
    overlayCtx.restore();
456✔
2536
}
2537

2538
type WalkRowsCallback = (
2539
    drawY: number,
2540
    row: number,
2541
    rowHeight: number,
2542
    isSticky: boolean,
2543
    isTrailingRow: boolean
2544
) => boolean | void;
2545

2546
function walkRowsInCol(
2547
    startRow: number,
2548
    drawY: number,
2549
    height: number,
2550
    rows: number,
2551
    getRowHeight: (row: number) => number,
2552
    trailingRowType: TrailingRowType,
2553
    cb: WalkRowsCallback
2554
): void {
2555
    let y = drawY;
4,693✔
2556
    let row = startRow;
4,693✔
2557
    let doSticky = trailingRowType === "sticky";
4,693✔
2558
    while (y < height || doSticky) {
4,693✔
2559
        const doingSticky = doSticky && y >= height;
136,875✔
2560
        if (doingSticky) {
136,875✔
2561
            doSticky = false;
4,424✔
2562
            row = rows - 1;
4,424✔
2563
        }
2564
        const rh = getRowHeight(row);
136,875✔
2565

2566
        if (doingSticky) {
136,875✔
2567
            y = height - rh;
4,424✔
2568
        }
2569

2570
        const isMovedStickyRow = doSticky && row === rows - 1;
136,875✔
2571

2572
        if (!isMovedStickyRow && cb(y, row, rh, doingSticky, trailingRowType !== "none" && row === rows - 1) === true) {
136,875✔
2573
            break;
191✔
2574
        }
2575

2576
        if (doingSticky) {
136,684✔
2577
            break;
4,422✔
2578
        }
2579
        y += rh;
132,262✔
2580
        row++;
132,262✔
2581
    }
2582
}
2583

2584
type WalkColsCallback = (
2585
    col: MappedGridColumn,
2586
    drawX: number,
2587
    drawY: number,
2588
    clipX: number,
2589
    startRow: number
2590
) => boolean | void;
2591

2592
function walkColumns(
2593
    effectiveCols: readonly MappedGridColumn[],
2594
    cellYOffset: number,
2595
    translateX: number,
2596
    translateY: number,
2597
    totalHeaderHeight: number,
2598
    cb: WalkColsCallback
2599
): void {
2600
    let x = 0;
2,104✔
2601
    let clipX = 0; // this tracks the total width of sticky cols
2,104✔
2602
    const drawY = totalHeaderHeight + translateY;
2,104✔
2603
    for (const c of effectiveCols) {
2,104✔
2604
        const drawX = c.sticky ? clipX : x + translateX;
11,975✔
2605
        if (cb(c, drawX, drawY, clipX, cellYOffset) === true) {
11,975✔
2606
            break;
654✔
2607
        }
2608

2609
        x += c.width;
11,321✔
2610
        clipX += c.sticky ? c.width : 0;
11,321✔
2611
    }
2612
}
2613

2614
type WalkGroupsCallback = (colSpan: Item, group: string, x: number, y: number, width: number, height: number) => void;
2615
function walkGroups(
2616
    effectiveCols: readonly MappedGridColumn[],
2617
    width: number,
2618
    translateX: number,
2619
    groupHeaderHeight: number,
2620
    cb: WalkGroupsCallback
2621
): void {
2622
    let x = 0;
71✔
2623
    let clipX = 0;
71✔
2624
    for (let index = 0; index < effectiveCols.length; index++) {
71✔
2625
        const startCol = effectiveCols[index];
115✔
2626

2627
        let end = index + 1;
115✔
2628
        let boxWidth = startCol.width;
115✔
2629
        if (startCol.sticky) {
115✔
2630
            clipX += boxWidth;
9✔
2631
        }
2632
        while (
115✔
2633
            end < effectiveCols.length &&
1,817✔
2634
            isGroupEqual(effectiveCols[end].group, startCol.group) &&
2635
            effectiveCols[end].sticky === effectiveCols[index].sticky
2636
        ) {
2637
            const endCol = effectiveCols[end];
551✔
2638
            boxWidth += endCol.width;
551✔
2639
            end++;
551✔
2640
            index++;
551✔
2641
            if (endCol.sticky) {
551!
2642
                clipX += endCol.width;
×
2643
            }
2644
        }
2645

2646
        const t = startCol.sticky ? 0 : translateX;
115✔
2647
        const localX = x + t;
115✔
2648
        const delta = startCol.sticky ? 0 : Math.max(0, clipX - localX);
115✔
2649
        const w = Math.min(boxWidth - delta, width - (localX + delta));
115✔
2650
        cb(
115✔
2651
            [startCol.sourceIndex, effectiveCols[end - 1].sourceIndex],
2652
            startCol.group ?? "",
345✔
2653
            localX + delta,
2654
            0,
2655
            w,
2656
            groupHeaderHeight
2657
        );
2658

2659
        x += boxWidth;
115✔
2660
    }
2661
}
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