From 80e3fb78e490644dcfd47a68b92a46ec0baf7c61 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 25 Aug 2024 06:15:22 -0700 Subject: [PATCH 01/14] initial UI work --- .../gridGL/UI/gridHeadings/GridHeadings.ts | 29 ++++++++++++++++--- quadratic-client/src/app/theme/colors.ts | 1 + 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts index bc392fd952..61c6a97264 100644 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts @@ -87,8 +87,18 @@ export class GridHeadings extends Container { const cursor = sheets.sheet.cursor; this.headingsGraphics.lineStyle(0); + if (bounds.left < 0) { + this.headingsGraphics.beginFill(colors.headerOutOfBoundsColor); + this.headingsGraphics.drawRect(bounds.left + this.rowWidth, bounds.top, -bounds.left - this.rowWidth, cellHeight); + this.headingsGraphics.endFill(); + } this.headingsGraphics.beginFill(colors.headerBackgroundColor); - this.columnRect = new Rectangle(bounds.left, bounds.top, bounds.width, cellHeight); + this.columnRect = new Rectangle( + Math.max(0, bounds.left), + bounds.top, + bounds.width - (bounds.left < 0 ? bounds.left : 0), + cellHeight + ); this.headingsGraphics.drawShape(this.columnRect); this.headingsGraphics.endFill(); @@ -197,7 +207,7 @@ export class GridHeadings extends Container { const selected = Array.isArray(this.selectedColumns) ? this.selectedColumns.includes(column) : false; // only show the label if selected or mod calculation - if (selected || mod === 0 || column % mod === 0) { + if (column >= 0 && (selected || mod === 0 || column % mod === 0)) { const charactersWidth = (this.characterSize.width * column.toString().length) / scale; // only show labels that will fit (unless grid lines are hidden) @@ -261,10 +271,21 @@ export class GridHeadings extends Container { (LABEL_PADDING_ROWS / viewport.scale.x) * 2; this.rowWidth = Math.max(this.rowWidth, CELL_HEIGHT / viewport.scale.x); + if (bounds.top < 0) { + this.headingsGraphics.beginFill(colors.headerOutOfBoundsColor); + this.headingsGraphics.drawRect(bounds.left, bounds.top, this.rowWidth, -bounds.left); + this.headingsGraphics.endFill(); + } + // draw background of vertical bar this.headingsGraphics.lineStyle(0); this.headingsGraphics.beginFill(colors.headerBackgroundColor); - this.columnRect = new Rectangle(bounds.left, bounds.top, this.rowWidth, bounds.height); + this.columnRect = new Rectangle( + bounds.left, + bounds.top + (bounds.top < 0 ? -bounds.top : 0), + this.rowWidth, + bounds.height + (bounds.top < 0 ? bounds.top : 0) + ); this.headingsGraphics.drawShape(this.columnRect); this.headingsGraphics.endFill(); this.rowRect = new Rectangle(bounds.left, bounds.top, this.rowWidth, bounds.height); @@ -370,7 +391,7 @@ export class GridHeadings extends Container { const selected = Array.isArray(this.selectedRows) ? this.selectedRows.includes(row) : false; // only show the label if selected or mod calculation - if (selected || mod === 0 || row % mod === 0) { + if (row >= 0 && (selected || mod === 0 || row % mod === 0)) { // only show labels that will fit (unless grid lines are hidden) // if (currentHeight > halfCharacterHeight * 2 || pixiApp.gridLines.alpha < 0.25) { // don't show numbers if it overlaps with the selected value (eg, hides 0 if selected 1 overlaps it) diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index 5b523bafbc..2898b85ce7 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -27,6 +27,7 @@ export const colors = { independence: 0x5d576b, headerBackgroundColor: 0xffffff, + headerOutOfBoundsColor: 0xdddddd, headerSelectedBackgroundColor: 0xe7f7ff, headerSelectedRowColumnBackgroundColor: 0xb6e7ff, headerCornerBackgroundColor: 0xffffff, From 64cb2b63df65b2c47361120b63099325bddbb240 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 25 Aug 2024 07:30:37 -0700 Subject: [PATCH 02/14] remove negative offsets from grid lines and grid headings; add a background class for pixi --- .../src/app/grid/controller/Sheets.ts | 2 +- .../src/app/gridGL/UI/GridLines.ts | 67 ++++++++++++++++--- .../gridGL/UI/gridHeadings/GridHeadings.ts | 49 +++++++++++--- .../src/app/gridGL/pixiApp/PixiApp.ts | 15 +++-- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 2 +- .../src/app/gridGL/pixiApp/Update.ts | 13 ++-- .../src/app/gridGL/pixiApp/background.ts | 35 ++++++++++ quadratic-client/src/app/theme/colors.ts | 3 +- 8 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/pixiApp/background.ts diff --git a/quadratic-client/src/app/grid/controller/Sheets.ts b/quadratic-client/src/app/grid/controller/Sheets.ts index 992b08dfa1..2d1d05073d 100644 --- a/quadratic-client/src/app/grid/controller/Sheets.ts +++ b/quadratic-client/src/app/grid/controller/Sheets.ts @@ -187,7 +187,7 @@ class Sheets { this._current = value; pixiApp.viewport.dirty = true; pixiApp.gridLines.dirty = true; - pixiApp.axesLines.dirty = true; + // pixiApp.axesLines.dirty = true; pixiApp.headings.dirty = true; pixiApp.cursor.dirty = true; pixiApp.multiplayerCursor.dirty = true; diff --git a/quadratic-client/src/app/gridGL/UI/GridLines.ts b/quadratic-client/src/app/gridGL/UI/GridLines.ts index 5d12590f7e..3650b2befa 100644 --- a/quadratic-client/src/app/gridGL/UI/GridLines.ts +++ b/quadratic-client/src/app/gridGL/UI/GridLines.ts @@ -26,7 +26,6 @@ export class GridLines extends Graphics { gridLinesY: GridLine[] = []; draw(bounds: Rectangle): void { - this.lineStyle({ width: 1, color: colors.gridLines, alpha: 0.125, alignment: 0.5, native: false }); const range = this.drawHorizontalLines(bounds); this.drawVerticalLines(bounds, range); this.dirty = false; @@ -53,7 +52,7 @@ export class GridLines extends Graphics { this.alpha = gridAlpha; this.visible = true; - this.lineStyle(1, colors.gridLines, 0.125, 0.5, true); + this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); this.gridLinesX = []; this.gridLinesY = []; const range = this.drawHorizontalLines(bounds); @@ -71,7 +70,20 @@ export class GridLines extends Graphics { let column = index; const offset = bounds.left - position; let size = 0; - for (let x = bounds.left; x <= bounds.right + size - 1; x += size) { + + // draw negative space + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + let x = bounds.left; + while (column < 0) { + this.moveTo(x - offset, bounds.top); + this.lineTo(x - offset, bounds.bottom); + size = sheets.sheet.offsets.getColumnWidth(column); + x += size; + column++; + } + + // draw positive space + while (x < bounds.right + size - 1) { // don't draw grid lines when hidden if (size !== 0) { const lines = gridOverflowLines.getLinesInRange(column, range); @@ -83,12 +95,23 @@ export class GridLines extends Graphics { this.lineTo(x - offset, end); } } else { - this.moveTo(x - offset, bounds.top); - this.lineTo(x - offset, bounds.bottom); + if (bounds.top < 0) { + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.moveTo(x - offset, bounds.top); + this.lineTo(x - offset, 0); + this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); + this.lineTo(x - offset, bounds.bottom); + this.gridLinesX.push({ column, x: x - offset, y: 0, w: 1, h: bounds.bottom }); + } else { + this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); + this.moveTo(x - offset, bounds.top); + this.lineTo(x - offset, bounds.bottom); + this.gridLinesX.push({ column, x: x - offset, y: bounds.top, w: 1, h: bounds.bottom - bounds.top }); + } } - this.gridLinesX.push({ column, x: x - offset, y: bounds.top, w: 1, h: bounds.bottom - bounds.top }); } size = sheets.sheet.offsets.getColumnWidth(column); + x += size; column++; } } @@ -103,14 +126,38 @@ export class GridLines extends Graphics { let row = index; const offset = bounds.top - position; let size = 0; - for (let y = bounds.top; y <= bounds.bottom + size - 1; y += size) { + + // draw negative space + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + let y = bounds.top; + while (row < 0) { + this.moveTo(bounds.left, y - offset); + this.lineTo(bounds.right, y - offset); + size = offsets.getRowHeight(row); + y += size; + row++; + } + + // draw positive space + while (y < bounds.bottom + size - 1) { // don't draw grid lines when hidden if (size !== 0) { - this.moveTo(bounds.left, y - offset); - this.lineTo(bounds.right, y - offset); - this.gridLinesY.push({ row, x: bounds.left, y: y - offset, w: bounds.right - bounds.left, h: 1 }); + if (bounds.left < 0) { + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.moveTo(bounds.left, y - offset); + this.lineTo(0, y - offset); + this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); + this.lineTo(bounds.right, y - offset); + this.gridLinesY.push({ row, x: 0, y: y - offset, w: 1, h: 1 }); + } else { + this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); + this.moveTo(bounds.left, y - offset); + this.lineTo(bounds.right, y - offset); + this.gridLinesY.push({ row, x: bounds.left, y: y - offset, w: bounds.right - bounds.left, h: 1 }); + } } size = offsets.getRowHeight(row); + y += size; row++; } return [index, row - 1]; diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts index 61c6a97264..8ba4101c6f 100644 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts @@ -88,7 +88,7 @@ export class GridHeadings extends Container { this.headingsGraphics.lineStyle(0); if (bounds.left < 0) { - this.headingsGraphics.beginFill(colors.headerOutOfBoundsColor); + this.headingsGraphics.beginFill(colors.outOfBoundsBackgroundColor); this.headingsGraphics.drawRect(bounds.left + this.rowWidth, bounds.top, -bounds.left - this.rowWidth, cellHeight); this.headingsGraphics.endFill(); } @@ -197,7 +197,13 @@ export class GridHeadings extends Container { for (let x = leftOffset; x <= rightOffset; x += currentWidth) { currentWidth = offsets.getColumnWidth(column); if (gridAlpha !== 0) { - this.headingsGraphics.lineStyle(1, colors.gridLines, 0.25 * gridAlpha, 0.5, true); + this.headingsGraphics.lineStyle( + 1, + x < 0 ? colors.gridLinesOutOfBounds : colors.gridLines, + gridAlpha, + 0.5, + true + ); this.headingsGraphics.moveTo(x, bounds.top); this.headingsGraphics.lineTo(x, bounds.top + cellHeight); this.gridLinesColumns.push({ column: column - 1, x, width: offsets.getColumnWidth(column - 1) }); @@ -272,7 +278,7 @@ export class GridHeadings extends Container { this.rowWidth = Math.max(this.rowWidth, CELL_HEIGHT / viewport.scale.x); if (bounds.top < 0) { - this.headingsGraphics.beginFill(colors.headerOutOfBoundsColor); + this.headingsGraphics.beginFill(colors.outOfBoundsBackgroundColor); this.headingsGraphics.drawRect(bounds.left, bounds.top, this.rowWidth, -bounds.left); this.headingsGraphics.endFill(); } @@ -381,7 +387,13 @@ export class GridHeadings extends Container { for (let y = topOffset; y <= bottomOffset; y += currentHeight) { currentHeight = offsets.getRowHeight(row); if (gridAlpha !== 0) { - this.headingsGraphics.lineStyle(1, colors.gridLines, 0.25 * gridAlpha, 0.5, true); + this.headingsGraphics.lineStyle( + 1, + y < 0 ? colors.gridLinesOutOfBounds : colors.gridLines, + gridAlpha, + 0.5, + true + ); this.headingsGraphics.moveTo(bounds.left, y); this.headingsGraphics.lineTo(bounds.left + this.rowWidth, y); this.gridLinesRows.push({ row: row - 1, y, height: offsets.getRowHeight(row - 1) }); @@ -447,11 +459,30 @@ export class GridHeadings extends Container { const { viewport } = pixiApp; const cellHeight = CELL_HEIGHT / viewport.scale.x; const bounds = viewport.getVisibleBounds(); - this.headingsGraphics.lineStyle(1, colors.gridLines, 0.25, 0.5, true); - this.headingsGraphics.moveTo(bounds.left + this.rowWidth, viewport.top); - this.headingsGraphics.lineTo(bounds.left + this.rowWidth, viewport.bottom); - this.headingsGraphics.moveTo(bounds.left, bounds.top + cellHeight); - this.headingsGraphics.lineTo(bounds.right, bounds.top + cellHeight); + + // draw horizontal line + if (bounds.left < 0) { + this.headingsGraphics.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.headingsGraphics.moveTo(bounds.left, bounds.top + cellHeight); + this.headingsGraphics.lineTo(0, bounds.top + cellHeight); + } + if (bounds.right > 0) { + this.headingsGraphics.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); + this.headingsGraphics.moveTo(0, bounds.top + cellHeight); + this.headingsGraphics.lineTo(bounds.right, bounds.top + cellHeight); + } + + // draw vertical line + if (bounds.top < 0) { + this.headingsGraphics.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.headingsGraphics.moveTo(bounds.left + this.rowWidth, bounds.top); + this.headingsGraphics.lineTo(bounds.left + this.rowWidth, 0); + } + if (bounds.bottom > 0) { + this.headingsGraphics.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); + this.headingsGraphics.moveTo(bounds.left + this.rowWidth, 0); + this.headingsGraphics.lineTo(bounds.left + this.rowWidth, bounds.bottom); + } } update(viewportDirty: boolean) { diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index 617502867e..ece4b8c78b 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -33,6 +33,7 @@ import { Viewport } from './Viewport'; import './pixiApp.css'; import { urlParams } from './urlParams/urlParams'; import { UIValidations } from '../UI/UIValidations'; +import { Background } from './background'; utils.skipHello(); @@ -51,6 +52,7 @@ export class PixiApp { viewport!: Viewport; gridLines!: GridLines; axesLines!: AxesLines; + background: Background; cursor!: Cursor; cellHighlights!: CellHighlights; multiplayerCursor!: UIMultiPlayerCursor; @@ -84,6 +86,7 @@ export class PixiApp { this.cellsSheets = new CellsSheets(); this.cellImages = new UICellImages(); this.validations = new UIValidations(); + this.background = new Background(); this.viewport = new Viewport(); } @@ -142,10 +145,10 @@ export class PixiApp { // useful for debugging at viewport locations this.debug = this.viewportContents.addChild(new Graphics()); - + this.background = this.viewportContents.addChild(this.background); this.cellsSheets = this.viewportContents.addChild(this.cellsSheets); this.gridLines = this.viewportContents.addChild(new GridLines()); - this.axesLines = this.viewportContents.addChild(new AxesLines()); + // this.axesLines = this.viewportContents.addChild(new AxesLines()); this.boxCells = this.viewportContents.addChild(new BoxCells()); this.cellImages = this.viewportContents.addChild(this.cellImages); this.multiplayerCursor = this.viewportContents.addChild(new UIMultiPlayerCursor()); @@ -186,7 +189,7 @@ export class PixiApp { viewportChanged = (): void => { this.viewport.dirty = true; this.gridLines.dirty = true; - this.axesLines.dirty = true; + // this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; @@ -226,7 +229,7 @@ export class PixiApp { this.renderer.resize(width, height); this.viewport.resize(width, height); this.gridLines.dirty = true; - this.axesLines.dirty = true; + // this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; @@ -236,7 +239,7 @@ export class PixiApp { // called before and after a render prepareForCopying(options?: { gridLines?: boolean; cull?: Rectangle }): Container { this.gridLines.visible = options?.gridLines ?? false; - this.axesLines.visible = false; + // this.axesLines.visible = false; this.cursor.visible = false; this.cellHighlights.visible = false; this.multiplayerCursor.visible = false; @@ -285,7 +288,7 @@ export class PixiApp { this.paused = true; this.viewport.dirty = true; this.gridLines.dirty = true; - this.axesLines.dirty = true; + // this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index 98db0cda80..e4c502c997 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -66,7 +66,7 @@ class PixiAppSettings { this.settings = defaultGridSettings; } pixiApp.gridLines.dirty = true; - pixiApp.axesLines.dirty = true; + // pixiApp.axesLines.dirty = true; pixiApp.headings.dirty = true; if ( diff --git a/quadratic-client/src/app/gridGL/pixiApp/Update.ts b/quadratic-client/src/app/gridGL/pixiApp/Update.ts index 922e7327c7..57de0fd257 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Update.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Update.ts @@ -100,7 +100,7 @@ export class Update { let rendererDirty = pixiApp.gridLines.dirty || - pixiApp.axesLines.dirty || + // pixiApp.axesLines.dirty || pixiApp.headings.dirty || pixiApp.boxCells.dirty || pixiApp.multiplayerCursor.dirty || @@ -113,9 +113,9 @@ export class Update { if (rendererDirty && debugShowWhyRendering) { console.log( - `dirty: ${pixiApp.viewport.dirty ? 'viewport ' : ''}${pixiApp.gridLines.dirty ? 'gridLines ' : ''}${ - pixiApp.axesLines.dirty ? 'axesLines ' : '' - }${pixiApp.headings.dirty ? 'headings ' : ''}${pixiApp.cursor.dirty ? 'cursor ' : ''}${ + `dirty: ${pixiApp.viewport.dirty ? 'viewport ' : ''}${pixiApp.gridLines.dirty ? 'gridLines ' : ''} + // ${pixiApp.axesLines.dirty ? 'axesLines ' : ''} + ${pixiApp.headings.dirty ? 'headings ' : ''}${pixiApp.cursor.dirty ? 'cursor ' : ''}${ pixiApp.multiplayerCursor.dirty ? 'multiplayer cursor' : pixiApp.cellImages.dirty ? 'uiImageResize' : '' } ${pixiApp.multiplayerCursor.dirty ? 'multiplayer cursor' : ''}${pixiApp.cellMoving.dirty ? 'cellMoving' : ''}` @@ -125,9 +125,10 @@ export class Update { debugTimeReset(); pixiApp.gridLines.update(); debugTimeCheck('[Update] gridLines'); - pixiApp.axesLines.update(); - debugTimeCheck('[Update] axesLines'); + // pixiApp.axesLines.update(); + // debugTimeCheck('[Update] axesLines'); pixiApp.headings.update(pixiApp.viewport.dirty); + pixiApp.background.update(pixiApp.viewport.dirty); debugTimeCheck('[Update] headings'); pixiApp.boxCells.update(); debugTimeCheck('[Update] boxCells'); diff --git a/quadratic-client/src/app/gridGL/pixiApp/background.ts b/quadratic-client/src/app/gridGL/pixiApp/background.ts new file mode 100644 index 0000000000..3a94b8750c --- /dev/null +++ b/quadratic-client/src/app/gridGL/pixiApp/background.ts @@ -0,0 +1,35 @@ +//! This shows the background for the grid. There are two portions of the +//! background: in-bounds and out-of-bounds. + +import { Graphics } from 'pixi.js'; +import { pixiApp } from './PixiApp'; +import { colors } from '@/app/theme/colors'; + +export class Background extends Graphics { + update(viewportDirty: boolean) { + if (!viewportDirty) return; + this.clear(); + const visibleBounds = pixiApp.viewport.getVisibleBounds(); + if (visibleBounds.left < 0) { + const right = Math.min(0, visibleBounds.right); + const bottom = Math.max(0, visibleBounds.bottom); + this.beginFill(colors.outOfBoundsBackgroundColor); + this.drawRect(visibleBounds.left, 0, right - visibleBounds.left, bottom); + this.endFill(); + } + if (visibleBounds.top < 0) { + const right = Math.max(0, visibleBounds.right); + const bottom = Math.min(0, visibleBounds.bottom); + this.beginFill(colors.outOfBoundsBackgroundColor); + this.drawRect(visibleBounds.left, visibleBounds.top, right - visibleBounds.left, bottom - visibleBounds.top); + this.endFill(); + } + + // draw normal area + this.beginFill(colors.gridBackground); + const x = Math.max(0, visibleBounds.left); + const y = Math.max(0, visibleBounds.top); + this.drawRect(x, y, visibleBounds.right - x, visibleBounds.bottom - y); + this.endFill(); + } +} diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index 2898b85ce7..a7736533b8 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -4,6 +4,7 @@ export const colors = { // Pulled from the CSS theme styles // hsla(from var(--border) h s 20% / a) - a is set in pixi gridLines: 0x233143, + gridLinesOutOfBounds: 0xfefefe, cellFontColor: 0x000000, cellColorUserText: 0x8ecb89, cellColorUserPython: 0x3776ab, @@ -27,7 +28,7 @@ export const colors = { independence: 0x5d576b, headerBackgroundColor: 0xffffff, - headerOutOfBoundsColor: 0xdddddd, + outOfBoundsBackgroundColor: 0xeeeeee, headerSelectedBackgroundColor: 0xe7f7ff, headerSelectedRowColumnBackgroundColor: 0xb6e7ff, headerCornerBackgroundColor: 0xffffff, From cf004b323a8f10757c3fbd37910560a02c5d681f Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 25 Aug 2024 08:00:33 -0700 Subject: [PATCH 03/14] border color experiment --- quadratic-client/src/app/theme/colors.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index a7736533b8..104771a623 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -1,9 +1,7 @@ import * as muiColors from '@mui/material/colors'; export const colors = { - // Pulled from the CSS theme styles - // hsla(from var(--border) h s 20% / a) - a is set in pixi - gridLines: 0x233143, + gridLines: 0xcccccc, gridLinesOutOfBounds: 0xfefefe, cellFontColor: 0x000000, cellColorUserText: 0x8ecb89, From d10c19ff1049fc2f77835ba1c994a977ca6945b2 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 25 Aug 2024 08:06:36 -0700 Subject: [PATCH 04/14] delete AxesLines --- .../src/app/grid/controller/Sheets.ts | 1 - .../src/app/gridGL/UI/AxesLines.ts | 33 ------------------- .../src/app/gridGL/pixiApp/PixiApp.ts | 8 ----- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 1 - .../src/app/gridGL/pixiApp/Update.ts | 4 --- .../ui/menus/CommandPalette/commands/View.tsx | 16 --------- .../menus/TopBar/SubMenus/QuadraticMenu.tsx | 3 -- .../src/shared/hooks/useTheme.tsx | 2 +- 8 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 quadratic-client/src/app/gridGL/UI/AxesLines.ts diff --git a/quadratic-client/src/app/grid/controller/Sheets.ts b/quadratic-client/src/app/grid/controller/Sheets.ts index 2d1d05073d..6414d029ce 100644 --- a/quadratic-client/src/app/grid/controller/Sheets.ts +++ b/quadratic-client/src/app/grid/controller/Sheets.ts @@ -187,7 +187,6 @@ class Sheets { this._current = value; pixiApp.viewport.dirty = true; pixiApp.gridLines.dirty = true; - // pixiApp.axesLines.dirty = true; pixiApp.headings.dirty = true; pixiApp.cursor.dirty = true; pixiApp.multiplayerCursor.dirty = true; diff --git a/quadratic-client/src/app/gridGL/UI/AxesLines.ts b/quadratic-client/src/app/gridGL/UI/AxesLines.ts deleted file mode 100644 index 076744545a..0000000000 --- a/quadratic-client/src/app/gridGL/UI/AxesLines.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { colors } from '@/app/theme/colors'; -import { Graphics } from 'pixi.js'; -import { pixiApp } from '../pixiApp/PixiApp'; -import { pixiAppSettings } from '../pixiApp/PixiAppSettings'; - -export class AxesLines extends Graphics { - dirty = true; - - update() { - if (this.dirty) { - this.dirty = false; - this.clear(); - - if (!pixiAppSettings.showGridAxes) { - this.visible = false; - pixiApp.setViewportDirty(); - return; - } - - this.visible = true; - this.lineStyle(10, colors.gridLines, 0.5, 0, true); - const viewport = pixiApp.viewport; - if (0 >= viewport.left && 0 <= viewport.right) { - this.moveTo(0, viewport.top); - this.lineTo(0, viewport.bottom); - } - if (0 >= viewport.top && 0 <= viewport.bottom) { - this.moveTo(viewport.left, 0); - this.lineTo(viewport.right, 0); - } - } - } -} diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index ece4b8c78b..3585bd9d77 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -18,7 +18,6 @@ import { } from '../../grid/actions/clipboard/clipboard'; import { sheets } from '../../grid/controller/Sheets'; import { htmlCellsHandler } from '../HTMLGrid/htmlCells/htmlCellsHandler'; -import { AxesLines } from '../UI/AxesLines'; import { Cursor } from '../UI/Cursor'; import { GridLines } from '../UI/GridLines'; import { HtmlPlaceholders } from '../UI/HtmlPlaceholders'; @@ -51,7 +50,6 @@ export class PixiApp { canvas!: HTMLCanvasElement; viewport!: Viewport; gridLines!: GridLines; - axesLines!: AxesLines; background: Background; cursor!: Cursor; cellHighlights!: CellHighlights; @@ -148,7 +146,6 @@ export class PixiApp { this.background = this.viewportContents.addChild(this.background); this.cellsSheets = this.viewportContents.addChild(this.cellsSheets); this.gridLines = this.viewportContents.addChild(new GridLines()); - // this.axesLines = this.viewportContents.addChild(new AxesLines()); this.boxCells = this.viewportContents.addChild(new BoxCells()); this.cellImages = this.viewportContents.addChild(this.cellImages); this.multiplayerCursor = this.viewportContents.addChild(new UIMultiPlayerCursor()); @@ -189,7 +186,6 @@ export class PixiApp { viewportChanged = (): void => { this.viewport.dirty = true; this.gridLines.dirty = true; - // this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; @@ -229,7 +225,6 @@ export class PixiApp { this.renderer.resize(width, height); this.viewport.resize(width, height); this.gridLines.dirty = true; - // this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; @@ -239,7 +234,6 @@ export class PixiApp { // called before and after a render prepareForCopying(options?: { gridLines?: boolean; cull?: Rectangle }): Container { this.gridLines.visible = options?.gridLines ?? false; - // this.axesLines.visible = false; this.cursor.visible = false; this.cellHighlights.visible = false; this.multiplayerCursor.visible = false; @@ -255,7 +249,6 @@ export class PixiApp { cleanUpAfterCopying(culled?: boolean): void { this.gridLines.visible = true; - this.axesLines.visible = true; this.cursor.visible = true; this.cellHighlights.visible = true; this.multiplayerCursor.visible = true; @@ -288,7 +281,6 @@ export class PixiApp { this.paused = true; this.viewport.dirty = true; this.gridLines.dirty = true; - // this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index e4c502c997..d3f97d54ec 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -66,7 +66,6 @@ class PixiAppSettings { this.settings = defaultGridSettings; } pixiApp.gridLines.dirty = true; - // pixiApp.axesLines.dirty = true; pixiApp.headings.dirty = true; if ( diff --git a/quadratic-client/src/app/gridGL/pixiApp/Update.ts b/quadratic-client/src/app/gridGL/pixiApp/Update.ts index 57de0fd257..ec176fae44 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Update.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Update.ts @@ -100,7 +100,6 @@ export class Update { let rendererDirty = pixiApp.gridLines.dirty || - // pixiApp.axesLines.dirty || pixiApp.headings.dirty || pixiApp.boxCells.dirty || pixiApp.multiplayerCursor.dirty || @@ -114,7 +113,6 @@ export class Update { if (rendererDirty && debugShowWhyRendering) { console.log( `dirty: ${pixiApp.viewport.dirty ? 'viewport ' : ''}${pixiApp.gridLines.dirty ? 'gridLines ' : ''} - // ${pixiApp.axesLines.dirty ? 'axesLines ' : ''} ${pixiApp.headings.dirty ? 'headings ' : ''}${pixiApp.cursor.dirty ? 'cursor ' : ''}${ pixiApp.multiplayerCursor.dirty ? 'multiplayer cursor' : pixiApp.cellImages.dirty ? 'uiImageResize' : '' } @@ -125,8 +123,6 @@ export class Update { debugTimeReset(); pixiApp.gridLines.update(); debugTimeCheck('[Update] gridLines'); - // pixiApp.axesLines.update(); - // debugTimeCheck('[Update] axesLines'); pixiApp.headings.update(pixiApp.viewport.dirty); pixiApp.background.update(pixiApp.viewport.dirty); debugTimeCheck('[Update] headings'); diff --git a/quadratic-client/src/app/ui/menus/CommandPalette/commands/View.tsx b/quadratic-client/src/app/ui/menus/CommandPalette/commands/View.tsx index c4be170534..d4da741423 100644 --- a/quadratic-client/src/app/ui/menus/CommandPalette/commands/View.tsx +++ b/quadratic-client/src/app/ui/menus/CommandPalette/commands/View.tsx @@ -24,22 +24,6 @@ const commands: CommandGroup = { }, }, - { - label: 'Axis', - Component: (props) => { - const settings = useGridSettings(); - return ( - } - action={() => { - settings.setShowGridAxes(!settings.showGridAxes); - }} - /> - ); - }, - }, - { label: 'Grid lines', Component: (props) => { diff --git a/quadratic-client/src/app/ui/menus/TopBar/SubMenus/QuadraticMenu.tsx b/quadratic-client/src/app/ui/menus/TopBar/SubMenus/QuadraticMenu.tsx index ee2e1f0910..fd6bc0dbc0 100644 --- a/quadratic-client/src/app/ui/menus/TopBar/SubMenus/QuadraticMenu.tsx +++ b/quadratic-client/src/app/ui/menus/TopBar/SubMenus/QuadraticMenu.tsx @@ -167,9 +167,6 @@ export const QuadraticMenu = () => { settings.setShowHeadings(!settings.showHeadings)}> - settings.setShowGridAxes(!settings.showGridAxes)}> - - settings.setShowGridLines(!settings.showGridLines)}> diff --git a/quadratic-client/src/shared/hooks/useTheme.tsx b/quadratic-client/src/shared/hooks/useTheme.tsx index c6f165e671..f6393ffc16 100644 --- a/quadratic-client/src/shared/hooks/useTheme.tsx +++ b/quadratic-client/src/shared/hooks/useTheme.tsx @@ -11,7 +11,7 @@ export const useTheme = () => { const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)'); const lightModePreference = window.matchMedia('(prefers-color-scheme: light)'); - // User change prefernce via UI preference + // User change preference via UI preference useEffect(() => { if (theme === 'dark' || theme === 'light') { changeTheme(theme); From 65214388a74838575548681a192a81f616cc30ae Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 25 Aug 2024 09:34:59 -0700 Subject: [PATCH 05/14] experimenting with visible bounds for sheet --- .../src/app/grid/controller/Sheets.ts | 1 + quadratic-client/src/app/grid/sheet/Sheet.ts | 5 +- .../src/app/gridGL/UI/GridLines.ts | 106 +++++++++++++--- .../gridGL/UI/gridHeadings/GridHeadings.ts | 117 +++++++++++++----- .../app/gridGL/UI/gridHeadings/outOfBounds.ts | 27 ++++ .../src/app/gridGL/pixiApp/background.ts | 26 +++- .../src/app/quadratic-core-types/index.d.ts | 2 +- quadratic-core/src/grid/file/current.rs | 2 + quadratic-core/src/grid/file/v1_5/file.rs | 1 + quadratic-core/src/grid/file/v1_6/schema.rs | 4 + quadratic-core/src/grid/sheet.rs | 6 + .../wasm_bindings/controller/sheet_info.rs | 33 ++++- 12 files changed, 271 insertions(+), 59 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/UI/gridHeadings/outOfBounds.ts diff --git a/quadratic-client/src/app/grid/controller/Sheets.ts b/quadratic-client/src/app/grid/controller/Sheets.ts index 6414d029ce..f2e70ea269 100644 --- a/quadratic-client/src/app/grid/controller/Sheets.ts +++ b/quadratic-client/src/app/grid/controller/Sheets.ts @@ -169,6 +169,7 @@ class Sheets { offsets: '', bounds: { type: 'empty' }, bounds_without_formatting: { type: 'empty' }, + visible_bounds: null, }, true ); diff --git a/quadratic-client/src/app/grid/sheet/Sheet.ts b/quadratic-client/src/app/grid/sheet/Sheet.ts index 49c14d3223..9db21d5412 100644 --- a/quadratic-client/src/app/grid/sheet/Sheet.ts +++ b/quadratic-client/src/app/grid/sheet/Sheet.ts @@ -15,7 +15,7 @@ export class Sheet { name: string; order: string; color?: string; - + visibleBounds?: [bigint, bigint]; offsets: SheetOffsets; bounds: GridBounds; boundsWithoutFormatting: GridBounds; @@ -35,6 +35,8 @@ export class Sheet { this.bounds = info.bounds; this.boundsWithoutFormatting = info.bounds_without_formatting; this.gridOverflowLines = new GridOverflowLines(); + // this.visibleBounds = info.visible_bounds ?? undefined; + this.visibleBounds = [5n, 10n]; events.on('sheetBounds', this.updateBounds); events.on('sheetValidations', this.sheetValidations); } @@ -55,6 +57,7 @@ export class Sheet { offsets: '', bounds: { type: 'empty' }, bounds_without_formatting: { type: 'empty' }, + visible_bounds: null, }, true ); diff --git a/quadratic-client/src/app/gridGL/UI/GridLines.ts b/quadratic-client/src/app/gridGL/UI/GridLines.ts index 3650b2befa..48a8afe4aa 100644 --- a/quadratic-client/src/app/gridGL/UI/GridLines.ts +++ b/quadratic-client/src/app/gridGL/UI/GridLines.ts @@ -8,6 +8,7 @@ import { colors } from '../../theme/colors'; import { pixiApp } from '../pixiApp/PixiApp'; import { pixiAppSettings } from '../pixiApp/PixiAppSettings'; import { calculateAlphaForGridLines } from './gridUtils'; +import { outOfBoundsBottom, outOfBoundsRight } from './gridHeadings/outOfBounds'; interface GridLine { column?: number; @@ -71,7 +72,7 @@ export class GridLines extends Graphics { const offset = bounds.left - position; let size = 0; - // draw negative space + // draw out of bounds (left) this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); let x = bounds.left; while (column < 0) { @@ -82,10 +83,13 @@ export class GridLines extends Graphics { column++; } - // draw positive space - while (x < bounds.right + size - 1) { - // don't draw grid lines when hidden + const oobRight = outOfBoundsRight(bounds.right); + const oobBottom = outOfBoundsBottom(bounds.bottom); + + // draw content + while (x < (oobRight ?? bounds.right) + size - 1) { if (size !== 0) { + // todo...need to take into account out of bounds for getLinesInRange const lines = gridOverflowLines.getLinesInRange(column, range); if (lines) { for (const [y0, y1] of lines) { @@ -96,17 +100,36 @@ export class GridLines extends Graphics { } } else { if (bounds.top < 0) { + // draw out of bounds above this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); this.moveTo(x - offset, bounds.top); this.lineTo(x - offset, 0); + this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); - this.lineTo(x - offset, bounds.bottom); - this.gridLinesX.push({ column, x: x - offset, y: 0, w: 1, h: bounds.bottom }); + if (oobBottom !== undefined) { + this.lineTo(x - offset, oobBottom); + this.gridLinesX.push({ column, x: x - offset, y: 0, w: 1, h: oobBottom }); + + // draw out of bounds below + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.lineTo(x - offset, bounds.bottom); + } else { + this.lineTo(x - offset, bounds.bottom); + this.gridLinesX.push({ column, x: x - offset, y: 0, w: 1, h: bounds.bottom }); + } } else { this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); - this.moveTo(x - offset, bounds.top); - this.lineTo(x - offset, bounds.bottom); - this.gridLinesX.push({ column, x: x - offset, y: bounds.top, w: 1, h: bounds.bottom - bounds.top }); + if (oobBottom !== undefined) { + this.moveTo(x - offset, bounds.top); + this.lineTo(x - offset, oobBottom); + this.gridLinesX.push({ column, x: x - offset, y: bounds.top, w: 1, h: oobBottom - bounds.top }); + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.lineTo(x - offset, bounds.bottom); + } else { + this.moveTo(x - offset, bounds.top); + this.lineTo(x - offset, bounds.bottom); + this.gridLinesX.push({ column, x: x - offset, y: bounds.top, w: 1, h: bounds.bottom - bounds.top }); + } } } } @@ -114,6 +137,18 @@ export class GridLines extends Graphics { x += size; column++; } + + // draw out of bounds (right) + if (oobRight !== undefined) { + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + while (x < bounds.right + size - 1) { + this.moveTo(x - offset, bounds.top); + this.lineTo(x - offset, bounds.bottom); + size = sheets.sheet.offsets.getColumnWidth(column); + x += size; + column++; + } + } } // @returns the vertical range of [rowStart, rowEnd] @@ -127,7 +162,10 @@ export class GridLines extends Graphics { const offset = bounds.top - position; let size = 0; - // draw negative space + const oobRight = outOfBoundsRight(bounds.right); + const oobBottom = outOfBoundsBottom(bounds.bottom); + + // draw out of bounds (top) this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); let y = bounds.top; while (row < 0) { @@ -138,22 +176,50 @@ export class GridLines extends Graphics { row++; } - // draw positive space + // draw content while (y < bounds.bottom + size - 1) { - // don't draw grid lines when hidden if (size !== 0) { - if (bounds.left < 0) { + // draw out of bounds (bottom) + if (oobBottom && y > oobBottom) { this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); this.moveTo(bounds.left, y - offset); - this.lineTo(0, y - offset); - this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); this.lineTo(bounds.right, y - offset); - this.gridLinesY.push({ row, x: 0, y: y - offset, w: 1, h: 1 }); } else { - this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); - this.moveTo(bounds.left, y - offset); - this.lineTo(bounds.right, y - offset); - this.gridLinesY.push({ row, x: bounds.left, y: y - offset, w: bounds.right - bounds.left, h: 1 }); + if (bounds.left < 0) { + // draw out of bounds (left) + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.moveTo(bounds.left, y - offset); + this.lineTo(0, y - offset); + + this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); + if (oobRight !== undefined) { + this.lineTo(oobRight, y - offset); + this.gridLinesY.push({ row, x: 0, y: y - offset, w: oobRight, h: 1 }); + + // draw out of bounds below + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.lineTo(bounds.right, y - offset); + } else { + this.lineTo(bounds.right, y - offset); + this.gridLinesY.push({ row, x: 0, y: y - offset, w: 1, h: 1 }); + } + } else { + if (oobRight !== undefined) { + this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); + this.moveTo(bounds.left, y - offset); + this.lineTo(oobRight, y - offset); + this.gridLinesY.push({ row, x: bounds.left, y: y - offset, w: bounds.right - bounds.left, h: 1 }); + + // draw out of bounds below + this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.lineTo(oobRight, y - offset); + } else { + this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); + this.moveTo(bounds.left, y - offset); + this.lineTo(bounds.right, y - offset); + this.gridLinesY.push({ row, x: bounds.left, y: y - offset, w: bounds.right - bounds.left, h: 1 }); + } + } } } size = offsets.getRowHeight(row); diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts index 8ba4101c6f..c57577ca31 100644 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts @@ -10,6 +10,7 @@ import { Size } from '../../types/size'; import { calculateAlphaForGridLines } from '../gridUtils'; import { GridHeadingsLabels } from './GridHeadingsLabels'; import { getColumnA1Notation } from './getA1Notation'; +import { outOfBoundsBottom, outOfBoundsRight } from './outOfBounds'; type Selected = 'all' | number[] | undefined; @@ -87,20 +88,37 @@ export class GridHeadings extends Container { const cursor = sheets.sheet.cursor; this.headingsGraphics.lineStyle(0); + + // draw out of bounds to the left if (bounds.left < 0) { this.headingsGraphics.beginFill(colors.outOfBoundsBackgroundColor); this.headingsGraphics.drawRect(bounds.left + this.rowWidth, bounds.top, -bounds.left - this.rowWidth, cellHeight); this.headingsGraphics.endFill(); } - this.headingsGraphics.beginFill(colors.headerBackgroundColor); - this.columnRect = new Rectangle( - Math.max(0, bounds.left), - bounds.top, - bounds.width - (bounds.left < 0 ? bounds.left : 0), - cellHeight - ); - this.headingsGraphics.drawShape(this.columnRect); - this.headingsGraphics.endFill(); + + const oobRight = outOfBoundsRight(bounds.right); + + // draw content + if (oobRight === undefined || bounds.left < oobRight) { + this.headingsGraphics.beginFill(colors.headerBackgroundColor); + this.columnRect = new Rectangle( + Math.max(0, bounds.left), + bounds.top, + (oobRight !== undefined ? oobRight : bounds.right) - Math.max(0, bounds.left), + cellHeight + ); + this.headingsGraphics.drawShape(this.columnRect); + this.headingsGraphics.endFill(); + } + + // draw out of bounds to the right + if (oobRight !== undefined) { + this.headingsGraphics.beginFill(colors.outOfBoundsBackgroundColor); + this.headingsGraphics.drawRect(oobRight, bounds.top, bounds.right - oobRight, cellHeight); + this.headingsGraphics.endFill(); + } + + // todo... // fill the entire viewport if all cells are selected if (cursor.columnRow?.all) { @@ -194,9 +212,11 @@ export class GridHeadings extends Container { // keep track of last label to ensure we don't overlap let lastLabel: { left: number; right: number; selected: boolean } | undefined = undefined; - for (let x = leftOffset; x <= rightOffset; x += currentWidth) { + const oobRight = outOfBoundsRight(bounds.right); + + for (let x = leftOffset; x <= (oobRight ? oobRight : rightOffset); x += currentWidth) { currentWidth = offsets.getColumnWidth(column); - if (gridAlpha !== 0) { + if (gridAlpha !== 0 && x > 0) { this.headingsGraphics.lineStyle( 1, x < 0 ? colors.gridLinesOutOfBounds : colors.gridLines, @@ -212,8 +232,14 @@ export class GridHeadings extends Container { // show selected numbers const selected = Array.isArray(this.selectedColumns) ? this.selectedColumns.includes(column) : false; + const visibleBounds = sheets.sheet.visibleBounds; + // only show the label if selected or mod calculation - if (column >= 0 && (selected || mod === 0 || column % mod === 0)) { + if ( + column >= 0 && + (!visibleBounds || column <= visibleBounds[0]) && + (selected || mod === 0 || column % mod === 0) + ) { const charactersWidth = (this.characterSize.width * column.toString().length) / scale; // only show labels that will fit (unless grid lines are hidden) @@ -277,24 +303,36 @@ export class GridHeadings extends Container { (LABEL_PADDING_ROWS / viewport.scale.x) * 2; this.rowWidth = Math.max(this.rowWidth, CELL_HEIGHT / viewport.scale.x); + // draw out of bounds above if (bounds.top < 0) { this.headingsGraphics.beginFill(colors.outOfBoundsBackgroundColor); this.headingsGraphics.drawRect(bounds.left, bounds.top, this.rowWidth, -bounds.left); this.headingsGraphics.endFill(); } - // draw background of vertical bar - this.headingsGraphics.lineStyle(0); - this.headingsGraphics.beginFill(colors.headerBackgroundColor); - this.columnRect = new Rectangle( - bounds.left, - bounds.top + (bounds.top < 0 ? -bounds.top : 0), - this.rowWidth, - bounds.height + (bounds.top < 0 ? bounds.top : 0) - ); - this.headingsGraphics.drawShape(this.columnRect); - this.headingsGraphics.endFill(); - this.rowRect = new Rectangle(bounds.left, bounds.top, this.rowWidth, bounds.height); + const oobBottom = outOfBoundsBottom(bounds.bottom); + + // draw content + if (oobBottom === undefined || bounds.top < oobBottom) { + this.headingsGraphics.lineStyle(0); + this.headingsGraphics.beginFill(colors.headerBackgroundColor); + this.columnRect = new Rectangle( + bounds.left, + bounds.top + (bounds.top < 0 ? -bounds.top : 0), + this.rowWidth, + bounds.height + (bounds.top < 0 ? bounds.top : 0) + ); + this.headingsGraphics.drawShape(this.columnRect); + this.headingsGraphics.endFill(); + this.rowRect = new Rectangle(bounds.left, bounds.top, this.rowWidth, bounds.height); + } + + // draw out of bounds below + if (oobBottom !== undefined) { + this.headingsGraphics.beginFill(colors.outOfBoundsBackgroundColor); + this.headingsGraphics.drawRect(bounds.left, oobBottom, this.rowWidth, bounds.bottom - oobBottom); + this.headingsGraphics.endFill(); + } // fill the entire viewport if all cells are selected if (cursor.columnRow?.all) { @@ -374,6 +412,8 @@ export class GridHeadings extends Container { mod = this.findIntervalY(skipNumbers); } + const oobBottom = outOfBoundsBottom(bounds.bottom); + const x = bounds.left + this.rowWidth / 2; let row = start.index; let currentHeight = 0; @@ -384,16 +424,10 @@ export class GridHeadings extends Container { const halfCharacterHeight = this.characterSize.height / scale; - for (let y = topOffset; y <= bottomOffset; y += currentHeight) { + for (let y = topOffset; y <= (oobBottom !== undefined ? oobBottom : bottomOffset); y += currentHeight) { currentHeight = offsets.getRowHeight(row); - if (gridAlpha !== 0) { - this.headingsGraphics.lineStyle( - 1, - y < 0 ? colors.gridLinesOutOfBounds : colors.gridLines, - gridAlpha, - 0.5, - true - ); + if (gridAlpha !== 0 && y >= 0) { + this.headingsGraphics.lineStyle(1, colors.gridLines, gridAlpha, 0.5, true); this.headingsGraphics.moveTo(bounds.left, y); this.headingsGraphics.lineTo(bounds.left + this.rowWidth, y); this.gridLinesRows.push({ row: row - 1, y, height: offsets.getRowHeight(row - 1) }); @@ -402,8 +436,10 @@ export class GridHeadings extends Container { // show selected numbers const selected = Array.isArray(this.selectedRows) ? this.selectedRows.includes(row) : false; + const visibleBounds = sheets.sheet.visibleBounds; + // only show the label if selected or mod calculation - if (row >= 0 && (selected || mod === 0 || row % mod === 0)) { + if (row >= 0 && (!visibleBounds || row <= visibleBounds[1]) && (selected || mod === 0 || row % mod === 0)) { // only show labels that will fit (unless grid lines are hidden) // if (currentHeight > halfCharacterHeight * 2 || pixiApp.gridLines.alpha < 0.25) { // don't show numbers if it overlaps with the selected value (eg, hides 0 if selected 1 overlaps it) @@ -460,6 +496,9 @@ export class GridHeadings extends Container { const cellHeight = CELL_HEIGHT / viewport.scale.x; const bounds = viewport.getVisibleBounds(); + const oobRight = outOfBoundsRight(bounds.right); + const oobBottom = outOfBoundsBottom(bounds.bottom); + // draw horizontal line if (bounds.left < 0) { this.headingsGraphics.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); @@ -469,6 +508,11 @@ export class GridHeadings extends Container { if (bounds.right > 0) { this.headingsGraphics.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); this.headingsGraphics.moveTo(0, bounds.top + cellHeight); + this.headingsGraphics.lineTo(oobRight ? oobRight : bounds.right, bounds.top + cellHeight); + } + if (oobRight) { + this.headingsGraphics.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.headingsGraphics.moveTo(oobRight, bounds.top + cellHeight); this.headingsGraphics.lineTo(bounds.right, bounds.top + cellHeight); } @@ -483,6 +527,11 @@ export class GridHeadings extends Container { this.headingsGraphics.moveTo(bounds.left + this.rowWidth, 0); this.headingsGraphics.lineTo(bounds.left + this.rowWidth, bounds.bottom); } + if (oobBottom) { + this.headingsGraphics.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); + this.headingsGraphics.moveTo(bounds.left + this.rowWidth, oobBottom); + this.headingsGraphics.lineTo(bounds.left + this.rowWidth, bounds.bottom); + } } update(viewportDirty: boolean) { diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/outOfBounds.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/outOfBounds.ts new file mode 100644 index 0000000000..1555226063 --- /dev/null +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/outOfBounds.ts @@ -0,0 +1,27 @@ +import { sheets } from '@/app/grid/controller/Sheets'; + +// determine the horizontal out of bounds based on sheet's visibleBounds +// @right the visible bounds to the right -- skips check if not provided +export function outOfBoundsRight(right?: number): number | undefined { + const offsets = sheets.sheet.offsets; + const visibleBounds = sheets.sheet.visibleBounds; + let outOfBounds = visibleBounds ? offsets.getColumnPlacement(Number(visibleBounds[0]) + 1).position : undefined; + if (right !== undefined && outOfBounds !== undefined && outOfBounds > right) { + outOfBounds = undefined; + } + + return outOfBounds; +} + +// determine the vertical out of bounds based on sheet's visibleBounds +// @bottom the visible bounds to the bottom -- skips check if not provided +export function outOfBoundsBottom(bottom?: number): number | undefined { + const offsets = sheets.sheet.offsets; + const visibleBounds = sheets.sheet.visibleBounds; + let outOfBounds = visibleBounds ? offsets.getRowPlacement(Number(visibleBounds[1]) + 1).position : undefined; + if (bottom !== undefined && outOfBounds !== undefined && outOfBounds > bottom) { + outOfBounds = undefined; + } + + return outOfBounds; +} diff --git a/quadratic-client/src/app/gridGL/pixiApp/background.ts b/quadratic-client/src/app/gridGL/pixiApp/background.ts index 3a94b8750c..54cb356c8a 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/background.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/background.ts @@ -4,12 +4,18 @@ import { Graphics } from 'pixi.js'; import { pixiApp } from './PixiApp'; import { colors } from '@/app/theme/colors'; +import { outOfBoundsBottom, outOfBoundsRight } from '../UI/gridHeadings/outOfBounds'; export class Background extends Graphics { update(viewportDirty: boolean) { if (!viewportDirty) return; this.clear(); const visibleBounds = pixiApp.viewport.getVisibleBounds(); + + const oobRight = outOfBoundsRight(visibleBounds.right); + const oobBottom = outOfBoundsBottom(visibleBounds.bottom); + + // draw out of bounds area (left) if (visibleBounds.left < 0) { const right = Math.min(0, visibleBounds.right); const bottom = Math.max(0, visibleBounds.bottom); @@ -17,6 +23,8 @@ export class Background extends Graphics { this.drawRect(visibleBounds.left, 0, right - visibleBounds.left, bottom); this.endFill(); } + + // draw out of bounds area (top) if (visibleBounds.top < 0) { const right = Math.max(0, visibleBounds.right); const bottom = Math.min(0, visibleBounds.bottom); @@ -29,7 +37,23 @@ export class Background extends Graphics { this.beginFill(colors.gridBackground); const x = Math.max(0, visibleBounds.left); const y = Math.max(0, visibleBounds.top); - this.drawRect(x, y, visibleBounds.right - x, visibleBounds.bottom - y); + const width = Math.min(oobRight ?? visibleBounds.right, visibleBounds.right) - x; + const height = Math.min(oobBottom ?? visibleBounds.bottom, visibleBounds.bottom) - y; + this.drawRect(x, y, width, height); this.endFill(); + + // draw out of bounds (right) + if (oobRight !== undefined) { + this.beginFill(colors.outOfBoundsBackgroundColor); + this.drawRect(oobRight, 0, visibleBounds.right - oobRight, visibleBounds.bottom); + this.endFill(); + } + + // draw out of bounds (bottom) + if (oobBottom !== undefined) { + this.beginFill(colors.outOfBoundsBackgroundColor); + this.drawRect(visibleBounds.left, oobBottom, visibleBounds.width, visibleBounds.bottom - oobBottom); + this.endFill(); + } } } diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 434c4d0a38..c26ee65835 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -40,7 +40,7 @@ export interface SheetRect { min: Pos, max: Pos, sheet_id: SheetId, } export interface Selection { sheet_id: SheetId, x: bigint, y: bigint, rects: Array | null, rows: Array | null, columns: Array | null, all: boolean, } export interface Placement { index: number, position: number, size: number, } export interface ColumnRow { column: number, row: number, } -export interface SheetInfo { sheet_id: string, name: string, order: string, color: string | null, offsets: string, bounds: GridBounds, bounds_without_formatting: GridBounds, } +export interface SheetInfo { sheet_id: string, name: string, order: string, color: string | null, offsets: string, bounds: GridBounds, bounds_without_formatting: GridBounds, visible_bounds: [bigint, bigint] | null, } export type PasteSpecial = "None" | "Values" | "Formats"; export interface Rgba { red: number, green: number, blue: number, alpha: number, } export type CellBorderLine = "line1" | "line2" | "line3" | "dotted" | "dashed" | "double"; diff --git a/quadratic-core/src/grid/file/current.rs b/quadratic-core/src/grid/file/current.rs index 2284f30f7f..604e186ce9 100644 --- a/quadratic-core/src/grid/file/current.rs +++ b/quadratic-core/src/grid/file/current.rs @@ -348,6 +348,7 @@ pub fn import_sheet(sheet: current::Sheet) -> Result { order: sheet.order.to_owned(), offsets: SheetOffsets::import(&sheet.offsets), columns: import_column_builder(&sheet.columns)?, + visible_bounds: sheet.visible_bounds.to_owned(), // borders set after sheet is loaded // todo: borders need to be refactored @@ -748,6 +749,7 @@ pub(crate) fn export_sheet(sheet: Sheet) -> current::Sheet { name: sheet.name.to_owned(), color: sheet.color.to_owned(), order: sheet.order.to_owned(), + visible_bounds: sheet.visible_bounds.to_owned(), offsets: sheet.offsets.export(), borders: export_borders_builder(&sheet), formats_all: sheet.format_all.as_ref().and_then(export_format), diff --git a/quadratic-core/src/grid/file/v1_5/file.rs b/quadratic-core/src/grid/file/v1_5/file.rs index 7fe14b7173..b815b10fab 100644 --- a/quadratic-core/src/grid/file/v1_5/file.rs +++ b/quadratic-core/src/grid/file/v1_5/file.rs @@ -373,6 +373,7 @@ fn upgrade_sheet(sheet: &v1_5::Sheet) -> v1_6::Sheet { name: sheet.name.clone(), color: sheet.color.clone(), order: sheet.order.clone(), + visible_bounds: None, offsets: sheet.offsets.clone(), columns: upgrade_columns(sheet), borders: upgrade_borders(sheet), diff --git a/quadratic-core/src/grid/file/v1_6/schema.rs b/quadratic-core/src/grid/file/v1_6/schema.rs index 8adf6b9178..9e57cc41d0 100644 --- a/quadratic-core/src/grid/file/v1_6/schema.rs +++ b/quadratic-core/src/grid/file/v1_6/schema.rs @@ -112,6 +112,10 @@ pub struct Sheet { pub name: String, pub color: Option, pub order: String, + + #[serde(default)] + pub visible_bounds: Option<(i64, i64)>, + pub offsets: Offsets, pub columns: Vec<(i64, Column)>, pub borders: Borders, diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 52344e3a3f..37af99deef 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -43,6 +43,11 @@ pub struct Sheet { pub color: Option, pub order: String, + // used to track what the visible size of the sheet (everything outside this + // area is shown as out of bounds) + #[serde(default)] + pub visible_bounds: Option<(i64, i64)>, + pub offsets: SheetOffsets, #[serde(with = "crate::util::btreemap_serde")] @@ -95,6 +100,7 @@ impl Sheet { name, color: None, order, + visible_bounds: None, offsets: SheetOffsets::default(), diff --git a/quadratic-core/src/wasm_bindings/controller/sheet_info.rs b/quadratic-core/src/wasm_bindings/controller/sheet_info.rs index 5924cffb26..3bae73713f 100644 --- a/quadratic-core/src/wasm_bindings/controller/sheet_info.rs +++ b/quadratic-core/src/wasm_bindings/controller/sheet_info.rs @@ -1,9 +1,9 @@ use serde::{Deserialize, Serialize}; +use ts_rs::TS; use crate::grid::{GridBounds, Sheet}; -#[derive(Serialize, Deserialize)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[derive(Serialize, Deserialize, TS)] pub struct SheetInfo { pub sheet_id: String, pub name: String, @@ -12,6 +12,7 @@ pub struct SheetInfo { pub offsets: String, pub bounds: GridBounds, pub bounds_without_formatting: GridBounds, + pub visible_bounds: Option<(i64, i64)>, } impl From<&Sheet> for SheetInfo { @@ -25,6 +26,7 @@ impl From<&Sheet> for SheetInfo { offsets, bounds: sheet.bounds(false), bounds_without_formatting: sheet.bounds(true), + visible_bounds: sheet.visible_bounds, } } } @@ -46,3 +48,30 @@ impl From<&Sheet> for SheetBounds { } } } + +#[cfg(test)] +mod tests { + use serial_test::parallel; + + use crate::grid::{GridBounds, Sheet, SheetId}; + + #[test] + #[parallel] + fn sheet_info() { + let mut sheet = Sheet::new(SheetId::test(), "test name".to_string(), "A0".to_string()); + sheet.color = Some("red".to_string()); + sheet.visible_bounds = Some((10, 10)); + let sheet_info = crate::wasm_bindings::controller::SheetInfo::from(&sheet); + assert_eq!(sheet_info.sheet_id, SheetId::test().to_string()); + assert_eq!(sheet_info.name, "test name"); + assert_eq!(sheet_info.order, "A0"); + assert_eq!(sheet_info.color, Some("red".to_string())); + assert_eq!(sheet_info.offsets, "{\"column_widths\":{\"default\":100.0,\"sizes\":{}},\"row_heights\":{\"default\":21.0,\"sizes\":{}},\"thumbnail\":[13,35]}"); + assert_eq!(sheet_info.bounds, GridBounds::default()); + assert_eq!( + sheet_info.bounds_without_formatting, + crate::grid::GridBounds::default() + ); + assert_eq!(sheet_info.visible_bounds, Some((10, 10))); + } +} From 7c0bd417fdfcfbaa5a59abe54718b0708bfe2bed Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 26 Aug 2024 06:00:15 -0700 Subject: [PATCH 06/14] setSheetSize operations and client->core calls --- .../quadraticCore/coreClientMessages.ts | 12 ++++- .../quadraticCore/quadraticCore.ts | 4 ++ .../web-workers/quadraticCore/worker/core.ts | 5 ++ .../quadraticCore/worker/coreClient.ts | 4 ++ .../execute_operation/execute_sheets.rs | 50 ++++++++++++++++++- .../execution/execute_operation/mod.rs | 4 ++ .../src/controller/operations/operation.rs | 12 +++++ .../src/controller/operations/sheets.rs | 33 +++++++++++- .../src/controller/user_actions/sheets.rs | 47 +++++++++++++++++ quadratic-core/src/grid/file/current.rs | 4 +- quadratic-core/src/grid/sheet.rs | 4 +- .../wasm_bindings/controller/sheet_info.rs | 4 +- .../src/wasm_bindings/controller/sheets.rs | 18 +++++++ 13 files changed, 192 insertions(+), 9 deletions(-) diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index 73037ef42b..4f69cf0bfb 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -960,6 +960,15 @@ export interface CoreClientValidateInput { validationId: string | undefined; } +export interface ClientCoreSetSheetSize { + type: 'clientCoreSetSheetSize'; + sheetId: string; + width: number | undefined; + height: number | undefined; + auto: boolean; + cursor: string; +} + export type ClientCoreMessage = | ClientCoreLoad | ClientCoreGetCodeCell @@ -1031,7 +1040,8 @@ export type ClientCoreMessage = | ClientCoreGetValidationFromPos | ClientCoreGetValidationList | ClientCoreGetDisplayCell - | ClientCoreValidateInput; + | ClientCoreValidateInput + | ClientCoreSetSheetSize; export type CoreClientMessage = | CoreClientGetCodeCell diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index 6ab127806e..44dcdc39ef 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -716,6 +716,10 @@ class QuadraticCore { this.send({ type: 'clientCoreSetSheetColor', sheetId, color, cursor }); } + setSheetSize(sheetId: string, width: number | undefined, height: number | undefined, auto: boolean, cursor: string) { + this.send({ type: 'clientCoreSetSheetSize', sheetId, width, height, auto, cursor }); + } + duplicateSheet(sheetId: string, cursor: string) { this.send({ type: 'clientCoreDuplicateSheet', sheetId, cursor }); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 6c77d87d8f..4ddf996090 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1031,6 +1031,11 @@ class Core { return JSON.parse(validationId); } } + + setSheetSize(sheetId: string, width: number | undefined, height: number | undefined, auto: boolean, cursor: string) { + if (!this.gridController) throw new Error('Expected gridController to be defined'); + this.gridController.setSheetSize(sheetId, width, height, auto, cursor); + } } export const core = new Core(); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index c4cc2bd459..308b56fb99 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -562,6 +562,10 @@ class CoreClient { }); return; + case 'clientCoreSetSheetSize': + core.setSheetSize(e.data.sheetId, e.data.width, e.data.height, e.data.auto, e.data.cursor); + return; + default: if (e.data.id !== undefined) { // handle responses from requests to quadratic-core diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_sheets.rs b/quadratic-core/src/controller/execution/execute_operation/execute_sheets.rs index 292d73b8e6..076adcd22f 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_sheets.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_sheets.rs @@ -246,6 +246,29 @@ impl GridController { }); } } + + pub fn execute_set_sheet_size(&mut self, transaction: &mut PendingTransaction, op: Operation) { + if let Operation::SetSheetSize { sheet_id, size } = op { + let Some(sheet) = self.try_sheet_mut(sheet_id) else { + // sheet may have been deleted + return; + }; + let old_size = sheet.sheet_size; + sheet.sheet_size = size; + + transaction + .forward_operations + .push(Operation::SetSheetSize { sheet_id, size }); + transaction + .reverse_operations + .push(Operation::SetSheetSize { + sheet_id, + size: old_size, + }); + + self.send_sheet_info(sheet_id); + } + } } #[cfg(test)] @@ -260,7 +283,7 @@ mod tests { CellValue, SheetPos, }; use bigdecimal::BigDecimal; - use serial_test::serial; + use serial_test::{parallel, serial}; #[test] #[serial] @@ -573,4 +596,29 @@ mod tests { true, ); } + + #[test] + #[parallel] + fn set_sheet_size() { + let mut gc = GridController::test(); + let sheet_id = gc.sheet_ids()[0]; + let size = Some((10, 20)); + gc.set_sheet_size(sheet_id, size, false, None); + assert_eq!(gc.grid.sheets()[0].sheet_size, size); + let sheet_info = SheetInfo::from(gc.sheet(sheet_id)); + expect_js_call( + "jsSheetInfoUpdate", + serde_json::to_string(&sheet_info).unwrap(), + true, + ); + + gc.undo(None); + assert_eq!(gc.grid.sheets()[0].sheet_size, None); + let sheet_info = SheetInfo::from(gc.sheet(sheet_id)); + expect_js_call( + "jsSheetInfoUpdate", + serde_json::to_string(&sheet_info).unwrap(), + true, + ); + } } diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 5cbc6c79e3..18a1826c05 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -56,6 +56,10 @@ impl GridController { Operation::SetValidationWarning { .. } => { self.execute_set_validation_warning(transaction, op); } + + Operation::SetSheetSize { .. } => { + self.execute_set_sheet_size(transaction, op); + } } if cfg!(target_family = "wasm") || cfg!(test) { diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index b2d831507e..b195a95b92 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -135,6 +135,11 @@ pub enum Operation { sheet_pos: SheetPos, validation_id: Option, }, + + SetSheetSize { + sheet_id: SheetId, + size: Option<(i64, i64)>, + }, } impl fmt::Display for Operation { @@ -258,6 +263,13 @@ impl fmt::Display for Operation { sheet_pos, validation_id ) } + Operation::SetSheetSize { sheet_id, size } => { + write!( + fmt, + "SetSheetSize {{ sheet_id: {}, size: {:?} }}", + sheet_id, size + ) + } } } } diff --git a/quadratic-core/src/controller/operations/sheets.rs b/quadratic-core/src/controller/operations/sheets.rs index 4348aabaae..e8f778f7ca 100644 --- a/quadratic-core/src/controller/operations/sheets.rs +++ b/quadratic-core/src/controller/operations/sheets.rs @@ -3,7 +3,7 @@ use lexicon_fractional_index::key_between; use crate::{ controller::GridController, - grid::{file::sheet_schema::export_sheet, Sheet, SheetId}, + grid::{file::sheet_schema::export_sheet, GridBounds, Sheet, SheetId}, util, }; @@ -112,6 +112,37 @@ impl GridController { ops.extend(code_run_ops); ops } + + pub fn set_sheet_size_operations( + &mut self, + sheet_id: SheetId, + size: Option<(i64, i64)>, + auto: bool, + ) -> Vec { + if auto { + let Some(sheet) = self.try_sheet(sheet_id) else { + // sheet may have been deleted + return vec![]; + }; + let bounds = sheet.bounds(false); + + match bounds { + GridBounds::NonEmpty(rect) => { + // we no longer support negative bounds + if rect.max.x < 0 || rect.max.y < 0 { + return vec![]; + } + vec![Operation::SetSheetSize { + sheet_id, + size: Some((rect.max.x, rect.max.y)), + }] + } + GridBounds::Empty => vec![], + } + } else { + vec![Operation::SetSheetSize { sheet_id, size }] + } + } } #[cfg(test)] diff --git a/quadratic-core/src/controller/user_actions/sheets.rs b/quadratic-core/src/controller/user_actions/sheets.rs index 9a110198d1..efe95e371c 100644 --- a/quadratic-core/src/controller/user_actions/sheets.rs +++ b/quadratic-core/src/controller/user_actions/sheets.rs @@ -33,6 +33,7 @@ impl GridController { let ops = self.delete_sheet_operations(sheet_id); self.start_user_transaction(ops, cursor, TransactionName::SheetDelete); } + pub fn move_sheet( &mut self, sheet_id: SheetId, @@ -42,10 +43,22 @@ impl GridController { let ops = self.move_sheet_operations(sheet_id, to_before); self.start_user_transaction(ops, cursor, TransactionName::SetSheetMetadata); } + pub fn duplicate_sheet(&mut self, sheet_id: SheetId, cursor: Option) { let ops = self.duplicate_sheet_operations(sheet_id); self.start_user_transaction(ops, cursor, TransactionName::DuplicateSheet); } + + pub fn set_sheet_size( + &mut self, + sheet_id: SheetId, + size: Option<(i64, i64)>, + auto: bool, + cursor: Option, + ) { + let ops = self.set_sheet_size_operations(sheet_id, size, auto); + self.start_user_transaction(ops, cursor, TransactionName::SetSheetMetadata); + } } #[cfg(test)] @@ -457,4 +470,38 @@ mod test { Some(CellValue::Number(BigDecimal::from(4))) ); } + + #[test] + #[serial] + fn sheet_size() { + let mut gc = GridController::test(); + let sheet_id = gc.sheet_ids()[0]; + + gc.set_sheet_size(sheet_id, Some((10, 10)), false, None); + assert_eq!(gc.sheet(sheet_id).sheet_size, Some((10, 10))); + + gc.undo(None); + assert_eq!(gc.sheet(sheet_id).sheet_size, None); + + gc.redo(None); + assert_eq!(gc.sheet(sheet_id).sheet_size, Some((10, 10))); + + gc.set_sheet_size(sheet_id, None, false, None); + assert_eq!(gc.sheet(sheet_id).sheet_size, None); + + gc.set_sheet_size(sheet_id, Some((10, 10)), false, None); + assert_eq!(gc.sheet(sheet_id).sheet_size, Some((10, 10))); + + gc.set_cell_values( + SheetPos { + x: 0, + y: 0, + sheet_id, + }, + vec![vec!["1"; 10]; 20], + None, + ); + gc.set_sheet_size(sheet_id, None, true, None); + assert_eq!(gc.sheet(sheet_id).sheet_size, Some((9, 19))); + } } diff --git a/quadratic-core/src/grid/file/current.rs b/quadratic-core/src/grid/file/current.rs index 604e186ce9..af7d0a2288 100644 --- a/quadratic-core/src/grid/file/current.rs +++ b/quadratic-core/src/grid/file/current.rs @@ -348,7 +348,7 @@ pub fn import_sheet(sheet: current::Sheet) -> Result { order: sheet.order.to_owned(), offsets: SheetOffsets::import(&sheet.offsets), columns: import_column_builder(&sheet.columns)?, - visible_bounds: sheet.visible_bounds.to_owned(), + sheet_size: sheet.visible_bounds.to_owned(), // borders set after sheet is loaded // todo: borders need to be refactored @@ -749,7 +749,7 @@ pub(crate) fn export_sheet(sheet: Sheet) -> current::Sheet { name: sheet.name.to_owned(), color: sheet.color.to_owned(), order: sheet.order.to_owned(), - visible_bounds: sheet.visible_bounds.to_owned(), + visible_bounds: sheet.sheet_size.to_owned(), offsets: sheet.offsets.export(), borders: export_borders_builder(&sheet), formats_all: sheet.format_all.as_ref().and_then(export_format), diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 37af99deef..388f172892 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -46,7 +46,7 @@ pub struct Sheet { // used to track what the visible size of the sheet (everything outside this // area is shown as out of bounds) #[serde(default)] - pub visible_bounds: Option<(i64, i64)>, + pub sheet_size: Option<(i64, i64)>, pub offsets: SheetOffsets, @@ -100,7 +100,7 @@ impl Sheet { name, color: None, order, - visible_bounds: None, + sheet_size: None, offsets: SheetOffsets::default(), diff --git a/quadratic-core/src/wasm_bindings/controller/sheet_info.rs b/quadratic-core/src/wasm_bindings/controller/sheet_info.rs index 3bae73713f..96f4ce828e 100644 --- a/quadratic-core/src/wasm_bindings/controller/sheet_info.rs +++ b/quadratic-core/src/wasm_bindings/controller/sheet_info.rs @@ -26,7 +26,7 @@ impl From<&Sheet> for SheetInfo { offsets, bounds: sheet.bounds(false), bounds_without_formatting: sheet.bounds(true), - visible_bounds: sheet.visible_bounds, + visible_bounds: sheet.sheet_size, } } } @@ -60,7 +60,7 @@ mod tests { fn sheet_info() { let mut sheet = Sheet::new(SheetId::test(), "test name".to_string(), "A0".to_string()); sheet.color = Some("red".to_string()); - sheet.visible_bounds = Some((10, 10)); + sheet.sheet_size = Some((10, 10)); let sheet_info = crate::wasm_bindings::controller::SheetInfo::from(&sheet); assert_eq!(sheet_info.sheet_id, SheetId::test().to_string()); assert_eq!(sheet_info.name, "test name"); diff --git a/quadratic-core/src/wasm_bindings/controller/sheets.rs b/quadratic-core/src/wasm_bindings/controller/sheets.rs index 437608edcd..e0bab215f6 100644 --- a/quadratic-core/src/wasm_bindings/controller/sheets.rs +++ b/quadratic-core/src/wasm_bindings/controller/sheets.rs @@ -106,4 +106,22 @@ impl GridController { &self.set_sheet_color(sheet_id, color, cursor), )?) } + + #[wasm_bindgen(js_name = "setSheetSize")] + pub fn js_set_sheet_size( + &mut self, + sheet_id: String, + width: Option, + height: Option, + auto: bool, + cursor: Option, + ) { + if let Ok(sheet_id) = SheetId::from_str(&sheet_id) { + let size = match (width, height) { + (Some(width), Some(height)) => Some((width as i64, height as i64)), + _ => None, + }; + self.set_sheet_size(sheet_id, size, auto, cursor); + } + } } From 9b3929ff5c43e8ec00ff20ba0d50bb4f935e8ae6 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 26 Aug 2024 06:30:32 -0700 Subject: [PATCH 07/14] adding submenu to sheet bar --- .../menus/SheetBar/SheetBarTabContextMenu.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx b/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx index 4a4e46bfe1..a5f38ead67 100644 --- a/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx +++ b/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx @@ -69,6 +69,24 @@ export const SheetBarTabContextMenu = (props: Props): JSX.Element => { }} /> + + { + quadraticCore.setSheetSize(sheets.sheet.id, undefined, undefined, true, sheets.getCursorPosition()); + handleClose(); + }} + > + Auto size based on content + + { + quadraticCore.setSheetSize(sheets.sheet.id, undefined, undefined, false, sheets.getCursorPosition()); + handleClose(); + }} + > + Remove sheet size + + Rename From fc7f301b4d3475578fdad2222ddf4b14e4cd7cf5 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 26 Aug 2024 08:45:01 -0700 Subject: [PATCH 08/14] working through vp movement --- .../src/app/grid/controller/Sheets.ts | 2 +- quadratic-client/src/app/grid/sheet/Sheet.ts | 24 ++++++++++--- .../gridGL/UI/gridHeadings/GridHeadings.ts | 12 +++---- .../app/gridGL/UI/gridHeadings/outOfBounds.ts | 8 ++--- .../src/app/gridGL/pixiApp/Viewport.ts | 34 ++++++++++++++++++- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../menus/SheetBar/SheetBarTabContextMenu.tsx | 4 ++- quadratic-core/src/grid/file/current.rs | 4 +-- quadratic-core/src/grid/file/v1_5/file.rs | 2 +- quadratic-core/src/grid/file/v1_6/schema.rs | 2 +- .../wasm_bindings/controller/sheet_info.rs | 6 ++-- 11 files changed, 73 insertions(+), 27 deletions(-) diff --git a/quadratic-client/src/app/grid/controller/Sheets.ts b/quadratic-client/src/app/grid/controller/Sheets.ts index f2e70ea269..788c7ce721 100644 --- a/quadratic-client/src/app/grid/controller/Sheets.ts +++ b/quadratic-client/src/app/grid/controller/Sheets.ts @@ -169,7 +169,7 @@ class Sheets { offsets: '', bounds: { type: 'empty' }, bounds_without_formatting: { type: 'empty' }, - visible_bounds: null, + sheet_size: null, }, true ); diff --git a/quadratic-client/src/app/grid/sheet/Sheet.ts b/quadratic-client/src/app/grid/sheet/Sheet.ts index 9db21d5412..df81ca048d 100644 --- a/quadratic-client/src/app/grid/sheet/Sheet.ts +++ b/quadratic-client/src/app/grid/sheet/Sheet.ts @@ -7,6 +7,7 @@ import { Rectangle } from 'pixi.js'; import { Coordinate } from '../../gridGL/types/size'; import { sheets } from '../controller/Sheets'; import { RectangleLike, SheetCursor } from './SheetCursor'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; export class Sheet { id: string; @@ -15,7 +16,7 @@ export class Sheet { name: string; order: string; color?: string; - visibleBounds?: [bigint, bigint]; + sheetSize?: [bigint, bigint]; offsets: SheetOffsets; bounds: GridBounds; boundsWithoutFormatting: GridBounds; @@ -35,8 +36,7 @@ export class Sheet { this.bounds = info.bounds; this.boundsWithoutFormatting = info.bounds_without_formatting; this.gridOverflowLines = new GridOverflowLines(); - // this.visibleBounds = info.visible_bounds ?? undefined; - this.visibleBounds = [5n, 10n]; + this.sheetSize = info.sheet_size ?? undefined; events.on('sheetBounds', this.updateBounds); events.on('sheetValidations', this.sheetValidations); } @@ -57,7 +57,7 @@ export class Sheet { offsets: '', bounds: { type: 'empty' }, bounds_without_formatting: { type: 'empty' }, - visible_bounds: null, + sheet_size: null, }, true ); @@ -85,6 +85,11 @@ export class Sheet { this.order = info.order; this.color = info.color ?? undefined; this.offsets = SheetOffsetsWasm.load(info.offsets); + if (this.sheetSize !== info.sheet_size) { + this.sheetSize = info.sheet_size ?? undefined; + pixiApp.gridLines.dirty = true; + pixiApp.setViewportDirty(); + } } //#endregion @@ -160,4 +165,15 @@ export class Sheet { getValidationById(id: string): Validation | undefined { return this.validations.find((v) => v.id === id); } + + // @returns the screen bounds of the sheet + getScreenBounds(): Rectangle | undefined { + if (this.bounds.type === 'empty') return; + const sheetSize = this.sheetSize; + const start = this.getCellOffsets(this.bounds.min.x, this.bounds.min.y); + const maxX = sheetSize ? Math.max(Number(sheetSize[0]), Number(this.bounds.max.x)) : Number(this.bounds.max.x); + const maxY = sheetSize ? Math.max(Number(sheetSize[1]), Number(this.bounds.max.y)) : Number(this.bounds.max.y); + const end = this.getCellOffsets(maxX, maxY); + return new Rectangle(start.x, start.y, end.x - start.x, end.y - start.y); + } } diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts index c57577ca31..6e440d9e54 100644 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts @@ -232,14 +232,10 @@ export class GridHeadings extends Container { // show selected numbers const selected = Array.isArray(this.selectedColumns) ? this.selectedColumns.includes(column) : false; - const visibleBounds = sheets.sheet.visibleBounds; + const sheetSize = sheets.sheet.sheetSize; // only show the label if selected or mod calculation - if ( - column >= 0 && - (!visibleBounds || column <= visibleBounds[0]) && - (selected || mod === 0 || column % mod === 0) - ) { + if (column >= 0 && (!sheetSize || column <= sheetSize[0]) && (selected || mod === 0 || column % mod === 0)) { const charactersWidth = (this.characterSize.width * column.toString().length) / scale; // only show labels that will fit (unless grid lines are hidden) @@ -436,10 +432,10 @@ export class GridHeadings extends Container { // show selected numbers const selected = Array.isArray(this.selectedRows) ? this.selectedRows.includes(row) : false; - const visibleBounds = sheets.sheet.visibleBounds; + const sheetSize = sheets.sheet.sheetSize; // only show the label if selected or mod calculation - if (row >= 0 && (!visibleBounds || row <= visibleBounds[1]) && (selected || mod === 0 || row % mod === 0)) { + if (row >= 0 && (!sheetSize || row <= sheetSize[1]) && (selected || mod === 0 || row % mod === 0)) { // only show labels that will fit (unless grid lines are hidden) // if (currentHeight > halfCharacterHeight * 2 || pixiApp.gridLines.alpha < 0.25) { // don't show numbers if it overlaps with the selected value (eg, hides 0 if selected 1 overlaps it) diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/outOfBounds.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/outOfBounds.ts index 1555226063..06daa5c683 100644 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/outOfBounds.ts +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/outOfBounds.ts @@ -4,8 +4,8 @@ import { sheets } from '@/app/grid/controller/Sheets'; // @right the visible bounds to the right -- skips check if not provided export function outOfBoundsRight(right?: number): number | undefined { const offsets = sheets.sheet.offsets; - const visibleBounds = sheets.sheet.visibleBounds; - let outOfBounds = visibleBounds ? offsets.getColumnPlacement(Number(visibleBounds[0]) + 1).position : undefined; + const sheetSize = sheets.sheet.sheetSize; + let outOfBounds = sheetSize ? offsets.getColumnPlacement(Number(sheetSize[0]) + 1).position : undefined; if (right !== undefined && outOfBounds !== undefined && outOfBounds > right) { outOfBounds = undefined; } @@ -17,8 +17,8 @@ export function outOfBoundsRight(right?: number): number | undefined { // @bottom the visible bounds to the bottom -- skips check if not provided export function outOfBoundsBottom(bottom?: number): number | undefined { const offsets = sheets.sheet.offsets; - const visibleBounds = sheets.sheet.visibleBounds; - let outOfBounds = visibleBounds ? offsets.getRowPlacement(Number(visibleBounds[1]) + 1).position : undefined; + const sheetSize = sheets.sheet.sheetSize; + let outOfBounds = sheetSize ? offsets.getRowPlacement(Number(sheetSize[1]) + 1).position : undefined; if (bottom !== undefined && outOfBounds !== undefined && outOfBounds > bottom) { outOfBounds = undefined; } diff --git a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts index 9b99518cef..2eaffe1f15 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts @@ -1,14 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { sheets } from '@/app/grid/controller/Sheets'; -import { Drag, Viewport as PixiViewport } from 'pixi-viewport'; +import { Decelerate, Drag, Viewport as PixiViewport } from 'pixi-viewport'; import { Point, Rectangle } from 'pixi.js'; import { isMobile } from 'react-device-detect'; import { HORIZONTAL_SCROLL_KEY, Wheel, ZOOM_KEY } from '../pixiOverride/Wheel'; +import { CELL_HEIGHT, CELL_WIDTH } from '@/shared/constants/gridConstants'; const MULTIPLAYER_VIEWPORT_EASE_TIME = 100; const MINIMUM_VIEWPORT_SCALE = 0.01; const MAXIMUM_VIEWPORT_SCALE = 10; const WHEEL_ZOOM_PERCENT = 1.5; +// bounce when the viewport is moved beyond the content/sheet size +const BOUNCE_BACK_HORIZONTAL_CELLS = 1; +const BOUNCE_BACK_VERTICAL_CELLS = 1; +const BOUNCE_BACK_TIME = 250; + export class Viewport extends PixiViewport { constructor() { super(); @@ -45,8 +52,33 @@ export class Viewport extends PixiViewport { // hack to ensure pointermove works outside of canvas this.off('pointerout'); + + this.on('moved-end', this.movedEnd); } + // handle gracefully bouncing the viewport back to the visible bounds + private movedEnd = () => { + const sheetBounds = sheets.sheet.getScreenBounds(); + const visibleBounds = this.getVisibleBounds(); + let centerX = this.center.x; + let centerY = this.center.y; + const minX = sheetBounds ? Math.min(sheetBounds.x, 0) : 0; + if (visibleBounds.right < minX) { + centerX = -visibleBounds.width / 2 + CELL_WIDTH * BOUNCE_BACK_HORIZONTAL_CELLS; + } + if (visibleBounds.bottom < 0) { + centerY = -visibleBounds.height / 2 + CELL_HEIGHT * BOUNCE_BACK_VERTICAL_CELLS; + } + if (centerX !== this.center.x || centerY !== this.center.y) { + this.animate({ + position: new Point(centerX, centerY), + time: BOUNCE_BACK_TIME, + removeOnInterrupt: true, + ease: 'easeInOutSine', + }); + } + }; + loadViewport() { const lastViewport = sheets.sheet.cursor.viewport; if (lastViewport) { diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index c26ee65835..6c38d9a7d0 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -40,7 +40,7 @@ export interface SheetRect { min: Pos, max: Pos, sheet_id: SheetId, } export interface Selection { sheet_id: SheetId, x: bigint, y: bigint, rects: Array | null, rows: Array | null, columns: Array | null, all: boolean, } export interface Placement { index: number, position: number, size: number, } export interface ColumnRow { column: number, row: number, } -export interface SheetInfo { sheet_id: string, name: string, order: string, color: string | null, offsets: string, bounds: GridBounds, bounds_without_formatting: GridBounds, visible_bounds: [bigint, bigint] | null, } +export interface SheetInfo { sheet_id: string, name: string, order: string, color: string | null, offsets: string, bounds: GridBounds, bounds_without_formatting: GridBounds, sheet_size: [bigint, bigint] | null, } export type PasteSpecial = "None" | "Values" | "Formats"; export interface Rgba { red: number, green: number, blue: number, alpha: number, } export type CellBorderLine = "line1" | "line2" | "line3" | "dotted" | "dashed" | "double"; diff --git a/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx b/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx index a5f38ead67..94f9a67096 100644 --- a/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx +++ b/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx @@ -69,6 +69,8 @@ export const SheetBarTabContextMenu = (props: Props): JSX.Element => { }} /> + Rename + { @@ -79,6 +81,7 @@ export const SheetBarTabContextMenu = (props: Props): JSX.Element => { Auto size based on content { quadraticCore.setSheetSize(sheets.sheet.id, undefined, undefined, false, sheets.getCursorPosition()); handleClose(); @@ -88,7 +91,6 @@ export const SheetBarTabContextMenu = (props: Props): JSX.Element => { - Rename Result { order: sheet.order.to_owned(), offsets: SheetOffsets::import(&sheet.offsets), columns: import_column_builder(&sheet.columns)?, - sheet_size: sheet.visible_bounds.to_owned(), + sheet_size: sheet.sheet_size.to_owned(), // borders set after sheet is loaded // todo: borders need to be refactored @@ -749,7 +749,7 @@ pub(crate) fn export_sheet(sheet: Sheet) -> current::Sheet { name: sheet.name.to_owned(), color: sheet.color.to_owned(), order: sheet.order.to_owned(), - visible_bounds: sheet.sheet_size.to_owned(), + sheet_size: sheet.sheet_size.to_owned(), offsets: sheet.offsets.export(), borders: export_borders_builder(&sheet), formats_all: sheet.format_all.as_ref().and_then(export_format), diff --git a/quadratic-core/src/grid/file/v1_5/file.rs b/quadratic-core/src/grid/file/v1_5/file.rs index b815b10fab..28e7388f7a 100644 --- a/quadratic-core/src/grid/file/v1_5/file.rs +++ b/quadratic-core/src/grid/file/v1_5/file.rs @@ -373,7 +373,7 @@ fn upgrade_sheet(sheet: &v1_5::Sheet) -> v1_6::Sheet { name: sheet.name.clone(), color: sheet.color.clone(), order: sheet.order.clone(), - visible_bounds: None, + sheet_size: None, offsets: sheet.offsets.clone(), columns: upgrade_columns(sheet), borders: upgrade_borders(sheet), diff --git a/quadratic-core/src/grid/file/v1_6/schema.rs b/quadratic-core/src/grid/file/v1_6/schema.rs index 9e57cc41d0..133998f545 100644 --- a/quadratic-core/src/grid/file/v1_6/schema.rs +++ b/quadratic-core/src/grid/file/v1_6/schema.rs @@ -114,7 +114,7 @@ pub struct Sheet { pub order: String, #[serde(default)] - pub visible_bounds: Option<(i64, i64)>, + pub sheet_size: Option<(i64, i64)>, pub offsets: Offsets, pub columns: Vec<(i64, Column)>, diff --git a/quadratic-core/src/wasm_bindings/controller/sheet_info.rs b/quadratic-core/src/wasm_bindings/controller/sheet_info.rs index 96f4ce828e..04229350ee 100644 --- a/quadratic-core/src/wasm_bindings/controller/sheet_info.rs +++ b/quadratic-core/src/wasm_bindings/controller/sheet_info.rs @@ -12,7 +12,7 @@ pub struct SheetInfo { pub offsets: String, pub bounds: GridBounds, pub bounds_without_formatting: GridBounds, - pub visible_bounds: Option<(i64, i64)>, + pub sheet_size: Option<(i64, i64)>, } impl From<&Sheet> for SheetInfo { @@ -26,7 +26,7 @@ impl From<&Sheet> for SheetInfo { offsets, bounds: sheet.bounds(false), bounds_without_formatting: sheet.bounds(true), - visible_bounds: sheet.sheet_size, + sheet_size: sheet.sheet_size, } } } @@ -72,6 +72,6 @@ mod tests { sheet_info.bounds_without_formatting, crate::grid::GridBounds::default() ); - assert_eq!(sheet_info.visible_bounds, Some((10, 10))); + assert_eq!(sheet_info.sheet_size, Some((10, 10))); } } From bfe6397ec9e949cf67e74c9fdf9fee4326f647f0 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 26 Aug 2024 09:18:39 -0700 Subject: [PATCH 09/14] ensure the grid stays visible --- package-lock.json | 19 +++++++++++++++-- quadratic-client/package.json | 2 ++ .../gridGL/UI/gridHeadings/GridHeadings.ts | 2 +- .../src/app/gridGL/pixiApp/Viewport.ts | 21 ++++++++++++++++--- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index e913c7b2f2..bf453a7241 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10014,6 +10014,19 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==" + }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -18483,8 +18496,8 @@ }, "node_modules/lodash.debounce": { "version": "4.0.8", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.get": { "version": "4.4.2", @@ -26892,6 +26905,7 @@ "@sentry/vite-plugin": "^2.15.0", "@szhsin/react-menu": "^4.0.2", "@tailwindcss/container-queries": "^0.1.1", + "@types/lodash.debounce": "^4.0.9", "@types/react-avatar-editor": "^13.0.2", "@types/ua-parser-js": "^0.7.39", "bignumber.js": "^9.1.2", @@ -26904,6 +26918,7 @@ "fontfaceobserver": "^2.3.0", "fuzzysort": "^2.0.4", "localforage": "^1.10.0", + "lodash.debounce": "^4.0.8", "mixpanel-browser": "^2.46.0", "monaco-editor": "^0.40.0", "next-themes": "^0.3.0", diff --git a/quadratic-client/package.json b/quadratic-client/package.json index 79c08fea26..6fac95a485 100644 --- a/quadratic-client/package.json +++ b/quadratic-client/package.json @@ -45,6 +45,7 @@ "@sentry/vite-plugin": "^2.15.0", "@szhsin/react-menu": "^4.0.2", "@tailwindcss/container-queries": "^0.1.1", + "@types/lodash.debounce": "^4.0.9", "@types/react-avatar-editor": "^13.0.2", "@types/ua-parser-js": "^0.7.39", "bignumber.js": "^9.1.2", @@ -57,6 +58,7 @@ "fontfaceobserver": "^2.3.0", "fuzzysort": "^2.0.4", "localforage": "^1.10.0", + "lodash.debounce": "^4.0.8", "mixpanel-browser": "^2.46.0", "monaco-editor": "^0.40.0", "next-themes": "^0.3.0", diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts index 6e440d9e54..8079f9d069 100644 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts @@ -34,7 +34,7 @@ export class GridHeadings extends Container { private selectedRows: number[] = []; private gridLinesColumns: { column: number; x: number; width: number }[] = []; private gridLinesRows: { row: number; y: number; height: number }[] = []; - private rowWidth = 0; + rowWidth = 0; headingSize: Size = { width: 0, height: 0 }; diff --git a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts index 2eaffe1f15..0e4e7b75e0 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts @@ -1,10 +1,11 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { sheets } from '@/app/grid/controller/Sheets'; -import { Decelerate, Drag, Viewport as PixiViewport } from 'pixi-viewport'; +import { Drag, Viewport as PixiViewport } from 'pixi-viewport'; import { Point, Rectangle } from 'pixi.js'; import { isMobile } from 'react-device-detect'; import { HORIZONTAL_SCROLL_KEY, Wheel, ZOOM_KEY } from '../pixiOverride/Wheel'; import { CELL_HEIGHT, CELL_WIDTH } from '@/shared/constants/gridConstants'; +import { pixiApp } from './PixiApp'; +import debounce from 'lodash.debounce'; const MULTIPLAYER_VIEWPORT_EASE_TIME = 100; const MINIMUM_VIEWPORT_SCALE = 0.01; @@ -15,6 +16,7 @@ const WHEEL_ZOOM_PERCENT = 1.5; const BOUNCE_BACK_HORIZONTAL_CELLS = 1; const BOUNCE_BACK_VERTICAL_CELLS = 1; const BOUNCE_BACK_TIME = 250; +const DEBOUNCE_TIME = 250; export class Viewport extends PixiViewport { constructor() { @@ -53,7 +55,8 @@ export class Viewport extends PixiViewport { // hack to ensure pointermove works outside of canvas this.off('pointerout'); - this.on('moved-end', this.movedEnd); + // bounce back the viewport when content is out of view + this.on('moved-end', debounce(this.movedEnd, DEBOUNCE_TIME)); } // handle gracefully bouncing the viewport back to the visible bounds @@ -63,11 +66,23 @@ export class Viewport extends PixiViewport { let centerX = this.center.x; let centerY = this.center.y; const minX = sheetBounds ? Math.min(sheetBounds.x, 0) : 0; + const maxX = sheetBounds ? sheetBounds.right : undefined; + const maxY = sheetBounds ? sheetBounds.bottom : undefined; + + // handle bouncing from the left/right of the screen if (visibleBounds.right < minX) { centerX = -visibleBounds.width / 2 + CELL_WIDTH * BOUNCE_BACK_HORIZONTAL_CELLS; + } else if (maxX !== undefined && visibleBounds.left > maxX) { + // we need the minus one to account for the fact that the right edge of the screen is not visible + centerX = + maxX + visibleBounds.width / 2 - CELL_WIDTH * (BOUNCE_BACK_HORIZONTAL_CELLS - 1) - pixiApp.headings.rowWidth; } + + // handle bouncing from the top/bottom of the screen if (visibleBounds.bottom < 0) { centerY = -visibleBounds.height / 2 + CELL_HEIGHT * BOUNCE_BACK_VERTICAL_CELLS; + } else if (maxY !== undefined && visibleBounds.top > maxY) { + centerY = maxY + visibleBounds.height / 2 - CELL_HEIGHT * BOUNCE_BACK_VERTICAL_CELLS; } if (centerX !== this.center.x || centerY !== this.center.y) { this.animate({ From 04bd8b1390b5183a35f9fbea8f8a11d8108d7a2c Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 26 Aug 2024 09:22:49 -0700 Subject: [PATCH 10/14] working through interaction --- quadratic-client/src/app/gridGL/pixiApp/Viewport.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts index 0e4e7b75e0..0c14680ea3 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts @@ -6,6 +6,7 @@ import { HORIZONTAL_SCROLL_KEY, Wheel, ZOOM_KEY } from '../pixiOverride/Wheel'; import { CELL_HEIGHT, CELL_WIDTH } from '@/shared/constants/gridConstants'; import { pixiApp } from './PixiApp'; import debounce from 'lodash.debounce'; +import { cellVisible } from '../interaction/viewportHelper'; const MULTIPLAYER_VIEWPORT_EASE_TIME = 100; const MINIMUM_VIEWPORT_SCALE = 0.01; @@ -61,6 +62,10 @@ export class Viewport extends PixiViewport { // handle gracefully bouncing the viewport back to the visible bounds private movedEnd = () => { + // don't do anything if cursor's cell is visible (this allows the user to + // move the viewport out of view with the cursor) + if (cellVisible()) return; + const sheetBounds = sheets.sheet.getScreenBounds(); const visibleBounds = this.getVisibleBounds(); let centerX = this.center.x; From e2292b5c1980752ed3c0e8b4e2fc26aa8a62fe82 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 26 Aug 2024 09:29:44 -0700 Subject: [PATCH 11/14] tweaks to the debounce --- .../src/app/gridGL/interaction/viewportHelper.ts | 6 ++++++ quadratic-client/src/app/gridGL/pixiApp/Viewport.ts | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/interaction/viewportHelper.ts b/quadratic-client/src/app/gridGL/interaction/viewportHelper.ts index 07d0300da0..18bb5a96bd 100644 --- a/quadratic-client/src/app/gridGL/interaction/viewportHelper.ts +++ b/quadratic-client/src/app/gridGL/interaction/viewportHelper.ts @@ -62,6 +62,12 @@ export function isColumnVisible(column: number): boolean { return true; } +export function isCellVisible( + coordinate = { x: sheets.sheet.cursor.cursorPosition.x, y: sheets.sheet.cursor.cursorPosition.y } +): boolean { + return isRowVisible(coordinate.y) && isColumnVisible(coordinate.x); +} + // Makes a cell visible in the viewport export function cellVisible( coordinate: Coordinate = { diff --git a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts index 0c14680ea3..bf17ae47c0 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts @@ -6,7 +6,7 @@ import { HORIZONTAL_SCROLL_KEY, Wheel, ZOOM_KEY } from '../pixiOverride/Wheel'; import { CELL_HEIGHT, CELL_WIDTH } from '@/shared/constants/gridConstants'; import { pixiApp } from './PixiApp'; import debounce from 'lodash.debounce'; -import { cellVisible } from '../interaction/viewportHelper'; +import { isCellVisible } from '../interaction/viewportHelper'; const MULTIPLAYER_VIEWPORT_EASE_TIME = 100; const MINIMUM_VIEWPORT_SCALE = 0.01; @@ -17,7 +17,7 @@ const WHEEL_ZOOM_PERCENT = 1.5; const BOUNCE_BACK_HORIZONTAL_CELLS = 1; const BOUNCE_BACK_VERTICAL_CELLS = 1; const BOUNCE_BACK_TIME = 250; -const DEBOUNCE_TIME = 250; +const DEBOUNCE_TIME = 500; export class Viewport extends PixiViewport { constructor() { @@ -64,7 +64,7 @@ export class Viewport extends PixiViewport { private movedEnd = () => { // don't do anything if cursor's cell is visible (this allows the user to // move the viewport out of view with the cursor) - if (cellVisible()) return; + if (isCellVisible()) return; const sheetBounds = sheets.sheet.getScreenBounds(); const visibleBounds = this.getVisibleBounds(); @@ -94,7 +94,7 @@ export class Viewport extends PixiViewport { position: new Point(centerX, centerY), time: BOUNCE_BACK_TIME, removeOnInterrupt: true, - ease: 'easeInOutSine', + ease: 'easeInSine', }); } }; From d3526ce1a8ea8410275378d5ecef895d4564865b Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 27 Aug 2024 05:50:08 -0700 Subject: [PATCH 12/14] clip text that overflows the sheetSize --- quadratic-client/src/app/grid/sheet/Sheet.ts | 13 ++++++++- .../src/app/gridGL/helpers/intersects.ts | 3 ++- .../interaction/keyboard/keyboardPosition.ts | 27 ++++++++++++------- .../src/app/gridGL/pixiApp/Viewport.ts | 2 +- .../worker/cellsLabel/CellLabel.ts | 9 +++++++ .../worker/cellsLabel/CellsLabels.ts | 8 ++++++ 6 files changed, 49 insertions(+), 13 deletions(-) diff --git a/quadratic-client/src/app/grid/sheet/Sheet.ts b/quadratic-client/src/app/grid/sheet/Sheet.ts index df81ca048d..fccadff265 100644 --- a/quadratic-client/src/app/grid/sheet/Sheet.ts +++ b/quadratic-client/src/app/grid/sheet/Sheet.ts @@ -167,7 +167,7 @@ export class Sheet { } // @returns the screen bounds of the sheet - getScreenBounds(): Rectangle | undefined { + getSheetSize(): Rectangle | undefined { if (this.bounds.type === 'empty') return; const sheetSize = this.sheetSize; const start = this.getCellOffsets(this.bounds.min.x, this.bounds.min.y); @@ -176,4 +176,15 @@ export class Sheet { const end = this.getCellOffsets(maxX, maxY); return new Rectangle(start.x, start.y, end.x - start.x, end.y - start.y); } + + // @returns the cell coordinate bounds of the sheet + // note: returns (0,0,Infinity,Infinity) if sheetSize is not set + getCellSheetSize(): Rectangle { + const sheetSize = this.sheetSize; + if (sheetSize) { + return new Rectangle(0, 0, Number(sheetSize[0]), Number(sheetSize[1])); + } else { + return new Rectangle(0, 0, Infinity, Infinity); + } + } } diff --git a/quadratic-client/src/app/gridGL/helpers/intersects.ts b/quadratic-client/src/app/gridGL/helpers/intersects.ts index 991525facf..225c9195c0 100644 --- a/quadratic-client/src/app/gridGL/helpers/intersects.ts +++ b/quadratic-client/src/app/gridGL/helpers/intersects.ts @@ -2,6 +2,7 @@ import { RectangleLike } from '@/app/grid/sheet/SheetCursor'; import { Rect } from '@/app/quadratic-core-types'; import { rectToRectangle } from '@/app/web-workers/quadraticCore/worker/rustConversions'; import { Circle, Point, Rectangle } from 'pixi.js'; +import { Coordinate } from '../types/size'; function left(rectangle: RectangleLike): number { return Math.min(rectangle.x, rectangle.x + rectangle.width); @@ -19,7 +20,7 @@ function bottom(rectangle: RectangleLike): number { return Math.max(rectangle.y, rectangle.y + rectangle.height); } -function rectanglePoint(rectangle: RectangleLike, point: Point): boolean { +function rectanglePoint(rectangle: RectangleLike, point: Point | Coordinate): boolean { return ( point.x >= left(rectangle) && point.x <= right(rectangle) && diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts index a384be54a0..fdddf4e5cd 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts @@ -8,6 +8,7 @@ import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Rectangle } from 'pixi.js'; import { sheets } from '../../../grid/controller/Sheets'; import { pixiAppSettings } from '../../pixiApp/PixiAppSettings'; +import { intersects } from '../../helpers/intersects'; function setCursorPosition(x: number, y: number) { const newPos = { x, y }; @@ -20,10 +21,6 @@ function setCursorPosition(x: number, y: number) { }); } -// todo: The QuadraticCore checks should be a single call within Rust instead of -// having TS handle the logic (this will reduce the number of calls into -// quadraticCore) - // handle cases for meta/ctrl keys with algorithm: // - if on an empty cell then select to the first cell with a value // - if on a filled cell then select to the cell before the next empty cell @@ -42,11 +39,17 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { const keyboardX = cursor.keyboardMovePosition.x; const keyboardY = cursor.keyboardMovePosition.y; + const sheetSize = sheets.sheet.getCellSheetSize(); + if (deltaX === 1) { let x = keyboardX; + + if (x === sheetSize.right) return; + const y = cursor.keyboardMovePosition.y; // always use the original cursor position to search const yCheck = cursor.cursorPosition.y; + // handle case of cell with content let nextCol: number | undefined = undefined; if (await quadraticCore.cellHasContent(sheetId, x, yCheck)) { @@ -103,6 +106,8 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { } } else if (deltaX === -1) { let x = keyboardX; + if (x === 0) return; + const y = cursor.keyboardMovePosition.y; // always use the original cursor position to search const yCheck = cursor.cursorPosition.y; @@ -313,12 +318,14 @@ function expandSelection(deltaX: number, deltaY: number) { function moveCursor(deltaX: number, deltaY: number) { const cursor = sheets.sheet.cursor; const newPos = { x: cursor.cursorPosition.x + deltaX, y: cursor.cursorPosition.y + deltaY }; - cursor.changePosition({ - columnRow: null, - multiCursor: null, - keyboardMovePosition: newPos, - cursorPosition: newPos, - }); + if (intersects.rectanglePoint(sheets.sheet.getCellSheetSize(), newPos)) { + cursor.changePosition({ + columnRow: null, + multiCursor: null, + keyboardMovePosition: newPos, + cursorPosition: newPos, + }); + } } export function keyboardPosition(event: KeyboardEvent): boolean { diff --git a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts index bf17ae47c0..1e9606134a 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts @@ -66,7 +66,7 @@ export class Viewport extends PixiViewport { // move the viewport out of view with the cursor) if (isCellVisible()) return; - const sheetBounds = sheets.sheet.getScreenBounds(); + const sheetBounds = sheets.sheet.getSheetSize(); const visibleBounds = this.getVisibleBounds(); let centerX = this.center.x; let centerY = this.center.y; diff --git a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts index 4fc458b80a..2622c8f869 100644 --- a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts +++ b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts @@ -269,6 +269,15 @@ export class CellLabel { const actualTop = Math.max(this.AABB.top, this.AABB.top + (this.AABB.height - this.textHeight) / 2); this.position.y = Math.max(actualTop, this.AABB.top); } + + // Adjust the clipping bounds based on the sheetSize. This removes overflow + // that happens at the sheet's borders. + if (this.overflowRight && this.location.x === this.cellsLabels.sheetSizeClip.right) { + this.cellClipRight = this.AABB.right; + } + if (this.overflowLeft && this.location.x === 0) { + this.cellClipLeft = this.AABB.left; + } }; public updateText = (labelMeshes: LabelMeshes): void => { diff --git a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellsLabels.ts b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellsLabels.ts index a23eabe2ae..93a75ea09e 100644 --- a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellsLabels.ts +++ b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellsLabels.ts @@ -38,6 +38,8 @@ export class CellsLabels { private dirtyRowHeadings: Map; private rowTransient: boolean; + sheetSizeClip: Rectangle; + constructor(sheetInfo: SheetInfo, bitmapFonts: RenderBitmapFonts) { this.sheetId = sheetInfo.sheet_id; const bounds = sheetInfo.bounds_without_formatting; @@ -57,6 +59,12 @@ export class CellsLabels { this.rowTransient = false; this.createHashes(); + + if (sheetInfo.sheet_size) { + this.sheetSizeClip = new Rectangle(0, 0, Number(sheetInfo.sheet_size[0]), Number(sheetInfo.sheet_size[1])); + } else { + this.sheetSizeClip = new Rectangle(0, 0, Infinity, Infinity); + } } updateSheetInfo(sheetInfo: SheetInfo) { From 102877cc6c7f4bbc44a563d4dc0bfa9ce7c2e73d Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 27 Aug 2024 11:00:04 -0700 Subject: [PATCH 13/14] working through set sheet size menu --- .../interaction/keyboard/keyboardPosition.ts | 143 +++++++++++++++++- .../menus/SheetBar/SheetBarTabContextMenu.tsx | 10 +- .../src/app/ui/menus/SheetBar/SheetSize.tsx | 79 ++++++++++ quadratic-core/src/grid/sheet/bounds.rs | 106 ++++++++++--- 4 files changed, 311 insertions(+), 27 deletions(-) create mode 100644 quadratic-client/src/app/ui/menus/SheetBar/SheetSize.tsx diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts index fdddf4e5cd..4d94fdfd13 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts @@ -25,8 +25,9 @@ function setCursorPosition(x: number, y: number) { // - if on an empty cell then select to the first cell with a value // - if on a filled cell then select to the cell before the next empty cell // - if on a filled cell but the next cell is empty then select to the first cell with a value -// - if there are no more cells then select the next cell over (excel selects to the end of the sheet; we don’t have an end (yet) so right now I select one cell over) +// - if there are no more cells then select the next cell over or if there is a sheetSize boundary, select to the boundary // the above checks are always made relative to the original cursor position (the highlighted cell) +// all moves are clamped by the sheetInfo.sheetSize async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { const cursor = sheets.sheet.cursor; const sheetId = sheets.sheet.id; @@ -44,7 +45,24 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { if (deltaX === 1) { let x = keyboardX; - if (x === sheetSize.right) return; + // make sure we don't go beyond the bounds (unless we are already beyond the + // bounds, in which case we allow the cursor to move anywhere) + if (!isOutOfBounds()) { + if (x === sheetSize.right) return; + if (x > sheetSize.right) { + cursor.changePosition({ + cursorPosition: { x: sheetSize.right, y: keyboardY }, + ensureVisible: true, + }); + return; + } else if (x < 0) { + cursor.changePosition({ + cursorPosition: { x: 0, y: keyboardY }, + ensureVisible: true, + }); + return; + } + } const y = cursor.keyboardMovePosition.y; // always use the original cursor position to search @@ -106,7 +124,25 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { } } else if (deltaX === -1) { let x = keyboardX; - if (x === 0) return; + + // make sure we don't go beyond the bounds (unless we are already beyond the + // bounds, in which case we allow the cursor to move anywhere) + if (!isOutOfBounds()) { + if (x === 0) return; + if (x < 0) { + cursor.changePosition({ + cursorPosition: { x: 0, y: keyboardY }, + ensureVisible: true, + }); + return; + } else if (x > sheetSize.right) { + cursor.changePosition({ + cursorPosition: { x: sheetSize.right, y: keyboardY }, + ensureVisible: true, + }); + return; + } + } const y = cursor.keyboardMovePosition.y; // always use the original cursor position to search @@ -167,6 +203,26 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { } } else if (deltaY === 1) { let y = keyboardY; + + // make sure we don't go beyond the bounds (unless we are already beyond the + // bounds, in which case we allow the cursor to move anywhere) + if (!isOutOfBounds()) { + if (y === sheetSize.bottom) return; + if (y > sheetSize.bottom) { + cursor.changePosition({ + cursorPosition: { x: keyboardX, y: sheetSize.bottom }, + ensureVisible: true, + }); + return; + } else if (y < 0) { + cursor.changePosition({ + cursorPosition: { x: keyboardX, y: 0 }, + ensureVisible: true, + }); + return; + } + } + const x = cursor.keyboardMovePosition.x; // always use the original cursor position to search const xCheck = cursor.cursorPosition.x; @@ -226,6 +282,26 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { } } else if (deltaY === -1) { let y = keyboardY; + + // make sure we don't go beyond the bounds (unless we are already beyond the + // bounds, in which case we allow the cursor to move anywhere) + if (!isOutOfBounds()) { + if (y === 0) return; + if (y < 0) { + cursor.changePosition({ + cursorPosition: { x: keyboardX, y: 0 }, + ensureVisible: true, + }); + return; + } else if (y > sheetSize.bottom) { + cursor.changePosition({ + cursorPosition: { x: keyboardX, y: sheetSize.bottom }, + ensureVisible: true, + }); + return; + } + } + const x = cursor.keyboardMovePosition.x; // always use the original cursor position to search const xCheck = cursor.cursorPosition.x; @@ -286,6 +362,28 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { } } +/// whether the cursor is out of bounds +function isOutOfBounds() { + const cursor = sheets.sheet.cursor; + + // we also need to check if any multiCursor are out of bounds + if (cursor.multiCursor) { + for (const multiCursor of cursor.multiCursor) { + if (!intersects.rectangleRectangle(multiCursor, sheets.sheet.getCellSheetSize())) { + return true; + } + } + } + const cursorPosition = cursor.cursorPosition; + const sheetSize = sheets.sheet.getCellSheetSize(); + return ( + cursorPosition.x < 0 || + cursorPosition.x > sheetSize.right || + cursorPosition.y < 0 || + cursorPosition.y > sheetSize.bottom + ); +} + // use arrow to select when shift key is pressed function expandSelection(deltaX: number, deltaY: number) { const cursor = sheets.sheet.cursor; @@ -299,6 +397,17 @@ function expandSelection(deltaX: number, deltaY: number) { // the last multiCursor entry, which is what we change with the keyboard const lastMultiCursor = multiCursor[multiCursor.length - 1]; + // if out of bounds, allow expansion of selection anywhere; otherwise clamp to sheetSize + const sheetSize = sheets.sheet.getCellSheetSize(); + if ( + !isOutOfBounds() && + (movePosition.x + deltaX < 0 || + movePosition.x + deltaY > sheetSize.right || + movePosition.y + deltaY < 0 || + movePosition.y + deltaY > sheetSize.bottom) + ) { + return; + } const newMovePosition = { x: movePosition.x + deltaX, y: movePosition.y + deltaY }; lastMultiCursor.x = downPosition.x < newMovePosition.x ? downPosition.x : newMovePosition.x; lastMultiCursor.y = downPosition.y < newMovePosition.y ? downPosition.y : newMovePosition.y; @@ -318,7 +427,7 @@ function expandSelection(deltaX: number, deltaY: number) { function moveCursor(deltaX: number, deltaY: number) { const cursor = sheets.sheet.cursor; const newPos = { x: cursor.cursorPosition.x + deltaX, y: cursor.cursorPosition.y + deltaY }; - if (intersects.rectanglePoint(sheets.sheet.getCellSheetSize(), newPos)) { + if (isOutOfBounds() || intersects.rectanglePoint(sheets.sheet.getCellSheetSize(), newPos)) { cursor.changePosition({ columnRow: null, multiCursor: null, @@ -326,6 +435,32 @@ function moveCursor(deltaX: number, deltaY: number) { cursorPosition: newPos, }); } + + // handle when cursor is out of bounds + else { + if (deltaX && cursor.cursorPosition.x + deltaX < 0) { + cursor.changePosition({ + cursorPosition: { x: 0, y: cursor.cursorPosition.y }, + ensureVisible: true, + }); + } else if (deltaX && cursor.cursorPosition.x + deltaX > sheets.sheet.getCellSheetSize().right) { + cursor.changePosition({ + cursorPosition: { x: sheets.sheet.getCellSheetSize().right, y: cursor.cursorPosition.y }, + ensureVisible: true, + }); + } + if (deltaY && cursor.cursorPosition.y + deltaY < 0) { + cursor.changePosition({ + cursorPosition: { x: cursor.cursorPosition.x, y: 0 }, + ensureVisible: true, + }); + } else if (deltaY && cursor.cursorPosition.y + deltaY > sheets.sheet.getCellSheetSize().bottom) { + cursor.changePosition({ + cursorPosition: { x: cursor.cursorPosition.x, y: sheets.sheet.getCellSheetSize().bottom }, + ensureVisible: true, + }); + } + } } export function keyboardPosition(event: KeyboardEvent): boolean { diff --git a/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx b/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx index 94f9a67096..fb5083b7cf 100644 --- a/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx +++ b/quadratic-client/src/app/ui/menus/SheetBar/SheetBarTabContextMenu.tsx @@ -1,12 +1,13 @@ import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Box } from '@mui/material'; -import { ControlledMenu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu'; +import { ControlledMenu, FocusableItem, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu'; import mixpanel from 'mixpanel-browser'; import { ColorResult } from 'react-color'; import { sheets } from '../../../grid/controller/Sheets'; import { convertReactColorToString } from '../../../helpers/convertColor'; import { focusGrid } from '../../../helpers/focusGrid'; import { QColorPicker } from '../../components/qColorPicker'; +import { SheetSize } from './SheetSize'; interface Props { contextMenu?: { x: number; y: number; id: string; name: string }; @@ -71,14 +72,15 @@ export const SheetBarTabContextMenu = (props: Props): JSX.Element => { Rename - + + {({ ref }) => } { quadraticCore.setSheetSize(sheets.sheet.id, undefined, undefined, true, sheets.getCursorPosition()); handleClose(); }} > - Auto size based on content + Fit content { handleClose(); }} > - Remove sheet size + Remove size limits diff --git a/quadratic-client/src/app/ui/menus/SheetBar/SheetSize.tsx b/quadratic-client/src/app/ui/menus/SheetBar/SheetSize.tsx new file mode 100644 index 0000000000..83e28610f8 --- /dev/null +++ b/quadratic-client/src/app/ui/menus/SheetBar/SheetSize.tsx @@ -0,0 +1,79 @@ +import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { Input } from '@/shared/shadcn/ui/input'; +import { Label } from '@/shared/shadcn/ui/label'; +import { forwardRef, KeyboardEvent, useEffect, useRef, useState } from 'react'; + +interface Props { + close: () => void; +} + +export const SheetSize = forwardRef((props, ref): JSX.Element => { + const { close } = props; + + const widthRef = useRef(null); + const heightRef = useRef(null); + + const [size, setSize] = useState<[bigint, bigint] | undefined>(); + useEffect(() => { + const updateSize = () => { + setSize(sheets.sheet.sheetSize); + }; + + updateSize(); + events.on('sheetInfo', updateSize); + return () => { + events.off('sheetInfo', updateSize); + }; + }, []); + + const onChange = () => { + if (!widthRef.current || !heightRef.current) return; + const width = parseInt(widthRef.current.value); + const height = parseInt(heightRef.current.value); + + if (width && height && !(BigInt(width) === size?.[0] || BigInt(height) === size?.[1])) { + quadraticCore.setSheetSize(sheets.sheet.id, width, height, false, sheets.getCursorPosition()); + } + }; + + const handleEnter = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + onChange(); + close(); + } + }; + + return ( +
+
+ + +
+
+ + +
+
+ ); +}); diff --git a/quadratic-core/src/grid/sheet/bounds.rs b/quadratic-core/src/grid/sheet/bounds.rs index 116b6dbb23..f5091591ea 100644 --- a/quadratic-core/src/grid/sheet/bounds.rs +++ b/quadratic-core/src/grid/sheet/bounds.rs @@ -255,6 +255,7 @@ impl Sheet { /// finds the nearest column with or without content /// if reverse is true it searches to the left of the start /// if with_content is true it searches for a column with content; otherwise it searches for a column without content + /// if we reach the start/end of the sheetSize, then we return the sheetSize column /// /// Returns the found column matching the criteria of with_content pub fn find_next_column( @@ -265,14 +266,26 @@ impl Sheet { with_content: bool, ) -> Option { let Some(bounds) = self.row_bounds(row, true) else { + if !reverse { + if let Some(sheet_size) = self.sheet_size { + return Some(sheet_size.0); + } + } else { + return Some(0); + } return if with_content { None } else { Some(column_start) }; }; + + // sheet_size bounds + let min = 0; + let max = self.sheet_size.map_or(i64::MAX, |size| size.0); + let mut x = column_start; - while (reverse && x >= bounds.0) || (!reverse && x <= bounds.1) { + while (reverse && x >= bounds.0 && x >= min) || (!reverse && x <= bounds.1 && x <= max) { let has_content = self.display_value(Pos { x, y: row }); if has_content.is_some_and(|cell_value| cell_value != CellValue::Blank) { if with_content { @@ -283,6 +296,15 @@ impl Sheet { } x += if reverse { -1 } else { 1 }; } + + // if we've reached the end of the sheetSize then return the sheetSize column + if !reverse && self.sheet_size.is_some() { + return Some(max); + } + if x <= min { + return Some(min); + } + let has_content = self.display_value(Pos { x, y: row }); if with_content == has_content.is_some() { Some(x) @@ -294,6 +316,7 @@ impl Sheet { /// finds the next column with or without content /// if reverse is true it searches to the left of the start /// if with_content is true it searches for a column with content; otherwise it searches for a column without content + /// if we reach the start/end of the sheetSize, then we return the sheetSize row /// /// Returns the found row matching the criteria of with_content pub fn find_next_row( @@ -304,10 +327,22 @@ impl Sheet { with_content: bool, ) -> Option { let Some(bounds) = self.column_bounds(column, true) else { + if !reverse { + if let Some(sheet_size) = self.sheet_size { + return Some(sheet_size.1); + } + } else { + return Some(0); + } return if with_content { None } else { Some(row_start) }; }; + + // sheet_size bounds + let min = 0; + let max = self.sheet_size.map_or(i64::MAX, |size| size.1); + let mut y = row_start; - while (reverse && y >= bounds.0) || (!reverse && y <= bounds.1) { + while (reverse && y >= bounds.0 && y >= min) || (!reverse && y <= bounds.1 && y <= max) { let has_content = self.display_value(Pos { x: column, y }); if has_content.is_some_and(|cell_value| cell_value != CellValue::Blank) { if with_content { @@ -318,6 +353,15 @@ impl Sheet { } y += if reverse { -1 } else { 1 }; } + + // if we've reached the end of the sheetSize then return the sheetSize row + if !reverse && self.sheet_size.is_some() { + return Some(max); + } + if y <= min { + return Some(min); + } + let has_content = self.display_value(Pos { x: column, y }); if with_content == has_content.is_some() { Some(y) @@ -560,7 +604,7 @@ mod test { #[test] #[parallel] - fn test_find_next_column() { + fn find_next_column() { let mut sheet = Sheet::test(); sheet.set_cell_value(Pos { x: 1, y: 2 }, CellValue::Text(String::from("test"))); @@ -569,15 +613,14 @@ mod test { assert_eq!(sheet.find_next_column(0, 0, false, false), Some(0)); assert_eq!(sheet.find_next_column(0, 0, false, true), None); assert_eq!(sheet.find_next_column(0, 0, true, false), Some(0)); - assert_eq!(sheet.find_next_column(0, 0, true, true), None); - assert_eq!(sheet.find_next_column(-1, 2, false, true), Some(1)); - assert_eq!(sheet.find_next_column(-1, 2, true, true), None); + assert_eq!(sheet.find_next_column(0, 0, true, true), Some(0)); + assert_eq!(sheet.find_next_column(3, 2, false, true), None); assert_eq!(sheet.find_next_column(3, 2, true, true), Some(1)); assert_eq!(sheet.find_next_column(2, 2, false, true), None); assert_eq!(sheet.find_next_column(2, 2, true, true), Some(1)); assert_eq!(sheet.find_next_column(0, 2, false, true), Some(1)); - assert_eq!(sheet.find_next_column(0, 2, true, true), None); + assert_eq!(sheet.find_next_column(0, 2, true, true), Some(0)); assert_eq!(sheet.find_next_column(1, 2, false, false), Some(2)); assert_eq!(sheet.find_next_column(1, 2, true, false), Some(0)); @@ -588,41 +631,52 @@ mod test { assert_eq!(sheet.find_next_column(2, 2, false, false), Some(4)); assert_eq!(sheet.find_next_column(2, 2, true, false), Some(0)); assert_eq!(sheet.find_next_column(3, 2, true, false), Some(0)); + + sheet.sheet_size = Some((20, 20)); + assert_eq!(sheet.find_next_column(3, 2, false, false), Some(20)); + assert_eq!(sheet.find_next_column(20, 2, false, false), Some(20)); + assert_eq!(sheet.find_next_column(5, 3, false, false), Some(20)); + assert_eq!(sheet.find_next_column(5, 3, false, true), Some(20)); + assert_eq!(sheet.find_next_column(5, 3, true, true), Some(0)); + + assert_eq!(sheet.find_next_column(0, 0, true, false), Some(0)); + assert_eq!(sheet.find_next_column(0, 0, true, true), Some(0)); + sheet.set_cell_value(Pos { x: 0, y: 0 }, CellValue::Text("test".to_string())); + assert_eq!(sheet.find_next_column(0, 0, true, false), Some(0)); + assert_eq!(sheet.find_next_column(0, 0, true, true), Some(0)); } #[test] #[parallel] - fn test_find_next_column_code() { + fn find_next_column_code() { let mut sheet = Sheet::test(); sheet.test_set_code_run_array(0, 0, vec!["1", "2", "3"], false); - assert_eq!(sheet.find_next_column(-1, 0, false, true), Some(0)); assert_eq!(sheet.find_next_column(0, 0, false, false), Some(3)); assert_eq!(sheet.find_next_column(2, 0, false, false), Some(3)); assert_eq!(sheet.find_next_column(4, 0, true, true), Some(2)); - assert_eq!(sheet.find_next_column(2, 0, true, false), Some(-1)); + assert_eq!(sheet.find_next_column(2, 0, true, false), Some(0)); } #[test] #[parallel] - fn test_find_next_row() { + fn find_next_row() { let mut sheet = Sheet::test(); - let _ = sheet.set_cell_value(Pos { x: 2, y: 1 }, CellValue::Text(String::from("test"))); + sheet.set_cell_value(Pos { x: 2, y: 1 }, CellValue::Text(String::from("test"))); sheet.set_cell_value(Pos { x: 10, y: 10 }, CellValue::Text(String::from("test"))); assert_eq!(sheet.find_next_row(0, 0, false, false), Some(0)); assert_eq!(sheet.find_next_row(0, 0, false, true), None); assert_eq!(sheet.find_next_row(0, 0, true, false), Some(0)); - assert_eq!(sheet.find_next_row(0, 0, true, true), None); - assert_eq!(sheet.find_next_row(-1, 2, false, true), Some(1)); - assert_eq!(sheet.find_next_row(-1, 2, true, true), None); + assert_eq!(sheet.find_next_row(0, 0, true, true), Some(0)); + assert_eq!(sheet.find_next_row(3, 2, false, true), None); assert_eq!(sheet.find_next_row(3, 2, true, true), Some(1)); assert_eq!(sheet.find_next_row(2, 2, false, true), None); assert_eq!(sheet.find_next_row(2, 2, true, true), Some(1)); assert_eq!(sheet.find_next_row(0, 2, false, true), Some(1)); - assert_eq!(sheet.find_next_row(0, 2, true, true), None); + assert_eq!(sheet.find_next_row(0, 2, true, true), Some(0)); assert_eq!(sheet.find_next_row(1, 2, false, false), Some(2)); assert_eq!(sheet.find_next_row(1, 2, true, false), Some(0)); @@ -631,20 +685,34 @@ mod test { assert_eq!(sheet.find_next_row(1, 2, false, false), Some(4)); assert_eq!(sheet.find_next_row(2, 2, false, false), Some(4)); + assert_eq!(sheet.find_next_row(2, 2, true, false), Some(0)); assert_eq!(sheet.find_next_row(3, 2, true, false), Some(0)); + + sheet.sheet_size = Some((20, 20)); + assert_eq!(sheet.find_next_row(3, 2, false, false), Some(20)); + assert_eq!(sheet.find_next_row(20, 2, false, false), Some(20)); + assert_eq!(sheet.find_next_row(5, 3, false, false), Some(20)); + assert_eq!(sheet.find_next_row(5, 3, false, true), Some(20)); + assert_eq!(sheet.find_next_row(5, 3, true, true), Some(0)); + + assert_eq!(sheet.find_next_row(0, 0, true, false), Some(0)); + assert_eq!(sheet.find_next_row(0, 0, true, true), Some(0)); + + sheet.set_cell_value(Pos { x: 0, y: 0 }, CellValue::Text("test".to_string())); + assert_eq!(sheet.find_next_row(0, 0, true, false), Some(0)); + assert_eq!(sheet.find_next_row(0, 0, true, true), Some(0)); } #[test] #[parallel] - fn test_find_next_row_code() { + fn find_next_row_code() { let mut sheet = Sheet::test(); sheet.test_set_code_run_array(0, 0, vec!["1", "2", "3"], true); - assert_eq!(sheet.find_next_row(-1, 0, false, true), Some(0)); assert_eq!(sheet.find_next_row(0, 0, false, false), Some(3)); assert_eq!(sheet.find_next_row(2, 0, false, false), Some(3)); assert_eq!(sheet.find_next_row(4, 0, true, true), Some(2)); - assert_eq!(sheet.find_next_row(2, 0, true, false), Some(-1)); + assert_eq!(sheet.find_next_row(2, 0, true, false), Some(0)); } #[test] From 17c44571dc8780299f347024fee8a171148836fa Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 10 Sep 2024 10:01:46 -0700 Subject: [PATCH 14/14] fixing gridlines --- .../src/app/gridGL/UI/GridLines.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/quadratic-client/src/app/gridGL/UI/GridLines.ts b/quadratic-client/src/app/gridGL/UI/GridLines.ts index 48a8afe4aa..58ea3e2b22 100644 --- a/quadratic-client/src/app/gridGL/UI/GridLines.ts +++ b/quadratic-client/src/app/gridGL/UI/GridLines.ts @@ -56,6 +56,11 @@ export class GridLines extends Graphics { this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); this.gridLinesX = []; this.gridLinesY = []; + + bounds.width += bounds.x < 0 ? -bounds.x : 0; + bounds.height += bounds.y < 0 ? -bounds.y : 0; + bounds.x = Math.min(bounds.x, 0); + bounds.y = Math.min(bounds.y, 0); const range = this.drawHorizontalLines(bounds); this.drawVerticalLines(bounds, range); } @@ -75,9 +80,10 @@ export class GridLines extends Graphics { // draw out of bounds (left) this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); let x = bounds.left; + // this can be optimized to start at the first column that is not out of bounds while (column < 0) { - this.moveTo(x - offset, bounds.top); - this.lineTo(x - offset, bounds.bottom); + // this.moveTo(x - offset, bounds.top); + // this.lineTo(x - offset, bounds.bottom); size = sheets.sheet.offsets.getColumnWidth(column); x += size; column++; @@ -102,8 +108,8 @@ export class GridLines extends Graphics { if (bounds.top < 0) { // draw out of bounds above this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); - this.moveTo(x - offset, bounds.top); - this.lineTo(x - offset, 0); + // this.moveTo(x - offset, bounds.top); + this.moveTo(x - offset, Math.max(0, bounds.top)); this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); if (oobBottom !== undefined) { @@ -168,9 +174,10 @@ export class GridLines extends Graphics { // draw out of bounds (top) this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); let y = bounds.top; + // this can be optimized to start at the first row that is not out of bounds while (row < 0) { - this.moveTo(bounds.left, y - offset); - this.lineTo(bounds.right, y - offset); + // this.moveTo(bounds.left, y - offset); + // this.lineTo(bounds.right, y - offset); size = offsets.getRowHeight(row); y += size; row++; @@ -188,8 +195,8 @@ export class GridLines extends Graphics { if (bounds.left < 0) { // draw out of bounds (left) this.lineStyle({ width: 1, color: colors.gridLinesOutOfBounds, alignment: 0.5, native: true }); - this.moveTo(bounds.left, y - offset); - this.lineTo(0, y - offset); + // this.moveTo(bounds.left, y - offset); + this.moveTo(Math.max(bounds.left, 0), y - offset); this.lineStyle({ width: 1, color: colors.gridLines, alignment: 0.5, native: true }); if (oobRight !== undefined) {