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

glideapps / glide-data-grid / 16380845235

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

Pull #1070

github

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

2955 of 3670 branches covered (80.52%)

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

489 existing lines in 5 files now uncovered.

18110 of 19904 relevant lines covered (90.99%)

3096.01 hits per line

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

86.1
/packages/core/src/internal/data-grid/render/data-grid-render.lines.ts
1
/* eslint-disable sonarjs/no-duplicate-string */
1✔
2
/* eslint-disable unicorn/no-for-loop */
1✔
3
import { type Rectangle, CompactSelection } from "../data-grid-types.js";
1✔
4
import { CellSet } from "../cell-set.js";
1✔
5
import groupBy from "lodash/groupBy.js";
1✔
6
import { getStickyWidth, type MappedGridColumn, getFreezeTrailingHeight } from "./data-grid-lib.js";
1✔
7
import { mergeAndRealizeTheme, type FullTheme } from "../../../common/styles.js";
1✔
8
import { blendCache } from "../color-parser.js";
1✔
9
import { intersectRect } from "../../../common/math.js";
1✔
10
import { getSkipPoint, walkColumns, walkRowsInCol } from "./data-grid-render.walk.js";
1✔
11
import { type GetRowThemeCallback } from "./data-grid-render.cells.js";
1✔
12

1✔
13
export function drawBlanks(
1✔
14
    ctx: CanvasRenderingContext2D,
421✔
15
    effectiveColumns: readonly MappedGridColumn[],
421✔
16
    allColumns: readonly MappedGridColumn[],
421✔
17
    width: number,
421✔
18
    height: number,
421✔
19
    totalHeaderHeight: number,
421✔
20
    translateX: number,
421✔
21
    translateY: number,
421✔
22
    cellYOffset: number,
421✔
23
    rows: number,
421✔
24
    getRowHeight: (row: number) => number,
421✔
25
    getRowTheme: GetRowThemeCallback | undefined,
421✔
26
    selectedRows: CompactSelection,
421✔
27
    disabledRows: CompactSelection,
421✔
28
    freezeTrailingRows: number,
421✔
29
    hasAppendRow: boolean,
421✔
30
    drawRegions: readonly Rectangle[],
421✔
31
    damage: CellSet | undefined,
421✔
32
    theme: FullTheme
421✔
33
): void {
421✔
34
    if (
421✔
35
        damage !== undefined ||
421✔
36
        effectiveColumns[effectiveColumns.length - 1] !== allColumns[effectiveColumns.length - 1]
421✔
37
    )
421✔
38
        return;
421✔
39

418✔
40
    const skipPoint = getSkipPoint(drawRegions);
418✔
41

418✔
42
    walkColumns(
418✔
43
        effectiveColumns,
418✔
44
        cellYOffset,
418✔
45
        translateX,
418✔
46
        translateY,
418✔
47
        totalHeaderHeight,
418✔
48
        (c, drawX, colDrawY, clipX, startRow) => {
418✔
49
            if (c !== effectiveColumns[effectiveColumns.length - 1]) return;
4,098✔
50
            drawX += c.width;
418✔
51
            const x = Math.max(drawX, clipX);
418✔
52
            if (x > width) return;
418✔
53
            ctx.save();
21✔
54
            ctx.beginPath();
21✔
55
            ctx.rect(x, totalHeaderHeight + 1, 10_000, height - totalHeaderHeight - 1);
21✔
56
            ctx.clip();
21✔
57

21✔
58
            walkRowsInCol(
21✔
59
                startRow,
21✔
60
                colDrawY,
21✔
61
                height,
21✔
62
                rows,
21✔
63
                getRowHeight,
21✔
64
                freezeTrailingRows,
21✔
65
                hasAppendRow,
21✔
66
                skipPoint,
21✔
67
                (drawY, row, rh, isSticky) => {
21✔
68
                    if (
657✔
69
                        !isSticky &&
657✔
70
                        drawRegions.length > 0 &&
647!
71
                        !drawRegions.some(dr =>
×
72
                            intersectRect(drawX, drawY, 10_000, rh, dr.x, dr.y, dr.width, dr.height)
×
73
                        )
×
74
                    ) {
657!
75
                        return;
×
76
                    }
×
77

657✔
78
                    const rowSelected = selectedRows.hasIndex(row);
657✔
79
                    const rowDisabled = disabledRows.hasIndex(row);
657✔
80

657✔
81
                    ctx.beginPath();
657✔
82

657✔
83
                    const rowTheme = getRowTheme?.(row);
657!
84

657✔
85
                    const blankTheme = rowTheme === undefined ? theme : mergeAndRealizeTheme(theme, rowTheme);
657!
86

657✔
87
                    if (blankTheme.bgCell !== theme.bgCell) {
657!
88
                        ctx.fillStyle = blankTheme.bgCell;
×
89
                        ctx.fillRect(drawX, drawY, 10_000, rh);
×
90
                    }
×
91
                    if (rowDisabled) {
657✔
92
                        ctx.fillStyle = blankTheme.bgHeader;
10✔
93
                        ctx.fillRect(drawX, drawY, 10_000, rh);
10✔
94
                    }
10✔
95
                    if (rowSelected) {
657!
96
                        ctx.fillStyle = blankTheme.accentLight;
×
97
                        ctx.fillRect(drawX, drawY, 10_000, rh);
×
98
                    }
×
99
                }
657✔
100
            );
21✔
101

21✔
102
            ctx.restore();
21✔
103
        }
4,098✔
104
    );
418✔
105
}
418✔
106

1✔
107
export function overdrawStickyBoundaries(
1✔
108
    ctx: CanvasRenderingContext2D,
421✔
109
    effectiveCols: readonly MappedGridColumn[],
421✔
110
    width: number,
421✔
111
    height: number,
421✔
112
    freezeTrailingRows: number,
421✔
113
    rows: number,
421✔
114
    verticalBorder: (col: number) => boolean,
421✔
115
    getRowHeight: (row: number) => number,
421✔
116
    theme: FullTheme
421✔
117
) {
421✔
118
    let drawFreezeBorder = false;
421✔
119
    for (const c of effectiveCols) {
421✔
120
        if (c.sticky) continue;
490✔
121
        drawFreezeBorder = verticalBorder(c.sourceIndex);
421✔
122
        break;
421✔
123
    }
421✔
124
    const hColor = theme.horizontalBorderColor ?? theme.borderColor;
421✔
125
    const vColor = theme.borderColor;
421✔
126
    const drawX = drawFreezeBorder ? getStickyWidth(effectiveCols) : 0;
421!
127

421✔
128
    let vStroke: string | undefined;
421✔
129
    if (drawX !== 0) {
421✔
130
        vStroke = blendCache(vColor, theme.bgCell);
67✔
131
        ctx.beginPath();
67✔
132
        ctx.moveTo(drawX + 0.5, 0);
67✔
133
        ctx.lineTo(drawX + 0.5, height);
67✔
134
        ctx.strokeStyle = vStroke;
67✔
135
        ctx.stroke();
67✔
136
    }
67✔
137

421✔
138
    if (freezeTrailingRows > 0) {
421✔
139
        const hStroke = vColor === hColor && vStroke !== undefined ? vStroke : blendCache(hColor, theme.bgCell);
403✔
140
        const h = getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight);
403✔
141
        ctx.beginPath();
403✔
142
        ctx.moveTo(0, height - h + 0.5);
403✔
143
        ctx.lineTo(width, height - h + 0.5);
403✔
144
        ctx.strokeStyle = hStroke;
403✔
145
        ctx.stroke();
403✔
146
    }
403✔
147
}
421✔
148

1✔
149
const getMinMaxXY = (drawRegions: Rectangle[] | undefined, width: number, height: number) => {
1✔
150
    let minX = 0;
1,287✔
151
    let maxX = width;
1,287✔
152
    let minY = 0;
1,287✔
153
    let maxY = height;
1,287✔
154

1,287✔
155
    if (drawRegions !== undefined && drawRegions.length > 0) {
1,287✔
156
        minX = Number.MAX_SAFE_INTEGER;
8✔
157
        minY = Number.MAX_SAFE_INTEGER;
8✔
158
        maxX = Number.MIN_SAFE_INTEGER;
8✔
159
        maxY = Number.MIN_SAFE_INTEGER;
8✔
160
        for (const r of drawRegions) {
8✔
161
            minX = Math.min(minX, r.x - 1);
8✔
162
            maxX = Math.max(maxX, r.x + r.width + 1);
8✔
163
            minY = Math.min(minY, r.y - 1);
8✔
164
            maxY = Math.max(maxY, r.y + r.height + 1);
8✔
165
        }
8✔
166
    }
8✔
167

1,287✔
168
    return { minX, maxX, minY, maxY };
1,287✔
169
};
1,287✔
170

1✔
171
export function drawExtraRowThemes(
1✔
172
    ctx: CanvasRenderingContext2D,
421✔
173
    effectiveCols: readonly MappedGridColumn[],
421✔
174
    cellYOffset: number,
421✔
175
    translateX: number,
421✔
176
    translateY: number,
421✔
177
    width: number,
421✔
178
    height: number,
421✔
179
    drawRegions: Rectangle[] | undefined,
421✔
180
    totalHeaderHeight: number,
421✔
181
    getRowHeight: (row: number) => number,
421✔
182
    getRowThemeOverride: GetRowThemeCallback | undefined,
421✔
183
    verticalBorder: (col: number) => boolean,
421✔
184
    freezeTrailingRows: number,
421✔
185
    rows: number,
421✔
186
    theme: FullTheme
421✔
187
) {
421✔
188
    const bgCell = theme.bgCell;
421✔
189

421✔
190
    const { minX, maxX, minY, maxY } = getMinMaxXY(drawRegions, width, height);
421✔
191

421✔
192
    const toDraw: { x: number; y: number; w: number; h: number; color: string }[] = [];
421✔
193

421✔
194
    const freezeY = height - getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight);
421✔
195

421✔
196
    // row overflow
421✔
197
    let y = totalHeaderHeight;
421✔
198
    let row = cellYOffset;
421✔
199
    let extraRowsStartY = 0;
421✔
200
    while (y + translateY < freezeY) {
421✔
201
        const ty = y + translateY;
12,615✔
202
        const rh = getRowHeight(row);
12,615✔
203
        if (ty >= minY && ty <= maxY - 1) {
12,615✔
204
            const rowTheme = getRowThemeOverride?.(row);
12,560!
205
            const rowThemeBgCell = rowTheme?.bgCell;
12,560!
206
            const needDraw =
12,560✔
207
                rowThemeBgCell !== undefined && rowThemeBgCell !== bgCell && row >= rows - freezeTrailingRows;
12,560!
208
            if (needDraw) {
12,560!
209
                toDraw.push({
×
210
                    x: minX,
×
211
                    y: ty,
×
212
                    w: maxX - minX,
×
213
                    h: rh,
×
214
                    color: rowThemeBgCell,
×
215
                });
×
216
            }
×
217
        }
12,560✔
218

12,615✔
219
        y += rh;
12,615✔
220
        if (row < rows - freezeTrailingRows) extraRowsStartY = y;
12,615✔
221
        row++;
12,615✔
222
    }
12,615✔
223

421✔
224
    // column overflow
421✔
225
    let x = 0;
421✔
226
    const h = Math.min(freezeY, maxY) - extraRowsStartY;
421✔
227
    if (h > 0) {
421✔
228
        for (let index = 0; index < effectiveCols.length; index++) {
18✔
229
            const c = effectiveCols[index];
180✔
230
            if (c.width === 0) continue;
180!
231
            const tx = c.sticky ? x : x + translateX;
180!
232
            const colThemeBgCell = c.themeOverride?.bgCell;
180!
233
            if (
180✔
234
                colThemeBgCell !== undefined &&
180!
235
                colThemeBgCell !== bgCell &&
×
236
                tx >= minX &&
×
237
                tx <= maxX &&
×
238
                verticalBorder(index + 1)
×
239
            ) {
180!
240
                toDraw.push({
×
241
                    x: tx,
×
242
                    y: extraRowsStartY,
×
243
                    w: c.width,
×
244
                    h,
×
245
                    color: colThemeBgCell,
×
246
                });
×
247
            }
×
248

180✔
249
            x += c.width;
180✔
250
        }
180✔
251
    }
18✔
252

421✔
253
    if (toDraw.length === 0) return;
421!
254

×
255
    let color: string | undefined;
×
256
    ctx.beginPath();
×
257
    // render in reverse order because we computed and added the columns last, but they should actually be lower
×
258
    // priority than the rows.
×
259
    for (let i = toDraw.length - 1; i >= 0; i--) {
×
260
        const r = toDraw[i];
×
261
        if (color === undefined) {
×
262
            color = r.color;
×
263
        } else if (r.color !== color) {
×
264
            ctx.fillStyle = color;
×
265
            ctx.fill();
×
266
            ctx.beginPath();
×
267
            color = r.color;
×
268
        }
×
269
        ctx.rect(r.x, r.y, r.w, r.h);
×
270
    }
×
271
    if (color !== undefined) {
×
272
        ctx.fillStyle = color;
×
273
        ctx.fill();
×
274
    }
×
275
    ctx.beginPath();
×
276
}
×
277

1✔
278
// lines are effectively drawn on the top left edge of a cell.
1✔
279
export function drawGridLines(
1✔
280
    ctx: CanvasRenderingContext2D,
866✔
281
    effectiveCols: readonly MappedGridColumn[],
866✔
282
    cellYOffset: number,
866✔
283
    translateX: number,
866✔
284
    translateY: number,
866✔
285
    width: number,
866✔
286
    height: number,
866✔
287
    drawRegions: Rectangle[] | undefined,
866✔
288
    spans: Rectangle[] | undefined,
866✔
289
    groupHeaderHeight: number,
866✔
290
    totalHeaderHeight: number,
866✔
291
    getRowHeight: (row: number) => number,
866✔
292
    getRowThemeOverride: GetRowThemeCallback | undefined,
866✔
293
    verticalBorder: (col: number) => boolean,
866✔
294
    freezeTrailingRows: number,
866✔
295
    rows: number,
866✔
296
    theme: FullTheme,
866✔
297
    verticalOnly: boolean = false,
866✔
298
    selectedColumns?: CompactSelection,
866✔
299
    accentInner: boolean = false,
866✔
300
    selectedRows?: CompactSelection
866✔
301
) {
866✔
302
    if (spans !== undefined) {
866✔
303
        ctx.beginPath();
4✔
304
        ctx.save();
4✔
305
        ctx.rect(0, 0, width, height);
4✔
306
        for (const span of spans) {
4✔
307
            ctx.rect(span.x + 1, span.y + 1, span.width - 1, span.height - 1);
4✔
308
        }
4✔
309
        ctx.clip("evenodd");
4✔
310
    }
4✔
311
    const hColor = theme.horizontalBorderColor ?? theme.borderColor;
866✔
312
    const vColor = theme.borderColor;
866✔
313
    const selectedVColor = theme.accentColor;
866✔
314
    const selectedHColor = theme.accentColor;
866✔
315

866✔
316
    const { minX, maxX, minY, maxY } = getMinMaxXY(drawRegions, width, height);
866✔
317

866✔
318
    const toDraw: { x1: number; y1: number; x2: number; y2: number; color: string }[] = [];
866✔
319

866✔
320
    ctx.beginPath();
866✔
321

866✔
322
    // vertical lines
866✔
323
    let x = 0.5;
866✔
324
    for (let index = 0; index < effectiveCols.length; index++) {
866✔
325
        const c = effectiveCols[index];
8,482✔
326
        if (c.width === 0) continue;
8,482!
327
        x += c.width;
8,482✔
328
        const tx = c.sticky ? x : x + translateX;
8,482✔
329
        if (tx >= minX && tx <= maxX && verticalBorder(index + 1)) {
8,482✔
330
            const leftSelected = selectedColumns?.hasIndex(c.sourceIndex) ?? false;
7,647!
331
            const rightSelected =
7,647✔
332
                index < effectiveCols.length - 1
7,647✔
333
                    ? selectedColumns?.hasIndex(effectiveCols[index + 1].sourceIndex) ?? false
7,600!
334
                    : false;
47✔
335
            console.log("leftSelected", {leftSelected, rightSelected, selectedColumns});
7,647✔
336
            const color = accentInner
7,647!
NEW
UNCOV
337
                ? leftSelected || rightSelected
×
NEW
UNCOV
338
                    ? selectedVColor
×
NEW
UNCOV
339
                    : vColor
×
340
                : leftSelected !== rightSelected
7,647✔
341
                ? selectedVColor
97✔
342
                : vColor;
7,550✔
343
            toDraw.push({
7,647✔
344
                x1: tx,
7,647✔
345
                y1: Math.max(groupHeaderHeight, minY),
7,647✔
346
                x2: tx,
7,647✔
347
                y2: Math.min(height, maxY),
7,647✔
348
                color,
7,647✔
349
            });
7,647✔
350
        }
7,647✔
351
    }
8,482✔
352

866✔
353
    let freezeY = height + 0.5;
866✔
354
    for (let i = rows - freezeTrailingRows; i < rows; i++) {
866✔
355
        const rh = getRowHeight(i);
834✔
356
        freezeY -= rh;
834✔
357
        toDraw.push({ x1: minX, y1: freezeY, x2: maxX, y2: freezeY, color: hColor });
834✔
358
    }
834✔
359

866✔
360
    if (verticalOnly !== true) {
866✔
361
        // horizontal lines
421✔
362
        let y = totalHeaderHeight + 0.5;
421✔
363
        let row = cellYOffset;
421✔
364
        const target = freezeY;
421✔
365
        while (y + translateY < target) {
421✔
366
            const ty = y + translateY;
12,615✔
367
            if (ty >= minY && ty <= maxY - 1) {
12,615✔
368
                const rowTheme = getRowThemeOverride?.(row);
12,560!
369

12,560✔
370
                let color = rowTheme?.horizontalBorderColor ?? rowTheme?.borderColor ?? hColor;
12,560!
371

12,560✔
372
                if (selectedRows !== undefined) {
12,560✔
373
                    const currentSelected = selectedRows.hasIndex(row);
12,560✔
374
                    const prevSelected = row > 0 ? selectedRows.hasIndex(row - 1) : false;
12,560✔
375

12,560✔
376
                    // Accent the line if it is a boundary between selected and unselected rows.
12,560✔
377
                    if (currentSelected !== prevSelected && (currentSelected || prevSelected)) {
12,560✔
378
                        color = selectedHColor;
46✔
379
                    }
46✔
380
                }
12,560✔
381

12,560✔
382
                toDraw.push({
12,560✔
383
                    x1: minX,
12,560✔
384
                    y1: ty,
12,560✔
385
                    x2: maxX,
12,560✔
386
                    y2: ty,
12,560✔
387
                    color,
12,560✔
388
                });
12,560✔
389
            }
12,560✔
390

12,615✔
391
            y += getRowHeight(row);
12,615✔
392
            row++;
12,615✔
393
        }
12,615✔
394
    }
421✔
395

866✔
396
    const groups = groupBy(toDraw, line => line.color);
866✔
397
    for (const g of Object.keys(groups)) {
866✔
398
        ctx.strokeStyle = g;
942✔
399
        for (const line of groups[g]) {
942✔
400
            ctx.moveTo(line.x1, line.y1);
21,041✔
401
            ctx.lineTo(line.x2, line.y2);
21,041✔
402
        }
21,041✔
403
        ctx.stroke();
942✔
404
        ctx.beginPath();
942✔
405
    }
942✔
406

866✔
407
    if (spans !== undefined) {
866✔
408
        ctx.restore();
4✔
409
    }
4✔
410
}
866✔
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