diff --git a/quadratic-client/src/app/grid/actions/clipboard/clipboard.ts b/quadratic-client/src/app/grid/actions/clipboard/clipboard.ts index 2e7e433fea..47e9eb22aa 100644 --- a/quadratic-client/src/app/grid/actions/clipboard/clipboard.ts +++ b/quadratic-client/src/app/grid/actions/clipboard/clipboard.ts @@ -31,6 +31,7 @@ export const copyToClipboardEvent = async (e: ClipboardEvent) => { debugTimeReset(); const jsClipboard = await quadraticCore.copyToClipboard(sheets.getRustSelection()); await toClipboard(jsClipboard.plainText, jsClipboard.html); + pixiApp.copy.changeCopyRanges(); debugTimeCheck('copy to clipboard'); }; @@ -111,6 +112,7 @@ export const copyToClipboard = async () => { debugTimeReset(); const jsClipboard = await quadraticCore.copyToClipboard(sheets.getRustSelection()); await toClipboard(jsClipboard.plainText, jsClipboard.html); + pixiApp.copy.changeCopyRanges(); debugTimeCheck('copy to clipboard'); }; diff --git a/quadratic-client/src/app/grid/sheet/Sheet.ts b/quadratic-client/src/app/grid/sheet/Sheet.ts index 9ccc21b7f2..a6d2d6f190 100644 --- a/quadratic-client/src/app/grid/sheet/Sheet.ts +++ b/quadratic-client/src/app/grid/sheet/Sheet.ts @@ -149,9 +149,14 @@ export class Sheet { } // @returns screen rectangle for a column/row rectangle - getScreenRectangle(column: number, row: number, width: number, height: number): Rectangle { - const topLeft = this.getCellOffsets(column, row); - const bottomRight = this.getCellOffsets(column + width, row + height); + getScreenRectangle( + column: number | BigInt, + row: number | BigInt, + width: number | BigInt, + height: number | BigInt + ): Rectangle { + const topLeft = this.getCellOffsets(Number(column), Number(row)); + const bottomRight = this.getCellOffsets(Number(column) + Number(width), Number(row) + Number(height)); return new Rectangle(topLeft.left, topLeft.top, bottomRight.left - topLeft.left, bottomRight.top - topLeft.top); } diff --git a/quadratic-client/src/app/grid/sheet/SheetCursor.ts b/quadratic-client/src/app/grid/sheet/SheetCursor.ts index 01877d6cc4..4ceb43da0f 100644 --- a/quadratic-client/src/app/grid/sheet/SheetCursor.ts +++ b/quadratic-client/src/app/grid/sheet/SheetCursor.ts @@ -288,4 +288,14 @@ export class SheetCursor { return []; } } + + getRanges(): CellRefRange[] { + const rangesStringified = this.jsSelection.getRanges(); + try { + return JSON.parse(rangesStringified); + } catch (e) { + console.error(e); + return []; + } + } } diff --git a/quadratic-client/src/app/gridGL/UI/UICopy.ts b/quadratic-client/src/app/gridGL/UI/UICopy.ts new file mode 100644 index 0000000000..fe908d2d64 --- /dev/null +++ b/quadratic-client/src/app/gridGL/UI/UICopy.ts @@ -0,0 +1,95 @@ +import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { DASHED } from '@/app/gridGL/generateTextures'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { drawDashedRectangleMarching } from '@/app/gridGL/UI/cellHighlights/cellHighlightsDraw'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; +import { CellRefRange } from '@/app/quadratic-core-types'; +import { Graphics } from 'pixi.js'; + +const MARCH_TIME = 80; +const ALPHA = 0.5; + +// walking rectangle offset +const RECT_OFFSET = 1; + +export class UICopy extends Graphics { + private sheetId?: string; + private ranges?: CellRefRange[]; + private time = 0; + private march = 0; + private dirty = false; + + constructor() { + super(); + events.on('changeSheet', this.updateNextTick); + events.on('viewportChanged', this.updateNextTick); + events.on('transactionStart', this.clearCopyRanges); + } + + destroy() { + events.off('changeSheet', this.updateNextTick); + events.off('viewportChanged', this.updateNextTick); + events.off('transactionStart', this.clearCopyRanges); + super.destroy(); + } + + isShowing(): boolean { + return !!this.ranges && this.sheetId === sheets.sheet.id; + } + + private updateNextTick = () => (this.dirty = true); + + clearCopyRanges = () => { + this.clear(); + pixiApp.setViewportDirty(); + this.ranges = undefined; + this.sheetId = undefined; + }; + + changeCopyRanges() { + const range = sheets.sheet.cursor.getRanges(); + this.ranges = range; + this.time = 0; + this.march = 0; + this.sheetId = sheets.sheet.id; + } + + private draw() { + if (!this.ranges) return; + let render = false; + this.ranges.forEach((cellRefRange) => { + const color = getCSSVariableTint('primary'); + render ||= drawDashedRectangleMarching({ + g: this, + color, + march: this.march, + noFill: true, + alpha: ALPHA, + offset: RECT_OFFSET, + range: cellRefRange, + }); + }); + if (render) { + pixiApp.setViewportDirty(); + } + } + + update() { + if (!this.ranges) return; + if (this.sheetId !== sheets.sheet.id) { + this.clear(); + return; + } + const drawFrame = Date.now() - this.time > MARCH_TIME; + if (drawFrame) { + this.march = (this.march + 1) % Math.floor(DASHED); + this.time = Date.now(); + } + if (drawFrame || this.dirty) { + this.clear(); + this.draw(); + this.dirty = false; + } + } +} diff --git a/quadratic-client/src/app/gridGL/UI/cellHighlights/cellHighlightsDraw.ts b/quadratic-client/src/app/gridGL/UI/cellHighlights/cellHighlightsDraw.ts index 6d8a665f59..c30c613bab 100644 --- a/quadratic-client/src/app/gridGL/UI/cellHighlights/cellHighlightsDraw.ts +++ b/quadratic-client/src/app/gridGL/UI/cellHighlights/cellHighlightsDraw.ts @@ -61,35 +61,43 @@ export function drawDashedRectangleMarching(options: { g: Graphics; color: number; march: number; + noFill?: boolean; + alpha?: number; + offset?: number; range: CellRefRange; -}) { - const { g, color, march, range } = options; +}): boolean { + const { g, color, march, noFill, alpha, offset = 0, range } = options; const selectionRect = getRangeScreenRectangleFromCellRefRange(range); const bounds = pixiApp.viewport.getVisibleBounds(); if (!intersects.rectangleRectangle(selectionRect, bounds)) { - return; + return false; } - const minX = selectionRect.left; - const minY = selectionRect.top; - const maxX = selectionRect.right; - const maxY = selectionRect.bottom; + const minX = selectionRect.left + offset; + const minY = selectionRect.top + offset; + const maxX = selectionRect.right - offset; + const maxY = selectionRect.bottom - offset; - g.clear(); + if (!noFill) { + g.clear(); + } g.lineStyle({ alignment: 0, }); - g.moveTo(minX, minY); - g.beginFill(color, FILL_ALPHA); - g.drawRect(minX, minY, maxX - minX, maxY - minY); - g.endFill(); + if (!noFill) { + g.beginFill(color, FILL_ALPHA); + g.drawRect(minX, minY, maxX - minX, maxY - minY); + g.endFill(); + } + g.moveTo(minX, minY); g.lineStyle({ width: CURSOR_THICKNESS, color, alignment: 0, + alpha, }); const clamp = (n: number, min: number, max: number): number => { @@ -132,4 +140,6 @@ export function drawDashedRectangleMarching(options: { g.moveTo(minX + DASHED_THICKNESS, clamp(y - DASHED / 2, minY, maxY)); g.lineTo(minX + DASHED_THICKNESS, clamp(y, minY, maxY)); } + + return true; } diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardViewport.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardViewport.ts index 5563e5f147..27c66ddd7b 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardViewport.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardViewport.ts @@ -68,6 +68,12 @@ export function keyboardViewport(event: React.KeyboardEvent): boole // Close overlay if (matchShortcut(Action.CloseOverlay, event)) { + // clear copy range if it is showing + if (pixiApp.copy.isShowing()) { + pixiApp.copy.clearCopyRanges(); + return true; + } + if (gridSettings.presentationMode) { setGridSettings({ ...gridSettings, presentationMode: false }); return true; diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index 06c009f991..ad23e14538 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -13,6 +13,7 @@ import { GridLines } from '@/app/gridGL/UI/GridLines'; import { HtmlPlaceholders } from '@/app/gridGL/UI/HtmlPlaceholders'; import { UICellImages } from '@/app/gridGL/UI/UICellImages'; import { UICellMoving } from '@/app/gridGL/UI/UICellMoving'; +import { UICopy } from '@/app/gridGL/UI/UICopy'; import { UIMultiPlayerCursor } from '@/app/gridGL/UI/UIMultiplayerCursor'; import { UIValidations } from '@/app/gridGL/UI/UIValidations'; import { BoxCells } from '@/app/gridGL/UI/boxCells'; @@ -67,6 +68,7 @@ export class PixiApp { imagePlaceholders!: Container; cellImages!: UICellImages; validations: UIValidations; + copy: UICopy; renderer!: Renderer; momentumDetector: MomentumScrollDetector; @@ -93,6 +95,7 @@ export class PixiApp { this.viewport = new Viewport(); this.background = new Background(); this.momentumDetector = new MomentumScrollDetector(); + this.copy = new UICopy(); } init() { @@ -157,6 +160,7 @@ export class PixiApp { this.gridLines = this.viewportContents.addChild(new GridLines()); this.boxCells = this.viewportContents.addChild(new BoxCells()); this.cellImages = this.viewportContents.addChild(this.cellImages); + this.copy = this.viewportContents.addChild(this.copy); this.multiplayerCursor = this.viewportContents.addChild(new UIMultiPlayerCursor()); this.cursor = this.viewportContents.addChild(new Cursor()); this.htmlPlaceholders = this.viewportContents.addChild(new HtmlPlaceholders()); diff --git a/quadratic-client/src/app/gridGL/pixiApp/Update.ts b/quadratic-client/src/app/gridGL/pixiApp/Update.ts index a95048619b..e85e8b6f78 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Update.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Update.ts @@ -103,6 +103,8 @@ export class Update { pixiApp.validations.update(pixiApp.viewport.dirty); debugTimeCheck('[Update] backgrounds'); pixiApp.background.update(pixiApp.viewport.dirty); + debugTimeCheck('[Update] copy'); + pixiApp.copy.update(); if (pixiApp.viewport.dirty || rendererDirty) { debugTimeReset();