From 72576d5b3256e79d690e8eb831fe38c2ce8400a7 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 3 May 2024 12:07:13 -0700 Subject: [PATCH 01/16] initial work on moving cells --- quadratic-client/src/app/events/events.ts | 2 + .../src/app/gridGL/UI/UICellMoving.ts | 55 +++++++ .../app/gridGL/interaction/pointer/Pointer.ts | 25 +++- .../interaction/pointer/PointerCellMoving.ts | 136 ++++++++++++++++++ .../src/app/gridGL/pixiApp/PixiApp.ts | 5 + .../src/app/gridGL/pixiApp/Update.ts | 5 +- quadratic-client/src/app/theme/colors.ts | 4 + .../menus/ContextMenu/FloatingContextMenu.tsx | 11 +- 8 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/UI/UICellMoving.ts create mode 100644 quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 52014f5acf..264be5cd85 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -77,6 +77,8 @@ interface EventTypes { resizeHeadingColumn: (column: number) => void; offlineTransactions: (transactions: number, operations: number) => void; + + cellMoving: (move: boolean) => void; } export const events = new EventEmitter(); diff --git a/quadratic-client/src/app/gridGL/UI/UICellMoving.ts b/quadratic-client/src/app/gridGL/UI/UICellMoving.ts new file mode 100644 index 0000000000..de9bda98cb --- /dev/null +++ b/quadratic-client/src/app/gridGL/UI/UICellMoving.ts @@ -0,0 +1,55 @@ +import { sheets } from '@/app/grid/controller/Sheets'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { colors } from '@/app/theme/colors'; +import { BitmapText, Container, Graphics } from 'pixi.js'; + +const MOVING_THICKNESS = 2; + +export class UICellMoving extends Container { + private graphics: Graphics; + private location: BitmapText; + + dirty = false; + + constructor() { + super(); + this.graphics = this.addChild(new Graphics()); + this.location = this.addChild(new BitmapText('', { fontName: 'OpenSans', fontSize: 12 })); + this.visible = false; + } + + private drawMove() { + const moving = pixiApp.pointer.pointerCellMoving.moving; + if (!moving) { + throw new Error('Expected moving to be defined in drawMove'); + } + this.visible = true; + this.graphics.clear(); + this.graphics.lineStyle(1, colors.movingCells, MOVING_THICKNESS); + console.log('drawing...'); + const offsets = sheets.sheet.offsets; + const start = offsets.getCellOffsets(moving.toColumn, moving.toRow); + const end = offsets.getCellOffsets(moving.toColumn + moving.width, moving.toRow + moving.height); + this.graphics.drawRect(start.x, start.y, end.x + end.w - start.x, end.y + end.h - start.y); + } + + update() { + if (this.dirty) { + this.dirty = false; + switch (pixiApp.pointer.pointerCellMoving.state) { + case 'hover': + if (this.visible) { + this.visible = false; + } + break; + case 'move': + this.drawMove(); + break; + default: + if (this.visible) { + this.visible = false; + } + } + } + } +} diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts index 1741c4aa6e..800d75e618 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts @@ -1,3 +1,4 @@ +import { PointerCellMoving } from '@/app/gridGL/interaction/pointer/PointerCellMoving'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { Viewport } from 'pixi-viewport'; @@ -15,6 +16,7 @@ export class Pointer { pointerHtmlCells: PointerHtmlCells; pointerCursor: PointerCursor; pointerDown: PointerDown; + pointerCellMoving: PointerCellMoving; constructor(viewport: Viewport) { this.pointerHeading = new PointerHeading(); @@ -22,6 +24,7 @@ export class Pointer { this.pointerDown = new PointerDown(); this.pointerCursor = new PointerCursor(); this.pointerHtmlCells = new PointerHtmlCells(); + this.pointerCellMoving = new PointerCellMoving(); viewport.on('pointerdown', this.handlePointerDown); viewport.on('pointermove', this.pointerMove); @@ -75,32 +78,43 @@ export class Pointer { if (this.isMoreThanOneTouch(e)) return; const world = pixiApp.viewport.toWorld(e.data.global); const event = e.data.originalEvent as PointerEvent; - this.pointerHtmlCells.pointerDown(e) || + this.pointerCellMoving.pointerDown(event) || + this.pointerHtmlCells.pointerDown(e) || this.pointerHeading.pointerDown(world, event) || this.pointerAutoComplete.pointerDown(world) || this.pointerDown.pointerDown(world, event); + + this.updateCursor(); }; private pointerMove = (e: InteractionEvent): void => { if (this.isMoreThanOneTouch(e) || this.isOverCodeEditor(e)) return; const world = pixiApp.viewport.toWorld(e.data.global); - this.pointerHtmlCells.pointerMove(e) || + const event = e.data.originalEvent as PointerEvent; + this.pointerCellMoving.pointerMove(event, world) || + this.pointerHtmlCells.pointerMove(e) || this.pointerHeading.pointerMove(world) || this.pointerAutoComplete.pointerMove(world) || this.pointerDown.pointerMove(world) || this.pointerCursor.pointerMove(world); - // change the cursor based on pointer priority + this.updateCursor(); + }; + + // change the cursor based on pointer priority + private updateCursor() { const cursor = + pixiApp.pointer.pointerCellMoving.cursor ?? pixiApp.pointer.pointerHtmlCells.cursor ?? pixiApp.pointer.pointerHeading.cursor ?? pixiApp.pointer.pointerAutoComplete.cursor; pixiApp.canvas.style.cursor = cursor ?? 'unset'; - }; + } private pointerUp = (e: InteractionEvent): void => { if (this.isMoreThanOneTouch(e)) return; - this.pointerHtmlCells.pointerUp() || + this.pointerCellMoving.pointerUp() || + this.pointerHtmlCells.pointerUp() || this.pointerHeading.pointerUp() || this.pointerAutoComplete.pointerUp() || this.pointerDown.pointerUp(); @@ -116,6 +130,7 @@ export class Pointer { return true; } return ( + this.pointerCellMoving.handleEscape() || this.pointerHtmlCells.handleEscape() || this.pointerHeading.handleEscape() || this.pointerAutoComplete.handleEscape() diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts new file mode 100644 index 0000000000..ce9fbf539c --- /dev/null +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -0,0 +1,136 @@ +import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { PanMode, pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; +import { Point } from 'pixi.js'; +import { isMobile } from 'react-device-detect'; + +// Distance from top left corner to trigger a cell move. +const TOP_LEFT_CORNER_THRESHOLD_SQUARED = 100; + +interface MoveCells { + column: number; + row: number; + width: number; + height: number; + toColumn: number; + toRow: number; +} + +export class PointerCellMoving { + moving?: MoveCells; + state?: 'hover' | 'move'; + + get cursor(): string | undefined { + switch (this.state) { + case 'move': + return 'grabbing'; + case 'hover': + return 'grab'; + default: + return undefined; + } + } + + findCorner(world: Point): Point { + return world; + } + pointerDown(event: PointerEvent): boolean { + if (isMobile || pixiAppSettings.panMode !== PanMode.Disabled || event.button === 1) return false; + + if (this.state === 'hover') { + this.state = 'move'; + events.emit('cellMoving', true); + return true; + } + return false; + } + + // Completes the move + private completeMove() { + if (this.state !== 'move') { + throw new Error('Expected moving to be defined in completeMove'); + } + // move the cells + + this.reset(); + } + + private reset() { + this.moving = undefined; + if (this.state === 'move') { + pixiApp.cellMoving.dirty = true; + events.emit('cellMoving', false); + } + this.state = undefined; + } + + private pointerMoveMoving(world: Point) { + if (this.state !== 'move' || !this.moving) { + throw new Error('Expected moving to be defined in pointerMoveMoving'); + } + const offsets = sheets.sheet.offsets; + const position = offsets.getColumnRowFromScreen(world.x, world.y); + if (this.moving.toColumn !== position.column || this.moving.toRow !== position.row) { + this.moving.toColumn = position.column; + this.moving.toRow = position.row; + pixiApp.cellMoving.dirty = true; + } + } + + private pointerMoveHover(world: Point): boolean { + const sheet = sheets.sheet; + const offsets = sheet.offsets; + const { column, row } = offsets.getColumnRowFromScreen(world.x, world.y); + + const origin = sheet.cursor.originPosition; + + // if not hovering over current selection, then there's nothing to move + if (column !== origin.x || row !== origin.y) { + this.reset(); + return false; + } + + // Check if we overlap the hit-box: (1) x and y are greater than the corner; (2) distance to the corner is less than the threshold + const position = offsets.getCellOffsets(column, row); + if ( + world.x >= position.x && + world.y >= position.y && + Math.pow(position.x - world.x, 2) + Math.pow(position.y - world.y, 2) <= TOP_LEFT_CORNER_THRESHOLD_SQUARED + ) { + this.state = 'hover'; + const rectangle = sheet.cursor.getRectangle(); + this.moving = { column, row, width: rectangle.width, height: rectangle.height, toColumn: column, toRow: row }; + return true; + } + this.reset(); + return false; + } + + pointerMove(event: PointerEvent, world: Point): boolean { + if (isMobile || pixiAppSettings.panMode !== PanMode.Disabled || event.button === 1) return false; + + if (this.state === 'move') { + this.pointerMoveMoving(world); + return true; + } else { + return this.pointerMoveHover(world); + } + } + + pointerUp(): boolean { + if (this.state === 'move') { + this.completeMove(); + return true; + } + return false; + } + + handleEscape(): boolean { + if (this.state === 'move') { + this.reset(); + return true; + } + return false; + } +} diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index 5473f13dc6..bfe1e0c67c 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -1,4 +1,5 @@ import { events } from '@/app/events/events'; +import { UICellMoving } from '@/app/gridGL/UI/UICellMoving'; import { isEmbed } from '@/app/helpers/isEmbed'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; @@ -44,6 +45,8 @@ export class PixiApp { private waitingForFirstRender?: Function; private alreadyRendered = false; + // todo: UI should be pulled out and separated into its own class + highlightedCells = new HighlightedCells(); canvas!: HTMLCanvasElement; viewport!: Viewport; @@ -51,6 +54,7 @@ export class PixiApp { axesLines!: AxesLines; cursor!: Cursor; multiplayerCursor!: UIMultiPlayerCursor; + cellMoving!: UICellMoving; headings!: GridHeadings; boxCells!: BoxCells; cellsSheets: CellsSheets; @@ -170,6 +174,7 @@ export class PixiApp { this.boxCells = this.viewportContents.addChild(new BoxCells()); this.multiplayerCursor = this.viewportContents.addChild(new UIMultiPlayerCursor()); this.cursor = this.viewportContents.addChild(new Cursor()); + this.cellMoving = this.viewportContents.addChild(new UICellMoving()); this.headings = this.viewportContents.addChild(new GridHeadings()); this.reset(); diff --git a/quadratic-client/src/app/gridGL/pixiApp/Update.ts b/quadratic-client/src/app/gridGL/pixiApp/Update.ts index 36c4d3c016..dfe9e79c06 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Update.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Update.ts @@ -90,6 +90,7 @@ export class Update { pixiApp.headings.dirty || pixiApp.boxCells.dirty || pixiApp.multiplayerCursor.dirty || + pixiApp.cellMoving.dirty || pixiApp.cursor.dirty; if (rendererDirty && debugShowWhyRendering) { @@ -98,7 +99,7 @@ export class Update { pixiApp.axesLines.dirty ? 'axesLines ' : '' }${pixiApp.headings.dirty ? 'headings ' : ''}${pixiApp.cursor.dirty ? 'cursor ' : ''}${ pixiApp.multiplayerCursor.dirty ? 'multiplayer cursor' : '' - }` + }${pixiApp.cellMoving.dirty ? 'cellMoving' : ''}` ); } @@ -115,6 +116,8 @@ export class Update { debugTimeCheck('[Update] cursor'); pixiApp.multiplayerCursor.update(); debugTimeCheck('[Update] multiplayerCursor'); + pixiApp.cellMoving.update(); + debugTimeCheck('[Update] cellMoving'); if (pixiApp.viewport.dirty || rendererDirty) { debugTimeReset(); diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index 9fef65f1be..8c8f51cb8e 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -13,6 +13,10 @@ export const colors = { cellColorError: 0xf25f5c, cursorCell: 0x6cd4ff, searchCell: 0x50c878, + + // todo: this should be changed + movingCells: 0x6cd4ff, + independence: 0x5d576b, headerBackgroundColor: 0xffffff, headerSelectedBackgroundColor: 0xe7f7ff, diff --git a/quadratic-client/src/app/ui/menus/ContextMenu/FloatingContextMenu.tsx b/quadratic-client/src/app/ui/menus/ContextMenu/FloatingContextMenu.tsx index 4ba2147a45..377dc55a3c 100644 --- a/quadratic-client/src/app/ui/menus/ContextMenu/FloatingContextMenu.tsx +++ b/quadratic-client/src/app/ui/menus/ContextMenu/FloatingContextMenu.tsx @@ -1,5 +1,6 @@ import { downloadSelectionAsCsvAction, hasPermissionToEditFile } from '@/app/actions'; import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; +import { events } from '@/app/events/events'; import { copySelectionToPNG, fullClipboardSupport, pasteFromClipboard } from '@/app/grid/actions/clipboard/clipboard'; import { sheets } from '@/app/grid/controller/Sheets'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; @@ -48,7 +49,7 @@ import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; import { Divider, IconButton, Toolbar } from '@mui/material'; import { ControlledMenu, Menu, MenuDivider, MenuInstance, MenuItem, useMenuState } from '@szhsin/react-menu'; import mixpanel from 'mixpanel-browser'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useRecoilValue } from 'recoil'; interface Props { @@ -129,6 +130,8 @@ export const FloatingContextMenu = (props: Props) => { // Hide if currently selecting if (pixiApp.pointer?.pointerDown?.active) visibility = 'hidden'; + if (pixiApp.pointer.pointerCellMoving.state) visibility = 'hidden'; + // Hide if in presentation mode if (pixiAppSettings.presentationMode) visibility = 'hidden'; @@ -179,8 +182,12 @@ export const FloatingContextMenu = (props: Props) => { return transform; }, [container, showContextMenu, editorInteractionState.permissions, moreMenuToggle]); + // trigger is used to hide the menu when cellMoving + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, setTrigger] = useState(0); useEffect(() => { const { viewport } = pixiApp; + const trigger = () => setTrigger((prev) => prev + 1); if (!viewport) return; viewport.on('moved', updateContextMenuCSSTransform); @@ -188,6 +195,7 @@ export const FloatingContextMenu = (props: Props) => { document.addEventListener('pointerup', updateContextMenuCSSTransform); window.addEventListener('resize', updateContextMenuCSSTransform); window.addEventListener('keyup', updateContextMenuCSSTransform); + events.on('cellMoving', trigger); return () => { viewport.removeListener('moved', updateContextMenuCSSTransform); @@ -195,6 +203,7 @@ export const FloatingContextMenu = (props: Props) => { document.removeEventListener('pointerup', updateContextMenuCSSTransform); window.removeEventListener('resize', updateContextMenuCSSTransform); window.addEventListener('keyup', updateContextMenuCSSTransform); + events.off('cellMoving', trigger); }; }, [updateContextMenuCSSTransform]); From 22e4fff62eb988db9983238cb4d802b4768ae5cc Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 4 May 2024 05:51:23 -0700 Subject: [PATCH 02/16] removing console.logs --- quadratic-client/src/app/gridGL/UI/UICellMoving.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/quadratic-client/src/app/gridGL/UI/UICellMoving.ts b/quadratic-client/src/app/gridGL/UI/UICellMoving.ts index de9bda98cb..c9b88a9e99 100644 --- a/quadratic-client/src/app/gridGL/UI/UICellMoving.ts +++ b/quadratic-client/src/app/gridGL/UI/UICellMoving.ts @@ -3,7 +3,7 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { colors } from '@/app/theme/colors'; import { BitmapText, Container, Graphics } from 'pixi.js'; -const MOVING_THICKNESS = 2; +const MOVING_THICKNESS = 3; export class UICellMoving extends Container { private graphics: Graphics; @@ -26,7 +26,6 @@ export class UICellMoving extends Container { this.visible = true; this.graphics.clear(); this.graphics.lineStyle(1, colors.movingCells, MOVING_THICKNESS); - console.log('drawing...'); const offsets = sheets.sheet.offsets; const start = offsets.getCellOffsets(moving.toColumn, moving.toRow); const end = offsets.getCellOffsets(moving.toColumn + moving.width, moving.toRow + moving.height); From cae06584926166f727758d7e47e30e1bc6d94ee0 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 5 May 2024 06:55:06 -0700 Subject: [PATCH 03/16] attempt #2 for the core stuff --- .../execute_operation/execute_move_cells.rs | 69 +++++++++++++++++++ .../execution/execute_operation/mod.rs | 2 + .../src/controller/operations/operation.rs | 8 +++ quadratic-core/src/grid/sheet/code.rs | 17 +++++ 4 files changed, 96 insertions(+) create mode 100644 quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs b/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs new file mode 100644 index 0000000000..fa4ce33d4e --- /dev/null +++ b/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs @@ -0,0 +1,69 @@ +use crate::{ + controller::{ + active_transactions::pending_transaction::PendingTransaction, + operations::operation::Operation, GridController, + }, + SheetRect, +}; + +impl GridController { + pub fn execute_move_cells(&mut self, transaction: &mut PendingTransaction, op: Operation) { + if let Operation::MoveCells { source, dest } = op { + if !transaction.is_user() || transaction.is_undo_redo() { + transaction + .forward_operations + .push(Operation::MoveCells { source, dest }); + let dest_sheet_rect: SheetRect = ( + dest.x, + dest.y, + source.width() as i64, + source.height() as i64, + dest.sheet_id, + ) + .into(); + transaction.reverse_operations.push(Operation::MoveCells { + source: dest_sheet_rect.clone(), + dest: (source.min.x, source.min.y, source.sheet_id).into(), + }); + + // copy (pos, code_run) within source rect + let Some(source_sheet) = self.try_sheet(source.sheet_id) else { + return; + }; + let code_runs = source_sheet.remove_code_runs_in_rect(source.into()); + + // get source and dest sheets + let (Some(source_sheet), Some(dest_sheet)) = ( + self.grid.try_sheet_mut(source.sheet_id), + self.grid.try_sheet_mut(dest.sheet_id), + ) else { + return; + }; + + // delete source cell values and set them in the dest sheet + // (this also deletes any related code_runs) + let cell_values = source_sheet.delete_cell_values(source.into()); + dest_sheet.set_cell_values(dest_sheet_rect.into(), &cell_values); + + // add code_runs to dest sheet + code_runs.iter().for_each(|(pos, code_run)| { + dest_sheet.set_code_run( + (pos.x - source.min.x + dest.x, pos.y - source.min.y + dest.y).into(), + Some(*code_run.to_owned()), + ); + }); + + if transaction.is_user() { + self.check_deleted_code_runs(transaction, &source); + self.check_deleted_code_runs(transaction, &dest_sheet_rect); + self.add_compute_operations(transaction, &source, None); + self.add_compute_operations(transaction, &dest_sheet_rect, None); + self.check_all_spills(transaction, source.sheet_id); + if source.sheet_id != dest.sheet_id { + self.check_all_spills(transaction, dest.sheet_id); + } + } + } + } + } +} diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 0ae6e56b52..7997463985 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -6,6 +6,7 @@ pub mod execute_borders; pub mod execute_code; pub mod execute_cursor; pub mod execute_formats; +pub mod execute_move_cells; pub mod execute_offsets; pub mod execute_sheets; pub mod execute_values; @@ -24,6 +25,7 @@ impl GridController { Operation::ComputeCode { .. } => self.execute_compute_code(transaction, op), Operation::SetCellFormats { .. } => self.execute_set_cell_formats(transaction, op), Operation::SetBorders { .. } => self.execute_set_borders(transaction, op), + Operation::MoveCells { .. } => self.execute_move_cells(transaction, op), Operation::AddSheet { .. } => self.execute_add_sheet(transaction, op), Operation::DeleteSheet { .. } => self.execute_delete_sheet(transaction, op), diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index d5684fb7c2..a4562c6721 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -85,6 +85,11 @@ pub enum Operation { SetCursor { sheet_rect: SheetRect, }, + + MoveCells { + source: SheetRect, + dest: SheetPos, + }, } impl fmt::Display for Operation { @@ -161,6 +166,9 @@ impl fmt::Display for Operation { sheet_id, new_sheet_id ) } + Operation::MoveCells { source, dest } => { + write!(fmt, "MoveCells {{ source: {} dest: {} }}", source, dest) + } } } } diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 6cbf2ecc1b..bfeb680a2a 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -87,6 +87,23 @@ impl Sheet { }) } + /// Removes code_runs in a rect and returns them without cloning + pub fn remove_code_runs_in_rect(&mut self, rect: Rect) -> Vec<(Pos, CodeRun)> { + self.code_runs + .iter_mut(). + if rect.contains(*pos) { + if let Some(code_run) = self.code_runs.shift_remove(pos) { + Some((*pos, code_run)) + } else { + None + } + } else { + None + } + }) + .collect::>() + } + pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { self.code_runs .iter() From 35c42be747cca76e302c72b46a628f7f07dad345 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 5 May 2024 08:40:00 -0700 Subject: [PATCH 04/16] moveCells using clipboard functions --- .../interaction/pointer/PointerCellMoving.ts | 15 +- .../src/app/helpers/parseEditorPythonCell.ts | 2 +- .../src/app/quadratic-core-types/index.d.ts | 3 +- .../app/ui/menus/CodeEditor/CodeEditor.tsx | 3 +- .../ui/menus/CodeEditor/CodeEditorBody.tsx | 3 +- .../CodeEditor/useEditorCellHighlights.ts | 4 +- .../quadraticCore/coreClientMessages.ts | 16 +- .../quadraticCore/quadraticCore.ts | 24 +++ .../web-workers/quadraticCore/worker/core.ts | 17 ++ .../quadraticCore/worker/coreClient.ts | 4 + quadratic-core/src/bin/export_types.rs | 8 +- .../active_transactions/transaction_name.rs | 1 + .../execute_operation/execute_move_cells.rs | 156 +++++++++++------- .../src/controller/operations/clipboard.rs | 4 + .../src/controller/user_actions/clipboard.rs | 52 ++++++ quadratic-core/src/grid/sheet/code.rs | 25 ++- .../src/wasm_bindings/controller/clipboard.rs | 14 +- 17 files changed, 272 insertions(+), 79 deletions(-) diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index ce9fbf539c..f8c23192a2 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -2,6 +2,7 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { PanMode, pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Point } from 'pixi.js'; import { isMobile } from 'react-device-detect'; @@ -48,11 +49,19 @@ export class PointerCellMoving { // Completes the move private completeMove() { - if (this.state !== 'move') { + if (this.state !== 'move' || !this.moving) { throw new Error('Expected moving to be defined in completeMove'); } - // move the cells - + quadraticCore.moveCells( + this.moving.column, + this.moving.row, + this.moving.width, + this.moving.height, + sheets.sheet.id, + this.moving.toColumn, + this.moving.toRow, + sheets.sheet.id + ); this.reset(); } diff --git a/quadratic-client/src/app/helpers/parseEditorPythonCell.ts b/quadratic-client/src/app/helpers/parseEditorPythonCell.ts index 87c0e8935c..4347162f52 100644 --- a/quadratic-client/src/app/helpers/parseEditorPythonCell.ts +++ b/quadratic-client/src/app/helpers/parseEditorPythonCell.ts @@ -1,4 +1,4 @@ -import { SheetRect } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { SheetRect } from '@/app/quadratic-core-types'; import { ParseFormulaReturnType } from './formulaNotation'; export function parsePython(cellsAccessed?: SheetRect[] | null) { 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 94a8cebeca..b689ac0164 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -34,6 +34,7 @@ export interface Rect { min: Pos, max: Pos, } export interface Span { start: number, end: number, } export interface SearchOptions { case_sensitive?: boolean, whole_cell?: boolean, search_code?: boolean, sheet_id?: string, } export interface SheetPos { x: bigint, y: bigint, sheet_id: SheetId, } +export interface SheetRect { min: Pos, max: Pos, sheet_id: SheetId, } 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, } @@ -48,5 +49,5 @@ export interface JsCodeResult { transaction_id: string, success: boolean, error_ export interface MinMax { min: number, max: number, } export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } export interface SheetBounds { sheet_id: string, bounds: GridBounds, bounds_without_formatting: GridBounds, } -export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet"; +export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells"; export interface JsGetCellResponse { x: bigint, y: bigint, value: string, type_name: string, } diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx index 974b2e47da..b0336b2a68 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx @@ -2,8 +2,7 @@ import { usePythonState } from '@/app/atoms/usePythonState'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { Coordinate, SheetPosTS } from '@/app/gridGL/types/size'; -import { JsCodeCell, JsRenderCodeCell, Pos } from '@/app/quadratic-core-types'; -import { SheetRect } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { JsCodeCell, JsRenderCodeCell, Pos, SheetRect } from '@/app/quadratic-core-types'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { EvaluationResult } from '@/app/web-workers/pythonWebWorker/pythonTypes'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorBody.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorBody.tsx index 84b8b9cce7..bd16465379 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorBody.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorBody.tsx @@ -1,4 +1,4 @@ -import { SheetRect, provideCompletionItems, provideHover } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { provideCompletionItems, provideHover } from '@/app/quadratic-rust-client/quadratic_rust_client'; import Editor, { Monaco } from '@monaco-editor/react'; import monaco from 'monaco-editor'; import { useCallback, useEffect, useState } from 'react'; @@ -19,6 +19,7 @@ import { useEditorCellHighlights } from './useEditorCellHighlights'; // TODO(ddimaria): leave this as we're looking to add this back in once improved // import { useEditorDiagnostics } from './useEditorDiagnostics'; // import { Diagnostic } from 'vscode-languageserver-types'; +import { SheetRect } from '@/app/quadratic-core-types'; import { EvaluationResult } from '@/app/web-workers/pythonWebWorker/pythonTypes'; import useEventListener from '@/shared/hooks/useEventListener'; import { useEditorOnSelectionChange } from './useEditorOnSelectionChange'; diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/useEditorCellHighlights.ts b/quadratic-client/src/app/ui/menus/CodeEditor/useEditorCellHighlights.ts index 7371af7513..03292ccb43 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/useEditorCellHighlights.ts +++ b/quadratic-client/src/app/ui/menus/CodeEditor/useEditorCellHighlights.ts @@ -1,7 +1,7 @@ import { Coordinate } from '@/app/gridGL/types/size'; import { parsePython } from '@/app/helpers/parseEditorPythonCell'; -import { CodeCellLanguage } from '@/app/quadratic-core-types'; -import { SheetRect, parseFormula } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { CodeCellLanguage, SheetRect } from '@/app/quadratic-core-types'; +import { parseFormula } from '@/app/quadratic-rust-client/quadratic_rust_client'; import monaco, { editor } from 'monaco-editor'; import { useEffect, useRef } from 'react'; import { useRecoilValue } from 'recoil'; diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index 56103e90d7..1f2e9b3ab3 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -798,6 +798,19 @@ export interface CoreClientUndoRedo { redo: boolean; } +export interface ClientCoreMoveCells { + type: 'clientCoreMoveCells'; + sourceSheetId: string; + sourceX: number; + sourceY: number; + sourceWidth: number; + sourceHeight: number; + targetSheetId: string; + targetX: number; + targetY: number; + cursor: string; +} + //#endregion export type ClientCoreMessage = @@ -854,7 +867,8 @@ export type ClientCoreMessage = | ClientCoreCommitSingleResize | ClientCoreInitPython | ClientCoreImportExcel - | ClientCoreCancelExecution; + | ClientCoreCancelExecution + | ClientCoreMoveCells; 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 0df1870f3a..741e494f28 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -774,6 +774,30 @@ class QuadraticCore { }); } + moveCells( + sourceX: number, + sourceY: number, + sourceWidth: number, + sourceHeight: number, + sourceSheetId: string, + targetX: number, + targetY: number, + targetSheetId: string + ) { + this.send({ + type: 'clientCoreMoveCells', + sourceSheetId, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + targetSheetId, + targetX, + targetY, + cursor: sheets.getCursorPosition(), + }); + } + //#endregion //#region Bounds 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 ce0be24503..9c7765a280 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -19,6 +19,7 @@ import { MinMax, SearchOptions, SheetPos, + SheetRect, } from '@/app/quadratic-core-types'; import initCore, { GridController, Pos, Rect } from '@/app/quadratic-core/quadratic_core'; import { MultiplayerCoreReceiveTransaction } from '@/app/web-workers/multiplayerWebWorker/multiplayerCoreMessages'; @@ -29,6 +30,7 @@ import { ClientCoreFindNextRow, ClientCoreImportExcel, ClientCoreLoad, + ClientCoreMoveCells, ClientCoreSummarizeSelection, } from '../coreClientMessages'; import { coreClient } from './coreClient'; @@ -920,6 +922,21 @@ class Core { if (!this.gridController) throw new Error('Expected gridController to be defined'); this.gridController.removeCellNumericFormat(sheetId, pointsToRect(x, y, width, height), cursor); } + + moveCells(message: ClientCoreMoveCells) { + if (!this.gridController) throw new Error('Expected gridController to be defined'); + const source: SheetRect = { + min: { x: BigInt(message.sourceX), y: BigInt(message.sourceY) }, + max: { x: BigInt(message.sourceX + message.sourceWidth), y: BigInt(message.sourceY + message.sourceHeight) }, + sheet_id: { id: message.sourceSheetId }, + }; + const dest: SheetPos = { + x: BigInt(message.targetX), + y: BigInt(message.targetY), + sheet_id: { id: message.targetSheetId }, + }; + this.gridController.moveCells(source, dest, message.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 5abe4136fa..31429590c3 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -516,6 +516,10 @@ class CoreClient { core.removeCellNumericFormat(e.data.sheetId, e.data.x, e.data.y, e.data.width, e.data.height, e.data.cursor); break; + case 'clientCoreMoveCells': + core.moveCells(e.data); + break; + default: console.warn('[coreClient] Unhandled message type', e.data); } diff --git a/quadratic-core/src/bin/export_types.rs b/quadratic-core/src/bin/export_types.rs index 6e529b1d49..e7d8fb35f5 100644 --- a/quadratic-core/src/bin/export_types.rs +++ b/quadratic-core/src/bin/export_types.rs @@ -75,6 +75,7 @@ fn main() { Span, SearchOptions, SheetPos, + SheetRect, Placement, ColumnRow, SheetInfo, @@ -94,7 +95,10 @@ fn main() { ); if create_dir_all("../quadratic-client/src/app/quadratic-core-types").is_ok() { - std::fs::write("../quadratic-client/src/app/quadratic-core-types/index.d.ts", s) - .expect("failed to write types file"); + std::fs::write( + "../quadratic-client/src/app/quadratic-core-types/index.d.ts", + s, + ) + .expect("failed to write types file"); } } diff --git a/quadratic-core/src/controller/active_transactions/transaction_name.rs b/quadratic-core/src/controller/active_transactions/transaction_name.rs index b991f9c7ac..b4e3f55ec9 100644 --- a/quadratic-core/src/controller/active_transactions/transaction_name.rs +++ b/quadratic-core/src/controller/active_transactions/transaction_name.rs @@ -18,4 +18,5 @@ pub enum TransactionName { SheetAdd, SheetDelete, DuplicateSheet, + MoveCells, } diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs b/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs index fa4ce33d4e..3b2c77ebb2 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs @@ -1,69 +1,113 @@ -use crate::{ - controller::{ - active_transactions::pending_transaction::PendingTransaction, - operations::operation::Operation, GridController, - }, - SheetRect, +use std::collections::VecDeque; + +use crate::controller::{ + active_transactions::pending_transaction::PendingTransaction, operations::operation::Operation, + user_actions::clipboard::PasteSpecial, GridController, }; impl GridController { pub fn execute_move_cells(&mut self, transaction: &mut PendingTransaction, op: Operation) { if let Operation::MoveCells { source, dest } = op { - if !transaction.is_user() || transaction.is_undo_redo() { - transaction - .forward_operations - .push(Operation::MoveCells { source, dest }); - let dest_sheet_rect: SheetRect = ( - dest.x, - dest.y, - source.width() as i64, - source.height() as i64, - dest.sheet_id, - ) - .into(); - transaction.reverse_operations.push(Operation::MoveCells { - source: dest_sheet_rect.clone(), - dest: (source.min.x, source.min.y, source.sheet_id).into(), - }); + // we replace the MoveCells operation with a series of cut/paste + // operations so we don't have to reimplement it. There's definitely + // a more efficient way to do this. todo: when rewriting the data + // store, we should implement higher-level functions that would more + // easily implement cut/paste/move without resorting to this + // approach. + let mut operations = VecDeque::new(); + let (cut_ops, _, html) = self.cut_to_clipboard_operations(source); + operations.extend(cut_ops); + if let Ok(paste_ops) = self.paste_html_operations(dest, html, PasteSpecial::None) { + operations.extend(paste_ops); + } + operations.extend(transaction.operations.drain(..)); + transaction.operations = operations; - // copy (pos, code_run) within source rect - let Some(source_sheet) = self.try_sheet(source.sheet_id) else { - return; - }; - let code_runs = source_sheet.remove_code_runs_in_rect(source.into()); + // if !transaction.is_user() || transaction.is_undo_redo() { + // transaction + // .forward_operations + // .push(Operation::MoveCells { source, dest }); + // let dest_sheet_rect: SheetRect = ( + // dest.x, + // dest.y, + // source.width() as i64, + // source.height() as i64, + // dest.sheet_id, + // ) + // .into(); - // get source and dest sheets - let (Some(source_sheet), Some(dest_sheet)) = ( - self.grid.try_sheet_mut(source.sheet_id), - self.grid.try_sheet_mut(dest.sheet_id), - ) else { - return; - }; + // // copy (pos, code_run) within source rect + // let Some(source_sheet) = self.try_sheet(source.sheet_id) else { + // return; + // }; + // let code_runs = source_sheet.remove_code_runs_in_rect(source.into()); - // delete source cell values and set them in the dest sheet - // (this also deletes any related code_runs) - let cell_values = source_sheet.delete_cell_values(source.into()); - dest_sheet.set_cell_values(dest_sheet_rect.into(), &cell_values); + // // get source and dest sheets + // let (Some(source_sheet), Some(dest_sheet)) = ( + // self.grid.try_sheet_mut(source.sheet_id), + // self.grid.try_sheet_mut(dest.sheet_id), + // ) else { + // return; + // }; - // add code_runs to dest sheet - code_runs.iter().for_each(|(pos, code_run)| { - dest_sheet.set_code_run( - (pos.x - source.min.x + dest.x, pos.y - source.min.y + dest.y).into(), - Some(*code_run.to_owned()), - ); - }); + // // delete source cell values and set them in the dest sheet + // // (this also deletes any related code_runs) + // let cell_values = source_sheet.delete_cell_values(source.into()); + // let old_dest_values = + // dest_sheet.merge_cell_values(dest_sheet_rect.into(), &cell_values); - if transaction.is_user() { - self.check_deleted_code_runs(transaction, &source); - self.check_deleted_code_runs(transaction, &dest_sheet_rect); - self.add_compute_operations(transaction, &source, None); - self.add_compute_operations(transaction, &dest_sheet_rect, None); - self.check_all_spills(transaction, source.sheet_id); - if source.sheet_id != dest.sheet_id { - self.check_all_spills(transaction, dest.sheet_id); - } - } - } + // transaction + // .reverse_operations + // .push(Operation::SetCellValues { + // sheet_pos: source.into(), + // values: cell_values, + // }); + // transaction + // .reverse_operations + // .push(Operation::SetCellValues { + // sheet_pos: dest_sheet_rect.into(), + // values: old_dest_values, + // }); + + // // add code_runs to dest sheet + // code_runs + // .iter() + // .enumerate() + // .for_each(|(index, (pos, code_run))| { + // let old_code_run = dest_sheet.set_code_run( + // (pos.x - source.min.x + dest.x, pos.y - source.min.y + dest.y).into(), + // Some(*code_run.to_owned()), + // ); + // transaction.reverse_operations.push(Operation::SetCodeRun { + // sheet_pos: ( + // pos.x - source.min.x + dest.x, + // pos.y - source.min.y + dest.y, + // dest.sheet_id, + // ) + // .into(), + // code_run: old_code_run, + // index, + // }); + // transaction.reverse_operation.push(Operation::SetCodeRun { + // sheet_pos: pos.to_sheet_pos(source.sheet_id), + // code_run, + // index, + // }) + // }); + + // // todo: formats & borders + + // if transaction.is_user() { + // self.check_deleted_code_runs(transaction, &source); + // self.check_deleted_code_runs(transaction, &dest_sheet_rect); + // self.add_compute_operations(transaction, &source, None); + // self.add_compute_operations(transaction, &dest_sheet_rect, None); + // self.check_all_spills(transaction, source.sheet_id); + // if source.sheet_id != dest.sheet_id { + // self.check_all_spills(transaction, dest.sheet_id); + // } + // } + // } } } } diff --git a/quadratic-core/src/controller/operations/clipboard.rs b/quadratic-core/src/controller/operations/clipboard.rs index 71211dbaaa..b0e80b53e4 100644 --- a/quadratic-core/src/controller/operations/clipboard.rs +++ b/quadratic-core/src/controller/operations/clipboard.rs @@ -268,4 +268,8 @@ impl GridController { } } } + + pub fn move_cells_operations(&mut self, source: SheetRect, dest: SheetPos) -> Vec { + vec![Operation::MoveCells { source, dest }] + } } diff --git a/quadratic-core/src/controller/user_actions/clipboard.rs b/quadratic-core/src/controller/user_actions/clipboard.rs index c1a01d997e..c444d652be 100644 --- a/quadratic-core/src/controller/user_actions/clipboard.rs +++ b/quadratic-core/src/controller/user_actions/clipboard.rs @@ -244,6 +244,17 @@ impl GridController { ); } } + + pub fn move_cells(&mut self, source: SheetRect, dest: SheetPos, cursor: Option) { + let ops = self.move_cells_operations(source, dest); + self.start_user_transaction( + ops, + cursor, + TransactionName::PasteClipboard, + Some(source.sheet_id), + Some(source.into()), + ); + } } #[cfg(test)] @@ -902,4 +913,45 @@ mod test { let sheet = gc.sheet(sheet_id); assert_eq!(sheet.display_value(Pos { x: 0, y: 0 }), None); } + + #[test] + fn move_cells() { + let mut gc = GridController::default(); + let sheet_id = gc.sheet_ids()[0]; + + set_formula_code_cell(&mut gc, sheet_id, "{1, 2, 3; 4, 5, 6}", 0, 0); + set_cell_value(&mut gc, sheet_id, "100", 0, 2); + + gc.move_cells( + SheetRect::new_pos_span(Pos { x: 0, y: 0 }, Pos { x: 3, y: 2 }, sheet_id), + (10, 10, sheet_id).into(), + None, + ); + + let sheet = gc.sheet(sheet_id); + assert_eq!( + sheet.display_value((10, 10).into()), + Some(CellValue::Number(BigDecimal::from(1))) + ); + assert_eq!( + sheet.display_value((10, 12).into()), + Some(CellValue::Number(BigDecimal::from(100))) + ); + assert_eq!(sheet.display_value((0, 0).into()), None); + assert_eq!(sheet.display_value((0, 2).into()), None); + + gc.undo(None); + + let sheet = gc.sheet(sheet_id); + assert_eq!(sheet.display_value((10, 10).into()), None); + assert_eq!(sheet.display_value((10, 12).into()), None); + assert_eq!( + sheet.display_value((0, 0).into()), + Some(CellValue::Number(BigDecimal::from(1))) + ); + assert_eq!( + sheet.display_value((0, 2).into()), + Some(CellValue::Number(BigDecimal::from(100))) + ); + } } diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index bfeb680a2a..95dda42c05 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -88,20 +88,27 @@ impl Sheet { } /// Removes code_runs in a rect and returns them without cloning - pub fn remove_code_runs_in_rect(&mut self, rect: Rect) -> Vec<(Pos, CodeRun)> { - self.code_runs - .iter_mut(). + /// Returned usize is the index within the original CodeRun vec + pub fn remove_code_runs_in_rect(&mut self, rect: Rect) -> Vec<(usize, Pos, CodeRun)> { + let pos: Vec<_> = self + .code_runs + .iter_mut() + .enumerate() + .flat_map(|(index, (pos, _))| { if rect.contains(*pos) { - if let Some(code_run) = self.code_runs.shift_remove(pos) { - Some((*pos, code_run)) - } else { - None - } + Some((index, *pos)) } else { None } }) - .collect::>() + .collect(); + pos.iter() + .flat_map(|(index, pos)| { + self.code_runs + .shift_remove(pos) + .map(|code_run| (*index, *pos, code_run)) + }) + .collect() } pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { diff --git a/quadratic-core/src/wasm_bindings/controller/clipboard.rs b/quadratic-core/src/wasm_bindings/controller/clipboard.rs index 961d22f01c..e86bb64761 100644 --- a/quadratic-core/src/wasm_bindings/controller/clipboard.rs +++ b/quadratic-core/src/wasm_bindings/controller/clipboard.rs @@ -32,7 +32,6 @@ impl GridController { Ok(serde_wasm_bindgen::to_value(&output).map_err(|e| e.to_string())?) } - /// Returns [`TransactionSummary`] #[wasm_bindgen(js_name = "pasteFromClipboard")] pub fn js_paste_from_clipboard( &mut self, @@ -64,4 +63,17 @@ impl GridController { ); Ok(()) } + + #[wasm_bindgen(js_name = "moveCells")] + pub fn js_move_cells( + &mut self, + source: JsValue, + dest: JsValue, + cursor: Option, + ) -> Result<(), JsValue> { + let source = serde_wasm_bindgen::from_value(source).map_err(|e| e.to_string())?; + let dest = serde_wasm_bindgen::from_value(dest).map_err(|e| e.to_string())?; + self.move_cells(source, dest, cursor); + Ok(()) + } } From c41c81dfc1d12e3ef1ef7ff241f010b12d7eb3f4 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 6 May 2024 04:39:12 -0700 Subject: [PATCH 05/16] scroll viewport when moving cells to the edge of the screen --- .../gridGL/interaction/pointer/PointerCellMoving.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index f8c23192a2..22744f9682 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -9,6 +9,10 @@ import { isMobile } from 'react-device-detect'; // Distance from top left corner to trigger a cell move. const TOP_LEFT_CORNER_THRESHOLD_SQUARED = 100; +// Speed when turning on the mouseEdges plugin for pixi-viewport +const MOUSE_EDGES_SPEED = 8; +const MOUSE_EDGES_DISTANCE = 20; + interface MoveCells { column: number; row: number; @@ -42,6 +46,11 @@ export class PointerCellMoving { if (this.state === 'hover') { this.state = 'move'; events.emit('cellMoving', true); + pixiApp.viewport.mouseEdges({ + distance: MOUSE_EDGES_DISTANCE, + allowButtons: true, + speed: MOUSE_EDGES_SPEED / pixiApp.viewport.scale.x, + }); return true; } return false; @@ -70,6 +79,7 @@ export class PointerCellMoving { if (this.state === 'move') { pixiApp.cellMoving.dirty = true; events.emit('cellMoving', false); + pixiApp.viewport.plugins.remove('mouseEdges'); } this.state = undefined; } From 10c22e695d25d05d6b658dd6a23587bdba2331de Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 6 May 2024 04:44:55 -0700 Subject: [PATCH 06/16] fix bug with mouse-edges --- .../app/gridGL/interaction/pointer/PointerCellMoving.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index 22744f9682..355c7dd4ac 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -71,7 +71,6 @@ export class PointerCellMoving { this.moving.toRow, sheets.sheet.id ); - this.reset(); } private reset() { @@ -79,9 +78,11 @@ export class PointerCellMoving { if (this.state === 'move') { pixiApp.cellMoving.dirty = true; events.emit('cellMoving', false); - pixiApp.viewport.plugins.remove('mouseEdges'); + debugger; + console.log(pixiApp.viewport.plugins); + pixiApp.viewport.plugins.remove('mouse-edges'); + this.state = undefined; } - this.state = undefined; } private pointerMoveMoving(world: Point) { @@ -140,6 +141,7 @@ export class PointerCellMoving { pointerUp(): boolean { if (this.state === 'move') { this.completeMove(); + this.reset(); return true; } return false; From 26f40c569ee35ba34109546f9ad9b83971f861eb Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 6 May 2024 04:45:47 -0700 Subject: [PATCH 07/16] remove debug/console.log --- .../src/app/gridGL/interaction/pointer/PointerCellMoving.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index 355c7dd4ac..d75f17d0c2 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -78,8 +78,6 @@ export class PointerCellMoving { if (this.state === 'move') { pixiApp.cellMoving.dirty = true; events.emit('cellMoving', false); - debugger; - console.log(pixiApp.viewport.plugins); pixiApp.viewport.plugins.remove('mouse-edges'); this.state = undefined; } From 65c4117b6bf40e89ad2fd98acc33cf24f5f2e199 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 6 May 2024 07:30:50 -0700 Subject: [PATCH 08/16] fix reset so it always clears state --- .../src/app/gridGL/interaction/pointer/PointerCellMoving.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index d75f17d0c2..fa18caca7a 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -43,7 +43,7 @@ export class PointerCellMoving { pointerDown(event: PointerEvent): boolean { if (isMobile || pixiAppSettings.panMode !== PanMode.Disabled || event.button === 1) return false; - if (this.state === 'hover') { + if (this.state === 'hover' && this.moving) { this.state = 'move'; events.emit('cellMoving', true); pixiApp.viewport.mouseEdges({ @@ -79,8 +79,8 @@ export class PointerCellMoving { pixiApp.cellMoving.dirty = true; events.emit('cellMoving', false); pixiApp.viewport.plugins.remove('mouse-edges'); - this.state = undefined; } + this.state = undefined; } private pointerMoveMoving(world: Point) { From 2a5e7d3cd5b2fae7e1c9047566c31f8f7c573567 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 6 May 2024 11:07:30 -0700 Subject: [PATCH 09/16] change logic for drag corner --- quadratic-client/src/app/gridGL/UI/Cursor.ts | 14 ++++++++++++- .../interaction/pointer/PointerCellMoving.ts | 21 ++++++------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index a2358d2d70..06e24a928c 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -29,6 +29,9 @@ export class Cursor extends Graphics { startCell: CursorCell; endCell: CursorCell; + // cursor rectangle for normal cells + cursorRectangle?: Rectangle; + constructor() { super(); this.indicator = new Rectangle(); @@ -78,8 +81,14 @@ export class Cursor extends Graphics { } // hide cursor if code editor is open and CodeCursor is in the same cell - if (editorInteractionState.showCodeEditor && editor_selected_cell.x === cell.x && editor_selected_cell.y === cell.y) + if ( + editorInteractionState.showCodeEditor && + editor_selected_cell.x === cell.x && + editor_selected_cell.y === cell.y + ) { + this.cursorRectangle = undefined; return; + } // draw cursor this.lineStyle({ @@ -102,6 +111,9 @@ export class Cursor extends Graphics { alignment: 1, }); this.drawRect(x, y, width, height); + this.cursorRectangle = undefined; + } else { + this.cursorRectangle = new Rectangle(x, y, x + width, y + height); } } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index fa18caca7a..34fd231eb8 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -7,7 +7,7 @@ import { Point } from 'pixi.js'; import { isMobile } from 'react-device-detect'; // Distance from top left corner to trigger a cell move. -const TOP_LEFT_CORNER_THRESHOLD_SQUARED = 100; +const TOP_LEFT_CORNER_THRESHOLD_SQUARED = 50; // Speed when turning on the mouseEdges plugin for pixi-viewport const MOUSE_EDGES_SPEED = 8; @@ -98,23 +98,14 @@ export class PointerCellMoving { private pointerMoveHover(world: Point): boolean { const sheet = sheets.sheet; - const offsets = sheet.offsets; - const { column, row } = offsets.getColumnRowFromScreen(world.x, world.y); - const origin = sheet.cursor.originPosition; + const column = origin.x; + const row = origin.y; - // if not hovering over current selection, then there's nothing to move - if (column !== origin.x || row !== origin.y) { - this.reset(); - return false; - } - - // Check if we overlap the hit-box: (1) x and y are greater than the corner; (2) distance to the corner is less than the threshold - const position = offsets.getCellOffsets(column, row); + const cursor = pixiApp.cursor.cursorRectangle; if ( - world.x >= position.x && - world.y >= position.y && - Math.pow(position.x - world.x, 2) + Math.pow(position.y - world.y, 2) <= TOP_LEFT_CORNER_THRESHOLD_SQUARED + cursor && + Math.pow(cursor.x - world.x, 2) + Math.pow(cursor.y - world.y, 2) <= TOP_LEFT_CORNER_THRESHOLD_SQUARED ) { this.state = 'hover'; const rectangle = sheet.cursor.getRectangle(); From 36c096d7fcb61bf6df25bad3f85060ca31ec09a2 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 7 May 2024 05:02:36 -0700 Subject: [PATCH 10/16] can drag along borders as well --- quadratic-client/src/app/gridGL/UI/Cursor.ts | 3 +- .../interaction/pointer/PointerCellMoving.ts | 62 +++++++++++++++++-- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index 06e24a928c..79c545926f 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -126,12 +126,13 @@ export class Cursor extends Graphics { this.beginFill(colors.cursorCell, FILL_ALPHA); this.startCell = sheet.getCellOffsets(cursor.originPosition.x, cursor.originPosition.y); this.endCell = sheet.getCellOffsets(cursor.terminalPosition.x, cursor.terminalPosition.y); - this.drawRect( + this.cursorRectangle = new Rectangle( this.startCell.x, this.startCell.y, this.endCell.x + this.endCell.width - this.startCell.x, this.endCell.y + this.endCell.height - this.startCell.y ); + this.drawShape(this.cursorRectangle); } else { this.startCell = sheet.getCellOffsets(cursor.cursorPosition.x, cursor.cursorPosition.y); this.endCell = sheet.getCellOffsets(cursor.cursorPosition.x, cursor.cursorPosition.y); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index 34fd231eb8..a7402c46e5 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -1,13 +1,15 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { PanMode, pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { Point } from 'pixi.js'; +import { Point, Rectangle } from 'pixi.js'; import { isMobile } from 'react-device-detect'; // Distance from top left corner to trigger a cell move. const TOP_LEFT_CORNER_THRESHOLD_SQUARED = 50; +const BORDER_THRESHOLD = 8; // Speed when turning on the mouseEdges plugin for pixi-viewport const MOUSE_EDGES_SPEED = 8; @@ -96,17 +98,65 @@ export class PointerCellMoving { } } + private moveOverlaps(world: Point): boolean { + const cursorRectangle = pixiApp.cursor.cursorRectangle; + if (!cursorRectangle) return false; + + // top-left corner + threshold + if ( + Math.pow(cursorRectangle.x - world.x, 2) + Math.pow(cursorRectangle.y - world.y, 2) <= + TOP_LEFT_CORNER_THRESHOLD_SQUARED + ) { + return true; + } + + // if overlap indicator (autocomplete), then return false + const indicator = pixiApp.cursor.indicator; + if (intersects.rectanglePoint(indicator, world)) { + return false; + } + + // if overlaps any of the borders (with threshold), then return true + const left = new Rectangle( + cursorRectangle.x - BORDER_THRESHOLD / 2, + cursorRectangle.y, + BORDER_THRESHOLD, + cursorRectangle.height + ); + const right = new Rectangle( + cursorRectangle.x + cursorRectangle.width - BORDER_THRESHOLD / 2, + cursorRectangle.y, + BORDER_THRESHOLD, + cursorRectangle.height + ); + const top = new Rectangle( + cursorRectangle.x, + cursorRectangle.y - BORDER_THRESHOLD / 2, + cursorRectangle.width, + BORDER_THRESHOLD + ); + const bottom = new Rectangle( + cursorRectangle.x, + cursorRectangle.y + cursorRectangle.height - BORDER_THRESHOLD / 2, + cursorRectangle.width, + BORDER_THRESHOLD + ); + + return ( + intersects.rectanglePoint(left, world) || + intersects.rectanglePoint(right, world) || + intersects.rectanglePoint(top, world) || + intersects.rectanglePoint(bottom, world) + ); + } + private pointerMoveHover(world: Point): boolean { const sheet = sheets.sheet; const origin = sheet.cursor.originPosition; const column = origin.x; const row = origin.y; - const cursor = pixiApp.cursor.cursorRectangle; - if ( - cursor && - Math.pow(cursor.x - world.x, 2) + Math.pow(cursor.y - world.y, 2) <= TOP_LEFT_CORNER_THRESHOLD_SQUARED - ) { + if (this.moveOverlaps(world)) { this.state = 'hover'; const rectangle = sheet.cursor.getRectangle(); this.moving = { column, row, width: rectangle.width, height: rectangle.height, toColumn: column, toRow: row }; From 2e5c41b8fd90c4ba3b42e5a5327f25400bde124d Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 7 May 2024 05:12:07 -0700 Subject: [PATCH 11/16] fix based on PR feedback --- .../execute_operation/execute_move_cells.rs | 86 ------------------- quadratic-core/src/grid/sheet/code.rs | 2 +- 2 files changed, 1 insertion(+), 87 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs b/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs index 3b2c77ebb2..7fdb92c960 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs @@ -22,92 +22,6 @@ impl GridController { } operations.extend(transaction.operations.drain(..)); transaction.operations = operations; - - // if !transaction.is_user() || transaction.is_undo_redo() { - // transaction - // .forward_operations - // .push(Operation::MoveCells { source, dest }); - // let dest_sheet_rect: SheetRect = ( - // dest.x, - // dest.y, - // source.width() as i64, - // source.height() as i64, - // dest.sheet_id, - // ) - // .into(); - - // // copy (pos, code_run) within source rect - // let Some(source_sheet) = self.try_sheet(source.sheet_id) else { - // return; - // }; - // let code_runs = source_sheet.remove_code_runs_in_rect(source.into()); - - // // get source and dest sheets - // let (Some(source_sheet), Some(dest_sheet)) = ( - // self.grid.try_sheet_mut(source.sheet_id), - // self.grid.try_sheet_mut(dest.sheet_id), - // ) else { - // return; - // }; - - // // delete source cell values and set them in the dest sheet - // // (this also deletes any related code_runs) - // let cell_values = source_sheet.delete_cell_values(source.into()); - // let old_dest_values = - // dest_sheet.merge_cell_values(dest_sheet_rect.into(), &cell_values); - - // transaction - // .reverse_operations - // .push(Operation::SetCellValues { - // sheet_pos: source.into(), - // values: cell_values, - // }); - // transaction - // .reverse_operations - // .push(Operation::SetCellValues { - // sheet_pos: dest_sheet_rect.into(), - // values: old_dest_values, - // }); - - // // add code_runs to dest sheet - // code_runs - // .iter() - // .enumerate() - // .for_each(|(index, (pos, code_run))| { - // let old_code_run = dest_sheet.set_code_run( - // (pos.x - source.min.x + dest.x, pos.y - source.min.y + dest.y).into(), - // Some(*code_run.to_owned()), - // ); - // transaction.reverse_operations.push(Operation::SetCodeRun { - // sheet_pos: ( - // pos.x - source.min.x + dest.x, - // pos.y - source.min.y + dest.y, - // dest.sheet_id, - // ) - // .into(), - // code_run: old_code_run, - // index, - // }); - // transaction.reverse_operation.push(Operation::SetCodeRun { - // sheet_pos: pos.to_sheet_pos(source.sheet_id), - // code_run, - // index, - // }) - // }); - - // // todo: formats & borders - - // if transaction.is_user() { - // self.check_deleted_code_runs(transaction, &source); - // self.check_deleted_code_runs(transaction, &dest_sheet_rect); - // self.add_compute_operations(transaction, &source, None); - // self.add_compute_operations(transaction, &dest_sheet_rect, None); - // self.check_all_spills(transaction, source.sheet_id); - // if source.sheet_id != dest.sheet_id { - // self.check_all_spills(transaction, dest.sheet_id); - // } - // } - // } } } } diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 95dda42c05..c1669141a2 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -92,7 +92,7 @@ impl Sheet { pub fn remove_code_runs_in_rect(&mut self, rect: Rect) -> Vec<(usize, Pos, CodeRun)> { let pos: Vec<_> = self .code_runs - .iter_mut() + .iter() .enumerate() .flat_map(|(index, (pos, _))| { if rect.contains(*pos) { From 91c72ee2a57cc7c6d01291c42e03c64a8f6fe8bc Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 7 May 2024 05:14:56 -0700 Subject: [PATCH 12/16] clippy fixes --- .../controller/active_transactions/pending_transaction.rs | 8 ++++++-- .../execution/execute_operation/execute_sheets.rs | 6 +++--- quadratic-core/src/formulas/tests.rs | 4 ++-- quadratic-core/src/grid/series.rs | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index 5cd370c896..49642bd29a 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -189,7 +189,9 @@ mod tests { color: Some("red".to_string()), }, ]; - transaction.forward_operations = forward_operations.clone(); + transaction + .forward_operations + .clone_from(&forward_operations); let reverse_operations = vec![ Operation::SetSheetName { sheet_id, name }, Operation::SetSheetColor { @@ -197,7 +199,9 @@ mod tests { color: None, }, ]; - transaction.reverse_operations = reverse_operations.clone(); + transaction + .reverse_operations + .clone_from(&reverse_operations); let forward_transaction = transaction.to_forward_transaction(); assert_eq!(forward_transaction.id, transaction.id); assert_eq!(forward_transaction.operations, forward_operations); 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 78d5af015a..0b4e76d85b 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_sheets.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_sheets.rs @@ -101,7 +101,7 @@ impl GridController { return; }; let original_order = sheet.order.clone(); - sheet.order = order.clone(); + sheet.order.clone_from(&order); self.grid.move_sheet(target, order.clone()); if old_first != self.grid.first_sheet_id() { @@ -133,7 +133,7 @@ impl GridController { return; }; let old_name = sheet.name.clone(); - sheet.name = name.clone(); + sheet.name.clone_from(&name); transaction .forward_operations @@ -161,7 +161,7 @@ impl GridController { return; }; let old_color = sheet.color.clone(); - sheet.color = color.clone(); + sheet.color.clone_from(&color); transaction .forward_operations diff --git a/quadratic-core/src/formulas/tests.rs b/quadratic-core/src/formulas/tests.rs index 37c5a53667..b1c0408de4 100644 --- a/quadratic-core/src/formulas/tests.rs +++ b/quadratic-core/src/formulas/tests.rs @@ -330,11 +330,11 @@ fn test_sheet_references() { let id1 = g.sheets()[0].id; let name1 = "MySheet".to_string(); - g.sheets_mut()[0].name = name1.clone(); + g.sheets_mut()[0].name.clone_from(&name1); let id2 = g.add_sheet(None); let name2 = "My Other Sheet".to_string(); - g.sheets_mut()[1].name = name2.clone(); + g.sheets_mut()[1].name.clone_from(&name2); let _ = g.try_sheet_mut(id1).unwrap().set_cell_value(pos![A1], 42); let _ = g.try_sheet_mut(id1).unwrap().set_cell_value(pos![A3], 6); diff --git a/quadratic-core/src/grid/series.rs b/quadratic-core/src/grid/series.rs index 7724a6fcae..4e97689eab 100644 --- a/quadratic-core/src/grid/series.rs +++ b/quadratic-core/src/grid/series.rs @@ -172,7 +172,7 @@ pub fn find_number_series(options: SeriesOptions) -> Vec { let mut current = numbers[numbers.len() - 1].to_owned(); if negative { - current = numbers[0].to_owned(); + numbers[0].clone_into(&mut current); } let calc = |val: &BigDecimal| match (&addition, &multiplication, negative) { From c4cd22923eaf05018885402d3288c73e9b3b6af0 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 8 May 2024 06:23:40 -0700 Subject: [PATCH 13/16] properly offset the cursor drag --- .../interaction/pointer/PointerCellMoving.ts | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index a7402c46e5..6d5777a7ca 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -22,6 +22,7 @@ interface MoveCells { height: number; toColumn: number; toRow: number; + offset: { x: number; y: number }; } export class PointerCellMoving { @@ -90,7 +91,12 @@ export class PointerCellMoving { throw new Error('Expected moving to be defined in pointerMoveMoving'); } const offsets = sheets.sheet.offsets; - const position = offsets.getColumnRowFromScreen(world.x, world.y); + const position = offsets.getColumnRowFromScreen(world.x + this.moving.offset.x, world.y + this.moving.offset.y); + pixiApp.debug + .clear() + .beginFill(0xff0000) + .drawCircle(world.x + this.moving.offset.x, world.y + this.moving.offset.y, 5) + .endFill(); if (this.moving.toColumn !== position.column || this.moving.toRow !== position.row) { this.moving.toColumn = position.column; this.moving.toRow = position.row; @@ -98,7 +104,7 @@ export class PointerCellMoving { } } - private moveOverlaps(world: Point): boolean { + private moveOverlaps(world: Point): false | 'corner' | 'top' | 'bottom' | 'left' | 'right' { const cursorRectangle = pixiApp.cursor.cursorRectangle; if (!cursorRectangle) return false; @@ -107,7 +113,7 @@ export class PointerCellMoving { Math.pow(cursorRectangle.x - world.x, 2) + Math.pow(cursorRectangle.y - world.y, 2) <= TOP_LEFT_CORNER_THRESHOLD_SQUARED ) { - return true; + return 'corner'; } // if overlap indicator (autocomplete), then return false @@ -123,31 +129,41 @@ export class PointerCellMoving { BORDER_THRESHOLD, cursorRectangle.height ); + if (intersects.rectanglePoint(left, world)) { + return 'left'; + } + const right = new Rectangle( cursorRectangle.x + cursorRectangle.width - BORDER_THRESHOLD / 2, cursorRectangle.y, BORDER_THRESHOLD, cursorRectangle.height ); + if (intersects.rectanglePoint(right, world)) { + return 'right'; + } + const top = new Rectangle( cursorRectangle.x, cursorRectangle.y - BORDER_THRESHOLD / 2, cursorRectangle.width, BORDER_THRESHOLD ); + if (intersects.rectanglePoint(top, world)) { + return 'top'; + } + const bottom = new Rectangle( cursorRectangle.x, cursorRectangle.y + cursorRectangle.height - BORDER_THRESHOLD / 2, cursorRectangle.width, BORDER_THRESHOLD ); + if (intersects.rectanglePoint(bottom, world)) { + return 'bottom'; + } - return ( - intersects.rectanglePoint(left, world) || - intersects.rectanglePoint(right, world) || - intersects.rectanglePoint(top, world) || - intersects.rectanglePoint(bottom, world) - ); + return false; } private pointerMoveHover(world: Point): boolean { @@ -156,10 +172,31 @@ export class PointerCellMoving { const column = origin.x; const row = origin.y; - if (this.moveOverlaps(world)) { + const overlap = this.moveOverlaps(world); + if (overlap) { this.state = 'hover'; const rectangle = sheet.cursor.getRectangle(); - this.moving = { column, row, width: rectangle.width, height: rectangle.height, toColumn: column, toRow: row }; + const screenRectangle = pixiApp.cursor.cursorRectangle; + if (!screenRectangle) return false; + let adjustX = 0, + adjustY = 0; + if (overlap === 'right') { + adjustX = sheets.sheet.offsets.getColumnWidth(rectangle.right); + } else if (overlap === 'bottom') { + adjustY = sheets.sheet.offsets.getRowHeight(rectangle.bottom); + } + this.moving = { + column, + row, + width: rectangle.width, + height: rectangle.height, + toColumn: column, + toRow: row, + offset: { + x: screenRectangle.x - world.x + adjustX, + y: screenRectangle.y - world.y + adjustY, + }, + }; return true; } this.reset(); From 0538d2cabb587f17e5b300e12e492ad0739ced80 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 8 May 2024 06:47:32 -0700 Subject: [PATCH 14/16] add tests --- .../src/controller/operations/clipboard.rs | 16 +++++++++++++ quadratic-core/src/grid/sheet/code.rs | 24 ------------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/quadratic-core/src/controller/operations/clipboard.rs b/quadratic-core/src/controller/operations/clipboard.rs index b0e80b53e4..247c162d18 100644 --- a/quadratic-core/src/controller/operations/clipboard.rs +++ b/quadratic-core/src/controller/operations/clipboard.rs @@ -273,3 +273,19 @@ impl GridController { vec![Operation::MoveCells { source, dest }] } } + +#[cfg(test)] +mod test { + use crate::controller::{operations::operation::Operation, GridController}; + + #[test] + fn move_cell_operations() { + let mut gc = GridController::test(); + let sheet_id = gc.sheet_ids()[0]; + let source = (0, 0, 2, 2, sheet_id).into(); + let dest = (2, 2, sheet_id).into(); + let operations = gc.move_cells_operations(source, dest); + assert_eq!(operations.len(), 1); + assert_eq!(operations[0], Operation::MoveCells { source, dest }); + } +} diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index c1669141a2..6cbf2ecc1b 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -87,30 +87,6 @@ impl Sheet { }) } - /// Removes code_runs in a rect and returns them without cloning - /// Returned usize is the index within the original CodeRun vec - pub fn remove_code_runs_in_rect(&mut self, rect: Rect) -> Vec<(usize, Pos, CodeRun)> { - let pos: Vec<_> = self - .code_runs - .iter() - .enumerate() - .flat_map(|(index, (pos, _))| { - if rect.contains(*pos) { - Some((index, *pos)) - } else { - None - } - }) - .collect(); - pos.iter() - .flat_map(|(index, pos)| { - self.code_runs - .shift_remove(pos) - .map(|code_run| (*index, *pos, code_run)) - }) - .collect() - } - pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { self.code_runs .iter() From 3586041d45a5c4858abf081ad05aa3383fb88acd Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 8 May 2024 10:15:12 -0700 Subject: [PATCH 15/16] removed debug; fixed bug with single cell moving on right and bottom border --- quadratic-client/src/app/gridGL/UI/Cursor.ts | 2 +- .../app/gridGL/interaction/pointer/PointerCellMoving.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index 79c545926f..987a13a070 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -113,7 +113,7 @@ export class Cursor extends Graphics { this.drawRect(x, y, width, height); this.cursorRectangle = undefined; } else { - this.cursorRectangle = new Rectangle(x, y, x + width, y + height); + this.cursorRectangle = new Rectangle(x, y, width, height); } } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index 6d5777a7ca..76567de809 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -92,11 +92,6 @@ export class PointerCellMoving { } const offsets = sheets.sheet.offsets; const position = offsets.getColumnRowFromScreen(world.x + this.moving.offset.x, world.y + this.moving.offset.y); - pixiApp.debug - .clear() - .beginFill(0xff0000) - .drawCircle(world.x + this.moving.offset.x, world.y + this.moving.offset.y, 5) - .endFill(); if (this.moving.toColumn !== position.column || this.moving.toRow !== position.row) { this.moving.toColumn = position.column; this.moving.toRow = position.row; @@ -152,13 +147,13 @@ export class PointerCellMoving { if (intersects.rectanglePoint(top, world)) { return 'top'; } - const bottom = new Rectangle( cursorRectangle.x, cursorRectangle.y + cursorRectangle.height - BORDER_THRESHOLD / 2, cursorRectangle.width, BORDER_THRESHOLD ); + console.log(cursorRectangle.y, cursorRectangle.height, bottom, world); if (intersects.rectanglePoint(bottom, world)) { return 'bottom'; } From 7859a7f62c6334c00d8b56d84e9ab2029202e225 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 8 May 2024 11:49:42 -0700 Subject: [PATCH 16/16] removing console.log --- .../src/app/gridGL/interaction/pointer/PointerCellMoving.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index 76567de809..e1ac10a888 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -153,7 +153,7 @@ export class PointerCellMoving { cursorRectangle.width, BORDER_THRESHOLD ); - console.log(cursorRectangle.y, cursorRectangle.height, bottom, world); + if (intersects.rectanglePoint(bottom, world)) { return 'bottom'; }