Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(canvas) Grid Swap!
Browse files Browse the repository at this point in the history
- Added `rearrangeGridSwapStrategy` to `moveOrReorderStrategies`.
seanparsons committed Jul 2, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 870ccb4 commit 5d9d4dc
Showing 3 changed files with 428 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ import { retargetStrategyToChildrenOfFragmentLikeElements } from './strategies/f
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import { gridRearrangeMoveStrategy } from './strategies/grid-rearrange-move-strategy'
import { resizeGridStrategy } from './strategies/resize-grid-strategy'
import { rearrangeGridSwapStrategy } from './strategies/rearrange-grid-swap-strategy'

export type CanvasStrategyFactory = (
canvasState: InteractionCanvasState,
@@ -93,6 +94,7 @@ const moveOrReorderStrategies: MetaCanvasStrategy = (
convertToAbsoluteAndMoveAndSetParentFixedStrategy,
reorderSliderStategy,
gridRearrangeMoveStrategy,
rearrangeGridSwapStrategy,
],
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { getPrintedUiJsCode, renderTestEditorWithCode } from '../../ui-jsx.test-utils'
import * as EP from '../../../../core/shared/element-path'
import { selectComponents } from '../../../../components/editor/actions/meta-actions'
import { CanvasControlsContainerID } from '../../controls/new-canvas-controls'
import {
mouseDownAtPoint,
mouseMoveToPoint,
mouseUpAtPoint,
pressKey,
} from '../../event-helpers.test-utils'
import { canvasPoint } from '../../../../core/shared/math-utils'
import { GridCellTestId } from '../../controls/grid-controls'

const testProject = `
import * as React from 'react'
import { Storyboard } from 'utopia-api'
export var storyboard = (
<Storyboard data-uid='sb'>
<div
data-uid='grid'
data-testid='grid'
style={{
position: 'absolute',
left: 25,
top: 305,
display: 'grid',
gap: 10,
width: 600,
height: 600,
gridTemplateColumns: '2.4fr 1fr 1fr',
gridTemplateRows: '99px 109px 90px',
height: 'max-content',
}}
>
<div
data-uid='row-1-column-1'
data-testid='row-1-column-1'
style={{
backgroundColor: 'green',
}}
/>
<div
data-uid='row-2-column-1'
data-testid='row-2-column-1'
style={{ backgroundColor: 'blue' }}
/>
<div
data-uid='row-1-column-3'
data-testid='row-1-column-3'
style={{ backgroundColor: 'pink' }}
/>
<div
data-uid='row-1-column-2'
data-testid='row-1-column-2'
style={{
backgroundColor: 'green',
}}
/>
<div
data-uid='row-2-column-2'
data-testid='row-2-column-2'
style={{
backgroundColor: 'blue',
}}
/>
<div
data-uid='row-2-column-3'
data-testid='row-2-column-3'
style={{ backgroundColor: 'pink' }}
/>
<div
data-uid='row-3-column-1'
data-testid='row-3-column-1'
style={{ backgroundColor: 'green' }}
/>
<div
data-uid='row-3-column-2'
data-testid='row-3-column-2'
style={{ backgroundColor: 'blue' }}
/>
<div
data-uid='row-3-column-3'
data-testid='row-3-column-3'
style={{ backgroundColor: 'pink' }}
/>
</div>
</Storyboard>
)
`

describe('swap an element', () => {
it('swap out an element for another from a different column and row', async () => {
const renderResult = await renderTestEditorWithCode(testProject, 'await-first-dom-report')
const draggedItem = EP.fromString(`sb/grid/row-1-column-2`)
await renderResult.dispatch(selectComponents([draggedItem], false), true)
await renderResult.getDispatchFollowUpActionsFinished()
const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
const draggedItemElement = renderResult.renderedDOM.getByTestId(
GridCellTestId(EP.fromString(`sb/grid/row-1-column-2`)),
)
const draggedItemRect = draggedItemElement.getBoundingClientRect()
const startPoint = canvasPoint({
x: draggedItemRect.x + draggedItemRect.width / 2,
y: draggedItemRect.y + draggedItemRect.height / 2,
})
const swapTargetElement = renderResult.renderedDOM.getByTestId(
GridCellTestId(EP.fromString(`sb/grid/row-2-column-1`)),
)
const swapTargetRect = swapTargetElement.getBoundingClientRect()
const endPoint = canvasPoint({
x: swapTargetRect.x + swapTargetRect.width / 2,
y: swapTargetRect.y + swapTargetRect.height / 2,
})
await mouseMoveToPoint(draggedItemElement, startPoint)
await mouseDownAtPoint(draggedItemElement, startPoint)
await mouseMoveToPoint(canvasControlsLayer, endPoint)
await pressKey('Tab')
await renderResult.getDispatchFollowUpActionsFinished()
expect(
renderResult.getEditorState().editor.canvas.interactionSession?.userPreferredStrategy,
).toEqual('rearrange-grid-swap-strategy')
await mouseUpAtPoint(canvasControlsLayer, endPoint)
await renderResult.getDispatchFollowUpActionsFinished()

expect(getPrintedUiJsCode(renderResult.getEditorState()))
.toEqual(`import * as React from 'react'
import { Storyboard } from 'utopia-api'
export var storyboard = (
<Storyboard data-uid='sb'>
<div
data-uid='grid'
data-testid='grid'
style={{
position: 'absolute',
left: 25,
top: 305,
display: 'grid',
gap: 10,
width: 600,
height: 600,
gridTemplateColumns: '2.4fr 1fr 1fr',
gridTemplateRows: '99px 109px 90px',
height: 'max-content',
}}
>
<div
data-uid='row-1-column-1'
data-testid='row-1-column-1'
style={{ backgroundColor: 'green' }}
/>
<div
data-uid='row-1-column-2'
data-testid='row-1-column-2'
style={{
backgroundColor: 'green',
gridColumnStart: 'auto',
gridColumnEnd: 'auto',
gridRowStart: 'auto',
gridRowEnd: 'auto',
}}
/>
<div
data-uid='row-1-column-3'
data-testid='row-1-column-3'
style={{ backgroundColor: 'pink' }}
/>
<div
data-uid='row-2-column-1'
data-testid='row-2-column-1'
style={{
backgroundColor: 'blue',
gridColumnStart: 'auto',
gridColumnEnd: 'auto',
gridRowStart: 'auto',
gridRowEnd: 'auto',
}}
/>
<div
data-uid='row-2-column-2'
data-testid='row-2-column-2'
style={{ backgroundColor: 'blue' }}
/>
<div
data-uid='row-2-column-3'
data-testid='row-2-column-3'
style={{ backgroundColor: 'pink' }}
/>
<div
data-uid='row-3-column-1'
data-testid='row-3-column-1'
style={{ backgroundColor: 'green' }}
/>
<div
data-uid='row-3-column-2'
data-testid='row-3-column-2'
style={{ backgroundColor: 'blue' }}
/>
<div
data-uid='row-3-column-3'
data-testid='row-3-column-3'
style={{ backgroundColor: 'pink' }}
/>
</div>
</Storyboard>
)
`)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import { stripNulls } from '../../../../core/shared/array-utils'
import * as EP from '../../../../core/shared/element-path'
import type {
ElementInstanceMetadata,
GridElementProperties,
GridPosition,
} from '../../../../core/shared/element-template'
import {
isFiniteRectangle,
offsetPoint,
rectContainsPointInclusive,
} from '../../../../core/shared/math-utils'
import { optionalMap } from '../../../../core/shared/optional-utils'
import type { ElementPath } from '../../../../core/shared/project-file-types'
import { create } from '../../../../core/shared/property-path'
import type { CanvasCommand } from '../../commands/commands'
import { deleteProperties } from '../../commands/delete-properties-command'
import { rearrangeChildren } from '../../commands/rearrange-children-command'
import { setProperty } from '../../commands/set-property-command'
import { GridControls } from '../../controls/grid-controls'
import { recurseIntoChildrenOfMapOrFragment } from '../../gap-utils'
import type { CanvasStrategyFactory } from '../canvas-strategies'
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies'
import type { InteractionCanvasState } from '../canvas-strategy-types'
import {
getTargetPathsFromInteractionTarget,
emptyStrategyApplicationResult,
strategyApplicationResult,
} from '../canvas-strategy-types'
import type { InteractionSession } from '../interaction-state'

export const rearrangeGridSwapStrategy: CanvasStrategyFactory = (
canvasState: InteractionCanvasState,
interactionSession: InteractionSession | null,
) => {
const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget)
if (selectedElements.length !== 1) {
return null
}

const selectedElement = selectedElements[0]
const ok = MetadataUtils.isGridLayoutedContainer(
MetadataUtils.findElementByElementPath(
canvasState.startingMetadata,
EP.parentPath(selectedElement),
),
)
if (!ok) {
return null
}

const children = recurseIntoChildrenOfMapOrFragment(
canvasState.startingMetadata,
canvasState.startingAllElementProps,
canvasState.startingElementPathTree,
EP.parentPath(selectedElement),
)

return {
id: 'rearrange-grid-swap-strategy',
name: 'Rearrange Grid (Swap)',
descriptiveLabel: 'Rearrange Grid (Swap)',
icon: {
category: 'tools',
type: 'pointer',
},
controlsToRender: [
{
control: GridControls,
props: {},
key: `grid-controls-${EP.toString(selectedElement)}`,
show: 'always-visible',
},
],
fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 1),
apply: () => {
if (
interactionSession == null ||
interactionSession.interactionData.type !== 'DRAG' ||
interactionSession.interactionData.drag == null ||
interactionSession.activeControl.type !== 'GRID_CELL_HANDLE'
) {
return emptyStrategyApplicationResult
}

const pointOnCanvas = offsetPoint(
interactionSession.interactionData.dragStart,
interactionSession.interactionData.drag,
)

const pointerOverChild = children.find(
(c) =>
c.globalFrame != null &&
isFiniteRectangle(c.globalFrame) &&
rectContainsPointInclusive(c.globalFrame, pointOnCanvas),
)

let commands: CanvasCommand[] = []

if (
pointerOverChild != null &&
EP.toUid(pointerOverChild.elementPath) !== interactionSession.activeControl.id
) {
commands.push(
...swapChildrenCommands({
grabbedElementUid: interactionSession.activeControl.id,
swapToElementUid: EP.toUid(pointerOverChild.elementPath),
children: children,
parentPath: EP.parentPath(selectedElement),
}),
)
}

if (commands == null) {
return emptyStrategyApplicationResult
}

return strategyApplicationResult(commands)
},
}
}

const GridPositioningProps: Array<keyof React.CSSProperties> = [
'gridColumn',
'gridRow',
'gridColumnStart',
'gridColumnEnd',
'gridRowStart',
'gridRowEnd',
]

function gridPositionToValue(p: GridPosition | null): string | number | null {
if (p == null) {
return null
}
if (p === 'auto') {
return 'auto'
}

return p.numericalPosition
}

function setGridProps(elementPath: ElementPath, gridProps: GridElementProperties): CanvasCommand[] {
return stripNulls([
optionalMap(
(s) => setProperty('always', elementPath, create('style', 'gridColumnStart'), s),
gridPositionToValue(gridProps.gridColumnStart),
),
optionalMap(
(s) => setProperty('always', elementPath, create('style', 'gridColumnEnd'), s),
gridPositionToValue(gridProps.gridColumnEnd),
),
optionalMap(
(s) => setProperty('always', elementPath, create('style', 'gridRowStart'), s),
gridPositionToValue(gridProps.gridRowStart),
),
optionalMap(
(s) => setProperty('always', elementPath, create('style', 'gridRowEnd'), s),
gridPositionToValue(gridProps.gridRowEnd),
),
])
}

function swapChildrenCommands({
grabbedElementUid,
swapToElementUid,
children,
parentPath,
}: {
grabbedElementUid: string
swapToElementUid: string
children: ElementInstanceMetadata[]
parentPath: ElementPath
}): CanvasCommand[] {
const grabbedElement = children.find((c) => EP.toUid(c.elementPath) === grabbedElementUid)
const swapToElement = children.find((c) => EP.toUid(c.elementPath) === swapToElementUid)

if (grabbedElement == null || swapToElement == null) {
return []
}

const rearrangedChildren = children
.map((c) => {
if (EP.pathsEqual(c.elementPath, grabbedElement.elementPath)) {
return swapToElement.elementPath
}
if (EP.pathsEqual(c.elementPath, swapToElement.elementPath)) {
return grabbedElement.elementPath
}
return c.elementPath
})
.map((path) => EP.dynamicPathToStaticPath(path))

return [
rearrangeChildren('always', parentPath, rearrangedChildren),
deleteProperties(
'always',
swapToElement.elementPath,
GridPositioningProps.map((p) => create('style', p)),
),
deleteProperties(
'always',
grabbedElement.elementPath,
GridPositioningProps.map((p) => create('style', p)),
),
...setGridProps(
grabbedElement.elementPath,
swapToElement.specialSizeMeasurements.elementGridProperties,
),
...setGridProps(
swapToElement.elementPath,
grabbedElement.specialSizeMeasurements.elementGridProperties,
),
]
}

0 comments on commit 5d9d4dc

Please sign in to comment.