Skip to content

Commit

Permalink
Feat/grid drag initial delta (#6037)
Browse files Browse the repository at this point in the history
**Problem:**

When dragging a cell from a non-root cell, very non-ergonomic jumping
around happens.

**Fix:**

When calculating the target movement destination cell, accommodate for
the delta between the cell the mouse started the interaction over and
the root cell the element occupies.

Since this adds some math to the strategy helper code, I took this
occasion for refactoring that part of the spiky code to make it easier
to read and to maintain (with comments \0/ !)

Before:


https://github.com/concrete-utopia/utopia/assets/1081051/daa3d63e-e403-4fe1-9977-7b5d1f172c18

After:


https://github.com/concrete-utopia/utopia/assets/1081051/ca8fd82d-1c92-4008-989e-a4f37f6f6e27
  • Loading branch information
ruggi authored Jul 3, 2024
1 parent a7407e7 commit 4eba85d
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ export interface CustomStrategyState {
strategyGeneratedUidsCache: { [elementPath: string]: string | undefined }
elementsToRerender: Array<ElementPath>
action: ActiveFrameAction | null
targetGridCell: GridCellCoordinates | null
grid: GridCustomStrategyState
}

export type GridCustomStrategyState = {
targetCell: GridCellCoordinates | null
draggingFromCell: GridCellCoordinates | null
rootCell: GridCellCoordinates | null
}

export type CustomStrategyStatePatch = Partial<CustomStrategyState>
Expand All @@ -34,7 +40,11 @@ export function defaultCustomStrategyState(): CustomStrategyState {
strategyGeneratedUidsCache: {},
elementsToRerender: [],
action: null,
targetGridCell: null,
grid: {
targetCell: null,
draggingFromCell: null,
rootCell: null,
},
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ElementPath } from 'utopia-shared/src/types'
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import type {
ElementInstanceMetadata,
ElementInstanceMetadataMap,
GridElementProperties,
} from '../../../../core/shared/element-template'
Expand All @@ -14,10 +15,11 @@ import {
import { create } from '../../../../core/shared/property-path'
import type { CanvasCommand } from '../../commands/commands'
import { setProperty } from '../../commands/set-property-command'
import type { GridCellCoordinates } from '../../controls/grid-controls'
import { gridCellCoordinates } from '../../controls/grid-controls'
import { canvasPointToWindowPoint } from '../../dom-lookup'
import type { DragInteractionData } from '../interaction-state'
import type { GridCustomStrategyState } from '../canvas-strategy-types'
import type { GridCellCoordinates } from '../../controls/grid-controls'
import { gridCellCoordinates } from '../../controls/grid-controls'

export function getGridCellUnderMouse(mousePoint: WindowPoint) {
return getGridCellAtPoint(mousePoint, false)
Expand Down Expand Up @@ -77,13 +79,21 @@ export function runGridRearrangeMove(
interactionData: DragInteractionData,
canvasScale: number,
canvasOffset: CanvasVector,
targetGridCell: GridCellCoordinates | null,
customState: GridCustomStrategyState,
duplicating: boolean,
): { commands: CanvasCommand[]; targetGridCell: GridCellCoordinates | null } {
let commands: CanvasCommand[] = []

): {
commands: CanvasCommand[]
targetCell: GridCellCoordinates | null
draggingFromCell: GridCellCoordinates | null
rootCell: GridCellCoordinates | null
} {
if (interactionData.drag == null) {
return { commands: [], targetGridCell: null }
return {
commands: [],
targetCell: null,
rootCell: null,
draggingFromCell: null,
}
}

const mouseWindowPoint = canvasPointToWindowPoint(
Expand All @@ -92,63 +102,117 @@ export function runGridRearrangeMove(
canvasOffset,
)

let newTargetGridCell = targetGridCell ?? null
const cellUnderMouse = duplicating
? getGridCellUnderMouseRecursive(mouseWindowPoint)
: getGridCellUnderMouse(mouseWindowPoint)
if (cellUnderMouse != null) {
newTargetGridCell = cellUnderMouse.coordinates
}

if (newTargetGridCell == null || newTargetGridCell.row < 1 || newTargetGridCell.column < 1) {
return { commands: [], targetGridCell: null }
const newTargetCell = getTargetCell(customState.targetCell, duplicating, mouseWindowPoint)
if (newTargetCell == null) {
return {
commands: [],
targetCell: null,
draggingFromCell: null,
rootCell: null,
}
}

const originalElementMetadata = MetadataUtils.findElementByElementPath(
jsxMetadata,
selectedElement,
)
if (originalElementMetadata == null) {
return { commands: [], targetGridCell: null }
return {
commands: [],
targetCell: null,
rootCell: null,
draggingFromCell: null,
}
}

const gridProperties = getElementGridProperties(originalElementMetadata)

// calculate the difference between the cell the mouse started the interaction from, and the "root"
// cell of the element, meaning the top-left-most cell the element occupies.
const draggingFromCell = customState.draggingFromCell ?? newTargetCell
const rootCell =
customState.rootCell ?? gridCellCoordinates(gridProperties.row, gridProperties.column)
const coordsDiff = getCellCoordsDelta(draggingFromCell, rootCell)

// get the new adjusted row
const row = getCoordBounds(newTargetCell, 'row', gridProperties.width, coordsDiff.row)
// get the new adjusted column
const column = getCoordBounds(newTargetCell, 'column', gridProperties.height, coordsDiff.column)

return {
commands: [
setProperty('always', targetElement, create('style', 'gridColumnStart'), column.start),
setProperty('always', targetElement, create('style', 'gridColumnEnd'), column.end),
setProperty('always', targetElement, create('style', 'gridRowStart'), row.start),
setProperty('always', targetElement, create('style', 'gridRowEnd'), row.end),
],
targetCell: newTargetCell,
rootCell: rootCell,
draggingFromCell: draggingFromCell,
}
}

function getTargetCell(
previousTargetCell: GridCellCoordinates | null,
duplicating: boolean,
mouseWindowPoint: WindowPoint,
): GridCellCoordinates | null {
let cell = previousTargetCell ?? null
const cellUnderMouse = duplicating
? getGridCellUnderMouseRecursive(mouseWindowPoint)
: getGridCellUnderMouse(mouseWindowPoint)
if (cellUnderMouse != null) {
cell = cellUnderMouse.coordinates
}
if (cell == null || cell.row < 1 || cell.column < 1) {
return null
}
return cell
}

function getElementGridProperties(element: ElementInstanceMetadata): {
row: number
width: number
column: number
height: number
} {
// get the grid fixtures (start and end for column and row) from the element metadata
function getGridProperty(field: keyof GridElementProperties, fallback: number) {
const propValue = originalElementMetadata?.specialSizeMeasurements.elementGridProperties[field]
const propValue = element.specialSizeMeasurements.elementGridProperties[field]
return propValue == null || propValue === 'auto' ? 0 : propValue.numericalPosition ?? fallback
}

const gridColumnStart = getGridProperty('gridColumnStart', 0)
const gridColumnEnd = getGridProperty('gridColumnEnd', 1)
const gridRowStart = getGridProperty('gridRowStart', 0)
const gridRowEnd = getGridProperty('gridRowEnd', 1)

commands.push(
setProperty(
'always',
targetElement,
create('style', 'gridColumnStart'),
newTargetGridCell.column,
),
setProperty(
'always',
targetElement,
create('style', 'gridColumnEnd'),
Math.max(
newTargetGridCell.column,
newTargetGridCell.column + (gridColumnEnd - gridColumnStart),
),
),
setProperty('always', targetElement, create('style', 'gridRowStart'), newTargetGridCell.row),
setProperty(
'always',
targetElement,
create('style', 'gridRowEnd'),
Math.max(newTargetGridCell.row, newTargetGridCell.row + (gridRowEnd - gridRowStart)),
),
)
const column = getGridProperty('gridColumnStart', 0)
const height = getGridProperty('gridColumnEnd', 1) - column
const row = getGridProperty('gridRowStart', 0)
const width = getGridProperty('gridRowEnd', 1) - row

return {
commands: commands,
targetGridCell: newTargetGridCell,
row,
width,
column,
height,
}
}

function getCellCoordsDelta(
dragFrom: GridCellCoordinates,
rootCell: GridCellCoordinates,
): GridCellCoordinates {
const rowDiff = dragFrom.row - rootCell.row
const columnDiff = dragFrom.column - rootCell.column

return gridCellCoordinates(rowDiff, columnDiff)
}

function getCoordBounds(
cell: GridCellCoordinates,
coord: 'column' | 'row',
size: number, // width or height
adjustOffset: number, // adjustment based on the difference between the initial dragging cell and the root cell
): { start: number; end: number } {
// the start is the first cell's coord the element will occupy
const start = Math.max(1, cell[coord] - adjustOffset)
// the end is the last cell's coord the element will occupy
const end = Math.max(1, start + size)
return { start, end }
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,19 @@ export const gridRearrangeMoveDuplicateStrategy: CanvasStrategyFactory = (

const targetElement = EP.appendToPath(EP.parentPath(selectedElement), newUid)

const { commands: moveCommands, targetGridCell: newTargetGridCell } = runGridRearrangeMove(
const {
commands: moveCommands,
targetCell: targetGridCell,
draggingFromCell,
rootCell,
} = runGridRearrangeMove(
targetElement,
selectedElement,
canvasState.startingMetadata,
interactionSession.interactionData,
canvasState.scale,
canvasState.canvasOffset,
customState.targetGridCell,
customState.grid,
true,
)
if (moveCommands.length === 0) {
Expand All @@ -109,7 +114,11 @@ export const gridRearrangeMoveDuplicateStrategy: CanvasStrategyFactory = (
setCursorCommand(CSSCursor.Duplicate),
],
{
targetGridCell: newTargetGridCell,
grid: {
targetCell: targetGridCell,
draggingFromCell: draggingFromCell,
rootCell: rootCell,
},
duplicatedElementNewUids: duplicatedElementNewUids,
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,10 @@ describe('grid rearrange move strategy', () => {

await mouseDragFromPointToPoint(
sourceGridCell,
getRectCenter(
localRectangle({
x: sourceGridCell.getBoundingClientRect().x,
y: sourceGridCell.getBoundingClientRect().y,
width: sourceGridCell.getBoundingClientRect().width,
height: sourceGridCell.getBoundingClientRect().height,
}),
),
{
x: sourceGridCell.getBoundingClientRect().x + 10,
y: sourceGridCell.getBoundingClientRect().y + 10,
},
getRectCenter(
localRectangle({
x: targetGridCell.getBoundingClientRect().x,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,31 @@ export const gridRearrangeMoveStrategy: CanvasStrategyFactory = (

const targetElement = selectedElement

const { commands: moveCommands, targetGridCell: newTargetGridCell } = runGridRearrangeMove(
const {
commands: moveCommands,
targetCell: targetGridCell,
draggingFromCell,
rootCell,
} = runGridRearrangeMove(
targetElement,
selectedElement,
canvasState.startingMetadata,
interactionSession.interactionData,
canvasState.scale,
canvasState.canvasOffset,
customState.targetGridCell,
customState.grid,
false,
)
if (moveCommands.length === 0) {
return emptyStrategyApplicationResult
}

return strategyApplicationResult(moveCommands, {
targetGridCell: newTargetGridCell,
grid: {
targetCell: targetGridCell,
draggingFromCell: draggingFromCell,
rootCell: rootCell,
},
})
},
}
Expand Down
Loading

0 comments on commit 4eba85d

Please sign in to comment.