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

glideapps / glide-data-grid / 16143501631

08 Jul 2025 12:40PM UTC coverage: 89.936% (-1.2%) from 91.171%
16143501631

Pull #1059

github

web-flow
Merge 6ec6036d7 into 3041b6f44
Pull Request #1059: Freeze right columns

2968 of 3678 branches covered (80.7%)

610 of 707 new or added lines in 12 files covered. (86.28%)

210 existing lines in 7 files now uncovered.

18061 of 20082 relevant lines covered (89.94%)

4368.79 hits per line

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

15.06
/packages/core/src/internal/data-grid/render/data-grid-render.blit.ts
1
/* eslint-disable sonarjs/no-duplicate-string */
1✔
2
/* eslint-disable unicorn/no-for-loop */
1✔
3
import { deepEqual } from "../../../common/support.js";
1✔
4
import { type Rectangle } from "../data-grid-types.js";
1✔
5
import { getStickyWidth, type MappedGridColumn, getFreezeTrailingHeight } from "./data-grid-lib.js";
1✔
6
import { walkColumns } from "./data-grid-render.walk.js";
1✔
7
import type { DrawGridArg } from "./draw-grid-arg.js";
1✔
8

1✔
9
export interface BlitData {
1✔
10
    readonly cellXOffset: number;
1✔
11
    readonly cellYOffset: number;
1✔
12
    readonly translateX: number;
1✔
13
    readonly translateY: number;
1✔
14
    readonly mustDrawFocusOnHeader: boolean;
1✔
15
    readonly mustDrawHighlightRingsOnHeader: boolean;
1✔
16
    readonly lastBuffer: "a" | "b" | undefined;
1✔
17
    aBufferScroll: [boolean, boolean] | undefined;
1✔
18
    bBufferScroll: [boolean, boolean] | undefined;
1✔
19
}
1✔
20

1✔
21
export function blitLastFrame(
1✔
UNCOV
22
    ctx: CanvasRenderingContext2D,
×
UNCOV
23
    blitSource: HTMLCanvasElement,
×
UNCOV
24
    blitSourceScroll: [boolean, boolean] | undefined,
×
UNCOV
25
    targetScroll: [boolean, boolean] | undefined,
×
UNCOV
26
    last: BlitData,
×
UNCOV
27
    cellXOffset: number,
×
UNCOV
28
    cellYOffset: number,
×
UNCOV
29
    translateX: number,
×
UNCOV
30
    translateY: number,
×
UNCOV
31
    freezeTrailingRows: number,
×
UNCOV
32
    width: number,
×
UNCOV
33
    height: number,
×
UNCOV
34
    rows: number,
×
UNCOV
35
    totalHeaderHeight: number,
×
UNCOV
36
    dpr: number,
×
UNCOV
37
    mappedColumns: readonly MappedGridColumn[],
×
UNCOV
38
    effectiveCols: readonly MappedGridColumn[],
×
UNCOV
39
    getRowHeight: number | ((r: number) => number),
×
UNCOV
40
    doubleBuffer: boolean
×
UNCOV
41
): {
×
UNCOV
42
    regions: Rectangle[];
×
UNCOV
43
} {
×
UNCOV
44
    const drawRegions: Rectangle[] = [];
×
UNCOV
45

×
UNCOV
46
    ctx.imageSmoothingEnabled = false;
×
UNCOV
47
    const minY = Math.min(last.cellYOffset, cellYOffset);
×
UNCOV
48
    const maxY = Math.max(last.cellYOffset, cellYOffset);
×
UNCOV
49
    let deltaY = 0;
×
UNCOV
50
    if (typeof getRowHeight === "number") {
×
UNCOV
51
        deltaY += (maxY - minY) * getRowHeight;
×
UNCOV
52
    } else {
×
53
        for (let i = minY; i < maxY; i++) {
×
54
            deltaY += getRowHeight(i);
×
55
        }
×
56
    }
×
UNCOV
57
    if (cellYOffset > last.cellYOffset) {
×
UNCOV
58
        deltaY = -deltaY;
×
UNCOV
59
    }
×
UNCOV
60
    deltaY += translateY - last.translateY;
×
UNCOV
61

×
UNCOV
62
    const minX = Math.min(last.cellXOffset, cellXOffset);
×
UNCOV
63
    const maxX = Math.max(last.cellXOffset, cellXOffset);
×
UNCOV
64
    let deltaX = 0;
×
UNCOV
65
    for (let i = minX; i < maxX; i++) {
×
UNCOV
66
        deltaX += mappedColumns[i].width;
×
UNCOV
67
    }
×
UNCOV
68
    if (cellXOffset > last.cellXOffset) {
×
UNCOV
69
        deltaX = -deltaX;
×
UNCOV
70
    }
×
UNCOV
71
    deltaX += translateX - last.translateX;
×
UNCOV
72

×
NEW
73
    const [stickyLeftWidth, stickyRightWidth] = getStickyWidth(effectiveCols);
×
UNCOV
74

×
UNCOV
75
    if (deltaX !== 0 && deltaY !== 0) {
×
76
        return {
×
77
            regions: [],
×
78
        };
×
79
    }
×
UNCOV
80

×
UNCOV
81
    const freezeTrailingRowsHeight =
×
UNCOV
82
        freezeTrailingRows > 0 ? getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight) : 0;
×
UNCOV
83

×
NEW
84
    const blitWidth = width - stickyLeftWidth - Math.abs(deltaX) - stickyRightWidth;
×
UNCOV
85
    const blitHeight = height - totalHeaderHeight - freezeTrailingRowsHeight - Math.abs(deltaY) - 1;
×
UNCOV
86

×
UNCOV
87
    if (blitWidth > 150 && blitHeight > 150) {
×
UNCOV
88
        const args = {
×
UNCOV
89
            sx: 0,
×
UNCOV
90
            sy: 0,
×
UNCOV
91
            sw: width * dpr,
×
UNCOV
92
            sh: height * dpr,
×
UNCOV
93
            dx: 0,
×
UNCOV
94
            dy: 0,
×
UNCOV
95
            dw: width * dpr,
×
UNCOV
96
            dh: height * dpr,
×
UNCOV
97
        };
×
UNCOV
98

×
UNCOV
99
        // blit Y
×
UNCOV
100
        if (deltaY > 0) {
×
UNCOV
101
            // scrolling up
×
UNCOV
102
            args.sy = (totalHeaderHeight + 1) * dpr;
×
UNCOV
103
            args.sh = blitHeight * dpr;
×
UNCOV
104
            args.dy = (deltaY + totalHeaderHeight + 1) * dpr;
×
UNCOV
105
            args.dh = blitHeight * dpr;
×
UNCOV
106

×
UNCOV
107
            drawRegions.push({
×
UNCOV
108
                x: 0,
×
UNCOV
109
                y: totalHeaderHeight,
×
UNCOV
110
                width: width,
×
UNCOV
111
                height: deltaY + 1,
×
UNCOV
112
            });
×
UNCOV
113
        } else if (deltaY < 0) {
×
UNCOV
114
            // scrolling down
×
UNCOV
115
            args.sy = (-deltaY + totalHeaderHeight + 1) * dpr;
×
UNCOV
116
            args.sh = blitHeight * dpr;
×
UNCOV
117
            args.dy = (totalHeaderHeight + 1) * dpr;
×
UNCOV
118
            args.dh = blitHeight * dpr;
×
UNCOV
119

×
UNCOV
120
            drawRegions.push({
×
UNCOV
121
                x: 0,
×
UNCOV
122
                y: height + deltaY - freezeTrailingRowsHeight,
×
UNCOV
123
                width: width,
×
UNCOV
124
                height: -deltaY + freezeTrailingRowsHeight,
×
UNCOV
125
            });
×
UNCOV
126
        }
×
UNCOV
127

×
UNCOV
128
        // blit X
×
UNCOV
129
        if (deltaX > 0) {
×
UNCOV
130
            // pixels moving right
×
NEW
131
            args.sx = stickyLeftWidth * dpr;
×
UNCOV
132
            args.sw = blitWidth * dpr;
×
NEW
133
            args.dx = (deltaX + stickyLeftWidth) * dpr;
×
UNCOV
134
            args.dw = blitWidth * dpr;
×
UNCOV
135

×
UNCOV
136
            drawRegions.push({
×
NEW
137
                x: stickyLeftWidth - 1,
×
UNCOV
138
                y: 0,
×
UNCOV
139
                width: deltaX + 2, // extra width to account for first col not drawing a left side border
×
UNCOV
140
                height: height,
×
UNCOV
141
            });
×
UNCOV
142
        } else if (deltaX < 0) {
×
UNCOV
143
            // pixels moving left
×
NEW
144
            args.sx = (stickyLeftWidth - deltaX) * dpr;
×
UNCOV
145
            args.sw = blitWidth * dpr;
×
NEW
146
            args.dx = stickyLeftWidth * dpr;
×
UNCOV
147
            args.dw = blitWidth * dpr;
×
UNCOV
148

×
UNCOV
149
            drawRegions.push({
×
NEW
150
                x: width + deltaX - stickyRightWidth,
×
UNCOV
151
                y: 0,
×
NEW
152
                width: -deltaX + stickyRightWidth,
×
UNCOV
153
                height: height,
×
UNCOV
154
            });
×
UNCOV
155
        }
×
UNCOV
156

×
UNCOV
157
        ctx.setTransform(1, 0, 0, 1, 0, 0);
×
UNCOV
158
        if (doubleBuffer) {
×
159
            if (
×
NEW
160
                stickyLeftWidth > 0 &&
×
161
                deltaX !== 0 &&
×
162
                deltaY === 0 &&
×
163
                (targetScroll === undefined || blitSourceScroll?.[1] !== false)
×
164
            ) {
×
165
                // When double buffering the freeze columns can be offset by a couple pixels vertically between the two
×
166
                // buffers. We don't want to redraw them so we need to make sure to copy them between the buffers.
×
NEW
167
                const w = stickyLeftWidth * dpr;
×
168
                const h = height * dpr;
×
169
                ctx.drawImage(blitSource, 0, 0, w, h, 0, 0, w, h);
×
170
            }
×
NEW
171
            if (
×
NEW
172
                stickyRightWidth > 0 &&
×
NEW
173
                deltaX !== 0 &&
×
NEW
174
                deltaY === 0 &&
×
NEW
175
                (targetScroll === undefined || blitSourceScroll?.[1] !== false)
×
NEW
176
            ) {
×
NEW
177
                const x = (width - stickyRightWidth) * dpr;
×
NEW
178
                const w = stickyRightWidth * dpr;
×
NEW
179
                const h = height * dpr;
×
NEW
180
                ctx.drawImage(blitSource, x, 0, w, h, x, 0, w, h);
×
NEW
181
            }
×
182
            if (
×
183
                freezeTrailingRowsHeight > 0 &&
×
184
                deltaX === 0 &&
×
185
                deltaY !== 0 &&
×
186
                (targetScroll === undefined || blitSourceScroll?.[0] !== false)
×
187
            ) {
×
188
                const y = (height - freezeTrailingRowsHeight) * dpr;
×
189
                const w = width * dpr;
×
190
                const h = freezeTrailingRowsHeight * dpr;
×
191
                ctx.drawImage(blitSource, 0, y, w, h, 0, y, w, h);
×
192
            }
×
193
        }
×
UNCOV
194
        ctx.drawImage(blitSource, args.sx, args.sy, args.sw, args.sh, args.dx, args.dy, args.dw, args.dh);
×
UNCOV
195
        ctx.scale(dpr, dpr);
×
UNCOV
196
    }
×
UNCOV
197
    ctx.imageSmoothingEnabled = true;
×
UNCOV
198

×
UNCOV
199
    return {
×
UNCOV
200
        regions: drawRegions,
×
UNCOV
201
    };
×
UNCOV
202
}
×
203

1✔
204
export function blitResizedCol(
1✔
205
    last: BlitData,
×
206
    cellXOffset: number,
×
207
    cellYOffset: number,
×
208
    translateX: number,
×
209
    translateY: number,
×
210
    width: number,
×
211
    height: number,
×
212
    totalHeaderHeight: number,
×
213
    effectiveCols: readonly MappedGridColumn[],
×
NEW
214
    resizedIndex: number,
×
NEW
215
    freezeTrailingColumns: number
×
216
) {
×
217
    const drawRegions: Rectangle[] = [];
×
218

×
219
    // ctx.imageSmoothingEnabled = false;
×
220

×
221
    if (
×
222
        cellXOffset !== last.cellXOffset ||
×
223
        cellYOffset !== last.cellYOffset ||
×
224
        translateX !== last.translateX ||
×
225
        translateY !== last.translateY
×
226
    ) {
×
227
        return drawRegions;
×
228
    }
×
229

×
NEW
230
    walkColumns(
×
NEW
231
        effectiveCols,
×
NEW
232
        width,
×
NEW
233
        cellYOffset,
×
NEW
234
        translateX,
×
NEW
235
        translateY,
×
NEW
236
        totalHeaderHeight,
×
NEW
237
        freezeTrailingColumns,
×
NEW
238
        (c, drawX, _drawY, clipX) => {
×
NEW
239
            if (c.sourceIndex === resizedIndex) {
×
NEW
240
                const x = Math.max(drawX, clipX) + 1;
×
NEW
241
                drawRegions.push({
×
NEW
242
                    x,
×
NEW
243
                    y: 0,
×
NEW
244
                    width: width - x,
×
NEW
245
                    height,
×
NEW
246
                });
×
NEW
247
                return true;
×
NEW
248
            }
×
249
        }
×
NEW
250
    );
×
251
    return drawRegions;
×
252
}
×
253

1✔
254
export function computeCanBlit(current: DrawGridArg, last: DrawGridArg | undefined): boolean | number {
1✔
255
    if (last === undefined) return false;
670✔
256
    if (
598✔
257
        current.width !== last.width ||
598✔
258
        current.height !== last.height ||
448✔
259
        current.theme !== last.theme ||
448✔
260
        current.headerHeight !== last.headerHeight ||
448✔
261
        current.rowHeight !== last.rowHeight ||
448✔
262
        current.rows !== last.rows ||
448✔
263
        current.freezeColumns !== last.freezeColumns ||
446✔
264
        current.getRowThemeOverride !== last.getRowThemeOverride ||
18✔
265
        current.isFocused !== last.isFocused ||
18✔
266
        current.isResizing !== last.isResizing ||
18✔
267
        current.verticalBorder !== last.verticalBorder ||
8✔
268
        current.getCellContent !== last.getCellContent ||
8✔
269
        current.highlightRegions !== last.highlightRegions ||
7✔
270
        current.selection !== last.selection ||
7✔
271
        current.dragAndDropState !== last.dragAndDropState ||
7✔
272
        current.prelightCells !== last.prelightCells ||
7✔
273
        current.touchMode !== last.touchMode ||
4!
UNCOV
274
        current.maxScaleFactor !== last.maxScaleFactor
×
275
    ) {
670✔
276
        return false;
598✔
277
    }
598!
UNCOV
278
    if (current.mappedColumns !== last.mappedColumns) {
×
279
        if (current.mappedColumns.length > 100 || current.mappedColumns.length !== last.mappedColumns.length) {
×
280
            // The array is big, let's just redraw the damned thing rather than check these all. Or the number of cols
×
281
            // changed in which case I dont want to figure out what happened.
×
282
            return false;
×
283
        }
×
284
        // We want to know if only one column has resized. If this is the case we can do a special left/right sliding
×
285
        // blit. Or just not redraw shit on the left.
×
286
        let resized: number | undefined;
×
287
        for (let i = 0; i < current.mappedColumns.length; i++) {
×
288
            const curCol = current.mappedColumns[i];
×
289
            const lastCol = last.mappedColumns[i];
×
290

×
291
            if (deepEqual(curCol, lastCol)) continue;
×
292

×
293
            // two columns changed, abort
×
294
            if (resized !== undefined) return false;
×
295

×
296
            if (curCol.width === lastCol.width) return false;
×
297

×
298
            const { width, ...curRest } = curCol;
×
299
            const { width: lastWidth, ...lastRest } = lastCol;
×
300

×
301
            // more than width changed, abort
×
302
            if (!deepEqual(curRest, lastRest)) return false;
×
303
            resized = i;
×
304
        }
×
305
        if (resized === undefined) {
×
306
            // we never found a changed column, cool, we can blit
×
307
            return true;
×
308
        }
×
309
        return resized;
×
310
    }
×
UNCOV
311
    return true;
×
UNCOV
312
}
×
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