From 01fa8abc8dce76e21b5be2228cc3a3951e588afd Mon Sep 17 00:00:00 2001 From: Jason Smith Date: Sat, 6 Jan 2024 10:32:35 -0800 Subject: [PATCH] Freeze trailing row support (#848) * WIP for freeze trailing row support * Fix tests * WIP * Restore normal render * Fix highlight regions * Add support for recognizing many freeze regions * Improve clipping regions * More fixes * Fix render state provider * Add story * Fix test build * remove unneeded checks * Fix misnamed variables * Simplify * Multiple faster than loop * Fix dashed line tracking * Fix column select highlight * Remove dead comments * Fix scroll to * Oops * Fix aggressive scrolling when selecting on frozen rows * Fix tests --- .../core/src/common/image-window-loader.ts | 38 +- packages/core/src/common/math.ts | 243 +++++++++ .../core/src/common/render-state-provider.ts | 51 +- packages/core/src/data-editor/data-editor.tsx | 119 ++++- .../src/docs/examples/freeze-rows.stories.tsx | 68 +++ .../internal/data-grid-dnd/data-grid-dnd.tsx | 3 +- .../data-grid-search/data-grid-search.tsx | 3 +- .../src/internal/data-grid/data-grid-lib.ts | 47 +- .../internal/data-grid/data-grid-render.tsx | 501 ++++++++---------- .../src/internal/data-grid/data-grid-types.ts | 3 - .../internal/data-grid/data-grid.stories.tsx | 12 +- .../core/src/internal/data-grid/data-grid.tsx | 21 +- .../src/internal/data-grid/draw-grid-arg.ts | 4 +- .../image-window-loader-interface.ts | 2 +- .../scrolling-data-grid.stories.tsx | 3 +- .../scrolling-data-grid.tsx | 3 +- packages/core/test/data-grid.test.tsx | 3 +- .../core/test/image-window-loader.test.ts | 12 +- .../core/test/render-state-provider.test.ts | 2 +- 19 files changed, 730 insertions(+), 408 deletions(-) create mode 100644 packages/core/src/docs/examples/freeze-rows.stories.tsx diff --git a/packages/core/src/common/image-window-loader.ts b/packages/core/src/common/image-window-loader.ts index d3b5e9b7d..86234f8d9 100644 --- a/packages/core/src/common/image-window-loader.ts +++ b/packages/core/src/common/image-window-loader.ts @@ -1,7 +1,6 @@ -import { type Rectangle } from "../internal/data-grid/data-grid-types.js"; import { CellSet } from "../internal/data-grid/cell-set.js"; import throttle from "lodash/throttle.js"; -import { unpackCol, unpackRow, packColRowToNumber, unpackNumberToColRow } from "./render-state-provider.js"; +import { packColRowToNumber, unpackNumberToColRow, WindowingTrackerBase } from "./render-state-provider.js"; import type { ImageWindowLoader } from "../internal/data-grid/image-window-loader-interface.js"; interface LoadResult { @@ -13,27 +12,10 @@ interface LoadResult { const imgPool: HTMLImageElement[] = []; -class ImageWindowLoaderImpl implements ImageWindowLoader { +class ImageWindowLoaderImpl extends WindowingTrackerBase implements ImageWindowLoader { private imageLoaded: (locations: CellSet) => void = () => undefined; private loadedLocations: [number, number][] = []; - public visibleWindow: Rectangle = { - x: 0, - y: 0, - width: 0, - height: 0, - }; - - public freezeCols: number = 0; - - private isInWindow = (packed: number) => { - const col = unpackCol(packed); - const row = unpackRow(packed); - const w = this.visibleWindow; - if (col < this.freezeCols && row >= w.y && row <= w.y + w.height) return true; - return col >= w.x && col <= w.x + w.width && row >= w.y && row <= w.y + w.height; - }; - private cache: Record = {}; public setCallback(imageLoaded: (locations: CellSet) => void) { @@ -46,7 +28,7 @@ class ImageWindowLoaderImpl implements ImageWindowLoader { this.loadedLocations = []; }, 20); - private clearOutOfWindow = () => { + protected clearOutOfWindow = () => { const keys = Object.keys(this.cache); for (const key of keys) { const obj = this.cache[key]; @@ -69,20 +51,6 @@ class ImageWindowLoaderImpl implements ImageWindowLoader { } }; - public setWindow(newWindow: Rectangle, freezeCols: number): void { - if ( - this.visibleWindow.x === newWindow.x && - this.visibleWindow.y === newWindow.y && - this.visibleWindow.width === newWindow.width && - this.visibleWindow.height === newWindow.height && - this.freezeCols === freezeCols - ) - return; - this.visibleWindow = newWindow; - this.freezeCols = freezeCols; - this.clearOutOfWindow(); - } - private loadImage(url: string, col: number, row: number, key: string) { let loaded = false; const img = imgPool.pop() ?? new Image(); diff --git a/packages/core/src/common/math.ts b/packages/core/src/common/math.ts index f4b67944c..d6385e068 100644 --- a/packages/core/src/common/math.ts +++ b/packages/core/src/common/math.ts @@ -50,6 +50,10 @@ export function combineRects(a: Rectangle, b: Rectangle): Rectangle { return { x, y, width, height }; } +export function rectContains(a: Rectangle, b: Rectangle): boolean { + return a.x <= b.x && a.y <= b.y && a.x + a.width >= b.x + b.width && a.y + a.height >= b.y + b.height; +} + /** * This function is absolutely critical for the performance of the fill handle and highlight regions. If you don't * hug rectanges when they are dashed and they are huge you will get giant GPU stalls. The reason for the mod is @@ -93,3 +97,242 @@ export function hugRectToTarget(rect: Rectangle, width: number, height: number, return { x: left, y: top, width: right - left, height: bottom - top }; } + +interface SplitRect { + rect: Rectangle; + clip: Rectangle; +} + +export function splitRectIntoRegions( + rect: Rectangle, + splitIndicies: readonly [number, number, number, number], + width: number, + height: number, + splitLocations: readonly [number, number, number, number] +): SplitRect[] { + const [lSplit, tSplit, rSplit, bSplit] = splitIndicies; + const [lClip, tClip, rClip, bClip] = splitLocations; + const { x: inX, y: inY, width: inW, height: inH } = rect; + + const result: SplitRect[] = []; + + if (inW <= 0 || inH <= 0) return result; + + const inRight = inX + inW; + const inBottom = inY + inH; + + // The goal is to split the inbound rect into up to 9 regions based on the provided split indicies which are + // more or less cut lines. The cut lines are whole numbers as is the rect. We are dividing cells on a table. + // In theory there can be up to 9 regions returned, so we need to be careful to make sure we get them all and + // not return any empty regions. + + // compute some handy values + const isOverLeft = inX < lSplit; + const isOverTop = inY < tSplit; + const isOverRight = inX + inW > rSplit; + const isOverBottom = inY + inH > bSplit; + + const isOverCenterVert = + (inX > lSplit && inX < rSplit) || (inRight > lSplit && inRight < rSplit) || (inX < lSplit && inRight > rSplit); + const isOverCenterHoriz = + (inY > tSplit && inY < bSplit) || + (inBottom > tSplit && inBottom < bSplit) || + (inY < tSplit && inBottom > bSplit); + + const isOverCenter = isOverCenterVert && isOverCenterHoriz; + + // center + if (isOverCenter) { + const x = Math.max(inX, lSplit); + const y = Math.max(inY, tSplit); + const right = Math.min(inRight, rSplit); + const bottom = Math.min(inBottom, bSplit); + result.push({ + rect: { x, y, width: right - x, height: bottom - y }, + clip: { + x: lClip, + y: tClip, + width: rClip - lClip + 1, + height: bClip - tClip + 1, + }, + }); + } + + // top left + if (isOverLeft && isOverTop) { + const x = inX; + const y = inY; + const right = Math.min(inRight, lSplit); + const bottom = Math.min(inBottom, tSplit); + result.push({ + rect: { + x, + y, + width: right - x, + height: bottom - y, + }, + clip: { + x: 0, + y: 0, + width: lClip + 1, + height: tClip + 1, + }, + }); + } + + // top center + if (isOverTop && isOverCenterVert) { + const x = Math.max(inX, lSplit); + const y = inY; + const right = Math.min(inRight, rSplit); + const bottom = Math.min(inBottom, tSplit); + result.push({ + rect: { + x, + y, + width: right - x, + height: bottom - y, + }, + clip: { + x: lClip, + y: 0, + width: rClip - lClip + 1, + height: tClip + 1, + }, + }); + } + + // top right + if (isOverTop && isOverRight) { + const x = Math.max(inX, rSplit); + const y = inY; + const right = inRight; + const bottom = Math.min(inBottom, tSplit); + result.push({ + rect: { + x, + y, + width: right - x, + height: bottom - y, + }, + clip: { + x: rClip, + y: 0, + width: width - rClip + 1, + height: tClip + 1, + }, + }); + } + + // center left + if (isOverLeft && isOverCenterHoriz) { + const x = inX; + const y = Math.max(inY, tSplit); + const right = Math.min(inRight, lSplit); + const bottom = Math.min(inBottom, bSplit); + result.push({ + rect: { + x, + y, + width: right - x, + height: bottom - y, + }, + clip: { + x: 0, + y: tClip, + width: lClip + 1, + height: bClip - tClip + 1, + }, + }); + } + + // center right + if (isOverRight && isOverCenterHoriz) { + const x = Math.max(inX, rSplit); + const y = Math.max(inY, tSplit); + const right = inRight; + const bottom = Math.min(inBottom, bSplit); + result.push({ + rect: { + x, + y, + width: right - x, + height: bottom - y, + }, + clip: { + x: rClip, + y: tClip, + width: width - rClip + 1, + height: bClip - tClip + 1, + }, + }); + } + + // bottom left + if (isOverLeft && isOverBottom) { + const x = inX; + const y = Math.max(inY, bSplit); + const right = Math.min(inRight, lSplit); + const bottom = inBottom; + result.push({ + rect: { + x, + y, + width: right - x, + height: bottom - y, + }, + clip: { + x: 0, + y: bClip, + width: lClip + 1, + height: height - bClip + 1, + }, + }); + } + + // bottom center + if (isOverBottom && isOverCenterVert) { + const x = Math.max(inX, lSplit); + const y = Math.max(inY, bSplit); + const right = Math.min(inRight, rSplit); + const bottom = inBottom; + result.push({ + rect: { + x, + y, + width: right - x, + height: bottom - y, + }, + clip: { + x: lClip, + y: bClip, + width: rClip - lClip + 1, + height: height - bClip + 1, + }, + }); + } + + // bottom right + if (isOverRight && isOverBottom) { + const x = Math.max(inX, rSplit); + const y = Math.max(inY, bSplit); + const right = inRight; + const bottom = inBottom; + result.push({ + rect: { + x, + y, + width: right - x, + height: bottom - y, + }, + clip: { + x: rClip, + y: bClip, + width: width - rClip + 1, + height: height - bClip + 1, + }, + }); + } + + return result; +} diff --git a/packages/core/src/common/render-state-provider.ts b/packages/core/src/common/render-state-provider.ts index cb7800ef3..55e54b4f1 100644 --- a/packages/core/src/common/render-state-provider.ts +++ b/packages/core/src/common/render-state-provider.ts @@ -1,4 +1,5 @@ import type { Item, Rectangle } from "../internal/data-grid/data-grid-types.js"; +import { deepEqual } from "./support.js"; // max safe int 2^53 - 1 (minus 1 omitted from here on) // max safe columns is 2^21 or 2,097,151 @@ -26,24 +27,46 @@ export function unpackNumberToColRow(packed: number): [number, number] { return [col, row]; } -export class RenderStateProvider { - private visibleWindow: Rectangle = { +export abstract class WindowingTrackerBase { + public visibleWindow: Rectangle = { x: 0, y: 0, width: 0, height: 0, }; - private freezeCols: number = 0; + public freezeCols: number = 0; + public freezeRows: number[] = []; - private isInWindow = (packed: number) => { + protected isInWindow = (packed: number) => { const col = unpackCol(packed); const row = unpackRow(packed); const w = this.visibleWindow; - if (col < this.freezeCols && row >= w.y && row <= w.y + w.height) return true; - return col >= w.x && col <= w.x + w.width && row >= w.y && row <= w.y + w.height; + const colInWindow = (col >= w.x && col <= w.x + w.width) || col < this.freezeCols; + const rowInWindow = (row >= w.y && row <= w.y + w.height) || this.freezeRows.includes(row); + return colInWindow && rowInWindow; }; + protected abstract clearOutOfWindow: () => void; + + public setWindow(newWindow: Rectangle, freezeCols: number, freezeRows: number[]): void { + if ( + this.visibleWindow.x === newWindow.x && + this.visibleWindow.y === newWindow.y && + this.visibleWindow.width === newWindow.width && + this.visibleWindow.height === newWindow.height && + this.freezeCols === freezeCols && + deepEqual(this.freezeRows, freezeRows) + ) + return; + this.visibleWindow = newWindow; + this.freezeCols = freezeCols; + this.freezeRows = freezeRows; + this.clearOutOfWindow(); + } +} + +export class RenderStateProvider extends WindowingTrackerBase { private cache: Map = new Map(); public setValue = (location: Item, state: any): void => { @@ -54,25 +77,11 @@ export class RenderStateProvider { return this.cache.get(packColRowToNumber(location[0], location[1])); }; - private clearOutOfWindow = () => { + protected clearOutOfWindow = () => { for (const [key] of this.cache.entries()) { if (!this.isInWindow(key)) { this.cache.delete(key); } } }; - - public setWindow(newWindow: Rectangle, freezeCols: number): void { - if ( - this.visibleWindow.x === newWindow.x && - this.visibleWindow.y === newWindow.y && - this.visibleWindow.width === newWindow.width && - this.visibleWindow.height === newWindow.height && - this.freezeCols === freezeCols - ) - return; - this.visibleWindow = newWindow; - this.freezeCols = freezeCols; - this.clearOutOfWindow(); - } } diff --git a/packages/core/src/data-editor/data-editor.tsx b/packages/core/src/data-editor/data-editor.tsx index de5dddf1b..3a56fb22a 100644 --- a/packages/core/src/data-editor/data-editor.tsx +++ b/packages/core/src/data-editor/data-editor.tsx @@ -52,6 +52,7 @@ import { itemsAreEqual, itemIsInRect, gridSelectionHasItem, + getFreezeTrailingHeight, } from "../internal/data-grid/data-grid-lib.js"; import { GroupRename } from "./group-rename.js"; import { measureColumn, useColumnSizer } from "./use-column-sizer.js"; @@ -64,7 +65,7 @@ import { useAutoscroll } from "./use-autoscroll.js"; import type { CustomRenderer, CellRenderer, InternalCellRenderer } from "../cells/cell-types.js"; import { decodeHTML, type CopyBuffer } from "./copy-paste.js"; import { useRemAdjuster } from "./use-rem-adjuster.js"; -import { type Highlight } from "../internal/data-grid/data-grid-render.js"; +import { pointInRect, type Highlight } from "../internal/data-grid/data-grid-render.js"; import { withAlpha } from "../internal/data-grid/color-parser.js"; import { combineRects, getClosestRect } from "../common/math.js"; import { @@ -109,6 +110,7 @@ type Props = Partial< | "firstColAccessible" | "firstColSticky" | "freezeColumns" + | "hasAppendRow" | "getCellContent" | "getCellRenderer" | "getCellsForSelection" @@ -138,7 +140,6 @@ type Props = Partial< | "selectedColumns" | "selection" | "theme" - | "trailingRowType" | "translateX" | "translateY" | "verticalBorder" @@ -485,8 +486,15 @@ export interface DataEditorProps extends Props, Pick void; @@ -761,6 +769,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction vr.y + vr.height || row >= rowsRef.current; const isSelected = outerCol === vr.extras?.selected?.[0] && row === vr.extras?.selected[1]; - const isOutsideFreezeArea = - vr.extras?.freezeRegion === undefined || - vr.extras.freezeRegion.x > outerCol || - outerCol > vr.extras.freezeRegion.x + vr.extras.freezeRegion.width || - vr.extras.freezeRegion.y > row || - row > vr.extras.freezeRegion.y + vr.extras.freezeRegion.height; - if (isOutsideMainArea && !isSelected && isOutsideFreezeArea) { + let isInFreezeArea = false; + if (vr.extras?.freezeRegions !== undefined) { + for (const fr of vr.extras.freezeRegions) { + if (pointInRect(fr, outerCol, row)) { + isInFreezeArea = true; + break; + } + } + } + + if (isOutsideMainArea && !isSelected && !isInFreezeArea) { return loadingCell; } } @@ -1463,8 +1484,13 @@ const DataEditorImpl: React.ForwardRefRenderFunction 0) { + trailingRowHeight = getFreezeTrailingHeight( + mangledRows, + freezeTrailingRowsEffective, + rowHeight + ); } // scrollBounds is already scaled @@ -1515,7 +1541,10 @@ const DataEditorImpl: React.ForwardRefRenderFunction= mangledRows - freezeTrailingRowsEffective) + ) { scrollY = 0; } @@ -1534,7 +1563,17 @@ const DataEditorImpl: React.ForwardRefRenderFunction 0) { + freezeRegions.push({ + x: region.x - rowMarkerOffset, + y: rows - freezeTrailingRows, + width: region.width, + height: freezeTrailingRows, + }); + + if (freezeColumns > 0) { + freezeRegions.push({ + x: 0, + y: rows - freezeTrailingRows, + width: freezeColumns, + height: freezeTrailingRows, + }); + } + } + const newRegion = { x: region.x - rowMarkerOffset, y: region.y, @@ -2356,15 +2426,8 @@ const DataEditorImpl: React.ForwardRefRenderFunction ( + + + Rows can be frozen to make sure the user always sees them. + + }> + + + + ), + ], +}; + +export const FreezeRows: React.VFC = () => { + const { cols, getCellContent, setCellValueRaw, setCellValue } = useMockDataGenerator(60, false); + + const [numRows, setNumRows] = React.useState(50); + + const onRowAppended = React.useCallback(() => { + const newRow = numRows; + // our data source is a mock source that pre-fills data, so we are just clearing this here. You should not + // need to do this. + for (let c = 0; c < cols.length; c++) { + const cell = getCellContent([c, newRow]); + setCellValueRaw([c, newRow], clearCell(cell)); + } + // Tell the data grid there is another row + setNumRows(cv => cv + 1); + }, [cols.length, getCellContent, numRows, setCellValueRaw]); + + return ( + + ); +}; diff --git a/packages/core/src/internal/data-grid-dnd/data-grid-dnd.tsx b/packages/core/src/internal/data-grid-dnd/data-grid-dnd.tsx index a211176e0..84c9a56ab 100644 --- a/packages/core/src/internal/data-grid-dnd/data-grid-dnd.tsx +++ b/packages/core/src/internal/data-grid-dnd/data-grid-dnd.tsx @@ -408,7 +408,8 @@ const DataGridDnd: React.FunctionComponent = p => { smoothScrollX={p.smoothScrollX} smoothScrollY={p.smoothScrollY} theme={p.theme} - trailingRowType={p.trailingRowType} + freezeTrailingRows={p.freezeTrailingRows} + hasAppendRow={p.hasAppendRow} translateX={p.translateX} translateY={p.translateY} verticalBorder={p.verticalBorder} diff --git a/packages/core/src/internal/data-grid-search/data-grid-search.tsx b/packages/core/src/internal/data-grid-search/data-grid-search.tsx index e0032a4b9..11d401bb5 100644 --- a/packages/core/src/internal/data-grid-search/data-grid-search.tsx +++ b/packages/core/src/internal/data-grid-search/data-grid-search.tsx @@ -508,7 +508,8 @@ const DataGridSearch: React.FunctionComponent = p => { scrollRef={p.scrollRef} selection={p.selection} theme={p.theme} - trailingRowType={p.trailingRowType} + freezeTrailingRows={p.freezeTrailingRows} + hasAppendRow={p.hasAppendRow} translateX={p.translateX} translateY={p.translateY} verticalBorder={p.verticalBorder} diff --git a/packages/core/src/internal/data-grid/data-grid-lib.ts b/packages/core/src/internal/data-grid/data-grid-lib.ts index a28b51c7a..96d20e662 100644 --- a/packages/core/src/internal/data-grid/data-grid-lib.ts +++ b/packages/core/src/internal/data-grid/data-grid-lib.ts @@ -155,6 +155,22 @@ export function getStickyWidth( return result; } +export function getFreezeTrailingHeight( + rows: number, + freezeTrailingRows: number, + getRowHeight: number | ((row: number) => number) +): number { + if (typeof getRowHeight === "number") { + return freezeTrailingRows * getRowHeight; + } else { + let result = 0; + for (let i = rows - freezeTrailingRows; i < rows; i++) { + result += getRowHeight(i); + } + return result; + } +} + export function getEffectiveColumns( columns: readonly MappedGridColumn[], cellXOffset: number, @@ -224,18 +240,23 @@ export function getRowIndexForY( rowHeight: number | ((index: number) => number), cellYOffset: number, translateY: number, - lastRowSticky: boolean + freezeTrailingRows: number ): number | undefined { const totalHeaderHeight = headerHeight + groupHeaderHeight; if (hasGroups && targetY <= groupHeaderHeight) return -2; if (targetY <= totalHeaderHeight) return -1; - const lastRowHeight = typeof rowHeight === "number" ? rowHeight : rowHeight(rows - 1); - if (lastRowSticky && targetY > height - lastRowHeight) { - return rows - 1; + let y = height; + for (let fr = 0; fr < freezeTrailingRows; fr++) { + const row = rows - 1 - fr; + const rh = typeof rowHeight === "number" ? rowHeight : rowHeight(row); + y -= rh; + if (targetY >= y) { + return row; + } } - const effectiveRows = rows - (lastRowSticky ? 1 : 0); + const effectiveRows = rows - freezeTrailingRows; const ty = targetY - (translateY ?? 0); if (typeof rowHeight === "number") { @@ -694,7 +715,7 @@ export function computeBounds( translateY: number, rows: number, freezeColumns: number, - lastRowSticky: boolean, + freezeTrailingRows: number, mappedColumns: readonly MappedGridColumn[], rowHeight: number | ((index: number) => number) ): Rectangle { @@ -764,10 +785,16 @@ export function computeBounds( result.width = width - result.x; } } - } else if (lastRowSticky && row === rows - 1) { - const stickyHeight = typeof rowHeight === "number" ? rowHeight : rowHeight(row); - result.y = height - stickyHeight; - result.height = stickyHeight; + } else if (row >= rows - freezeTrailingRows) { + let dy = rows - row; + result.y = height; + while (dy > 0) { + const r = row + dy - 1; + result.height = typeof rowHeight === "number" ? rowHeight : rowHeight(r); + result.y -= result.height; + dy--; + } + result.height += 1; } else { const dir = cellYOffset > row ? -1 : 1; if (typeof rowHeight === "number") { diff --git a/packages/core/src/internal/data-grid/data-grid-render.tsx b/packages/core/src/internal/data-grid/data-grid-render.tsx index 12a93cded..e23251204 100644 --- a/packages/core/src/internal/data-grid/data-grid-render.tsx +++ b/packages/core/src/internal/data-grid/data-grid-render.tsx @@ -14,7 +14,6 @@ import { BooleanIndeterminate, headerCellCheckedMarker, headerCellUnheckedMarker, - type TrailingRowType, type DrawCellCallback, isInnerOnlyCell, type GridCell, @@ -33,6 +32,7 @@ import { computeBounds, getMiddleCenterBias, rectBottomRight, + getFreezeTrailingHeight, drawLastUpdateUnderlay, } from "./data-grid-lib.js"; import type { SpriteManager, SpriteVariant } from "./data-grid-sprites.js"; @@ -46,7 +46,7 @@ import type { DragAndDropState, DrawGridArg, HoverInfo } from "./draw-grid-arg.j import type { EnqueueCallback } from "./use-animation-queue.js"; import type { RenderStateProvider } from "../../common/render-state-provider.js"; import type { ImageWindowLoader } from "./image-window-loader-interface.js"; -import { hugRectToTarget } from "../../common/math.js"; +import { hugRectToTarget, rectContains, splitRectIntoRegions } from "../../common/math.js"; import type { GridMouseGroupHeaderEventArgs } from "./event-args.js"; // Future optimization opportunities @@ -208,7 +208,7 @@ function blitLastFrame( cellYOffset: number, translateX: number, translateY: number, - lastRowSticky: boolean, + freezeTrailingRows: number, width: number, height: number, rows: number, @@ -259,14 +259,11 @@ function blitLastFrame( }; } - const stickyRowHeight = lastRowSticky - ? typeof getRowHeight === "number" - ? getRowHeight - : getRowHeight(rows - 1) - : 0; + const freezeTrailingRowsHeight = + freezeTrailingRows > 0 ? getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight) : 0; const blitWidth = width - stickyWidth - Math.abs(deltaX); - const blitHeight = height - totalHeaderHeight - stickyRowHeight - Math.abs(deltaY) - 1; + const blitHeight = height - totalHeaderHeight - freezeTrailingRowsHeight - Math.abs(deltaY) - 1; if (blitWidth > 150 && blitHeight > 150) { blittedYOnly = deltaX === 0; @@ -305,9 +302,9 @@ function blitLastFrame( drawRegions.push({ x: 0, - y: height + deltaY - stickyRowHeight, + y: height + deltaY - freezeTrailingRowsHeight, width: width, - height: -deltaY + stickyRowHeight, + height: -deltaY + freezeTrailingRowsHeight, }); } @@ -413,7 +410,7 @@ function drawGridLines( getRowHeight: (row: number) => number, getRowThemeOverride: GetRowThemeCallback | undefined, verticalBorder: (col: number) => boolean, - trailingRowType: TrailingRowType, + freezeTrailingRows: number, rows: number, theme: FullTheme, verticalOnly: boolean = false @@ -470,22 +467,21 @@ function drawGridLines( } } - const stickyHeight = getRowHeight(rows - 1); - const stickyRowY = height - stickyHeight + 0.5; - const lastRowSticky = trailingRowType === "sticky"; - if (lastRowSticky) { - toDraw.push({ x1: minX, y1: stickyRowY, x2: maxX, y2: stickyRowY, color: hColor }); + let freezeY = height + 0.5; + for (let i = rows - freezeTrailingRows; i < rows; i++) { + const rh = getRowHeight(i); + freezeY -= rh; + toDraw.push({ x1: minX, y1: freezeY, x2: maxX, y2: freezeY, color: hColor }); } if (verticalOnly !== true) { // horizontal lines let y = totalHeaderHeight + 0.5; let row = cellYOffset; - const target = lastRowSticky ? height - stickyHeight : height; - while (y + translateY <= target) { + const target = freezeY; + while (y + translateY < target) { const ty = y + translateY; - // This shouldn't be needed it seems like... yet it is. We're not sure why. - if (ty >= minY && ty <= maxY - 1 && (!lastRowSticky || row !== rows - 1 || Math.abs(ty - stickyRowY) > 1)) { + if (ty >= minY && ty <= maxY - 1) { const rowTheme = getRowThemeOverride?.(row); toDraw.push({ x1: minX, @@ -1156,7 +1152,8 @@ function drawCells( disabledRows: CompactSelection, isFocused: boolean, drawFocus: boolean, - trailingRowType: TrailingRowType, + freezeTrailingRows: number, + hasAppendRow: boolean, drawRegions: readonly Rectangle[], damage: CellSet | undefined, selection: GridSelection, @@ -1181,7 +1178,8 @@ function drawCells( ctx.font = font; const deprepArg = { ctx }; const cellIndex: [number, number] = [0, 0]; - const stickyRowHeight = trailingRowType === "sticky" ? getRowHeight(rows - 1) : 0; + const freezeTrailingRowsHeight = + freezeTrailingRows > 0 ? getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight) : 0; let result: Rectangle[] | undefined; const handledSpans = new Set(); walkColumns( @@ -1237,7 +1235,8 @@ function drawCells( height, rows, getRowHeight, - trailingRowType, + freezeTrailingRows, + hasAppendRow, (drawY, row, rh, isSticky, isTrailingRow) => { if (row < 0) return; @@ -1349,7 +1348,7 @@ function drawCells( } if (!isSelected) { if (rowSelected) accentCount++; - if (colSelected && !isSticky) accentCount++; + if (colSelected && !isTrailingRow) accentCount++; } const bgCell = cell.kind === GridCellKind.Protected ? theme.bgCellMedium : theme.bgCell; @@ -1396,7 +1395,9 @@ function drawCells( // this is passing too many clip regions to the GPU at once can cause a performance hit. This // allows us to damage a large number of cells at once without issue. const top = drawY + 1; - const bottom = isSticky ? top + rh - 1 : Math.min(top + rh - 1, height - stickyRowHeight); + const bottom = isSticky + ? top + rh - 1 + : Math.min(top + rh - 1, height - freezeTrailingRowsHeight); const h = bottom - top; // however, not clipping at all is even better. We want to clip if we are the left most col @@ -1519,7 +1520,8 @@ function drawBlanks( getRowTheme: GetRowThemeCallback | undefined, selectedRows: CompactSelection, disabledRows: CompactSelection, - trailingRowType: TrailingRowType, + freezeTrailingRows: number, + hasAppendRow: boolean, drawRegions: readonly Rectangle[], damage: CellSet | undefined, theme: FullTheme @@ -1551,7 +1553,8 @@ function drawBlanks( height, rows, getRowHeight, - trailingRowType, + freezeTrailingRows, + hasAppendRow, (drawY, row, rh, isSticky) => { if ( !isSticky && @@ -1597,7 +1600,7 @@ function overdrawStickyBoundaries( effectiveCols: readonly MappedGridColumn[], width: number, height: number, - lastRowSticky: boolean, + freezeTrailingRows: number, rows: number, verticalBorder: (col: number) => boolean, getRowHeight: (row: number) => number, @@ -1621,8 +1624,8 @@ function overdrawStickyBoundaries( ctx.stroke(); } - if (lastRowSticky) { - const h = getRowHeight(rows - 1); + if (freezeTrailingRows > 0) { + const h = getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight); ctx.beginPath(); ctx.moveTo(0, height - h + 0.5); ctx.lineTo(width, height - h + 0.5); @@ -1644,7 +1647,7 @@ function drawHighlightRings( headerHeight: number, groupHeaderHeight: number, rowHeight: number | ((index: number) => number), - lastRowSticky: boolean, + freezeTrailingRows: number, rows: number, allHighlightRegions: readonly Highlight[] | undefined, theme: FullTheme @@ -1652,66 +1655,21 @@ function drawHighlightRings( const highlightRegions = allHighlightRegions?.filter(x => x.style !== "no-outline"); if (highlightRegions === undefined || highlightRegions.length === 0) return undefined; + + 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 drawRects = highlightRegions.map(h => { const r = h.range; - const topLeftBounds = computeBounds( - r.x, - r.y, - width, - height, - groupHeaderHeight, - headerHeight + groupHeaderHeight, - cellXOffset, - cellYOffset, - translateX, - translateY, - rows, - freezeColumns, - lastRowSticky, - mappedColumns, - rowHeight - ); - if (r.width === 1 && r.height === 1) { - if (r.x < freezeColumns) { - return [ - { - color: h.color, - style: h.style ?? "dashed", - rect: hugRectToTarget(topLeftBounds, width, height, 8), - }, - undefined, - ]; - } - return [ - undefined, - { color: h.color, style: h.style ?? "dashed", rect: hugRectToTarget(topLeftBounds, width, height, 8) }, - ]; - } + const style = h.style ?? "dashed"; - const bottomRightBounds = computeBounds( - r.x + r.width - 1, - r.y + r.height - 1, - width, - height, - groupHeaderHeight, - headerHeight + groupHeaderHeight, - cellXOffset, - cellYOffset, - translateX, - translateY, - rows, - freezeColumns, - lastRowSticky, - mappedColumns, - rowHeight - ); - if (r.x + r.width === mappedColumns.length) { - bottomRightBounds.width -= 1; - } - if (r.x < freezeColumns && r.x + r.width >= freezeColumns) { - const freezeSectionRightBounds = computeBounds( - freezeColumns - 1, - r.y + r.height - 1, + return splitRectIntoRegions(r, splitIndicies, width, height, splitLocations).map(arg => { + const rect = arg.rect; + const topLeftBounds = computeBounds( + rect.x, + rect.y, width, height, groupHeaderHeight, @@ -1722,136 +1680,86 @@ function drawHighlightRings( translateY, rows, freezeColumns, - lastRowSticky, + freezeTrailingRows, mappedColumns, rowHeight ); - const unfreezeSectionleftBounds = computeBounds( - freezeColumns, - r.y, - width, - height, - groupHeaderHeight, - headerHeight + groupHeaderHeight, - cellXOffset, - cellYOffset, - translateX, - translateY, - rows, - freezeColumns, - lastRowSticky, - mappedColumns, - rowHeight - ); - - return [ - { - color: h.color, - style: h.style ?? "dashed", - rect: hugRectToTarget( - { - x: topLeftBounds.x, - y: topLeftBounds.y, - width: freezeSectionRightBounds.x + freezeSectionRightBounds.width - topLeftBounds.x, - height: freezeSectionRightBounds.y + freezeSectionRightBounds.height - topLeftBounds.y, - }, - width, - height, - 8 - ), - }, - { - color: h.color, - style: h.style ?? "dashed", - rect: hugRectToTarget( - { - x: unfreezeSectionleftBounds.x, - y: unfreezeSectionleftBounds.y, - width: bottomRightBounds.x + bottomRightBounds.width - unfreezeSectionleftBounds.x, - height: bottomRightBounds.y + bottomRightBounds.height - unfreezeSectionleftBounds.y, - }, - width, - height, - 8 - ), - }, - ]; - } else { - return [ - undefined, - { - color: h.color, - style: h.style ?? "dashed", - rect: hugRectToTarget( - { - x: topLeftBounds.x, - y: topLeftBounds.y, - width: bottomRightBounds.x + bottomRightBounds.width - topLeftBounds.x, - height: bottomRightBounds.y + bottomRightBounds.height - topLeftBounds.y, - }, - width, - height, - 8 - ), - }, - ]; - } + const bottomRightBounds = + rect.width === 1 && rect.height === 1 + ? topLeftBounds + : computeBounds( + rect.x + rect.width - 1, + rect.y + rect.height - 1, + width, + height, + groupHeaderHeight, + headerHeight + groupHeaderHeight, + cellXOffset, + cellYOffset, + translateX, + translateY, + rows, + freezeColumns, + freezeTrailingRows, + mappedColumns, + rowHeight + ); + return { + color: h.color, + style, + clip: arg.clip, + rect: hugRectToTarget( + { + x: topLeftBounds.x, + y: topLeftBounds.y, + width: bottomRightBounds.x + bottomRightBounds.width - topLeftBounds.x, + height: bottomRightBounds.y + bottomRightBounds.height - topLeftBounds.y, + }, + width, + height, + 8 + ), + }; + }); }); - const stickyWidth = getStickyWidth(mappedColumns); - const drawCb = () => { - ctx.save(); + ctx.lineWidth = 1; + let dashed = false; - const setDashed = (dash: boolean) => { - if (dashed === dash) return; - ctx.setLineDash(dash ? [5, 3] : []); - dashed = dash; - }; - ctx.lineWidth = 1; - if (lastRowSticky) { - const lastRowHeight = typeof rowHeight === "function" ? rowHeight(rows - 1) : rowHeight; - ctx.beginPath(); - ctx.rect(0, 0, width, height - lastRowHeight + 1); - ctx.clip(); - } - ctx.beginPath(); for (const dr of drawRects) { - const [s] = dr; - if ( - s?.rect !== undefined && - intersectRect(0, 0, width, height, s.rect.x, s.rect.y, s.rect.width, s.rect.height) - ) { - setDashed(s.style === "dashed"); - ctx.strokeStyle = - s.style === "solid-outline" - ? blend(blend(s.color, theme.borderColor), theme.bgCell) - : withAlpha(s.color, 1); - ctx.strokeRect(s.rect.x + 0.5, s.rect.y + 0.5, s.rect.width - 1, s.rect.height - 1); - } - } - let clipped = false; - for (const dr of drawRects) { - const [, s] = dr; - if ( - s?.rect !== undefined && - intersectRect(0, 0, width, height, s.rect.x, s.rect.y, s.rect.width, s.rect.height) - ) { - setDashed(s.style === "dashed"); - if (!clipped && s.rect.x < stickyWidth) { - ctx.rect(stickyWidth, 0, width, height); - ctx.clip(); - clipped = true; + for (const s of dr) { + if ( + s?.rect !== undefined && + intersectRect(0, 0, width, height, s.rect.x, s.rect.y, s.rect.width, s.rect.height) + ) { + const needsClip = !rectContains(s.clip, s.rect); + if (needsClip) { + ctx.save(); + ctx.rect(s.clip.x, s.clip.y, s.clip.width, s.clip.height); + ctx.clip(); + } + if (s.style === "dashed" && !dashed) { + ctx.setLineDash([5, 3]); + dashed = true; + } else if ((s.style === "solid" || s.style === "solid-outline") && dashed) { + ctx.setLineDash([]); + dashed = false; + } + ctx.strokeStyle = + s.style === "solid-outline" + ? blend(blend(s.color, theme.borderColor), theme.bgCell) + : withAlpha(s.color, 1); + ctx.strokeRect(s.rect.x + 0.5, s.rect.y + 0.5, s.rect.width - 1, s.rect.height - 1); + if (needsClip) ctx.restore(); } - ctx.strokeStyle = - s.style === "solid-outline" - ? blend(blend(s.color, theme.borderColor), theme.bgCell) - : withAlpha(s.color, 1); - ctx.strokeRect(s.rect.x + 0.5, s.rect.y + 0.5, s.rect.width - 1, s.rect.height - 1); } } - ctx.restore(); + + if (dashed) { + ctx.setLineDash([]); + } }; drawCb(); @@ -1891,7 +1799,8 @@ function drawFocusRing( selectedCell: GridSelection, getRowHeight: (row: number) => number, getCellContent: (cell: Item) => InnerGridCell, - trailingRowType: TrailingRowType, + freezeTrailingRows: number, + hasAppendRow: boolean, fillHandle: boolean, rows: number ): (() => void) | undefined { @@ -1910,8 +1819,11 @@ function drawFocusRing( const cell = getCellContent(selectedCell.current.cell); const targetColSpan = cell.span ?? [targetCol, targetCol]; - const isStickyRow = trailingRowType === "sticky" && targetRow === rows - 1; - const stickRowHeight = trailingRowType === "sticky" && !isStickyRow ? getRowHeight(rows - 1) - 1 : 0; + const isStickyRow = targetRow >= rows - freezeTrailingRows; + const stickRowHeight = + freezeTrailingRows > 0 && !isStickyRow + ? getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight) - 1 + : 0; const fillHandleRow = fillHandleTarget[1]; @@ -1937,63 +1849,72 @@ function drawFocusRing( return; } - walkRowsInCol(startRow, colDrawY, height, rows, getRowHeight, trailingRowType, (drawY, row, rh) => { - if (row !== targetRow && row !== fillHandleRow) return; + walkRowsInCol( + startRow, + colDrawY, + height, + rows, + getRowHeight, + freezeTrailingRows, + hasAppendRow, + (drawY, row, rh) => { + if (row !== targetRow && row !== fillHandleRow) return; - let cellX = drawX; - let cellWidth = col.width; + let cellX = drawX; + let cellWidth = col.width; - const isLastColumn = col.sourceIndex === allColumns.length - 1; - const isLastRow = row === rows - 1; + const isLastColumn = col.sourceIndex === allColumns.length - 1; + const isLastRow = row === rows - 1; - if (cell.span !== undefined) { - const areas = getSpanBounds(cell.span, drawX, drawY, col.width, rh, col, allColumns); - const area = col.sticky ? areas[0] : areas[1]; + if (cell.span !== undefined) { + const areas = getSpanBounds(cell.span, drawX, drawY, col.width, rh, col, allColumns); + const area = col.sticky ? areas[0] : areas[1]; - if (area !== undefined) { - cellX = area.x; - cellWidth = area.width; + if (area !== undefined) { + cellX = area.x; + cellWidth = area.width; + } } - } - const doHandle = row === fillHandleRow && isFillHandleCol && fillHandle; - const doRing = row === targetRow && !isBeforeTarget && !isAfterTarget && drawCb === undefined; + const doHandle = row === fillHandleRow && isFillHandleCol && fillHandle; + const doRing = row === targetRow && !isBeforeTarget && !isAfterTarget && drawCb === undefined; - if (doHandle) { - drawHandleCb = () => { - if (clipX > cellX && !col.sticky && !doRing) { + if (doHandle) { + drawHandleCb = () => { + if (clipX > cellX && !col.sticky && !doRing) { + ctx.beginPath(); + ctx.rect(clipX, 0, width - clipX, height); + ctx.clip(); + } ctx.beginPath(); - ctx.rect(clipX, 0, width - clipX, height); - ctx.clip(); - } - ctx.beginPath(); - ctx.rect(cellX + cellWidth - 4, drawY + rh - 4, 4, 4); - ctx.fillStyle = col.themeOverride?.accentColor ?? theme.accentColor; - ctx.fill(); - }; - } + ctx.rect(cellX + cellWidth - 4, drawY + rh - 4, 4, 4); + ctx.fillStyle = col.themeOverride?.accentColor ?? theme.accentColor; + ctx.fill(); + }; + } - if (doRing) { - drawCb = () => { - if (clipX > cellX && !col.sticky) { + if (doRing) { + drawCb = () => { + if (clipX > cellX && !col.sticky) { + ctx.beginPath(); + ctx.rect(clipX, 0, width - clipX, height); + ctx.clip(); + } ctx.beginPath(); - ctx.rect(clipX, 0, width - clipX, height); - ctx.clip(); - } - ctx.beginPath(); - ctx.rect( - cellX + 0.5, - drawY + 0.5, - cellWidth - (isLastColumn ? 1 : 0), - rh - (isLastRow ? 1 : 0) - ); - ctx.strokeStyle = col.themeOverride?.accentColor ?? theme.accentColor; - ctx.lineWidth = 1; - ctx.stroke(); - }; + ctx.rect( + cellX + 0.5, + drawY + 0.5, + cellWidth - (isLastColumn ? 1 : 0), + rh - (isLastRow ? 1 : 0) + ); + ctx.strokeStyle = col.themeOverride?.accentColor ?? theme.accentColor; + ctx.lineWidth = 1; + ctx.stroke(); + }; + } + return drawCb !== undefined && (fillHandle ? drawHandleCb !== undefined : true); } - return drawCb !== undefined && (fillHandle ? drawHandleCb !== undefined : true); - }); + ); return drawCb !== undefined && (fillHandle ? drawHandleCb !== undefined : true); } @@ -2027,7 +1948,8 @@ function getLastRow( cellYOffset: number, rows: number, getRowHeight: (row: number) => number, - trailingRowType: TrailingRowType + freezeTrailingRows: number, + hasAppendRow: boolean ): number { let result = 0; walkColumns( @@ -2043,7 +1965,8 @@ function getLastRow( height, rows, getRowHeight, - trailingRowType, + freezeTrailingRows, + hasAppendRow, (_drawY, row, _rh, isSticky) => { if (!isSticky) { result = Math.max(row, result); @@ -2142,7 +2065,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { isResizing, selection, fillHandle, - lastRowSticky: trailingRowType, + freezeTrailingRows, rows, getCellContent, getGroupDetails, @@ -2160,6 +2083,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { hoverInfo, spriteManager, scrolling, + hasAppendRow, touchMode, enqueue, renderStateProvider, @@ -2309,7 +2233,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { getRowHeight, getRowThemeOverride, verticalBorder, - trailingRowType, + freezeTrailingRows, rows, theme, true @@ -2338,7 +2262,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { headerHeight, groupHeaderHeight, rowHeight, - trailingRowType === "sticky", + freezeTrailingRows, rows, highlightRegions, theme @@ -2360,7 +2284,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { selection, getRowHeight, getCellContent, - trailingRowType, + freezeTrailingRows, + hasAppendRow, fillHandle, rows ); @@ -2391,10 +2316,10 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { }, { x: cellXOffset, - y: rows - 1, + y: rows - freezeTrailingRows, width: viewRegionWidth, - height: 1, - when: trailingRowType !== "sticky", + height: freezeTrailingRows, + when: freezeTrailingRows > 0, }, ]); @@ -2418,7 +2343,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { disabledRows, isFocused, drawFocus, - trailingRowType, + freezeTrailingRows, + hasAppendRow, drawRegions, damage, selection, @@ -2460,7 +2386,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { selection, getRowHeight, getCellContent, - trailingRowType, + freezeTrailingRows, + hasAppendRow, fillHandle, rows ); @@ -2514,7 +2441,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { cellYOffset, translateX, translateY, - trailingRowType === "sticky", + freezeTrailingRows, width, height, rows, @@ -2548,7 +2475,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { effectiveCols, width, height, - trailingRowType === "sticky", + freezeTrailingRows, rows, verticalBorder, getRowHeight, @@ -2568,7 +2495,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { headerHeight, groupHeaderHeight, rowHeight, - trailingRowType === "sticky", + freezeTrailingRows, rows, highlightRegions, theme @@ -2590,7 +2517,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { selection, getRowHeight, getCellContent, - trailingRowType, + freezeTrailingRows, + hasAppendRow, fillHandle, rows ) @@ -2626,7 +2554,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { disabledRows, isFocused, drawFocus, - trailingRowType, + freezeTrailingRows, + hasAppendRow, drawRegions, damage, selection, @@ -2661,7 +2590,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { getRowThemeOverride, selection.rows, disabledRows, - trailingRowType, + freezeTrailingRows, + hasAppendRow, drawRegions, damage, theme @@ -2682,7 +2612,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { getRowHeight, getRowThemeOverride, verticalBorder, - trailingRowType, + freezeTrailingRows, rows, theme ); @@ -2716,7 +2646,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { cellYOffset, rows, getRowHeight, - trailingRowType + freezeTrailingRows, + hasAppendRow ); imageLoader?.setWindow( @@ -2726,7 +2657,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { width: effectiveCols.length, height: lastRowDrawn - cellYOffset, }, - freezeColumns + freezeColumns, + Array.from({ length: freezeTrailingRows }, (_, i) => rows - 1 - i) ); lastBlitData.current = { @@ -2757,27 +2689,28 @@ function walkRowsInCol( height: number, rows: number, getRowHeight: (row: number) => number, - trailingRowType: TrailingRowType, + freezeTrailingRows: number, + hasAppendRow: boolean, cb: WalkRowsCallback ): void { let y = drawY; let row = startRow; - const doSticky = trailingRowType === "sticky"; - const rowEnd = doSticky ? rows - 1 : rows; + const rowEnd = rows - freezeTrailingRows; while (y < height && row < rowEnd) { const rh = getRowHeight(row); - if (cb(y, row, rh, false, trailingRowType !== "none" && row === rows - 1) === true) { + if (cb(y, row, rh, false, hasAppendRow && row === rows - 1) === true) { break; } y += rh; row++; } - if (doSticky) { - row = rows - 1; + y = height; + for (let fr = 0; fr < freezeTrailingRows; fr++) { + row = rows - 1 - fr; const rh = getRowHeight(row); - y = height - rh; - cb(y, row, rh, true, true); + y -= rh; + cb(y, row, rh, true, hasAppendRow && row === rows - 1); } } diff --git a/packages/core/src/internal/data-grid/data-grid-types.ts b/packages/core/src/internal/data-grid/data-grid-types.ts index 0d4a4c69b..3e47d64d6 100644 --- a/packages/core/src/internal/data-grid/data-grid-types.ts +++ b/packages/core/src/internal/data-grid/data-grid-types.ts @@ -37,9 +37,6 @@ export type BooleanEmpty = null; /** @category Types */ export type BooleanIndeterminate = undefined; -/** @category Types */ -export type TrailingRowType = "sticky" | "appended" | "none"; - /** @category Types */ export type DrawHeaderCallback = ( args: { diff --git a/packages/core/src/internal/data-grid/data-grid.stories.tsx b/packages/core/src/internal/data-grid/data-grid.stories.tsx index 8cd8a840c..c971ec83c 100644 --- a/packages/core/src/internal/data-grid/data-grid.stories.tsx +++ b/packages/core/src/internal/data-grid/data-grid.stories.tsx @@ -126,7 +126,8 @@ export function Simplenotest() { freezeColumns={0} firstColAccessible={true} verticalBorder={() => true} - trailingRowType={"none"} + freezeTrailingRows={0} + hasAppendRow={false} isResizing={false} isDragging={false} theme={mergeAndRealizeTheme(getDataEditorTheme())} @@ -215,7 +216,8 @@ export function SelectedCellnotest() { freezeColumns={0} firstColAccessible={true} verticalBorder={() => true} - trailingRowType={"none"} + freezeTrailingRows={0} + hasAppendRow={false} isResizing={false} isDragging={false} theme={mergeAndRealizeTheme(getDataEditorTheme())} @@ -300,7 +302,8 @@ export function SelectedRownotest() { freezeColumns={0} firstColAccessible={true} verticalBorder={() => true} - trailingRowType={"none"} + freezeTrailingRows={0} + hasAppendRow={false} isResizing={false} isDragging={false} theme={mergeAndRealizeTheme(getDataEditorTheme())} @@ -385,7 +388,8 @@ export const SelectedColumnnotest = () => { freezeColumns={0} firstColAccessible={true} verticalBorder={() => true} - trailingRowType={"none"} + freezeTrailingRows={0} + hasAppendRow={false} isResizing={false} isDragging={false} theme={mergeAndRealizeTheme(getDataEditorTheme())} diff --git a/packages/core/src/internal/data-grid/data-grid.tsx b/packages/core/src/internal/data-grid/data-grid.tsx index 525d82341..5a5603784 100644 --- a/packages/core/src/internal/data-grid/data-grid.tsx +++ b/packages/core/src/internal/data-grid/data-grid.tsx @@ -22,7 +22,6 @@ import { isInnerOnlyCell, booleanCellIsEditable, type InnerGridColumn, - type TrailingRowType, type DrawCellCallback, } from "./data-grid-types.js"; import { CellSet } from "./cell-set.js"; @@ -74,7 +73,8 @@ export interface DataGridProps { readonly accessibilityHeight: number; readonly freezeColumns: number; - readonly trailingRowType: TrailingRowType; + readonly freezeTrailingRows: number; + readonly hasAppendRow: boolean; readonly firstColAccessible: boolean; /** @@ -330,7 +330,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, selection, freezeColumns, onContextMenu, - trailingRowType: trailingRowType, + freezeTrailingRows, fixedShadowX = true, fixedShadowY = true, drawFocusRing = true, @@ -354,6 +354,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, isDraggable = false, allowResize, disabledRows, + hasAppendRow, getGroupDetails, theme, prelightCells, @@ -442,7 +443,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, translateY, rows, freezeColumns, - trailingRowType === "sticky", + freezeTrailingRows, mappedColumns, rowHeight ); @@ -470,7 +471,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, translateY, rows, freezeColumns, - trailingRowType, + freezeTrailingRows, mappedColumns, rowHeight, ] @@ -506,7 +507,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, rowHeight, cellYOffset, translateY, - trailingRowType === "sticky" + freezeTrailingRows ); const shiftKey = ev?.shiftKey === true; @@ -651,7 +652,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, rowHeight, cellYOffset, translateY, - trailingRowType, + freezeTrailingRows, getBoundsForItem, fillHandle, selection, @@ -731,8 +732,9 @@ const DataGrid: React.ForwardRefRenderFunction = (p, selection, fillHandle, drawCellCallback, + hasAppendRow, overrideCursor, - lastRowSticky: trailingRowType, + freezeTrailingRows, rows, drawFocus: drawFocusRing, getCellContent, @@ -795,11 +797,12 @@ const DataGrid: React.ForwardRefRenderFunction = (p, rowHeight, verticalBorder, isResizing, + hasAppendRow, resizeCol, isFocused, selection, fillHandle, - trailingRowType, + freezeTrailingRows, rows, drawFocusRing, getCellContent, diff --git a/packages/core/src/internal/data-grid/draw-grid-arg.ts b/packages/core/src/internal/data-grid/draw-grid-arg.ts index fc3b2e353..068470e00 100644 --- a/packages/core/src/internal/data-grid/draw-grid-arg.ts +++ b/packages/core/src/internal/data-grid/draw-grid-arg.ts @@ -8,7 +8,6 @@ import type { SpriteManager } from "./data-grid-sprites.js"; import type { CompactSelection, GridSelection, - TrailingRowType, Item, InnerGridCell, DrawHeaderCallback, @@ -53,7 +52,8 @@ export interface DrawGridArg { readonly drawFocus: boolean; readonly selection: GridSelection; readonly fillHandle: boolean; - readonly lastRowSticky: TrailingRowType; + readonly freezeTrailingRows: number; + readonly hasAppendRow: boolean; readonly hyperWrapping: boolean; readonly rows: number; readonly getCellContent: (cell: Item) => InnerGridCell; diff --git a/packages/core/src/internal/data-grid/image-window-loader-interface.ts b/packages/core/src/internal/data-grid/image-window-loader-interface.ts index 626139fea..e32ef93d1 100644 --- a/packages/core/src/internal/data-grid/image-window-loader-interface.ts +++ b/packages/core/src/internal/data-grid/image-window-loader-interface.ts @@ -3,7 +3,7 @@ import type { Rectangle } from "./data-grid-types.js"; /** @category Types */ export interface ImageWindowLoader { - setWindow(newWindow: Rectangle, freezeCols: number): void; + setWindow(newWindow: Rectangle, freezeCols: number, freezeRows: number[]): void; loadOrGetImage(url: string, col: number, row: number): HTMLImageElement | ImageBitmap | undefined; setCallback(imageLoaded: (locations: CellSet) => void): void; } diff --git a/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.stories.tsx b/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.stories.tsx index 0b76b8786..4a18e28b3 100644 --- a/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.stories.tsx +++ b/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.stories.tsx @@ -145,7 +145,8 @@ export function Simplenotest() { firstColAccessible={true} groupHeaderHeight={34} headerHeight={44} - trailingRowType={"none"} + freezeTrailingRows={0} + hasAppendRow={false} rowHeight={34} onVisibleRegionChanged={onVisibleRegionChanged} columns={columns} diff --git a/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.tsx b/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.tsx index 558efa226..3edab8074 100644 --- a/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.tsx +++ b/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.tsx @@ -298,7 +298,8 @@ const GridScroller: React.FunctionComponent = p => { rows={p.rows} selection={p.selection} theme={p.theme} - trailingRowType={p.trailingRowType} + freezeTrailingRows={p.freezeTrailingRows} + hasAppendRow={p.hasAppendRow} translateX={p.translateX} translateY={p.translateY} onColumnProposeMove={p.onColumnProposeMove} diff --git a/packages/core/test/data-grid.test.tsx b/packages/core/test/data-grid.test.tsx index 02cc1d37d..8ee56a96c 100644 --- a/packages/core/test/data-grid.test.tsx +++ b/packages/core/test/data-grid.test.tsx @@ -98,7 +98,8 @@ const basicProps: DataGridProps = { isDragging: false, isResizing: false, resizeColumn: undefined, - trailingRowType: "none", + freezeTrailingRows: 0, + hasAppendRow: false, rowHeight: 32, rows: 1000, verticalBorder: () => true, diff --git a/packages/core/test/image-window-loader.test.ts b/packages/core/test/image-window-loader.test.ts index 8a0f60c97..398d0fd33 100644 --- a/packages/core/test/image-window-loader.test.ts +++ b/packages/core/test/image-window-loader.test.ts @@ -19,7 +19,7 @@ describe("ImageWindowLoaderImpl", () => { }; const freezeCols = 5; - loader.setWindow(newWindow, freezeCols); + loader.setWindow(newWindow, freezeCols, []); // Assuming you modify your class to expose `visibleWindow` and `freezeCols` for testing expect(loader.visibleWindow).toEqual(newWindow); @@ -44,13 +44,13 @@ describe("ImageWindowLoaderImpl", () => { const freezeCols1 = 5; const freezeCols2 = 10; - loader.setWindow(window1, freezeCols1); + loader.setWindow(window1, freezeCols1, []); expect(spyClearOutOfWindow).toHaveBeenCalledTimes(1); - loader.setWindow(window2, freezeCols1); + loader.setWindow(window2, freezeCols1, []); expect(spyClearOutOfWindow).toHaveBeenCalledTimes(2); - loader.setWindow(window2, freezeCols2); + loader.setWindow(window2, freezeCols2, []); expect(spyClearOutOfWindow).toHaveBeenCalledTimes(3); // Cleanup @@ -68,8 +68,8 @@ describe("ImageWindowLoaderImpl", () => { }; const freezeCols = 5; - loader.setWindow(newWindow, freezeCols); - loader.setWindow(newWindow, freezeCols); + loader.setWindow(newWindow, freezeCols, []); + loader.setWindow(newWindow, freezeCols, []); expect(spyClearOutOfWindow).toHaveBeenCalledTimes(1); diff --git a/packages/core/test/render-state-provider.test.ts b/packages/core/test/render-state-provider.test.ts index 203f9abbc..82e772bb1 100644 --- a/packages/core/test/render-state-provider.test.ts +++ b/packages/core/test/render-state-provider.test.ts @@ -78,7 +78,7 @@ describe("Data Grid Utility Functions", () => { it("should update visible window and freeze columns correctly", () => { renderStateProvider.setValue([0, 30], "state"); renderStateProvider.setValue([1, 0], "state"); - renderStateProvider.setWindow(testRectangle, 1); + renderStateProvider.setWindow(testRectangle, 1, []); expect(renderStateProvider.getValue([0, 30])).to.equal("state"); expect(renderStateProvider.getValue([1, 0])).to.equal(undefined); });