Skip to content

Commit

Permalink
Grid move strategy (#6031)
Browse files Browse the repository at this point in the history
## Description
https://screenshot.click/01-24-zx11t-05yyj.mp4

This PR adds a strategy + canvas controls to move elements on a css
grid.


### Manual Tests:
I hereby swear that:

- [x] I opened a hydrogen project and it loaded
- [x] I could navigate to various routes in Preview mode

---------

Co-authored-by: Federico Ruggi <[email protected]>
  • Loading branch information
2 people authored and liady committed Dec 13, 2024
1 parent c918705 commit fe2da46
Show file tree
Hide file tree
Showing 9 changed files with 957 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import type { InsertionSubject, InsertionSubjectWrapper } from '../../editor/edi
import { generateUidWithExistingComponents } from '../../../core/model/element-template-utils'
import { retargetStrategyToChildrenOfFragmentLikeElements } from './strategies/fragment-like-helpers'
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import { gridRearrangeMoveStrategy } from './strategies/grid-rearrange-move-strategy'
import { resizeGridStrategy } from './strategies/resize-grid-strategy'

export type CanvasStrategyFactory = (
Expand Down Expand Up @@ -91,6 +92,7 @@ const moveOrReorderStrategies: MetaCanvasStrategy = (
convertToAbsoluteAndMoveStrategy,
convertToAbsoluteAndMoveAndSetParentFixedStrategy,
reorderSliderStategy,
gridRearrangeMoveStrategy,
],
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,18 @@ export function reorderSlider(): ReorderSlider {
}
}

export interface GridCellHandle {
type: 'GRID_CELL_HANDLE'
id: string
}

export function gridCellHandle(params: { id: string }): GridCellHandle {
return {
type: 'GRID_CELL_HANDLE',
id: params.id,
}
}

export type CanvasControlType =
| BoundingArea
| ResizeHandle
Expand All @@ -624,6 +636,7 @@ export type CanvasControlType =
| KeyboardCatcherControl
| ReorderSlider
| BorderRadiusResizeHandle
| GridCellHandle
| GridAxisHandle

export function isDragToPan(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as EP from '../../../../core/shared/element-path'
import { getRectCenter, localRectangle } from '../../../../core/shared/math-utils'
import { selectComponentsForTest } from '../../../../utils/utils.test-utils'
import { GridCellTestId } from '../../controls/grid-controls'
import { mouseDragFromPointToPoint } from '../../event-helpers.test-utils'
import { renderTestEditorWithCode } from '../../ui-jsx.test-utils'

describe('grid rearrange move strategy', () => {
it('can rearrange elements on a grid', async () => {
const editor = await renderTestEditorWithCode(ProjectCode, 'await-first-dom-report')

const elementPathToDrag = EP.fromString('sb/scene/grid/aaa')

await selectComponentsForTest(editor, [elementPathToDrag])

const sourceGridCell = editor.renderedDOM.getByTestId(GridCellTestId(elementPathToDrag))
const targetGridCell = editor.renderedDOM.getByTestId('gridcell-0-14')

await mouseDragFromPointToPoint(
sourceGridCell,
getRectCenter(
localRectangle({
x: sourceGridCell.getBoundingClientRect().x,
y: sourceGridCell.getBoundingClientRect().y,
width: sourceGridCell.getBoundingClientRect().width,
height: sourceGridCell.getBoundingClientRect().height,
}),
),
getRectCenter(
localRectangle({
x: targetGridCell.getBoundingClientRect().x,
y: targetGridCell.getBoundingClientRect().y,
width: targetGridCell.getBoundingClientRect().width,
height: targetGridCell.getBoundingClientRect().height,
}),
),
)

const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
editor.renderedDOM.getByTestId('aaa').style
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
gridColumnEnd: '7',
gridColumnStart: '3',
gridRowEnd: '4',
gridRowStart: '2',
})
})
})

const ProjectCode = `import * as React from 'react'
import { Scene, Storyboard, Placeholder } from 'utopia-api'
export var storyboard = (
<Storyboard data-uid='sb'>
<Scene
id='playground-scene'
commentId='playground-scene'
style={{
width: 847,
height: 895,
position: 'absolute',
left: 46,
top: 131,
}}
data-label='Playground'
data-uid='scene'
>
<div
style={{
display: 'grid',
gridTemplateRows: '75px 75px 75px 75px',
gridTemplateColumns:
'50px 50px 50px 50px 50px 50px 50px 50px 50px 50px 50px 50px',
gridGap: 16,
height: 482,
width: 786,
position: 'absolute',
left: 31,
top: 0,
}}
data-uid='grid'
>
<div
style={{
minHeight: 0,
backgroundColor: '#f3785f',
gridColumnEnd: 5,
gridColumnStart: 1,
gridRowEnd: 3,
gridRowStart: 1,
}}
data-uid='aaa'
data-testid='aaa'
/>
<Placeholder
style={{
minHeight: 0,
backgroundColor: '#23565b',
gridColumnStart: 5,
gridColumnEnd: 7,
gridRowStart: 1,
gridRowEnd: 3,
}}
data-uid='bbb'
/>
<Placeholder
style={{
minHeight: 0,
gridColumnEnd: 5,
gridRowEnd: 4,
gridColumnStart: 1,
gridRowStart: 3,
backgroundColor: '#0074ff',
}}
data-uid='ccc'
/>
<Placeholder
style={{
minHeight: 0,
gridColumnEnd: 9,
gridRowEnd: 4,
gridColumnStart: 5,
gridRowStart: 3,
}}
data-uid='ddd'
/>
</div>
</Scene>
</Storyboard>
)
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import * as EP from '../../../../core/shared/element-path'
import type { GridElementProperties } from '../../../../core/shared/element-template'
import { create } from '../../../../core/shared/property-path'
import type { CanvasCommand } from '../../commands/commands'
import { setProperty } from '../../commands/set-property-command'
import { GridControls, TargetGridCell } from '../../controls/grid-controls'
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 gridRearrangeMoveStrategy: 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
}

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

let commands: CanvasCommand[] = []

if (TargetGridCell.current.row > 0 && TargetGridCell.current.column > 0) {
const metadata = MetadataUtils.findElementByElementPath(
canvasState.startingMetadata,
selectedElement,
)

function getGridProperty(field: keyof GridElementProperties, fallback: number) {
const propValue = metadata?.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)

if (metadata != null) {
commands.push(
setProperty(
'always',
selectedElement,
create('style', 'gridColumnStart'),
TargetGridCell.current.column,
),
setProperty(
'always',
selectedElement,
create('style', 'gridColumnEnd'),
Math.max(
TargetGridCell.current.column,
TargetGridCell.current.column + (gridColumnEnd - gridColumnStart),
),
),
setProperty(
'always',
selectedElement,
create('style', 'gridRowStart'),
TargetGridCell.current.row,
),
setProperty(
'always',
selectedElement,
create('style', 'gridRowEnd'),
Math.max(
TargetGridCell.current.row,
TargetGridCell.current.row + (gridRowEnd - gridRowStart),
),
),
)
}
}

if (commands == null) {
return emptyStrategyApplicationResult
}

return strategyApplicationResult(commands)
},
}
}
Loading

0 comments on commit fe2da46

Please sign in to comment.