diff --git a/examples/example-datagrid/src/index.ts b/examples/example-datagrid/src/index.ts index 114a743fa..5707c5688 100644 --- a/examples/example-datagrid/src/index.ts +++ b/examples/example-datagrid/src/index.ts @@ -30,12 +30,30 @@ import { getKeyboardLayout } from '@lumino/keyboard'; import '../style/index.css'; class MergedCellModel extends DataModel { + constructor() { + super(); + + // Initializing the body groups + for ( + let r = 0, c = 0; + r < this.rowCount('body') && c < this.columnCount('body'); + r += 3, c += 3 + ) { + this.bodyGroups.push({ r1: r, c1: c, r2: r + 2, c2: c + 2 }); + } + + // Initializing the row-header groups + for (let r = 0; r < this.rowCount('body'); r += 2) { + this.rowHeaderGroups.push({ r1: r, c1: 0, r2: r + 1, c2: 1 }); + } + } + rowCount(region: DataModel.RowRegion): number { - return region === 'body' ? 20 : 3; + return region === 'body' ? 500 : 3; } columnCount(region: DataModel.ColumnRegion): number { - return region === 'body' ? 6 : 3; + return region === 'body' ? 500 : 3; } data(region: DataModel.CellRegion, row: number, column: number): any { @@ -53,11 +71,11 @@ class MergedCellModel extends DataModel { groupCount(region: DataModel.RowRegion): number { if (region === 'body') { - return 3; + return this.bodyGroups.length; } else if (region === 'column-header') { return 1; } else if (region === 'row-header') { - return 2; + return this.rowHeaderGroups.length; } else if (region === 'corner-header') { return 1; } @@ -66,11 +84,7 @@ class MergedCellModel extends DataModel { group(region: DataModel.CellRegion, groupIndex: number): CellGroup | null { if (region === 'body') { - return [ - { r1: 1, c1: 1, r2: 2, c2: 2 }, - { r1: 5, c1: 1, r2: 5, c2: 2 }, - { r1: 3, c1: 5, r2: 4, c2: 5 } - ][groupIndex]; + return this.bodyGroups[groupIndex]; } if (region === 'column-header') { @@ -78,10 +92,7 @@ class MergedCellModel extends DataModel { } if (region === 'row-header') { - return [ - { r1: 0, c1: 0, r2: 1, c2: 1 }, - { r1: 4, c1: 0, r2: 5, c2: 0 } - ][groupIndex]; + return this.rowHeaderGroups[groupIndex]; } if (region === 'corner-header') { @@ -90,6 +101,9 @@ class MergedCellModel extends DataModel { return null; } + + rowHeaderGroups: CellGroup[] = []; + bodyGroups: CellGroup[] = []; } class LargeDataModel extends DataModel { diff --git a/packages/datagrid/src/cellgroup.ts b/packages/datagrid/src/cellgroup.ts index 689fb7f62..92a22e265 100644 --- a/packages/datagrid/src/cellgroup.ts +++ b/packages/datagrid/src/cellgroup.ts @@ -4,7 +4,6 @@ */ import { DataModel } from './datamodel'; -import { SectionList } from './sectionlist'; /** * An interface describing a merged cell group. @@ -24,106 +23,6 @@ export interface CellGroup { * A collection of helper functions relating to merged cell groups */ export namespace CellGroup { - export function areCellsMerged( - dataModel: DataModel, - rgn: DataModel.CellRegion, - cell1: number[], - cell2: number[] - ): boolean { - const numGroups = dataModel.groupCount(rgn); - const [row1, column1] = cell1; - const [row2, column2] = cell2; - - for (let i = 0; i < numGroups; i++) { - const group = dataModel.group(rgn, i)!; - if ( - row1 >= group.r1 && - row1 <= group.r2 && - column1 >= group.c1 && - column1 <= group.c2 && - row2 >= group.r1 && - row2 <= group.r2 && - column2 >= group.c1 && - column2 <= group.c2 - ) { - return true; - } - } - return false; - } - - /** - * Calculates the cell boundary offsets needed for - * a row or column at the given index by taking - * into account merged cell groups in the region. - * @param dataModel - * @param regions - * @param axis - * @param sectionList - * @param index - */ - export function calculateMergeOffsets( - dataModel: DataModel, - regions: DataModel.CellRegion[], - axis: 'row' | 'column', - sectionList: SectionList, - index: number - ): [number, number, CellGroup] { - let mergeStartOffset = 0; - let mergeEndOffset = 0; - let mergedCellGroups: CellGroup[] = []; - - for (const region of regions) { - mergedCellGroups = mergedCellGroups.concat( - getCellGroupsAtRegion(dataModel, region) - ); - } - - let groupsAtAxis: CellGroup[] = []; - - if (axis === 'row') { - for (const region of regions) { - groupsAtAxis = groupsAtAxis.concat( - getCellGroupsAtRow(dataModel, region, index) - ); - } - } else { - for (const region of regions) { - groupsAtAxis = groupsAtAxis.concat( - getCellGroupsAtColumn(dataModel, region, index) - ); - } - } - - if (groupsAtAxis.length === 0) { - return [0, 0, { r1: -1, r2: -1, c1: -1, c2: -1 }]; - } - - let joinedGroup = groupsAtAxis[0]; - - for (let g = 0; g < mergedCellGroups.length; g++) { - const group = mergedCellGroups[g]; - if (areCellGroupsIntersectingAtAxis(joinedGroup, group, axis)) { - joinedGroup = joinCellGroups([group, joinedGroup]); - mergedCellGroups.splice(g, 1); - g = 0; - } - } - - let minRow = joinedGroup.r1; - let maxRow = joinedGroup.r2; - - for (let r = index - 1; r >= minRow; r--) { - mergeStartOffset += sectionList.sizeOf(r); - } - - for (let r = index + 1; r <= maxRow; r++) { - mergeEndOffset += sectionList.sizeOf(r); - } - - return [mergeStartOffset, mergeEndOffset, joinedGroup]; - } - /** * Checks if two cell-groups are intersecting * in the given axis. @@ -344,29 +243,6 @@ export namespace CellGroup { return groupsAtColumn; } - /** - * Checks if cell group 1 is above cell group 2. - * @param group1 cell group 1. - * @param group2 cell group 2. - * @returns boolean. - */ - export function isCellGroupAbove( - group1: CellGroup, - group2: CellGroup - ): boolean { - return group2.r2 >= group1.r1; - } - - /** - * Checks if cell group 1 is below cell group 2. - */ - export function isCellGroupBelow( - group1: CellGroup, - group2: CellGroup - ): boolean { - return group2.r1 <= group1.r2; - } - /** * Merges a target cell group with any cell groups * it intersects with at a given row or column. diff --git a/packages/datagrid/src/datagrid.ts b/packages/datagrid/src/datagrid.ts index ed0c69fc9..0159e8a86 100644 --- a/packages/datagrid/src/datagrid.ts +++ b/packages/datagrid/src/datagrid.ts @@ -39,8 +39,6 @@ import { ICellEditorController } from './celleditorcontroller'; -import { JSONExt } from '@lumino/coreutils'; - import { TextRenderer } from './textrenderer'; /** @@ -2146,7 +2144,7 @@ export class DataGrid extends Widget { } // Test whether there is content to blit. - let needBlit = curH > 0 && curH > 0 && width > 0 && height > 0; + let needBlit = curW > 0 && curH > 0 && width > 0 && height > 0; // Copy the valid canvas content into the buffer if needed. if (needBlit) { @@ -3188,16 +3186,6 @@ export class DataGrid extends Widget { return; } - // Render entire grid if scrolling merged cells grid - const paintEverything = Private.shouldPaintEverything(this._dataModel!); - - if (paintEverything) { - this.paintContent(0, 0, vw, vh); - this._paintOverlay(); - this._syncScrollState(); - return; - } - // Compute the size delta. let delta = newSize - oldSize; @@ -3267,6 +3255,47 @@ export class DataGrid extends Widget { this.paintContent(0, vh + delta, vw, -delta); } + // Repaint merged cells that are intersected by the resized row + // Otherwise it will be cut in two by the valid content, and drawn incorrectly + for (const rgn of ['body', 'row-header'] as DataModel.CellRegion[]) { + const cellGroups = CellGroup.getCellGroupsAtRow( + this.dataModel!, + rgn, + index + ); + + let paintRgn = { + region: rgn, + xMin: 0, + xMax: 0, + yMin: 0, + yMax: 0 + }; + + let backgroundColor = undefined; + + switch (rgn) { + case 'body': + paintRgn.xMin = this.headerWidth; + paintRgn.xMax = this.headerWidth + this.bodyWidth; + paintRgn.yMin = this.headerHeight; + paintRgn.yMax = this.headerHeight + this.bodyHeight; + + backgroundColor = this._style.backgroundColor; + break; + case 'row-header': + paintRgn.xMin = 0; + paintRgn.xMax = this.headerWidth; + paintRgn.yMin = this.headerHeight; + paintRgn.yMax = this.headerHeight + this.bodyHeight; + + backgroundColor = this._style.headerBackgroundColor; + break; + } + + this._paintMergedCells(cellGroups, paintRgn, backgroundColor); + } + // Paint the overlay. this._paintOverlay(); @@ -3310,16 +3339,6 @@ export class DataGrid extends Widget { return; } - // Render entire grid if scrolling merged cells grid - const paintEverything = Private.shouldPaintEverything(this._dataModel!); - - if (paintEverything) { - this.paintContent(0, 0, vw, vh); - this._paintOverlay(); - this._syncScrollState(); - return; - } - // Compute the size delta. let delta = newSize - oldSize; @@ -3389,6 +3408,47 @@ export class DataGrid extends Widget { this.paintContent(vw + delta, 0, -delta, vh); } + // Repaint merged cells that are intersected by the resized column + // Otherwise it will be cut in two by the valid content, and drawn incorrectly + for (const rgn of ['body', 'column-header'] as DataModel.CellRegion[]) { + const cellGroups = CellGroup.getCellGroupsAtColumn( + this.dataModel!, + rgn, + index + ); + + let paintRgn = { + region: rgn, + xMin: 0, + xMax: 0, + yMin: 0, + yMax: 0 + }; + + let backgroundColor = undefined; + + switch (rgn) { + case 'body': + paintRgn.xMin = this.headerWidth; + paintRgn.xMax = this.headerWidth + this.bodyWidth; + paintRgn.yMin = this.headerHeight; + paintRgn.yMax = this.headerHeight + this.bodyHeight; + + backgroundColor = this._style.backgroundColor; + break; + case 'column-header': + paintRgn.xMin = this.headerWidth; + paintRgn.xMax = this.headerWidth + this.bodyWidth; + paintRgn.yMin = 0; + paintRgn.yMax = this.headerHeight; + + backgroundColor = this._style.headerBackgroundColor; + break; + } + + this._paintMergedCells(cellGroups, paintRgn, backgroundColor); + } + // Paint the overlay. this._paintOverlay(); @@ -3432,16 +3492,6 @@ export class DataGrid extends Widget { return; } - // Render entire grid if scrolling merged cells grid - const paintEverything = Private.shouldPaintEverything(this._dataModel!); - - if (paintEverything) { - this.paintContent(0, 0, vw, vh); - this._paintOverlay(); - this._syncScrollState(); - return; - } - // Compute the size delta. let delta = newSize - oldSize; @@ -3484,7 +3534,49 @@ export class DataGrid extends Widget { let x = this.headerWidth + this._columnSections.offsetOf(c); this.paintContent(x, 0, vw - x, vh); } else if (delta < 0) { - this.paintContent(vw + delta, 0, -delta + 1, vh); + this.paintContent(vw + delta, 0, -delta, vh); + } + + // Repaint merged cells that are intersected by the resized row + // Otherwise it will be cut in two by the valid content, and drawn incorrectly + for (const rgn of [ + 'corner-header', + 'row-header' + ] as DataModel.CellRegion[]) { + const cellGroups = CellGroup.getCellGroupsAtColumn( + this.dataModel!, + rgn, + index + ); + + let paintRgn = { + region: rgn, + xMin: 0, + xMax: 0, + yMin: 0, + yMax: 0 + }; + + switch (rgn) { + case 'corner-header': + paintRgn.xMin = 0; + paintRgn.xMax = this.headerWidth; + paintRgn.yMin = 0; + paintRgn.yMax = this.headerHeight; + break; + case 'row-header': + paintRgn.xMin = 0; + paintRgn.xMax = this.headerWidth; + paintRgn.yMin = this.headerHeight; + paintRgn.yMax = this.headerHeight + this.bodyHeight; + break; + } + + this._paintMergedCells( + cellGroups, + paintRgn, + this._style.headerBackgroundColor + ); } // Paint the overlay. @@ -3530,16 +3622,6 @@ export class DataGrid extends Widget { return; } - // Render entire grid if scrolling merged cells grid - const paintEverything = Private.shouldPaintEverything(this._dataModel!); - - if (paintEverything) { - this.paintContent(0, 0, vw, vh); - this._paintOverlay(); - this._syncScrollState(); - return; - } - // Paint the overlay. this._paintOverlay(); @@ -3585,7 +3667,49 @@ export class DataGrid extends Widget { let y = this.headerHeight + this._rowSections.offsetOf(r); this.paintContent(0, y, vw, vh - y); } else if (delta < 0) { - this.paintContent(0, vh + delta, vw, -delta + 1); + this.paintContent(0, vh + delta, vw, -delta); + } + + // Repaint merged cells that are intersected by the resized row + // Otherwise it will be cut in two by the valid content, and drawn incorrectly + for (const rgn of [ + 'corner-header', + 'column-header' + ] as DataModel.CellRegion[]) { + const cellGroups = CellGroup.getCellGroupsAtRow( + this.dataModel!, + rgn, + index + ); + + let paintRgn = { + region: rgn, + xMin: 0, + xMax: 0, + yMin: 0, + yMax: 0 + }; + + switch (rgn) { + case 'corner-header': + paintRgn.xMin = 0; + paintRgn.xMax = this.headerWidth; + paintRgn.yMin = 0; + paintRgn.yMax = this.headerHeight; + break; + case 'column-header': + paintRgn.xMin = this.headerWidth; + paintRgn.xMax = this.headerWidth + this.bodyWidth; + paintRgn.yMin = 0; + paintRgn.yMax = this.headerHeight; + break; + } + + this._paintMergedCells( + cellGroups, + paintRgn, + this._style.headerBackgroundColor + ); } // Paint the overlay. @@ -3704,6 +3828,46 @@ export class DataGrid extends Widget { width, Math.abs(dy) ); + + // Repaint merged cells that are intersected by the scroll level + // Otherwise it will be cut in two by the valid content, and drawn incorrectly + for (const rgn of ['body', 'row-header'] as DataModel.CellRegion[]) { + const cellgroups = CellGroup.getCellGroupsAtRegion( + this.dataModel, + rgn + ); + + let paintRgn = { + region: rgn, + xMin: 0, + xMax: 0, + yMin: 0, + yMax: 0 + }; + + let backgroundColor = undefined; + + switch (rgn) { + case 'body': + paintRgn.xMin = this.headerWidth; + paintRgn.xMax = this.headerWidth + this.bodyWidth; + paintRgn.yMin = this.headerHeight; + paintRgn.yMax = this.headerHeight + this.bodyHeight; + + backgroundColor = this._style.backgroundColor; + break; + case 'row-header': + paintRgn.xMin = 0; + paintRgn.xMax = this.headerWidth; + paintRgn.yMin = this.headerHeight; + paintRgn.yMax = this.headerHeight + this.bodyHeight; + + backgroundColor = this._style.headerBackgroundColor; + break; + } + + this._paintMergedCells(cellgroups, paintRgn, backgroundColor); + } } } @@ -3728,6 +3892,46 @@ export class DataGrid extends Widget { Math.abs(dx), height ); + + // Repaint merged cells that are intersected by the scroll level + // Otherwise it will be cut in two by the valid content, and drawn incorrectly + for (const rgn of ['body', 'column-header'] as DataModel.CellRegion[]) { + const cellGroups = CellGroup.getCellGroupsAtRegion( + this.dataModel, + rgn + ); + + let paintRgn = { + region: rgn, + xMin: 0, + xMax: 0, + yMin: 0, + yMax: 0 + }; + + let backgroundColor = undefined; + + switch (rgn) { + case 'body': + paintRgn.xMin = this.headerWidth; + paintRgn.xMax = this.headerWidth + this.bodyWidth; + paintRgn.yMin = this.headerHeight; + paintRgn.yMax = this.headerHeight + this.bodyHeight; + + backgroundColor = this._style.backgroundColor; + break; + case 'column-header': + paintRgn.xMin = this.headerWidth; + paintRgn.xMax = this.headerWidth + this.bodyWidth; + paintRgn.yMin = 0; + paintRgn.yMax = this.headerHeight; + + backgroundColor = this._style.headerBackgroundColor; + break; + } + + this._paintMergedCells(cellGroups, paintRgn, backgroundColor); + } } } @@ -4124,6 +4328,17 @@ export class DataGrid extends Widget { rgn, this._style.verticalGridLineColor || this._style.gridLineColor ); + + // Get the cellgroups from the cell-region that intersects with the paint region + const cellGroups = CellGroup.getCellGroupsAtRegion( + this.dataModel!, + rgn.region + ).filter(group => { + return this.cellGroupInteresectsRegion(group, rgn); + }); + + // Draw merged cells + this._paintMergedCells(cellGroups, rgn, this._style.backgroundColor); } /** @@ -4259,6 +4474,17 @@ export class DataGrid extends Widget { rgn, this._style.headerVerticalGridLineColor || this._style.headerGridLineColor ); + + // Get the cellgroups from the cell-region that intersects with the paint region + const cellGroups = CellGroup.getCellGroupsAtRegion( + this.dataModel!, + rgn.region + ).filter(group => { + return this.cellGroupInteresectsRegion(group, rgn); + }); + + // Draw merged cells + this._paintMergedCells(cellGroups, rgn, this._style.headerBackgroundColor); } /** @@ -4394,6 +4620,17 @@ export class DataGrid extends Widget { rgn, this._style.headerVerticalGridLineColor || this._style.headerGridLineColor ); + + // Get the cellgroups from the cell-region that intersects with the paint region + const cellGroups = CellGroup.getCellGroupsAtRegion( + this.dataModel!, + rgn.region + ).filter(group => { + return this.cellGroupInteresectsRegion(group, rgn); + }); + + // Draw merged cells + this._paintMergedCells(cellGroups, rgn, this._style.headerBackgroundColor); } /** @@ -4513,6 +4750,17 @@ export class DataGrid extends Widget { rgn, this._style.headerVerticalGridLineColor || this._style.headerGridLineColor ); + + // Get the cellgroups from the cell-region that intersects with the paint region + const cellGroups = CellGroup.getCellGroupsAtRegion( + this.dataModel!, + rgn.region + ).filter(group => { + return this.cellGroupInteresectsRegion(group, rgn); + }); + + // Draw merged cells + this._paintMergedCells(cellGroups, rgn, this._style.headerBackgroundColor); } /** @@ -4652,47 +4900,6 @@ export class DataGrid extends Widget { return; } - // Determine if the cell intersects with a merged group at row or column - let intersectingColumnGroups = CellGroup.getCellGroupsAtColumn( - this._dataModel!, - rgn.region, - rgn.column - ); - let intersectingRowGroups = CellGroup.getCellGroupsAtRow( - this._dataModel!, - rgn.region, - rgn.row - ); - - // move the bounds of the region if edges of the region are part of a merge group. - // after the move, new region contains entirety of the merge groups - rgn = JSONExt.deepCopy(rgn); - - const joinedGroup = CellGroup.joinCellGroupWithMergedCellGroups( - this.dataModel!, - { - r1: rgn.row, - r2: rgn.row + rgn.rowSizes.length - 1, - c1: rgn.column, - c2: rgn.column + rgn.columnSizes.length - 1 - }, - rgn.region - ); - - for (let r = joinedGroup.r1; r < rgn.row; r++) { - const h = this._getRowSize(rgn.region, r); - rgn.y -= h; - rgn.rowSizes = [h].concat(rgn.rowSizes); - } - rgn.row = joinedGroup.r1; - - for (let c = joinedGroup.c1; c < rgn.column; c++) { - const w = this._getColumnSize(rgn.region, c); - rgn.x -= w; - rgn.columnSizes = [w].concat(rgn.columnSizes); - } - rgn.column = joinedGroup.c1; - // Set up the cell config object for rendering. let config = { x: 0, @@ -4718,9 +4925,6 @@ export class DataGrid extends Widget { // Loop over the columns in the region. for (let x = rgn.x, i = 0, n = rgn.columnSizes.length; i < n; ++i) { - let xOffset = 0; - let yOffset = 0; - // Fetch the size of the column. let width = rgn.columnSizes[i]; @@ -4729,8 +4933,6 @@ export class DataGrid extends Widget { continue; } - xOffset = width; - // Compute the column index. let column = rgn.column + i; @@ -4758,44 +4960,16 @@ export class DataGrid extends Widget { row, column ); - yOffset = height; - /** - * For merged cell regions, only rendering the merged region - * if the "parent" cell is the one being painted. Bail otherwise. - */ + // For merged cell regions, don't do anything, we draw merged regions later. if (groupIndex !== -1) { - const group = this.dataModel!.group(config.region, groupIndex)!; - if (group.r1 === row && group.c1 === column) { - width = 0; - for (let c = group.c1; c <= group.c2; c++) { - width += this._getColumnSize(config.region, c); - } - - height = 0; - for (let r = group.r1; r <= group.r2; r++) { - height += this._getRowSize(config.region, r); - } - } else { - y += yOffset; - continue; - } - } else { - /** - * Reset column width if we're rendering a column-header - * which is not part of a merged cell group. - */ - if (rgn.region == 'column-header') { - width = rgn.columnSizes[i]; - } + y += height; + continue; } // Clear the buffer rect for the cell. gc.clearRect(x, y, width, height); - // Save the GC state. - gc.save(); - // Get the value for the cell. let value: any; try { @@ -4846,42 +5020,197 @@ export class DataGrid extends Widget { let y1 = Math.max(rgn.yMin, config.y); let y2 = Math.min(config.y + config.height - 1, rgn.yMax); - if ( - intersectingColumnGroups.length !== 0 || - intersectingRowGroups.length !== 0 - ) { - if (x2 > x1 && y2 > y1) { - this._blitContent( - this._buffer, - x1, - y1, - x2 - x1 + 1, - y2 - y1 + 1, - x1, - y1 - ); - } - } else { - this._blitContent( - this._buffer, - x1, - y1, - x2 - x1 + 1, - y2 - y1 + 1, - x1, - y1 - ); - } + this._blitContent( + this._buffer, + x1, + y1, + x2 - x1 + 1, + y2 - y1 + 1, + x1, + y1 + ); // Increment the running Y coordinate. - y += yOffset; + y += height; } // Restore the GC state. gc.restore(); // Increment the running X coordinate. - x += xOffset; + x += width; + } + + // Dispose of the wrapped gc. + gc.dispose(); + + // Restore the final buffer gc state. + this._bufferGC.restore(); + } + + // TODO Move this in the utils file (but we need the PaintRegion typing) + private cellGroupInteresectsRegion( + group: CellGroup, + rgn: Private.PaintRegion + ) { + const rgnR1 = rgn.row; + const rgnR2 = rgn.row + rgn.rowSizes.length; + + const rgnC1 = rgn.column; + const rgnC2 = rgn.column + rgn.columnSizes.length; + + const dx = Math.min(group.r2, rgnR2) - Math.max(group.r1, rgnR1); + const dy = Math.min(group.c2, rgnC2) - Math.max(group.c1, rgnC1); + + return dx >= 0 && dy >= 0; + } + + /** + * Paint group cells. + */ + private _paintMergedCells( + cellGroups: CellGroup[], + rgn: Private.PixelRegion, + backgroundColor: string | undefined + ): void { + // Bail if there is no data model. + if (!this._dataModel) { + return; + } + + // Set up the cell config object for rendering. + let config = { + x: 0, + y: 0, + width: 0, + height: 0, + region: rgn.region, + row: 0, + column: 0, + value: null as any, + metadata: DataModel.emptyMetadata + }; + + if (backgroundColor) { + this._canvasGC.fillStyle = backgroundColor; + } + // Set the line width for the grid lines. + this._canvasGC.lineWidth = 1; + + // Save the buffer gc before wrapping. + this._bufferGC.save(); + + // Wrap the buffer gc for painting the cells. + let gc = new GraphicsContext(this._bufferGC); + + for (const group of cellGroups) { + let width = 0; + for (let c = group.c1; c <= group.c2; c++) { + width += this._getColumnSize(rgn.region, c); + } + + let height = 0; + for (let r = group.r1; r <= group.r2; r++) { + height += this._getRowSize(rgn.region, r); + } + + let value: any; + try { + value = this._dataModel.data(rgn.region, group.r1, group.c1); + } catch (err) { + value = undefined; + console.error(err); + } + + // Get the metadata for the cell. + let metadata: DataModel.Metadata; + try { + metadata = this._dataModel.metadata(rgn.region, group.r1, group.c1); + } catch (err) { + metadata = DataModel.emptyMetadata; + console.error(err); + } + + let x = 0; + let y = 0; + switch (rgn.region) { + case 'body': + x = + this._columnSections.offsetOf(group.c1) + + this.headerWidth - + this._scrollX; + y = + this._rowSections.offsetOf(group.r1) + + this.headerHeight - + this._scrollY; + break; + case 'column-header': + x = + this._columnSections.offsetOf(group.c1) + + this.headerWidth - + this._scrollX; + y = this._rowSections.offsetOf(group.r1); + break; + case 'row-header': + x = this._columnSections.offsetOf(group.c1); + y = + this._rowSections.offsetOf(group.r1) + + this.headerHeight - + this._scrollY; + break; + case 'corner-header': + x = this._columnSections.offsetOf(group.c1); + y = this._rowSections.offsetOf(group.r1); + break; + } + + config.x = x; + config.y = y; + config.width = width; + config.height = height; + config.region = rgn.region; + config.row = group.r1; + config.column = group.c1; + config.value = value; + config.metadata = metadata; + + // Compute the actual X bounds for the cell. + const x1 = Math.max(rgn.xMin, x); + const x2 = Math.min(x + width - 2, rgn.xMax); + + // Compute the actual Y bounds for the cell. + const y1 = Math.max(rgn.yMin, y); + const y2 = Math.min(y + height - 2, rgn.yMax); + + if (x2 <= x1 || y2 <= y1) { + continue; + } + + // Draw the background. + if (backgroundColor) { + this._canvasGC.fillRect(x1, y1, x2 - x1 + 1, y2 - y1 + 1); + } + + // Get the renderer for the cell. + let renderer = this._cellRenderers.get(config); + + // Clear the buffer rect for the cell. + gc.clearRect(config.x, config.y, width, height); + + // Save the GC state. + gc.save(); + + // Paint the cell into the off-screen buffer. + try { + renderer.paint(gc, config); + } catch (err) { + console.error(err); + } + + // Restore the GC state. + gc.restore(); + + this._blitContent(this._buffer, x1, y1, x2 - x1 + 1, y2 - y1 + 1, x1, y1); } // Dispose of the wrapped gc. @@ -4904,7 +5233,8 @@ export class DataGrid extends Widget { } // Compute the X bounds for the horizontal lines. - let x1 = Math.max(rgn.xMin, rgn.x); + const x1 = Math.max(rgn.xMin, rgn.x); + const x2 = Math.min(rgn.x + rgn.width, rgn.xMax + 1); // Begin the path for the grid lines. this._canvasGC.beginPath(); @@ -4913,8 +5243,8 @@ export class DataGrid extends Widget { this._canvasGC.lineWidth = 1; // Fetch the geometry. - let bh = this.bodyHeight; - let ph = this.pageHeight; + const bh = this.bodyHeight; + const ph = this.pageHeight; // Fetch the number of grid lines to be drawn. let n = rgn.rowSizes.length; @@ -4936,63 +5266,13 @@ export class DataGrid extends Widget { continue; } - let xStart = 0; - let lineStarted = false; - let lines = []; - let leftCurrent = x1; - - for (let c = rgn.column; c < rgn.column + rgn.columnSizes.length; c++) { - const cIndex = c - rgn.column; - const cellUp = [rgn.row + j, c]; - const cellDown = [rgn.row + j + 1, c]; - - if ( - CellGroup.areCellsMerged( - this.dataModel!, - rgn.region, - cellUp, - cellDown - ) - ) { - if (lineStarted) { - lines.push([xStart, leftCurrent]); - } - lineStarted = false; - } else { - if (!lineStarted) { - lineStarted = true; - xStart = leftCurrent; - } - } - - leftCurrent += rgn.columnSizes[cIndex]; - if (c === rgn.column) { - leftCurrent -= rgn.xMin - rgn.x; - } - } - - if (lineStarted) { - lines.push([xStart, rgn.xMax + 1]); - } - // Compute the Y position of the line. let pos = y + size - 1; // Draw the line if it's in range of the dirty rect. if (pos >= rgn.yMin && pos <= rgn.yMax) { - // Render entire grid if scrolling merged cells grid - const extendLines = Private.shouldPaintEverything(this._dataModel!); - if (extendLines) { - for (const line of lines) { - const [x1, x2] = line; - this._canvasGC.moveTo(x1, pos + 0.5); - this._canvasGC.lineTo(x2, pos + 0.5); - } - } else { - const x2 = Math.min(rgn.x + rgn.width, rgn.xMax + 1); - this._canvasGC.moveTo(x1, pos + 0.5); - this._canvasGC.lineTo(x2, pos + 0.5); - } + this._canvasGC.moveTo(x1, pos + 0.5); + this._canvasGC.lineTo(x2, pos + 0.5); } // Increment the running Y coordinate. @@ -5017,7 +5297,8 @@ export class DataGrid extends Widget { } // Compute the Y bounds for the vertical lines. - let y1 = Math.max(rgn.yMin, rgn.y); + const y1 = Math.max(rgn.yMin, rgn.y); + const y2 = Math.min(rgn.y + rgn.height, rgn.yMax + 1); // Begin the path for the grid lines this._canvasGC.beginPath(); @@ -5026,8 +5307,8 @@ export class DataGrid extends Widget { this._canvasGC.lineWidth = 1; // Fetch the geometry. - let bw = this.bodyWidth; - let pw = this.pageWidth; + const bw = this.bodyWidth; + const pw = this.pageWidth; // Fetch the number of grid lines to be drawn. let n = rgn.columnSizes.length; @@ -5049,63 +5330,13 @@ export class DataGrid extends Widget { continue; } - let yStart = 0; - let lineStarted = false; - let lines = []; - let topCurrent = y1; - - for (let r = rgn.row; r < rgn.row + rgn.rowSizes.length; r++) { - const rIndex = r - rgn.row; - const cellLeft = [r, rgn.column + i]; - const cellRight = [r, rgn.column + i + 1]; - - if ( - CellGroup.areCellsMerged( - this.dataModel!, - rgn.region, - cellLeft, - cellRight - ) - ) { - if (lineStarted) { - lines.push([yStart, topCurrent]); - } - lineStarted = false; - } else { - if (!lineStarted) { - lineStarted = true; - yStart = topCurrent; - } - } - - topCurrent += rgn.rowSizes[rIndex]; - if (r === rgn.row) { - topCurrent -= rgn.yMin - rgn.y; - } - } - - if (lineStarted) { - lines.push([yStart, rgn.yMax + 1]); - } - // Compute the X position of the line. let pos = x + size - 1; // Draw the line if it's in range of the dirty rect. if (pos >= rgn.xMin && pos <= rgn.xMax) { - // Render entire grid if scrolling merged cells grid - const extendLines = Private.shouldPaintEverything(this._dataModel!); - if (extendLines) { - for (const line of lines) { - // this._canvasGC.strokeStyle = color; - this._canvasGC.moveTo(pos + 0.5, line[0]); - this._canvasGC.lineTo(pos + 0.5, line[1]); - } - } else { - let y2 = Math.min(rgn.y + rgn.height, rgn.yMax + 1); - this._canvasGC.moveTo(pos + 0.5, y1); - this._canvasGC.lineTo(pos + 0.5, y2); - } + this._canvasGC.moveTo(pos + 0.5, y1); + this._canvasGC.lineTo(pos + 0.5, y2); } // Increment the running X coordinate. @@ -6398,34 +6629,6 @@ namespace Private { return canvas; } - /** - * A function to check whether the entire grid should be rendered - * when dealing with merged cell regions. - * @param dataModel grid's data model. - * @returns boolean. - */ - export function shouldPaintEverything(dataModel: DataModel): boolean { - const colGroups = CellGroup.getCellGroupsAtRegion( - dataModel!, - 'column-header' - ); - const rowHeaderGroups = CellGroup.getCellGroupsAtRegion( - dataModel!, - 'row-header' - ); - const cornerHeaderGroups = CellGroup.getCellGroupsAtRegion( - dataModel!, - 'corner-header' - ); - const bodyGroups = CellGroup.getCellGroupsAtRegion(dataModel!, 'body'); - return ( - colGroups.length > 0 || - rowHeaderGroups.length > 0 || - cornerHeaderGroups.length > 0 || - bodyGroups.length > 0 - ); - } - /** * Checks whether a given regions has merged cells in it. * @param dataModel grid's data model. @@ -6441,9 +6644,9 @@ namespace Private { } /** - * An object which represents a region to be painted. + * An object which represents a canvas region in pixels. */ - export type PaintRegion = { + export type PixelRegion = { /** * The min X coordinate the of the dirty viewport rect. * @@ -6476,6 +6679,16 @@ namespace Private { */ yMax: number; + /** + * The cell region. + */ + region: DataModel.CellRegion; + }; + + /** + * An object which represents a region to be painted. + */ + export type PaintRegion = PixelRegion & { /** * The X coordinate the of the region, in viewport coordinates. * @@ -6508,11 +6721,6 @@ namespace Private { */ height: number; - /** - * The cell region being painted. - */ - region: DataModel.CellRegion; - /** * The row index of the first cell in the region. */ diff --git a/review/api/datagrid.api.md b/review/api/datagrid.api.md index 9197a5041..ebd1c1c39 100644 --- a/review/api/datagrid.api.md +++ b/review/api/datagrid.api.md @@ -131,16 +131,11 @@ export interface CellGroup { export namespace CellGroup { export function areCellGroupsIntersecting(group1: CellGroup, group2: CellGroup): boolean; export function areCellGroupsIntersectingAtAxis(group1: CellGroup, group2: CellGroup, axis: 'row' | 'column'): boolean; - // (undocumented) - export function areCellsMerged(dataModel: DataModel, rgn: DataModel.CellRegion, cell1: number[], cell2: number[]): boolean; - export function calculateMergeOffsets(dataModel: DataModel, regions: DataModel.CellRegion[], axis: 'row' | 'column', sectionList: SectionList, index: number): [number, number, CellGroup]; export function getCellGroupsAtColumn(dataModel: DataModel, rgn: DataModel.CellRegion, column: number): CellGroup[]; export function getCellGroupsAtRegion(dataModel: DataModel, rgn: DataModel.CellRegion): CellGroup[]; export function getCellGroupsAtRow(dataModel: DataModel, rgn: DataModel.CellRegion, row: number): CellGroup[]; export function getGroup(dataModel: DataModel, rgn: DataModel.CellRegion, row: number, column: number): CellGroup | null; export function getGroupIndex(dataModel: DataModel, rgn: DataModel.CellRegion, row: number, column: number): number; - export function isCellGroupAbove(group1: CellGroup, group2: CellGroup): boolean; - export function isCellGroupBelow(group1: CellGroup, group2: CellGroup): boolean; export function joinCellGroups(groups: CellGroup[]): CellGroup; export function joinCellGroupsIntersectingAtAxis(dataModel: DataModel, regions: DataModel.CellRegion[], axis: 'row' | 'column', group: CellGroup): CellGroup; export function joinCellGroupWithMergedCellGroups(dataModel: DataModel, group: CellGroup, region: DataModel.CellRegion): CellGroup;