diff --git a/packages/core/src/docs/examples/row-grouping.stories.tsx b/packages/core/src/docs/examples/row-grouping.stories.tsx index 5d048d1c3..497fe7b03 100644 --- a/packages/core/src/docs/examples/row-grouping.stories.tsx +++ b/packages/core/src/docs/examples/row-grouping.stories.tsx @@ -137,6 +137,7 @@ export const RowGrouping: React.VFC = (p: { freezeColumns: number }) => { columns={cols} // verticalBorder={false} rows={rows} + smoothScrollY={true} /> ); }; diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.blit.ts b/packages/core/src/internal/data-grid/render/data-grid-render.blit.ts index 4f5600837..19175d0a5 100644 --- a/packages/core/src/internal/data-grid/render/data-grid-render.blit.ts +++ b/packages/core/src/internal/data-grid/render/data-grid-render.blit.ts @@ -37,7 +37,8 @@ export function blitLastFrame( mappedColumns: readonly MappedGridColumn[], effectiveCols: readonly MappedGridColumn[], getRowHeight: number | ((r: number) => number), - doubleBuffer: boolean + doubleBuffer: boolean, + stickyTopHeight: number ): { regions: Rectangle[]; } { @@ -81,6 +82,8 @@ export function blitLastFrame( const freezeTrailingRowsHeight = freezeTrailingRows > 0 ? getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight) : 0; + totalHeaderHeight += stickyTopHeight; + const blitWidth = width - stickyWidth - Math.abs(deltaX); const blitHeight = height - totalHeaderHeight - freezeTrailingRowsHeight - Math.abs(deltaY) - 1; @@ -200,7 +203,8 @@ export function blitResizedCol( height: number, totalHeaderHeight: number, effectiveCols: readonly MappedGridColumn[], - resizedIndex: number + resizedIndex: number, + stickyTopHeight: number ) { const drawRegions: Rectangle[] = []; @@ -215,6 +219,8 @@ export function blitResizedCol( return drawRegions; } + totalHeaderHeight += stickyTopHeight; + walkColumns(effectiveCols, cellYOffset, translateX, translateY, totalHeaderHeight, (c, drawX, _drawY, clipX) => { if (c.sourceIndex === resizedIndex) { const x = Math.max(drawX, clipX) + 1; diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.cells.ts b/packages/core/src/internal/data-grid/render/data-grid-render.cells.ts index dc84326c2..ab5a6dc26 100644 --- a/packages/core/src/internal/data-grid/render/data-grid-render.cells.ts +++ b/packages/core/src/internal/data-grid/render/data-grid-render.cells.ts @@ -107,7 +107,9 @@ export function drawCells( renderStateProvider: RenderStateProvider, getCellRenderer: GetCellRendererCallback, overrideCursor: (cursor: React.CSSProperties["cursor"]) => void, - minimumCellWidth: number + minimumCellWidth: number, + stickyRows?: number[], + stickyRegionHeight?: number ): Rectangle[] | undefined { let toDraw = damage?.size ?? Number.MAX_SAFE_INTEGER; const frameTime = performance.now(); @@ -369,7 +371,7 @@ export function drawCells( // because technically the bottom right corner of the outline are on other cells. ctx.fillRect( cellX + 1, - drawY + 1, + drawY + 1 + (stickyRegionHeight ?? 0), cellWidth - (isLastColumn ? 2 : 1), rh - (isLastRow ? 2 : 1) ); @@ -445,7 +447,9 @@ export function drawCells( } return toDraw <= 0; - } + }, + stickyRows, + totalHeaderHeight ); ctx.restore(); diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.lines.ts b/packages/core/src/internal/data-grid/render/data-grid-render.lines.ts index cd4b4cf8d..bb9ea9343 100644 --- a/packages/core/src/internal/data-grid/render/data-grid-render.lines.ts +++ b/packages/core/src/internal/data-grid/render/data-grid-render.lines.ts @@ -29,7 +29,8 @@ export function drawBlanks( hasAppendRow: boolean, drawRegions: readonly Rectangle[], damage: CellSet | undefined, - theme: FullTheme + theme: FullTheme, + stickyRows?: number[] ): void { if ( damage !== undefined || @@ -96,7 +97,8 @@ export function drawBlanks( ctx.fillStyle = blankTheme.accentLight; ctx.fillRect(drawX, drawY, 10_000, rh); } - } + }, + stickyRows ); ctx.restore(); @@ -144,6 +146,15 @@ export function overdrawStickyBoundaries( ctx.strokeStyle = hStroke; ctx.stroke(); } + + // stikcy rows + // const hStroke = vColor === hColor && vStroke !== undefined ? vStroke : blendCache(hColor, theme.bgCell); + // const h = getRowHeight(0); + // ctx.beginPath(); + // ctx.moveTo(0, h + 0.5); + // ctx.lineTo(width, h + 0.5); + // ctx.strokeStyle = hStroke; + // ctx.stroke(); } const getMinMaxXY = (drawRegions: Rectangle[] | undefined, width: number, height: number) => { @@ -294,7 +305,8 @@ export function drawGridLines( freezeTrailingRows: number, rows: number, theme: FullTheme, - verticalOnly: boolean = false + verticalOnly: boolean = false, + stickyTopHeight: number = 0 ) { if (spans !== undefined) { ctx.beginPath(); @@ -308,7 +320,7 @@ export function drawGridLines( const hColor = theme.horizontalBorderColor ?? theme.borderColor; const vColor = theme.borderColor; - const { minX, maxX, minY, maxY } = getMinMaxXY(drawRegions, width, height); + const { minX, maxX, maxY, minY } = getMinMaxXY(drawRegions, width, height); const toDraw: { x1: number; y1: number; x2: number; y2: number; color: string }[] = []; @@ -346,7 +358,7 @@ export function drawGridLines( const target = freezeY; while (y + translateY < target) { const ty = y + translateY; - if (ty >= minY && ty <= maxY - 1) { + if (ty >= minY && ty <= maxY - 1 && ty >= totalHeaderHeight + stickyTopHeight) { const rowTheme = getRowThemeOverride?.(row); toDraw.push({ x1: minX, diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.ts b/packages/core/src/internal/data-grid/render/data-grid-render.ts index 4946b1557..ffada6748 100644 --- a/packages/core/src/internal/data-grid/render/data-grid-render.ts +++ b/packages/core/src/internal/data-grid/render/data-grid-render.ts @@ -12,6 +12,7 @@ import { drawGridHeaders } from "./data-grid-render.header.js"; import { drawGridLines, overdrawStickyBoundaries, drawBlanks, drawExtraRowThemes } from "./data-grid-render.lines.js"; import { blitLastFrame, blitResizedCol, computeCanBlit } from "./data-grid-render.blit.js"; import { drawHighlightRings, drawFillHandle, drawColumnResizeOutline } from "./data-grid.render.rings.js"; +import { findLastIndex } from "lodash"; // Future optimization opportunities // - Create a cache of a buffer used to render the full view of a partially displayed column so that when @@ -255,6 +256,34 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { const effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, dragAndDropState, translateX); + const stickyRows = [0, 10, 30]; + + let stickyRegionHeight = 0; + const startRow = cellYOffset; + const drawY = totalHeaderHeight + translateY; + // find the sticky row that fits + const stickyRowIndex = findLastIndex(stickyRows, r => startRow >= r); + if (stickyRowIndex !== -1) { + const stickyRow = stickyRows[stickyRowIndex]; + const stickyRowHeight = getRowHeight(stickyRow); + const stickyRowBottom = stickyRowHeight; + + const nextStickyRow = stickyRows[stickyRowIndex + 1]; + if (nextStickyRow === undefined) { + stickyRegionHeight = stickyRowHeight; + } else { + const startRowHeight = getRowHeight(startRow); + const startRowVisibleHeight = startRowHeight + (drawY - (totalHeaderHeight ?? 0)); + + let nextStickyRowTop = startRowVisibleHeight; + for (let i = startRow + 1; i < nextStickyRow; i++) { + nextStickyRowTop += getRowHeight(i); + } + + stickyRegionHeight = nextStickyRowTop < stickyRowBottom ? nextStickyRowTop : stickyRowHeight; + } + } + let drawRegions: Rectangle[] = []; const mustDrawFocusOnHeader = drawFocus && selection.current?.cell[1] === cellYOffset && translateY === 0; @@ -337,7 +366,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { freezeTrailingRows, rows, highlightRegions, - theme + theme, + stickyRegionHeight ); } @@ -437,7 +467,9 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { renderStateProvider, getCellRenderer, overrideCursor, - minimumCellWidth + minimumCellWidth, + stickyRows, + stickyRegionHeight ); const selectionCurrent = selection.current; @@ -534,7 +566,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { mappedColumns, effectiveCols, rowHeight, - doubleBuffer + doubleBuffer, + stickyRegionHeight ); drawRegions = regions; } else if (canBlit !== false) { @@ -550,10 +583,24 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { height, totalHeaderHeight, effectiveCols, - resizedCol + resizedCol, + stickyRegionHeight ); } + if (drawRegions.length > 0) { + drawRegions.push({ x: 0, y: totalHeaderHeight * dpr, width: width * dpr, height: 55 * dpr }); + } + + drawRegions = [ + { + x: 0, + y: 0, + width: width, + height: height, + }, + ]; + overdrawStickyBoundaries( targetCtx, effectiveCols, @@ -582,7 +629,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { freezeTrailingRows, rows, highlightRegions, - theme + theme, + stickyRegionHeight ); // the overdraw may have nuked out our focus ring right edge. @@ -656,7 +704,9 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { renderStateProvider, getCellRenderer, overrideCursor, - minimumCellWidth + minimumCellWidth, + stickyRows, + stickyRegionHeight ); drawBlanks( @@ -678,7 +728,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { hasAppendRow, drawRegions, damage, - theme + theme, + stickyRows ); drawExtraRowThemes( @@ -716,7 +767,9 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { verticalBorder, freezeTrailingRows, rows, - theme + theme, + false, + stickyRegionHeight ); highlightRedraw?.(); diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.walk.ts b/packages/core/src/internal/data-grid/render/data-grid-render.walk.ts index dbb3c8867..fa82731ed 100644 --- a/packages/core/src/internal/data-grid/render/data-grid-render.walk.ts +++ b/packages/core/src/internal/data-grid/render/data-grid-render.walk.ts @@ -1,3 +1,4 @@ +import { findLastIndex } from "lodash"; import { type Item, type Rectangle } from "../data-grid-types.js"; import { type MappedGridColumn, isGroupEqual } from "./data-grid-lib.js"; @@ -26,7 +27,9 @@ export function walkRowsInCol( freezeTrailingRows: number, hasAppendRow: boolean, skipToY: number | undefined, - cb: WalkRowsCallback + cb: WalkRowsCallback, + stickyRows?: number[], + totalHeaderHeight?: number ): void { skipToY = skipToY ?? drawY; let y = drawY; @@ -52,6 +55,56 @@ export function walkRowsInCol( y -= rh; cb(y, row, rh, true, hasAppendRow && row === rows - 1); } + + y = 0; // with start row it is possible to identify the sticky rows + if (stickyRows) { + // find the sticky row that fits + const stickyRowIndex = findLastIndex(stickyRows, r => startRow >= r); + if (stickyRowIndex !== -1) { + const stickyRow = stickyRows[stickyRowIndex]; + const stickyRowHeight = getRowHeight(stickyRow); + const stickyRowBottom = stickyRowHeight; + + const nextStickyRow = stickyRows[stickyRowIndex + 1]; + if (nextStickyRow === undefined) { + cb(totalHeaderHeight ?? drawY, stickyRow, stickyRowHeight, true, false); + } else { + const nextStickyRowHeight = getRowHeight(nextStickyRow); + + const startRowHeight = getRowHeight(startRow); + const startRowVisibleHeight = startRowHeight + (drawY - (totalHeaderHeight ?? 0)); + + let nextStickyRowTop = startRowVisibleHeight; + for (let i = startRow + 1; i < nextStickyRow; i++) { + nextStickyRowTop += getRowHeight(i); + } + + if (nextStickyRowTop < stickyRowBottom) { + let offsetY = totalHeaderHeight ?? drawY; + offsetY -= stickyRowBottom - nextStickyRowTop; + cb(offsetY, stickyRow, stickyRowHeight, true, false); + // offsetY += stickyRowHeight; + // cb(offsetY, nextStickyRow, nextStickyRowHeight, true, false); + } else { + cb(totalHeaderHeight ?? drawY, stickyRow, stickyRowHeight, true, false); + } + + // console.log("stickyRows", { + // startRowHeight, + // startRowVisibleHeight, + // stickyRowHeight, + // stickyRowBottom, + // nextStickyRow, + // nextStickyRowHeight, + // nextStickyRowTop, + // drawYx: drawY - (totalHeaderHeight ?? 0), + // drawY, + // startRow, + // totalHeaderHeight, + // }); + } + } + } } export type WalkColsCallback = ( diff --git a/packages/core/src/internal/data-grid/render/data-grid.render.rings.ts b/packages/core/src/internal/data-grid/render/data-grid.render.rings.ts index 4f57ab119..2e392069f 100644 --- a/packages/core/src/internal/data-grid/render/data-grid.render.rings.ts +++ b/packages/core/src/internal/data-grid/render/data-grid.render.rings.ts @@ -24,7 +24,8 @@ export function drawHighlightRings( freezeTrailingRows: number, rows: number, allHighlightRegions: readonly Highlight[] | undefined, - theme: FullTheme + theme: FullTheme, + stickyRegionHeight: number ): (() => void) | undefined { const highlightRegions = allHighlightRegions?.filter(x => x.style !== "no-outline"); @@ -33,7 +34,7 @@ export function drawHighlightRings( const freezeLeft = getStickyWidth(mappedColumns); const freezeBottom = getFreezeTrailingHeight(rows, freezeTrailingRows, rowHeight); const splitIndicies = [freezeColumns, 0, mappedColumns.length, rows - freezeTrailingRows] as const; - const splitLocations = [freezeLeft, 0, width, height - freezeBottom] as const; + const splitLocations = [freezeLeft, headerHeight + stickyRegionHeight, width, height - freezeBottom] as const; const drawRects = highlightRegions.map(h => { const r = h.range;