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

glideapps / glide-data-grid / 15751463505

19 Jun 2025 06:50AM UTC coverage: 91.301% (+0.008%) from 91.293%
15751463505

Pull #969

github

web-flow
Merge 78d3af3d2 into 62f422ab7
Pull Request #969: expose renderers prop to allow custom internal renderers

2924 of 3614 branches covered (80.91%)

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

466 existing lines in 12 files now uncovered.

17874 of 19577 relevant lines covered (91.3%)

3113.73 hits per line

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

82.2
/packages/core/src/internal/data-grid/render/data-grid-render.header.ts
1
import { intersectRect, pointInRect } from "../../../common/math.js";
1✔
2
import { mergeAndRealizeTheme, type FullTheme } from "../../../common/styles.js";
1✔
3
import { direction } from "../../../common/utils.js";
1✔
4
import type { HoverValues } from "../animation-manager.js";
1✔
5
import type { CellSet } from "../cell-set.js";
1✔
6
import { withAlpha } from "../color-parser.js";
1✔
7
import type { SpriteManager, SpriteVariant } from "../data-grid-sprites.js";
1✔
8
import { GridColumnMenuIcon, type DrawHeaderCallback, type GridSelection, type Rectangle } from "../data-grid-types.js";
1✔
9
import {
1✔
10
    drawMenuDots,
1✔
11
    getMeasuredTextCache,
1✔
12
    getMiddleCenterBias,
1✔
13
    measureTextCached,
1✔
14
    roundedPoly,
1✔
15
    type MappedGridColumn,
1✔
16
} from "./data-grid-lib.js";
1✔
17
import type { GroupDetails, GroupDetailsCallback } from "./data-grid-render.cells.js";
1✔
18
import { walkColumns, walkGroups } from "./data-grid-render.walk.js";
1✔
19
import { drawCheckbox } from "./draw-checkbox.js";
1✔
20
import type { DragAndDropState, HoverInfo } from "./draw-grid-arg.js";
1✔
21

1✔
22
export function drawGridHeaders(
1✔
23
    ctx: CanvasRenderingContext2D,
443✔
24
    effectiveCols: readonly MappedGridColumn[],
443✔
25
    enableGroups: boolean,
443✔
26
    hovered: HoverInfo | undefined,
443✔
27
    width: number,
443✔
28
    translateX: number,
443✔
29
    headerHeight: number,
443✔
30
    groupHeaderHeight: number,
443✔
31
    dragAndDropState: DragAndDropState | undefined,
443✔
32
    isResizing: boolean,
443✔
33
    selection: GridSelection,
443✔
34
    outerTheme: FullTheme,
443✔
35
    spriteManager: SpriteManager,
443✔
36
    hoverValues: HoverValues,
443✔
37
    verticalBorder: (col: number) => boolean,
443✔
38
    getGroupDetails: GroupDetailsCallback,
443✔
39
    damage: CellSet | undefined,
443✔
40
    drawHeaderCallback: DrawHeaderCallback | undefined,
443✔
41
    touchMode: boolean
443✔
42
) {
443✔
43
    const totalHeaderHeight = headerHeight + groupHeaderHeight;
443✔
44
    if (totalHeaderHeight <= 0) return;
443!
45

443✔
46
    ctx.fillStyle = outerTheme.bgHeader;
443✔
47
    ctx.fillRect(0, 0, width, totalHeaderHeight);
443✔
48

443✔
49
    const hCol = hovered?.[0]?.[0];
443✔
50
    const hRow = hovered?.[0]?.[1];
443✔
51
    const hPosX = hovered?.[1]?.[0];
443✔
52
    const hPosY = hovered?.[1]?.[1];
443✔
53

443✔
54
    const font = outerTheme.headerFontFull;
443✔
55
    // Assinging the context font too much can be expensive, it can be worth it to minimze this
443✔
56
    ctx.font = font;
443✔
57
    walkColumns(effectiveCols, 0, translateX, 0, totalHeaderHeight, (c, x, _y, clipX) => {
443✔
58
        if (damage !== undefined && !damage.has([c.sourceIndex, -1])) return;
4,343✔
59
        const diff = Math.max(0, clipX - x);
4,104✔
60
        ctx.save();
4,104✔
61
        ctx.beginPath();
4,104✔
62
        ctx.rect(x + diff, groupHeaderHeight, c.width - diff, headerHeight);
4,104✔
63
        ctx.clip();
4,104✔
64

4,104✔
65
        const groupTheme = getGroupDetails(c.group ?? "").overrideTheme;
4,343✔
66
        const theme =
4,343✔
67
            c.themeOverride === undefined && groupTheme === undefined
4,343✔
68
                ? outerTheme
4,104!
69
                : mergeAndRealizeTheme(outerTheme, groupTheme, c.themeOverride);
×
70

4,343✔
71
        if (theme.bgHeader !== outerTheme.bgHeader) {
4,343!
72
            ctx.fillStyle = theme.bgHeader;
×
73
            ctx.fill();
×
74
        }
✔
75

4,104✔
76
        if (theme !== outerTheme) {
4,343!
77
            ctx.font = theme.baseFontFull;
×
78
        }
✔
79
        const selected = selection.columns.hasIndex(c.sourceIndex);
4,104✔
80
        const noHover = dragAndDropState !== undefined || isResizing;
4,343✔
81
        const hoveredBoolean = !noHover && hRow === -1 && hCol === c.sourceIndex;
4,343✔
82
        const hover = noHover
4,343✔
83
            ? 0
73✔
84
            : hoverValues.find(s => s.item[0] === c.sourceIndex && s.item[1] === -1)?.hoverAmount ?? 0;
4,031✔
85

4,343✔
86
        const hasSelectedCell = selection?.current !== undefined && selection.current.cell[0] === c.sourceIndex;
4,343✔
87

4,343✔
88
        const bgFillStyle = selected ? theme.accentColor : hasSelectedCell ? theme.bgHeaderHasFocus : theme.bgHeader;
4,343✔
89

4,343✔
90
        const y = enableGroups ? groupHeaderHeight : 0;
4,343✔
91
        const xOffset = c.sourceIndex === 0 ? 0 : 1;
4,343✔
92

4,343✔
93
        if (selected) {
4,343✔
94
            ctx.fillStyle = bgFillStyle;
64✔
95
            ctx.fillRect(x + xOffset, y, c.width - xOffset, headerHeight);
64✔
96
        } else if (hasSelectedCell || hover > 0) {
4,343✔
97
            ctx.beginPath();
214✔
98
            ctx.rect(x + xOffset, y, c.width - xOffset, headerHeight);
214✔
99
            if (hasSelectedCell) {
214✔
100
                ctx.fillStyle = theme.bgHeaderHasFocus;
198✔
101
                ctx.fill();
198✔
102
            }
198✔
103
            if (hover > 0) {
214✔
104
                ctx.globalAlpha = hover;
16✔
105
                ctx.fillStyle = theme.bgHeaderHovered;
16✔
106
                ctx.fill();
16✔
107
                ctx.globalAlpha = 1;
16✔
108
            }
16✔
109
        }
214✔
110

4,104✔
111
        drawHeader(
4,104✔
112
            ctx,
4,104✔
113
            x,
4,104✔
114
            y,
4,104✔
115
            c.width,
4,104✔
116
            headerHeight,
4,104✔
117
            c,
4,104✔
118
            selected,
4,104✔
119
            theme,
4,104✔
120
            hoveredBoolean,
4,104✔
121
            hoveredBoolean ? hPosX : undefined,
4,343✔
122
            hoveredBoolean ? hPosY : undefined,
4,343✔
123
            hasSelectedCell,
4,343✔
124
            hover,
4,343✔
125
            spriteManager,
4,343✔
126
            drawHeaderCallback,
4,343✔
127
            touchMode
4,343✔
128
        );
4,343✔
129
        ctx.restore();
4,343✔
130
    });
443✔
131

443✔
132
    if (enableGroups) {
443✔
133
        drawGroups(
17✔
134
            ctx,
17✔
135
            effectiveCols,
17✔
136
            width,
17✔
137
            translateX,
17✔
138
            groupHeaderHeight,
17✔
139
            hovered,
17✔
140
            outerTheme,
17✔
141
            spriteManager,
17✔
142
            hoverValues,
17✔
143
            verticalBorder,
17✔
144
            getGroupDetails,
17✔
145
            damage
17✔
146
        );
17✔
147
    }
17✔
148
}
443✔
149

1✔
150
export function drawGroups(
1✔
151
    ctx: CanvasRenderingContext2D,
17✔
152
    effectiveCols: readonly MappedGridColumn[],
17✔
153
    width: number,
17✔
154
    translateX: number,
17✔
155
    groupHeaderHeight: number,
17✔
156
    hovered: HoverInfo | undefined,
17✔
157
    theme: FullTheme,
17✔
158
    spriteManager: SpriteManager,
17✔
159
    _hoverValues: HoverValues,
17✔
160
    verticalBorder: (col: number) => boolean,
17✔
161
    getGroupDetails: GroupDetailsCallback,
17✔
162
    damage: CellSet | undefined
17✔
163
) {
17✔
164
    const xPad = 8;
17✔
165
    const [hCol, hRow] = hovered?.[0] ?? [];
17✔
166

17✔
167
    let finalX = 0;
17✔
168
    walkGroups(effectiveCols, width, translateX, groupHeaderHeight, (span, groupName, x, y, w, h) => {
17✔
169
        if (
74✔
170
            damage !== undefined &&
74✔
171
            !damage.hasItemInRectangle({
50✔
172
                x: span[0],
50✔
173
                y: -2,
50✔
174
                width: span[1] - span[0] + 1,
50✔
175
                height: 1,
50✔
176
            })
50✔
177
        )
74✔
178
            return;
74✔
179
        ctx.save();
29✔
180
        ctx.beginPath();
29✔
181
        ctx.rect(x, y, w, h);
29✔
182
        ctx.clip();
29✔
183

29✔
184
        const group = getGroupDetails(groupName);
29✔
185
        const groupTheme =
29✔
186
            group?.overrideTheme === undefined ? theme : mergeAndRealizeTheme(theme, group.overrideTheme);
74!
187
        const isHovered = hRow === -2 && hCol !== undefined && hCol >= span[0] && hCol <= span[1];
74✔
188
        const fillColor = isHovered
74✔
189
            ? groupTheme.bgGroupHeaderHovered ?? groupTheme.bgHeaderHovered
5✔
190
            : groupTheme.bgGroupHeader ?? groupTheme.bgHeader;
24✔
191

74✔
192
        if (fillColor !== theme.bgHeader) {
74✔
193
            ctx.fillStyle = fillColor;
5✔
194
            ctx.fill();
5✔
195
        }
5✔
196

29✔
197
        ctx.fillStyle = groupTheme.textGroupHeader ?? groupTheme.textHeader;
74!
198
        if (group !== undefined) {
74✔
199
            let drawX = x;
29✔
200
            if (group.icon !== undefined) {
29✔
201
                spriteManager.drawSprite(
4✔
202
                    group.icon,
4✔
203
                    "normal",
4✔
204
                    ctx,
4✔
205
                    drawX + xPad,
4✔
206
                    (groupHeaderHeight - 20) / 2,
4✔
207
                    20,
4✔
208
                    groupTheme
4✔
209
                );
4✔
210
                drawX += 26;
4✔
211
            }
4✔
212
            ctx.fillText(
29✔
213
                group.name,
29✔
214
                drawX + xPad,
29✔
215
                groupHeaderHeight / 2 + getMiddleCenterBias(ctx, theme.headerFontFull)
29✔
216
            );
29✔
217

29✔
218
            if (group.actions !== undefined && isHovered) {
29✔
219
                const actionBoxes = getActionBoundsForGroup({ x, y, width: w, height: h }, group.actions);
5✔
220

5✔
221
                ctx.beginPath();
5✔
222
                const fadeStartX = actionBoxes[0].x - 10;
5✔
223
                const fadeWidth = x + w - fadeStartX;
5✔
224
                ctx.rect(fadeStartX, 0, fadeWidth, groupHeaderHeight);
5✔
225
                const grad = ctx.createLinearGradient(fadeStartX, 0, fadeStartX + fadeWidth, 0);
5✔
226
                const trans = withAlpha(fillColor, 0);
5✔
227
                grad.addColorStop(0, trans);
5✔
228
                grad.addColorStop(10 / fadeWidth, fillColor);
5✔
229
                grad.addColorStop(1, fillColor);
5✔
230
                ctx.fillStyle = grad;
5✔
231

5✔
232
                ctx.fill();
5✔
233

5✔
234
                ctx.globalAlpha = 0.6;
5✔
235

5✔
236
                // eslint-disable-next-line prefer-const
5✔
237
                const [mouseX, mouseY] = hovered?.[1] ?? [-1, -1];
5!
238
                for (let i = 0; i < group.actions.length; i++) {
5✔
239
                    const action = group.actions[i];
5✔
240
                    const box = actionBoxes[i];
5✔
241
                    const actionHovered = pointInRect(box, mouseX + x, mouseY);
5✔
242
                    if (actionHovered) {
5✔
243
                        ctx.globalAlpha = 1;
5✔
244
                    }
5✔
245
                    spriteManager.drawSprite(
5✔
246
                        action.icon,
5✔
247
                        "normal",
5✔
248
                        ctx,
5✔
249
                        box.x + box.width / 2 - 10,
5✔
250
                        box.y + box.height / 2 - 10,
5✔
251
                        20,
5✔
252
                        groupTheme
5✔
253
                    );
5✔
254
                    if (actionHovered) {
5✔
255
                        ctx.globalAlpha = 0.6;
5✔
256
                    }
5✔
257
                }
5✔
258

5✔
259
                ctx.globalAlpha = 1;
5✔
260
            }
5✔
261
        }
29✔
262

29✔
263
        if (x !== 0 && verticalBorder(span[0])) {
74✔
264
            ctx.beginPath();
17✔
265
            ctx.moveTo(x + 0.5, 0);
17✔
266
            ctx.lineTo(x + 0.5, groupHeaderHeight);
17✔
267
            ctx.strokeStyle = theme.borderColor;
17✔
268
            ctx.lineWidth = 1;
17✔
269
            ctx.stroke();
17✔
270
        }
17✔
271

29✔
272
        ctx.restore();
29✔
273

29✔
274
        finalX = x + w;
29✔
275
    });
17✔
276

17✔
277
    ctx.beginPath();
17✔
278
    ctx.moveTo(finalX + 0.5, 0);
17✔
279
    ctx.lineTo(finalX + 0.5, groupHeaderHeight);
17✔
280

17✔
281
    ctx.moveTo(0, groupHeaderHeight + 0.5);
17✔
282
    ctx.lineTo(width, groupHeaderHeight + 0.5);
17✔
283
    ctx.strokeStyle = theme.borderColor;
17✔
284
    ctx.lineWidth = 1;
17✔
285
    ctx.stroke();
17✔
286
}
17✔
287

1✔
288
const menuButtonSize = 30;
1✔
289
function getHeaderMenuBounds(x: number, y: number, width: number, height: number, isRtl: boolean): Rectangle {
4,279✔
290
    if (isRtl) return { x, y, width: menuButtonSize, height: Math.min(menuButtonSize, height) };
4,279!
291
    return {
4,279✔
292
        x: x + width - menuButtonSize, // right align
4,279✔
293
        y: Math.max(y, y + height / 2 - menuButtonSize / 2), // center vertically
4,279✔
294
        width: menuButtonSize,
4,279✔
295
        height: Math.min(menuButtonSize, height),
4,279✔
296
    };
4,279✔
297
}
4,279✔
298

1✔
299
export function getActionBoundsForGroup(
1✔
300
    box: Rectangle,
8✔
301
    actions: NonNullable<GroupDetails["actions"]>
8✔
302
): readonly Rectangle[] {
8✔
303
    const result: Rectangle[] = [];
8✔
304
    let x = box.x + box.width - 26 * actions.length;
8✔
305
    const y = box.y + box.height / 2 - 13;
8✔
306
    const height = 26;
8✔
307
    const width = 26;
8✔
308
    for (let i = 0; i < actions.length; i++) {
8✔
309
        result.push({
8✔
310
            x,
8✔
311
            y,
8✔
312
            width,
8✔
313
            height,
8✔
314
        });
8✔
315
        x += 26;
8✔
316
    }
8✔
317
    return result;
8✔
318
}
8✔
319

1✔
320
type Mutable<T> = {
1✔
321
    -readonly [P in keyof T]: T[P];
1✔
322
};
1✔
323

1✔
324
interface HeaderLayout {
1✔
325
    readonly textBounds: Rectangle | undefined;
1✔
326
    readonly iconBounds: Rectangle | undefined;
1✔
327
    readonly iconOverlayBounds: Rectangle | undefined;
1✔
328
    readonly indicatorIconBounds: Rectangle | undefined;
1✔
329
    readonly menuBounds: Rectangle | undefined;
1✔
330
}
1✔
331

1✔
332
function flipHorizontal(
21,395✔
333
    toFlip: Mutable<Rectangle> | undefined,
21,395✔
334
    mirrorX: number,
21,395✔
335
    isRTL: boolean
21,395✔
336
): Mutable<Rectangle> | undefined {
21,395✔
337
    if (!isRTL || toFlip === undefined) return toFlip;
21,395!
338
    toFlip.x = mirrorX - (toFlip.x - mirrorX) - toFlip.width;
×
UNCOV
339
    return toFlip;
×
UNCOV
340
}
×
341

1✔
342
export function computeHeaderLayout(
1✔
343
    ctx: CanvasRenderingContext2D | undefined,
4,279✔
344
    c: MappedGridColumn,
4,279✔
345
    x: number,
4,279✔
346
    y: number,
4,279✔
347
    width: number,
4,279✔
348
    height: number,
4,279✔
349
    theme: FullTheme,
4,279✔
350
    isRTL: boolean
4,279✔
351
): HeaderLayout {
4,279✔
352
    const xPad = theme.cellHorizontalPadding;
4,279✔
353
    const headerIconSize = theme.headerIconSize;
4,279✔
354
    const menuBounds = getHeaderMenuBounds(x, y, width, height, false);
4,279✔
355

4,279✔
356
    let drawX = x + xPad;
4,279✔
357
    const iconBounds =
4,279✔
358
        c.icon === undefined
4,279✔
359
            ? undefined
155✔
360
            : {
4,124✔
361
                  x: drawX,
4,124✔
362
                  y: y + (height - headerIconSize) / 2,
4,124✔
363
                  width: headerIconSize,
4,124✔
364
                  height: headerIconSize,
4,124✔
365
              };
4,124✔
366

4,279✔
367
    const iconOverlayBounds =
4,279✔
368
        iconBounds === undefined || c.overlayIcon === undefined
4,279✔
369
            ? undefined
4,279!
370
            : {
×
371
                  x: iconBounds.x + 9,
×
372
                  y: iconBounds.y + 6,
×
373
                  width: 18,
×
UNCOV
374
                  height: 18,
×
UNCOV
375
              };
×
376

4,279✔
377
    if (iconBounds !== undefined) {
4,279✔
378
        drawX += Math.ceil(headerIconSize * 1.3);
4,124✔
379
    }
4,124✔
380

4,279✔
381
    const textBounds = {
4,279✔
382
        x: drawX,
4,279✔
383
        y: y,
4,279✔
384
        width: width - drawX,
4,279✔
385
        height: height,
4,279✔
386
    };
4,279✔
387

4,279✔
388
    let indicatorIconBounds: Rectangle | undefined = undefined;
4,279✔
389
    if (c.indicatorIcon !== undefined) {
4,279!
390
        const textWidth =
×
391
            ctx === undefined
×
392
                ? getMeasuredTextCache(c.title, theme.headerFontFull)?.width ?? 0
×
393
                : measureTextCached(c.title, ctx, theme.headerFontFull).width;
×
394
        textBounds.width = textWidth;
×
395
        drawX += textWidth + xPad;
×
396
        indicatorIconBounds = {
×
397
            x: drawX,
×
398
            y: y + (height - headerIconSize) / 2,
×
399
            width: headerIconSize,
×
400
            height: headerIconSize,
×
UNCOV
401
        };
×
UNCOV
402
    }
×
403

4,279✔
404
    const mirrorPoint = x + width / 2;
4,279✔
405

4,279✔
406
    return {
4,279✔
407
        menuBounds: flipHorizontal(menuBounds, mirrorPoint, isRTL),
4,279✔
408
        iconBounds: flipHorizontal(iconBounds, mirrorPoint, isRTL),
4,279✔
409
        iconOverlayBounds: flipHorizontal(iconOverlayBounds, mirrorPoint, isRTL),
4,279✔
410
        textBounds: flipHorizontal(textBounds, mirrorPoint, isRTL),
4,279✔
411
        indicatorIconBounds: flipHorizontal(indicatorIconBounds, mirrorPoint, isRTL),
4,279✔
412
    };
4,279✔
413
}
4,279✔
414

1✔
415
function drawHeaderInner(
4,104✔
416
    ctx: CanvasRenderingContext2D,
4,104✔
417
    x: number,
4,104✔
418
    y: number,
4,104✔
419
    width: number,
4,104✔
420
    height: number,
4,104✔
421
    c: MappedGridColumn,
4,104✔
422
    selected: boolean,
4,104✔
423
    theme: FullTheme,
4,104✔
424
    isHovered: boolean,
4,104✔
425
    posX: number | undefined,
4,104✔
426
    posY: number | undefined,
4,104✔
427
    hoverAmount: number,
4,104✔
428
    spriteManager: SpriteManager,
4,104✔
429
    touchMode: boolean,
4,104✔
430
    isRtl: boolean,
4,104✔
431
    headerLayout: HeaderLayout
4,104✔
432
) {
4,104✔
433
    if (c.rowMarker !== undefined && c.headerRowMarkerDisabled !== true) {
4,104✔
434
        const checked = c.rowMarkerChecked;
58✔
435
        if (checked !== true && c.headerRowMarkerAlwaysVisible !== true) {
58✔
436
            ctx.globalAlpha = hoverAmount;
55✔
437
        }
55✔
438
        const markerTheme =
58✔
439
            c.headerRowMarkerTheme !== undefined ? mergeAndRealizeTheme(theme, c.headerRowMarkerTheme) : theme;
58!
440
        drawCheckbox(
58✔
441
            ctx,
58✔
442
            markerTheme,
58✔
443
            checked,
58✔
444
            x,
58✔
445
            y,
58✔
446
            width,
58✔
447
            height,
58✔
448
            false,
58✔
449
            undefined,
58✔
450
            undefined,
58✔
451
            18,
58✔
452
            "center",
58✔
453
            c.rowMarker
58✔
454
        );
58✔
455
        if (checked !== true && c.headerRowMarkerAlwaysVisible !== true) {
58✔
456
            ctx.globalAlpha = 1;
55✔
457
        }
55✔
458
        return;
58✔
459
    }
58✔
460

4,046✔
461
    const fillStyle = selected ? theme.textHeaderSelected : theme.textHeader;
4,104✔
462

4,104✔
463
    const shouldDrawMenu =
4,104✔
464
        c.hasMenu === true && (isHovered || (touchMode && selected)) && headerLayout.menuBounds !== undefined;
4,104!
465

4,104✔
466
    if (c.icon !== undefined && headerLayout.iconBounds !== undefined) {
4,104✔
467
        let variant: SpriteVariant = selected ? "selected" : "normal";
3,979✔
468
        if (c.style === "highlight") {
3,979!
UNCOV
469
            variant = selected ? "selected" : "special";
×
UNCOV
470
        }
×
471
        spriteManager.drawSprite(
3,979✔
472
            c.icon,
3,979✔
473
            variant,
3,979✔
474
            ctx,
3,979✔
475
            headerLayout.iconBounds.x,
3,979✔
476
            headerLayout.iconBounds.y,
3,979✔
477
            headerLayout.iconBounds.width,
3,979✔
478
            theme
3,979✔
479
        );
3,979✔
480

3,979✔
481
        if (c.overlayIcon !== undefined && headerLayout.iconOverlayBounds !== undefined) {
3,979!
482
            spriteManager.drawSprite(
×
483
                c.overlayIcon,
×
484
                selected ? "selected" : "special",
×
485
                ctx,
×
486
                headerLayout.iconOverlayBounds.x,
×
487
                headerLayout.iconOverlayBounds.y,
×
488
                headerLayout.iconOverlayBounds.width,
×
489
                theme
×
UNCOV
490
            );
×
UNCOV
491
        }
×
492
    }
3,979✔
493

4,046✔
494
    if (shouldDrawMenu && width > 35) {
4,104✔
495
        const fadeWidth = 35;
5✔
496
        const fadeStart = isRtl ? fadeWidth : width - fadeWidth;
5!
497
        const fadeEnd = isRtl ? fadeWidth * 0.7 : width - fadeWidth * 0.7;
5!
498

5✔
499
        const fadeStartPercent = fadeStart / width;
5✔
500
        const fadeEndPercent = fadeEnd / width;
5✔
501

5✔
502
        const grad = ctx.createLinearGradient(x, 0, x + width, 0);
5✔
503
        const trans = withAlpha(fillStyle, 0);
5✔
504

5✔
505
        grad.addColorStop(isRtl ? 1 : 0, fillStyle);
5!
506
        grad.addColorStop(fadeStartPercent, fillStyle);
5✔
507
        grad.addColorStop(fadeEndPercent, trans);
5✔
508
        grad.addColorStop(isRtl ? 0 : 1, trans);
5!
509
        ctx.fillStyle = grad;
5✔
510
    } else {
4,104✔
511
        ctx.fillStyle = fillStyle;
4,041✔
512
    }
4,041✔
513

4,046✔
514
    if (isRtl) {
4,104!
UNCOV
515
        ctx.textAlign = "right";
×
UNCOV
516
    }
✔
517
    if (headerLayout.textBounds !== undefined) {
4,046✔
518
        ctx.fillText(
4,046✔
519
            c.title,
4,046✔
520
            isRtl ? headerLayout.textBounds.x + headerLayout.textBounds.width : headerLayout.textBounds.x,
4,046!
521
            y + height / 2 + getMiddleCenterBias(ctx, theme.headerFontFull)
4,046✔
522
        );
4,046✔
523
    }
4,046✔
524
    if (isRtl) {
4,104!
UNCOV
525
        ctx.textAlign = "left";
×
UNCOV
526
    }
✔
527

4,046✔
528
    if (
4,046✔
529
        c.indicatorIcon !== undefined &&
4,046!
530
        headerLayout.indicatorIconBounds !== undefined &&
×
531
        (!shouldDrawMenu ||
×
532
            !intersectRect(
×
533
                headerLayout.menuBounds.x,
×
534
                headerLayout.menuBounds.y,
×
535
                headerLayout.menuBounds.width,
×
536
                headerLayout.menuBounds.height,
×
537
                headerLayout.indicatorIconBounds.x,
×
538
                headerLayout.indicatorIconBounds.y,
×
539
                headerLayout.indicatorIconBounds.width,
×
UNCOV
540
                headerLayout.indicatorIconBounds.height
×
541
            ))
×
542
    ) {
4,104!
543
        let variant: SpriteVariant = selected ? "selected" : "normal";
×
544
        if (c.style === "highlight") {
×
545
            variant = selected ? "selected" : "special";
×
546
        }
×
547
        spriteManager.drawSprite(
×
548
            c.indicatorIcon,
×
549
            variant,
×
550
            ctx,
×
551
            headerLayout.indicatorIconBounds.x,
×
552
            headerLayout.indicatorIconBounds.y,
×
553
            headerLayout.indicatorIconBounds.width,
×
554
            theme
×
UNCOV
555
        );
×
UNCOV
556
    }
✔
557

4,046✔
558
    if (shouldDrawMenu && headerLayout.menuBounds !== undefined) {
4,104✔
559
        const menuBounds = headerLayout.menuBounds;
5✔
560

5✔
561
        const hovered = posX !== undefined && posY !== undefined && pointInRect(menuBounds, posX + x, posY + y);
5✔
562

5✔
563
        if (!hovered) {
5!
UNCOV
564
            ctx.globalAlpha = 0.7;
×
UNCOV
565
        }
×
566

5✔
567
        if (c.menuIcon === undefined || c.menuIcon === GridColumnMenuIcon.Triangle) {
5!
568
            // Draw the default triangle menu icon:
5✔
569
            ctx.beginPath();
5✔
570
            const triangleX = menuBounds.x + menuBounds.width / 2 - 5.5;
5✔
571
            const triangleY = menuBounds.y + menuBounds.height / 2 - 3;
5✔
572
            roundedPoly(
5✔
573
                ctx,
5✔
574
                [
5✔
575
                    {
5✔
576
                        x: triangleX,
5✔
577
                        y: triangleY,
5✔
578
                    },
5✔
579
                    {
5✔
580
                        x: triangleX + 11,
5✔
581
                        y: triangleY,
5✔
582
                    },
5✔
583
                    {
5✔
584
                        x: triangleX + 5.5,
5✔
585
                        y: triangleY + 6,
5✔
586
                    },
5✔
587
                ],
5✔
588
                1
5✔
589
            );
5✔
590
            ctx.fillStyle = fillStyle;
5✔
591
            ctx.fill();
5✔
592
        } else if (c.menuIcon === GridColumnMenuIcon.Dots) {
5!
593
            // Draw the three dots menu icon:
×
594
            ctx.beginPath();
×
595
            const dotsX = menuBounds.x + menuBounds.width / 2;
×
596
            const dotsY = menuBounds.y + menuBounds.height / 2;
×
597
            drawMenuDots(ctx, dotsX, dotsY);
×
598
            ctx.fillStyle = fillStyle;
×
599
            ctx.fill();
×
600
        } else {
×
601
            // Assume that the user has specified a valid sprite image as header icon:
×
602
            const iconX = menuBounds.x + (menuBounds.width - theme.headerIconSize) / 2;
×
603
            const iconY = menuBounds.y + (menuBounds.height - theme.headerIconSize) / 2;
×
UNCOV
604
            spriteManager.drawSprite(c.menuIcon, "normal", ctx, iconX, iconY, theme.headerIconSize, theme);
×
UNCOV
605
        }
×
606

5✔
607
        if (!hovered) {
5!
UNCOV
608
            ctx.globalAlpha = 1;
×
UNCOV
609
        }
×
610
    }
5✔
611
}
4,104✔
612

1✔
613
export function drawHeader(
1✔
614
    ctx: CanvasRenderingContext2D,
4,104✔
615
    x: number,
4,104✔
616
    y: number,
4,104✔
617
    width: number,
4,104✔
618
    height: number,
4,104✔
619
    c: MappedGridColumn,
4,104✔
620
    selected: boolean,
4,104✔
621
    theme: FullTheme,
4,104✔
622
    isHovered: boolean,
4,104✔
623
    posX: number | undefined,
4,104✔
624
    posY: number | undefined,
4,104✔
625
    hasSelectedCell: boolean,
4,104✔
626
    hoverAmount: number,
4,104✔
627
    spriteManager: SpriteManager,
4,104✔
628
    drawHeaderCallback: DrawHeaderCallback | undefined,
4,104✔
629
    touchMode: boolean
4,104✔
630
) {
4,104✔
631
    const isRtl = direction(c.title) === "rtl";
4,104✔
632
    const headerLayout = computeHeaderLayout(ctx, c, x, y, width, height, theme, isRtl);
4,104✔
633

4,104✔
634
    if (drawHeaderCallback !== undefined) {
4,104!
635
        drawHeaderCallback(
×
636
            {
×
637
                ctx,
×
638
                theme,
×
639
                rect: { x, y, width, height },
×
640
                column: c,
×
641
                columnIndex: c.sourceIndex,
×
642
                isSelected: selected,
×
643
                hoverAmount,
×
644
                isHovered,
×
645
                hasSelectedCell,
×
646
                spriteManager,
×
647
                menuBounds: headerLayout?.menuBounds ?? { x: 0, y: 0, height: 0, width: 0 },
×
648
                hoverX: posX,
×
649
                hoverY: posY,
×
650
            },
×
651
            () =>
×
652
                drawHeaderInner(
×
653
                    ctx,
×
654
                    x,
×
655
                    y,
×
656
                    width,
×
657
                    height,
×
658
                    c,
×
659
                    selected,
×
660
                    theme,
×
661
                    isHovered,
×
662
                    posX,
×
663
                    posY,
×
664
                    hoverAmount,
×
665
                    spriteManager,
×
666
                    touchMode,
×
UNCOV
667
                    isRtl,
×
UNCOV
668
                    headerLayout
×
UNCOV
669
                )
×
UNCOV
670
        );
×
671
    } else {
4,104✔
672
        drawHeaderInner(
4,104✔
673
            ctx,
4,104✔
674
            x,
4,104✔
675
            y,
4,104✔
676
            width,
4,104✔
677
            height,
4,104✔
678
            c,
4,104✔
679
            selected,
4,104✔
680
            theme,
4,104✔
681
            isHovered,
4,104✔
682
            posX,
4,104✔
683
            posY,
4,104✔
684
            hoverAmount,
4,104✔
685
            spriteManager,
4,104✔
686
            touchMode,
4,104✔
687
            isRtl,
4,104✔
688
            headerLayout
4,104✔
689
        );
4,104✔
690
    }
4,104✔
691
}
4,104✔
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