diff --git a/packages/core/src/data-editor/data-editor-fns.ts b/packages/core/src/data-editor/data-editor-fns.ts index f350cf8ce..f4f462da2 100644 --- a/packages/core/src/data-editor/data-editor-fns.ts +++ b/packages/core/src/data-editor/data-editor-fns.ts @@ -67,15 +67,27 @@ export function expandSelection( let left = r.x - rowMarkerOffset; let right = r.x + r.width - 1 - rowMarkerOffset; + let top = r.y; + let bottom = r.y + r.height - 1; for (const row of cells) { for (const cell of row) { - if (cell.span === undefined) continue; - left = Math.min(cell.span[0], left); - right = Math.max(cell.span[1], right); + if (cell.span !== undefined) { + left = Math.min(cell.span[0], left); + right = Math.max(cell.span[1], right); + } + if (cell?.rowSpan !== undefined) { + top = Math.min(cell.rowSpan[0], top); + bottom = Math.max(cell.rowSpan[1], bottom); + } } } - if (left === r.x - rowMarkerOffset && right === r.x + r.width - 1 - rowMarkerOffset) { + if ( + left === r.x - rowMarkerOffset && + right === r.x + r.width - 1 - rowMarkerOffset && + top === r.y && + bottom === r.y + r.height - 1 + ) { isFilled = true; } else { newVal = { @@ -83,9 +95,9 @@ export function expandSelection( cell: newVal.current.cell ?? [0, 0], range: { x: left + rowMarkerOffset, - y: r.y, + y: top, width: right - left + 1, - height: r.height, + height: bottom - top + 1, }, rangeStack: newVal.current.rangeStack, }, diff --git a/packages/core/src/docs/examples/span-cell.stories.tsx b/packages/core/src/docs/examples/span-cell.stories.tsx index d3d4ce0d3..e2235c68a 100644 --- a/packages/core/src/docs/examples/span-cell.stories.tsx +++ b/packages/core/src/docs/examples/span-cell.stories.tsx @@ -45,21 +45,21 @@ export const SpanCell: React.VFC = () => { const mangledGetCellContent = React.useCallback( cell => { const [col, row] = cell; - if (row === 6 && col >= 3 && col <= 4) { + if (col === 0 && row >= 2 && row <= 4) { return { kind: GridCellKind.Text, allowOverlay: false, - data: "Span Cell that is very long and will go past the cell limits", - span: [3, 4], - displayData: "Span Cell that is very long and will go past the cell limits", + data: "Row Span span", + rowSpan: [2, 4], + displayData: "we want to clip each cell individually rather than form a super clip region", }; } - if (row === 5) { + if (row === 1 && col >= 3 && col <= 5) { return { kind: GridCellKind.Text, allowOverlay: false, data: "Span Cell that is very long and will go past the cell limits", - span: [0, 99], + span: [3, 5], displayData: "Span Cell that is very long and will go past the cell limits", }; } @@ -71,7 +71,6 @@ export const SpanCell: React.VFC = () => { const getCellsForSelection = React.useCallback( (selection: Rectangle): CellArray => { const result: GridCell[][] = []; - for (let y = selection.y; y < selection.y + selection.height; y++) { const row: GridCell[] = []; for (let x = selection.x; x < selection.x + selection.width; x++) { 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 e4dd9a75f..05f41e9cd 100644 --- a/packages/core/src/internal/data-grid/data-grid-types.ts +++ b/packages/core/src/internal/data-grid/data-grid-types.ts @@ -311,6 +311,7 @@ export interface BaseGridCell { readonly style?: "normal" | "faded"; readonly themeOverride?: Partial; readonly span?: readonly [start: number, end: number]; + readonly rowSpan?: readonly [startRow: number, endRow: number]; readonly contentAlign?: "left" | "right" | "center"; readonly cursor?: CSSProperties["cursor"]; readonly copyData?: string; diff --git a/packages/core/src/internal/data-grid/render/data-grid-lib.ts b/packages/core/src/internal/data-grid/render/data-grid-lib.ts index 85729ddf3..b4f53cde4 100644 --- a/packages/core/src/internal/data-grid/render/data-grid-lib.ts +++ b/packages/core/src/internal/data-grid/render/data-grid-lib.ts @@ -76,11 +76,18 @@ export function cellIsSelected(location: Item, cell: InnerGridCell, selection: G if (location[1] !== selection.current.cell[1]) return false; - if (cell.span === undefined) { - return selection.current.cell[0] === location[0]; + if (cell.span !== undefined) { + return selection.current.cell[0] >= cell.span[0] && selection.current.cell[0] <= cell.span[1]; + } + if (cell.rowSpan !== undefined) { + return ( + selection.current.cell[0] === location[0] && + selection.current.cell[1] >= cell.rowSpan[0] && + selection.current.cell[1] <= cell.rowSpan[1] + ); } - return selection.current.cell[0] >= cell.span[0] && selection.current.cell[0] <= cell.span[1]; + return selection.current.cell[0] === location[0]; } export function itemIsInRect(location: Item, rect: Rectangle): boolean { 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..2355fa705 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 @@ -32,7 +32,7 @@ import type { RenderStateProvider } from "../../../common/render-state-provider. import type { ImageWindowLoader } from "../image-window-loader-interface.js"; import { intersectRect } from "../../../common/math.js"; import type { GridMouseGroupHeaderEventArgs } from "../event-args.js"; -import { getSkipPoint, getSpanBounds, walkColumns, walkRowsInCol } from "./data-grid-render.walk.js"; +import { getRowSpanBounds, getSkipPoint, getSpanBounds, walkColumns, walkRowsInCol } from "./data-grid-render.walk.js"; const loadingCell: InnerGridCell = { kind: GridCellKind.Loading, @@ -119,6 +119,7 @@ export function drawCells( freezeTrailingRows > 0 ? getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight) : 0; let result: Rectangle[] | undefined; let handledSpans: Set | undefined = undefined; + let handledRowSpans: Set | undefined = undefined; const skipPoint = getSkipPoint(drawRegions); @@ -220,9 +221,52 @@ export function drawCells( const cell: InnerGridCell = row < rows ? getCellContent(cellIndex) : loadingCell; let cellX = drawX; + let cellY = drawY; + let cellHeight = getRowHeight(row); let cellWidth = c.width; let drawingSpan = false; let skipContents = false; + + if (cell?.rowSpan !== undefined) { + const [start, end] = cell.rowSpan; + const spanKey = `${c.sourceIndex - 1},${start},${end}`; + if (handledRowSpans === undefined) handledRowSpans = new Set(); + if (!handledRowSpans.has(spanKey)) { + const area = getRowSpanBounds( + cell?.rowSpan as Item, + drawX, + drawY, + c.width, + row, + getRowHeight + ); + if (area !== undefined) { + cellY = area.y; + cellHeight = area.height; + handledRowSpans.add(spanKey); + ctx.restore(); + prepResult = undefined; + ctx.save(); + ctx.beginPath(); + ctx.rect(area.x, area.y, area.width, area.height); + if (result === undefined) { + result = []; + } + result.push({ + x: area.x, + y: area.y, + width: area.width, + height: area.height, + }); + ctx.clip(); + drawingSpan = true; + } + } else { + toDraw--; + return; + } + } + if (cell.span !== undefined) { const [startCol, endCol] = cell.span; const spanKey = `${row},${startCol},${endCol},${c.sticky}`; //alloc @@ -280,12 +324,17 @@ export function drawCells( selection.columns.some( index => cell.span !== undefined && index >= cell.span[0] && index <= cell.span[1] //alloc ); + const rowSpanIsHighlighted = + cell.rowSpan !== undefined && + selection.columns.some( + index => cell.rowSpan !== undefined && index >= cell.rowSpan[0] && index <= cell.rowSpan[1] //alloc + ); if (isSelected && !isFocused && drawFocus) { accentCount = 0; } else if (isSelected && drawFocus) { accentCount = Math.max(accentCount, 1); } - if (spanIsHighlighted) { + if (spanIsHighlighted || rowSpanIsHighlighted) { accentCount++; } if (!isSelected) { @@ -336,15 +385,15 @@ export function drawCells( // we want to clip each cell individually rather than form a super clip region. The reason for // 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 top = cellY + 1; const bottom = isSticky - ? top + rh - 1 - : Math.min(top + rh - 1, height - freezeTrailingRowsHeight); + ? top + cellHeight - 1 + : Math.min(top + cellHeight - 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 // or overlapping the bottom clip area. - if (h !== rh - 1 || cellX + 1 <= clipX) { + if (h !== cellHeight - 1 || cellX + 1 <= clipX) { didDamageClip = true; ctx.save(); ctx.beginPath(); @@ -369,12 +418,12 @@ export function drawCells( // because technically the bottom right corner of the outline are on other cells. ctx.fillRect( cellX + 1, - drawY + 1, + cellY + 1, cellWidth - (isLastColumn ? 2 : 1), - rh - (isLastRow ? 2 : 1) + cellHeight - (isLastRow ? 2 : 1) ); } else { - ctx.fillRect(cellX, drawY, cellWidth, rh); + ctx.fillRect(cellX, cellY, cellWidth, cellHeight); } } @@ -405,9 +454,9 @@ export function drawCells( isLastColumn, isLastRow, cellX, - drawY, + cellY, cellWidth, - rh, + cellHeight, accentCount > 0, theme, fill ?? theme.bgCell, 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..4f52c42d2 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 @@ -142,6 +142,37 @@ export function walkGroups( } } +export function getRowSpanBounds( + rowSpan: Item, + cellX: number, + cellY: number, + cellW: number, + row: number, + getRowHeight: (row: number) => number +): Rectangle | undefined { + const [startRow, endRow] = rowSpan; + const totalSpannedRows = endRow - startRow; + let tempY = cellY; + let tempH = totalSpannedRows * 34; + if (getRowHeight !== undefined) { + tempH = getRowHeight(row); + for (let x = row - 1; x >= startRow; x--) { + tempY -= getRowHeight(x); + tempH += getRowHeight(x); + } + for (let x = row + 1; x <= endRow; x++) { + tempH += getRowHeight(x); + } + } + const contentRect: Rectangle | undefined = { + x: cellX, + y: tempY, + width: cellW, + height: tempH, + }; + return contentRect; +} + export function getSpanBounds( span: Item, cellX: number, 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..a16316d52 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 @@ -5,7 +5,7 @@ import { getStickyWidth, type MappedGridColumn, computeBounds, getFreezeTrailing import { type FullTheme } from "../../../common/styles.js"; import { blend, withAlpha } from "../color-parser.js"; import { hugRectToTarget, intersectRect, rectContains, splitRectIntoRegions } from "../../../common/math.js"; -import { getSpanBounds, walkColumns, walkRowsInCol } from "./data-grid-render.walk.js"; +import { getRowSpanBounds, getSpanBounds, walkColumns, walkRowsInCol } from "./data-grid-render.walk.js"; import { type Highlight } from "./data-grid-render.cells.js"; export function drawHighlightRings( @@ -247,7 +247,18 @@ export function drawFillHandle( if (row !== targetRow && row !== fillHandleRow) return; let cellX = drawX; + let cellY = drawY; let cellWidth = col.width; + let cellHeight = getRowHeight(row); + + if (cell.rowSpan !== undefined) { + const area = getRowSpanBounds(cell.rowSpan, drawX, drawY, col.width, row, getRowHeight); + + if (area !== undefined) { + cellHeight = area.height; + cellY = area.y; + } + } if (cell.span !== undefined) { const areas = getSpanBounds(cell.span, drawX, drawY, col.width, rh, col, allColumns); @@ -269,7 +280,7 @@ export function drawFillHandle( ctx.clip(); } ctx.beginPath(); - ctx.rect(cellX + cellWidth - 4, drawY + rh - 4, 4, 4); + ctx.rect(cellX + cellWidth - 4, cellY + cellHeight - 4, 4, 4); ctx.fillStyle = col.themeOverride?.accentColor ?? theme.accentColor; ctx.fill(); };