diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index 896e9abd5155..0301f4ee9dde 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -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, ], ) } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-swap-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-swap-strategy.spec.browser2.tsx new file mode 100644 index 000000000000..4db6691175d2 --- /dev/null +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-swap-strategy.spec.browser2.tsx @@ -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 = ( + +
+
+
+
+
+
+
+
+
+
+
+ +) +` + +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 = ( + +
+
+
+
+
+
+
+
+
+
+
+ +) +`) + }) +}) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-swap-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-swap-strategy.ts new file mode 100644 index 000000000000..a9179e482166 --- /dev/null +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-swap-strategy.ts @@ -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 = [ + '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, + ), + ] +}