diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index c14f07eb91c0..aae5d84554ef 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { createSelector } from 'reselect' import { mapDropNulls, pushUniquelyBy, sortBy } from '../../../core/shared/array-utils' import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' import { arrayEqualsByReference, assertNever } from '../../../core/shared/utils' @@ -68,11 +67,10 @@ 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 { gridMoveRearrangeStrategy } from './strategies/grid-move-rearrange-strategy' import { resizeGridStrategy } from './strategies/resize-grid-strategy' -import { rearrangeGridSwapStrategy } from './strategies/rearrange-grid-swap-strategy' import { gridResizeElementStrategy } from './strategies/grid-resize-element-strategy' -import { gridRearrangeMoveDuplicateStrategy } from './strategies/grid-rearrange-move-duplicate-strategy' +import { gridMoveRearrangeDuplicateStrategy } from './strategies/grid-move-rearrange-duplicate-strategy' import { setGridGapStrategy } from './strategies/set-grid-gap-strategy' import type { CanvasCommand } from '../commands/commands' import { foldAndApplyCommandsInner } from '../commands/commands' @@ -89,6 +87,8 @@ import { GridControls, isGridControlsProps, } from '../controls/grid-controls-for-strategies' +import { gridMoveReorderStrategy } from './strategies/grid-move-reorder-strategy' +import { gridMoveAbsoluteStrategy } from './strategies/grid-move-absolute' export type CanvasStrategyFactory = ( canvasState: InteractionCanvasState, @@ -116,10 +116,11 @@ const moveOrReorderStrategies: MetaCanvasStrategy = ( convertToAbsoluteAndMoveStrategy, convertToAbsoluteAndMoveAndSetParentFixedStrategy, reorderSliderStategy, - gridRearrangeMoveStrategy, - rearrangeGridSwapStrategy, - gridRearrangeMoveDuplicateStrategy, + gridMoveRearrangeStrategy, + gridMoveRearrangeDuplicateStrategy, + gridMoveReorderStrategy, gridRearrangeResizeKeyboardStrategy, + gridMoveAbsoluteStrategy, ], ) } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts index 77a6260c8856..c889849c6d79 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts @@ -1,39 +1,39 @@ import type { ElementPath } from 'utopia-shared/src/types' import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import { mapDropNulls } from '../../../../core/shared/array-utils' import * as EP from '../../../../core/shared/element-path' import type { ElementInstanceMetadataMap, + GridAutoOrTemplateBase, GridPositionValue, + SpecialSizeMeasurements, } from '../../../../core/shared/element-template' import { - gridPositionValue, type ElementInstanceMetadata, type GridContainerProperties, type GridElementProperties, type GridPosition, } from '../../../../core/shared/element-template' import type { CanvasRectangle } from '../../../../core/shared/math-utils' -import { - canvasPoint, - canvasVector, - isInfinityRectangle, - offsetPoint, -} from '../../../../core/shared/math-utils' import * as PP from '../../../../core/shared/property-path' -import { absolute } from '../../../../utils/utils' +import { assertNever } from '../../../../core/shared/utils' import type { GridDimension } from '../../../inspector/common/css-utils' import { - cssNumber, gridCSSRepeat, isCSSKeyword, isGridCSSRepeat, isStaticGridRepeat, + printGridAutoOrTemplateBase, } from '../../../inspector/common/css-utils' import type { CanvasCommand } from '../../commands/commands' import { deleteProperties } from '../../commands/delete-properties-command' -import { reorderElement } from '../../commands/reorder-element-command' -import { setCssLengthProperty } from '../../commands/set-css-length-command' -import { setProperty } from '../../commands/set-property-command' +import type { PropertyToUpdate } from '../../commands/set-property-command' +import { + propertyToDelete, + propertyToSet, + setProperty, + updateBulkProperties, +} from '../../commands/set-property-command' import type { DragInteractionData } from '../interaction-state' import type { GridCellCoordinates } from './grid-cell-bounds' import { @@ -41,174 +41,6 @@ import { getGridChildCellCoordBoundsFromCanvas, gridCellCoordinates, } from './grid-cell-bounds' -import { mapDropNulls } from '../../../../core/shared/array-utils' -import { assertNever } from '../../../../core/shared/utils' -import { showGridControls } from '../../commands/show-grid-controls-command' - -export function runGridRearrangeMove( - targetElement: ElementPath, - selectedElement: ElementPath, - jsxMetadata: ElementInstanceMetadataMap, - interactionData: DragInteractionData, - gridCellGlobalFrames: GridCellGlobalFrames, - gridTemplate: GridContainerProperties, - newPathAfterReparent?: ElementPath, -): CanvasCommand[] { - if (interactionData.drag == null) { - return [] - } - - const originalElement = MetadataUtils.findElementByElementPath(jsxMetadata, selectedElement) - if (originalElement == null) { - return [] - } - - const isReparent = newPathAfterReparent != null - - const mousePos = offsetPoint(interactionData.dragStart, interactionData.drag) - const targetCellData = getClosestGridCellToPoint(gridCellGlobalFrames, mousePos) - const targetCellCoords = targetCellData?.gridCellCoordinates - if (targetCellCoords == null) { - return [] - } - - const originalElementGridConfiguration = isReparent - ? { - originalCellBounds: { width: 1, height: 1 }, //when reparenting, we just put it in a single cell - mouseCellPosInOriginalElement: { row: 0, column: 0 }, - } - : getOriginalElementGridConfiguration(gridCellGlobalFrames, interactionData, originalElement) - if (originalElementGridConfiguration == null) { - return [] - } - - const { originalCellBounds, mouseCellPosInOriginalElement } = originalElementGridConfiguration - - // get the new adjusted row - const row = Math.max(targetCellCoords.row - mouseCellPosInOriginalElement.row, 1) - // get the new adjusted column - const column = Math.max(targetCellCoords.column - mouseCellPosInOriginalElement.column, 1) - - const pathForCommands = newPathAfterReparent ?? targetElement // when reparenting, we want to use the new path for commands - const gridPath = EP.parentPath(pathForCommands) - - const gridCellMoveCommands = setGridPropsCommands(pathForCommands, gridTemplate, { - gridColumnStart: gridPositionValue(column), - gridColumnEnd: gridPositionValue(column + originalCellBounds.height), - gridRowStart: gridPositionValue(row), - gridRowEnd: gridPositionValue(row + originalCellBounds.width), - }) - - const gridTemplateColumns = - gridTemplate.gridTemplateColumns?.type === 'DIMENSIONS' - ? gridTemplate.gridTemplateColumns.dimensions.length - : 1 - - // The "pure" index in the grid children for the cell under mouse - const possiblyReorderIndex = getGridPositionIndex({ - row: targetCellCoords.row, - column: targetCellCoords.column, - gridTemplateColumns: gridTemplateColumns, - }) - - // The siblings of the grid element being moved - const siblings = MetadataUtils.getChildrenUnordered(jsxMetadata, gridPath) - .filter((s) => !EP.pathsEqual(s.elementPath, selectedElement)) - .map( - (s, index): SortableGridElementProperties => ({ - ...s.specialSizeMeasurements.elementGridProperties, - index: index, - path: s.elementPath, - }), - ) - - // Sort the siblings and the cell under mouse ascending based on their grid coordinates, so that - // the indexes grow left-right, top-bottom. - const cellsSortedByPosition = siblings - .concat({ - ...{ - gridColumnStart: gridPositionValue(targetCellCoords.column), - gridColumnEnd: gridPositionValue(targetCellCoords.column), - gridRowStart: gridPositionValue(targetCellCoords.row), - gridRowEnd: gridPositionValue(targetCellCoords.row), - }, - path: selectedElement, - index: siblings.length + 1, - }) - .sort(sortElementsByGridPosition(gridTemplateColumns)) - - // If rearranging, reorder to the index based on the sorted cells arrays. - const indexInSortedCellsForRearrange = cellsSortedByPosition.findIndex((s) => - EP.pathsEqual(selectedElement, s.path), - ) - - const moveType = getGridMoveType({ - elementPath: targetElement, - originalElementMetadata: originalElement, - possiblyReorderIndex: possiblyReorderIndex, - cellsSortedByPosition: cellsSortedByPosition, - isReparent: isReparent, - }) - - const updateGridControlsCommand = showGridControls( - 'mid-interaction', - gridPath, - targetCellData?.gridCellCoordinates ?? null, - gridCellCoordinates(row, column), - ) - - switch (moveType) { - case 'absolute': { - if (isReparent) { - return [] - } - const absoluteMoveCommands = gridChildAbsoluteMoveCommands( - MetadataUtils.findElementByElementPath(jsxMetadata, targetElement), - MetadataUtils.getFrameOrZeroRectInCanvasCoords(gridPath, jsxMetadata), - interactionData, - ) - return [...absoluteMoveCommands, updateGridControlsCommand] - } - case 'rearrange': { - const targetRootCell = gridCellCoordinates(row, column) - const canvasRect = getGlobalFrameOfGridCell(gridCellGlobalFrames, targetRootCell) - const absoluteMoveCommands = - canvasRect == null || isReparent - ? [] - : gridChildAbsoluteMoveCommands( - MetadataUtils.findElementByElementPath(jsxMetadata, targetElement), - canvasRect, - interactionData, - ) - return [ - ...gridCellMoveCommands, - ...absoluteMoveCommands, - reorderElement( - 'always', - pathForCommands, - absolute(Math.max(indexInSortedCellsForRearrange, 0)), - ), - updateGridControlsCommand, - ] - } - case 'reorder': { - return [ - reorderElement('always', pathForCommands, absolute(possiblyReorderIndex)), - deleteProperties('always', pathForCommands, [ - PP.create('style', 'gridColumn'), - PP.create('style', 'gridRow'), - PP.create('style', 'gridColumnStart'), - PP.create('style', 'gridColumnEnd'), - PP.create('style', 'gridRowStart'), - PP.create('style', 'gridRowEnd'), - ]), - updateGridControlsCommand, - ] - } - default: - assertNever(moveType) - } -} export function gridPositionToValue(p: GridPosition | null | undefined): string | number | null { if (p == null) { @@ -365,62 +197,12 @@ function asMaybeNamedLineOrValue( return value } -function gridChildAbsoluteMoveCommands( - targetMetadata: ElementInstanceMetadata | null, - containingRect: CanvasRectangle, - dragInteractionData: DragInteractionData, -): CanvasCommand[] { - if ( - targetMetadata == null || - targetMetadata.globalFrame == null || - isInfinityRectangle(targetMetadata.globalFrame) || - !MetadataUtils.isPositionAbsolute(targetMetadata) - ) { - return [] - } - - const offsetInTarget = canvasPoint({ - x: dragInteractionData.originalDragStart.x - targetMetadata.globalFrame.x, - y: dragInteractionData.originalDragStart.y - targetMetadata.globalFrame.y, - }) - - const dragOffset = offsetPoint( - dragInteractionData.originalDragStart, - dragInteractionData.drag ?? canvasPoint({ x: 0, y: 0 }), - ) - - const offset = canvasVector({ - x: dragOffset.x - containingRect.x - offsetInTarget.x, - y: dragOffset.y - containingRect.y - offsetInTarget.y, - }) - - return [ - deleteProperties('always', targetMetadata.elementPath, [ - PP.create('style', 'top'), - PP.create('style', 'left'), - PP.create('style', 'right'), - PP.create('style', 'bottom'), - ]), - setCssLengthProperty( - 'always', - targetMetadata.elementPath, - PP.create('style', 'top'), - { type: 'EXPLICIT_CSS_NUMBER', value: cssNumber(offset.y, null) }, - null, - ), - setCssLengthProperty( - 'always', - targetMetadata.elementPath, - PP.create('style', 'left'), - { type: 'EXPLICIT_CSS_NUMBER', value: cssNumber(offset.x, null) }, - null, - ), - ] +export type SortableGridElementProperties = GridElementProperties & { + path: ElementPath + index: number } -type SortableGridElementProperties = GridElementProperties & { path: ElementPath; index: number } - -function sortElementsByGridPosition(gridTemplateColumns: number) { +export function sortElementsByGridPosition(gridTemplateColumns: number) { return function (a: SortableGridElementProperties, b: SortableGridElementProperties): number { function getPosition(index: number, e: GridElementProperties) { if ( @@ -442,80 +224,11 @@ function sortElementsByGridPosition(gridTemplateColumns: number) { } } -type GridMoveType = - | 'reorder' // reorder the element in the code based on the ascending position, and remove explicit positioning props - | 'rearrange' // set explicit positioning props, and reorder based on the visual location - | 'absolute' // a regular absolute move, relative to the grid - -function getGridMoveType(params: { - elementPath: ElementPath - originalElementMetadata: ElementInstanceMetadata | null - possiblyReorderIndex: number - cellsSortedByPosition: SortableGridElementProperties[] - isReparent: boolean -}): GridMoveType { - const specialSizeMeasurements = params.originalElementMetadata?.specialSizeMeasurements - if ( - !params.isReparent && - specialSizeMeasurements != null && - MetadataUtils.isPositionAbsolute(params.originalElementMetadata) - ) { - return MetadataUtils.hasNoGridCellPositioning(specialSizeMeasurements) - ? 'absolute' - : 'rearrange' - } - if (params.possiblyReorderIndex >= params.cellsSortedByPosition.length) { - return 'rearrange' - } - - const elementGridProperties = specialSizeMeasurements?.elementGridProperties - const gridRowStart = gridPositionNumberValue(elementGridProperties?.gridRowStart ?? null) - const gridColumnStart = gridPositionNumberValue(elementGridProperties?.gridColumnStart ?? null) - const gridRowEnd = gridPositionNumberValue(elementGridProperties?.gridRowEnd ?? null) - const gridColumnEnd = gridPositionNumberValue(elementGridProperties?.gridColumnEnd ?? null) - - const isMultiCellChild = - (gridRowEnd != null && gridRowStart != null && gridRowEnd > gridRowStart + 1) || - (gridColumnEnd != null && gridColumnStart != null && gridColumnEnd > gridColumnStart + 1) - - if (isMultiCellChild) { - return 'rearrange' - } - - // The first element is intrinsically in order, so try to adjust for that - if (params.possiblyReorderIndex === 0) { - const isTheOnlyChild = params.cellsSortedByPosition.length === 1 - const isAlreadyTheFirstChild = - EP.toUid(params.cellsSortedByPosition[0].path) === EP.toUid(params.elementPath) - - const isAlreadyAtOrigin = gridRowStart === 1 && gridColumnStart === 1 - - if (isTheOnlyChild || isAlreadyTheFirstChild || isAlreadyAtOrigin) { - return 'reorder' - } - } - - const previousElement = params.cellsSortedByPosition.at(params.possiblyReorderIndex - 1) - if (previousElement == null) { - return 'rearrange' - } - const previousElementColumn = previousElement.gridColumnStart ?? null - const previousElementRow = previousElement.gridRowStart ?? null - return isGridPositionNumericValue(previousElementColumn) && - isGridPositionNumericValue(previousElementRow) - ? 'rearrange' - : 'reorder' -} - function isGridPositionNumericValue(p: GridPosition | null): p is GridPositionValue { return p != null && !(isCSSKeyword(p) && p.value === 'auto') } -function gridPositionNumberValue(p: GridPosition | null): number | null { - return isGridPositionNumericValue(p) ? p.numericalPosition : null -} - -function getGridPositionIndex(props: { +export function getGridPositionIndex(props: { row: number column: number gridTemplateColumns: number @@ -718,7 +431,7 @@ export function getGridRelatedIndexes(params: { return expandedRelatedIndexes[params.index] ?? [] } -function getOriginalElementGridConfiguration( +export function getOriginalElementGridConfiguration( gridCellGlobalFrames: GridCellGlobalFrames, interactionData: DragInteractionData, originalElement: ElementInstanceMetadata, @@ -788,3 +501,131 @@ export function isJustAutoGridDimension(dimensions: GridDimension[]): boolean { dimensions[0].value.value === 'auto' ) } + +type GridElementPinState = 'not-pinned' | 'auto-pinned' | 'pinned' + +export function getGridElementPinState( + elementGridPropertiesFromProps: GridElementProperties | null, +): GridElementPinState { + if ( + elementGridPropertiesFromProps?.gridColumnEnd == null || + elementGridPropertiesFromProps?.gridColumnStart == null || + elementGridPropertiesFromProps?.gridRowEnd == null || + elementGridPropertiesFromProps?.gridRowStart == null + ) { + return 'not-pinned' + } + if ( + isGridPositionNumericValue(elementGridPropertiesFromProps?.gridColumnEnd ?? null) || + isGridPositionNumericValue(elementGridPropertiesFromProps?.gridColumnStart ?? null) || + isGridPositionNumericValue(elementGridPropertiesFromProps?.gridRowEnd ?? null) || + isGridPositionNumericValue(elementGridPropertiesFromProps?.gridRowStart ?? null) + ) { + return 'pinned' + } + return 'auto-pinned' +} + +export function isFlowGridChild(child: ElementInstanceMetadata) { + return ( + getGridElementPinState(child.specialSizeMeasurements.elementGridPropertiesFromProps) !== + 'pinned' + ) +} + +function restoreGridTemplateFromProps(params: { + columns: GridAutoOrTemplateBase + rows: GridAutoOrTemplateBase +}): PropertyToUpdate[] { + let properties: PropertyToUpdate[] = [] + const newCols = printGridAutoOrTemplateBase(params.columns) + const newRows = printGridAutoOrTemplateBase(params.rows) + if (newCols === '') { + properties.push(propertyToDelete(PP.create('style', 'gridTemplateColumns'))) + } else { + properties.push(propertyToSet(PP.create('style', 'gridTemplateColumns'), newCols)) + } + if (newRows === '') { + properties.push(propertyToDelete(PP.create('style', 'gridTemplateRows'))) + } else { + properties.push(propertyToSet(PP.create('style', 'gridTemplateRows'), newRows)) + } + return properties +} + +type GridInitialTemplates = { + calculated: { + columns: GridAutoOrTemplateBase + rows: GridAutoOrTemplateBase + } + fromProps: { + columns: GridAutoOrTemplateBase + rows: GridAutoOrTemplateBase + } +} + +export function getParentGridTemplatesFromChildMeasurements( + specialSizeMeasurements: SpecialSizeMeasurements, +): GridInitialTemplates | null { + const parentTemplateCalculated = specialSizeMeasurements.parentContainerGridProperties + const parentTemplateFromProps = specialSizeMeasurements.parentContainerGridPropertiesFromProps + + const templateColsCalculated = parentTemplateCalculated.gridTemplateColumns + if (templateColsCalculated == null) { + return null + } + const templateRowsCalculated = parentTemplateCalculated.gridTemplateRows + if (templateRowsCalculated == null) { + return null + } + + const templateColsFromProps = parentTemplateFromProps.gridTemplateColumns + if (templateColsFromProps == null) { + return null + } + const templateRowsFromProps = parentTemplateFromProps.gridTemplateRows + if (templateRowsFromProps == null) { + return null + } + + return { + calculated: { + columns: templateColsCalculated, + rows: templateRowsCalculated, + }, + fromProps: { + columns: templateColsFromProps, + rows: templateRowsFromProps, + }, + } +} + +export function gridMoveStrategiesExtraCommands( + parentGridPath: ElementPath, + initialTemplates: GridInitialTemplates, +) { + const midInteractionCommands = [ + // during the interaction, freeze the template with the calculated values… + updateBulkProperties('mid-interaction', parentGridPath, [ + propertyToSet( + PP.create('style', 'gridTemplateColumns'), + printGridAutoOrTemplateBase(initialTemplates.calculated.columns), + ), + propertyToSet( + PP.create('style', 'gridTemplateRows'), + printGridAutoOrTemplateBase(initialTemplates.calculated.rows), + ), + ]), + ] + + const onCompleteCommands = [ + // …eventually, restore the grid template on complete. + updateBulkProperties( + 'on-complete', + parentGridPath, + restoreGridTemplateFromProps(initialTemplates.fromProps), + ), + ] + + return { midInteractionCommands, onCompleteCommands } +} diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts new file mode 100644 index 000000000000..c81b4b2c9aff --- /dev/null +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts @@ -0,0 +1,287 @@ +import type { ElementPath } from 'utopia-shared/src/types' +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import * as EP from '../../../../core/shared/element-path' +import { + type ElementInstanceMetadata, + type ElementInstanceMetadataMap, + type GridContainerProperties, +} from '../../../../core/shared/element-template' +import type { CanvasRectangle } from '../../../../core/shared/math-utils' +import { + canvasPoint, + canvasRectangle, + isInfinityRectangle, + offsetPoint, + pointDifference, + zeroRectangle, +} from '../../../../core/shared/math-utils' +import * as PP from '../../../../core/shared/property-path' +import { cssNumber } from '../../../inspector/common/css-utils' +import type { CanvasCommand } from '../../commands/commands' +import { deleteProperties } from '../../commands/delete-properties-command' +import { setCssLengthProperty } from '../../commands/set-css-length-command' +import { showGridControls } from '../../commands/show-grid-controls-command' +import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies' +import type { CanvasStrategyFactory } from '../canvas-strategies' +import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' +import type { InteractionCanvasState } from '../canvas-strategy-types' +import { + emptyStrategyApplicationResult, + getTargetPathsFromInteractionTarget, + strategyApplicationResult, +} from '../canvas-strategy-types' +import type { DragInteractionData, InteractionSession } from '../interaction-state' +import type { GridCellGlobalFrames } from './grid-helpers' +import { + findOriginalGrid, + getGlobalFrameOfGridCell, + getOriginalElementGridConfiguration, + getParentGridTemplatesFromChildMeasurements, + gridMoveStrategiesExtraCommands, +} from './grid-helpers' +import { runGridMoveRearrange } from './grid-move-rearrange-strategy' +import { getTargetGridCellData } from '../../../inspector/grid-helpers' + +export const gridMoveAbsoluteStrategy: CanvasStrategyFactory = ( + canvasState: InteractionCanvasState, + interactionSession: InteractionSession | null, +) => { + const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) + if ( + selectedElements.length !== 1 || + interactionSession == null || + interactionSession.interactionData.type !== 'DRAG' || + interactionSession.interactionData.drag == null || + interactionSession.activeControl.type !== 'GRID_CELL_HANDLE' || + interactionSession.interactionData.modifiers.alt + ) { + return null + } + + const selectedElement = selectedElements[0] + if (!MetadataUtils.isGridCell(canvasState.startingMetadata, selectedElement)) { + return null + } + + const selectedElementMetadata = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + selectedElement, + ) + if (selectedElementMetadata == null) { + return null + } + const initialTemplates = getParentGridTemplatesFromChildMeasurements( + selectedElementMetadata.specialSizeMeasurements, + ) + if (initialTemplates == null) { + return null + } + + const parentGridPath = findOriginalGrid( + canvasState.startingMetadata, + EP.parentPath(selectedElement), + ) // TODO don't use EP.parentPath + if (parentGridPath == null) { + return null + } + + const gridFrame = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + parentGridPath, + )?.globalFrame + if (gridFrame == null || isInfinityRectangle(gridFrame)) { + return null + } + + if (!MetadataUtils.isPositionAbsolute(selectedElementMetadata)) { + return null + } + + return { + id: 'absolute-grid-move-strategy', + name: 'Grid move (Abs)', + descriptiveLabel: 'Grid move (Abs)', + icon: { + category: 'tools', + type: 'pointer', + }, + controlsToRender: [controlsForGridPlaceholders(parentGridPath, 'visible-only-while-active')], + 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 + } + + const { commands, elementsToRerender } = getCommandsAndPatchForGridAbsoluteMove( + canvasState, + interactionSession.interactionData, + selectedElement, + parentGridPath, + ) + if (commands.length === 0) { + return emptyStrategyApplicationResult + } + + const { midInteractionCommands, onCompleteCommands } = gridMoveStrategiesExtraCommands( + parentGridPath, + initialTemplates, + ) + return strategyApplicationResult( + [...midInteractionCommands, ...onCompleteCommands, ...commands], + elementsToRerender, + ) + }, + } +} + +function getCommandsAndPatchForGridAbsoluteMove( + canvasState: InteractionCanvasState, + interactionData: DragInteractionData, + selectedElement: ElementPath, + gridPath: ElementPath, +): { + commands: CanvasCommand[] + elementsToRerender: ElementPath[] +} { + if (interactionData.drag == null) { + return { commands: [], elementsToRerender: [] } + } + + const selectedElementMetadata = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + selectedElement, + ) + if (selectedElementMetadata == null) { + return { commands: [], elementsToRerender: [] } + } + + const { parentGridCellGlobalFrames, parentContainerGridProperties } = + selectedElementMetadata.specialSizeMeasurements + if (parentGridCellGlobalFrames == null) { + return { commands: [], elementsToRerender: [] } + } + + const commands = runGridMoveAbsolute( + canvasState.startingMetadata, + interactionData, + selectedElementMetadata, + gridPath, + parentGridCellGlobalFrames, + parentContainerGridProperties, + ) + + return { + commands: commands, + elementsToRerender: [gridPath, selectedElement], + } +} + +function runGridMoveAbsolute( + jsxMetadata: ElementInstanceMetadataMap, + interactionData: DragInteractionData, + selectedElementMetadata: ElementInstanceMetadata, + gridPath: ElementPath, + gridCellGlobalFrames: GridCellGlobalFrames, + gridTemplate: GridContainerProperties, +): CanvasCommand[] { + if (interactionData.drag == null) { + return [] + } + + const gridConfig = getOriginalElementGridConfiguration( + gridCellGlobalFrames, + interactionData, + selectedElementMetadata, + ) + if (gridConfig == null) { + return [] + } + const { mouseCellPosInOriginalElement } = gridConfig + + const targetGridCellData = getTargetGridCellData( + interactionData, + gridCellGlobalFrames, + mouseCellPosInOriginalElement, + ) + if (targetGridCellData == null) { + return [] + } + const { targetCellCoords, targetRootCell } = targetGridCellData + + // if moving an absolutely-positioned child which does not have pinning + // props, do not set them at all. + if (MetadataUtils.hasNoGridCellPositioning(selectedElementMetadata.specialSizeMeasurements)) { + return [ + showGridControls('mid-interaction', gridPath, targetCellCoords, targetRootCell), + ...gridChildAbsoluteMoveCommands( + selectedElementMetadata, + MetadataUtils.getFrameOrZeroRectInCanvasCoords(gridPath, jsxMetadata), + interactionData, + ), + ] + } + + // otherwise, return a rearrange move + absolute adjustment + return [ + ...runGridMoveRearrange( + jsxMetadata, + interactionData, + selectedElementMetadata, + gridPath, + gridCellGlobalFrames, + gridTemplate, + null, + ), + ...gridChildAbsoluteMoveCommands( + selectedElementMetadata, + getGlobalFrameOfGridCell(gridCellGlobalFrames, targetRootCell) ?? + canvasRectangle(zeroRectangle), + interactionData, + ), + ] +} + +function gridChildAbsoluteMoveCommands( + element: ElementInstanceMetadata, + containingRect: CanvasRectangle, + dragInteractionData: DragInteractionData, +): CanvasCommand[] { + if ( + element.globalFrame == null || + isInfinityRectangle(element.globalFrame) || + dragInteractionData.drag == null + ) { + return [] + } + + const offsetInTarget = pointDifference(containingRect, element.globalFrame) + const dragOffset = offsetPoint(offsetInTarget, dragInteractionData.drag) + + return [ + deleteProperties('always', element.elementPath, [ + PP.create('style', 'top'), + PP.create('style', 'left'), + PP.create('style', 'right'), + PP.create('style', 'bottom'), + ]), + setCssLengthProperty( + 'always', + element.elementPath, + PP.create('style', 'top'), + { type: 'EXPLICIT_CSS_NUMBER', value: cssNumber(dragOffset.y, null) }, + null, + ), + setCssLengthProperty( + 'always', + element.elementPath, + PP.create('style', 'left'), + { type: 'EXPLICIT_CSS_NUMBER', value: cssNumber(dragOffset.x, null) }, + null, + ), + ] +} diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-duplicate-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-rearrange-duplicate-strategy.ts similarity index 64% rename from editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-duplicate-strategy.ts rename to editor/src/components/canvas/canvas-strategies/strategies/grid-move-rearrange-duplicate-strategy.ts index 91d3e60875ee..b3623409513c 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-duplicate-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-rearrange-duplicate-strategy.ts @@ -1,6 +1,7 @@ import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import { generateUidWithExistingComponents } from '../../../../core/model/element-template-utils' import * as EP from '../../../../core/shared/element-path' +import { isInfinityRectangle } from '../../../../core/shared/math-utils' import { CSSCursor } from '../../../../uuiui-deps' import { duplicateElement } from '../../commands/duplicate-element-command' import { setCursorCommand } from '../../commands/set-cursor-command' @@ -12,14 +13,19 @@ import type { CanvasStrategyFactory } from '../canvas-strategies' import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' import type { CustomStrategyState, InteractionCanvasState } from '../canvas-strategy-types' import { - getTargetPathsFromInteractionTarget, emptyStrategyApplicationResult, + getTargetPathsFromInteractionTarget, strategyApplicationResult, } from '../canvas-strategy-types' import type { InteractionSession } from '../interaction-state' -import { runGridRearrangeMove } from './grid-helpers' +import { + findOriginalGrid, + getParentGridTemplatesFromChildMeasurements, + gridMoveStrategiesExtraCommands, +} from './grid-helpers' +import { runGridMoveRearrange } from './grid-move-rearrange-strategy' -export const gridRearrangeMoveDuplicateStrategy: CanvasStrategyFactory = ( +export const gridMoveRearrangeDuplicateStrategy: CanvasStrategyFactory = ( canvasState: InteractionCanvasState, interactionSession: InteractionSession | null, customState: CustomStrategyState, @@ -41,15 +47,46 @@ export const gridRearrangeMoveDuplicateStrategy: CanvasStrategyFactory = ( return null } + const selectedElementMetadata = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + selectedElement, + ) + if (selectedElementMetadata == null) { + return null + } + + const initialTemplates = getParentGridTemplatesFromChildMeasurements( + selectedElementMetadata.specialSizeMeasurements, + ) + if (initialTemplates == null) { + return null + } + + const parentGridPath = findOriginalGrid( + canvasState.startingMetadata, + EP.parentPath(selectedElement), + ) // TODO don't use EP.parentPath + if (parentGridPath == null) { + return null + } + + const gridFrame = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + parentGridPath, + )?.globalFrame + if (gridFrame == null || isInfinityRectangle(gridFrame)) { + return null + } + return { id: 'rearrange-grid-move-duplicate-strategy', - name: 'Rearrange Grid (Duplicate)', - descriptiveLabel: 'Rearrange Grid (Duplicate)', + name: 'Grid Rearrange (Duplicate)', + descriptiveLabel: 'Grid Rearrange (Duplicate)', icon: { category: 'tools', type: 'pointer', }, - controlsToRender: [controlsForGridPlaceholders(EP.parentPath(selectedElement))], + controlsToRender: [controlsForGridPlaceholders(parentGridPath)], fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 3), apply: () => { if ( @@ -70,38 +107,35 @@ export const gridRearrangeMoveDuplicateStrategy: CanvasStrategyFactory = ( duplicatedElementNewUids[oldUid] = newUid } - const targetElement = EP.appendToPath(EP.parentPath(selectedElement), newUid) - - const selectedElementMetadata = MetadataUtils.findElementByElementPath( - canvasState.startingMetadata, - selectedElement, - ) - if (selectedElementMetadata == null) { - return emptyStrategyApplicationResult - } + const targetElement = EP.appendToPath(parentGridPath, newUid) const { parentGridCellGlobalFrames, parentContainerGridProperties } = selectedElementMetadata.specialSizeMeasurements - - const moveCommands = - parentGridCellGlobalFrames != null - ? runGridRearrangeMove( - targetElement, - selectedElement, - canvasState.startingMetadata, - interactionSession.interactionData, - parentGridCellGlobalFrames, - parentContainerGridProperties, - ) - : [] - if (moveCommands.length === 0) { + if (parentGridCellGlobalFrames == null) { return emptyStrategyApplicationResult } + const moveCommands = runGridMoveRearrange( + canvasState.startingMetadata, + interactionSession.interactionData, + selectedElementMetadata, + parentGridPath, + parentGridCellGlobalFrames, + parentContainerGridProperties, + null, + ) + + const { midInteractionCommands, onCompleteCommands } = gridMoveStrategiesExtraCommands( + parentGridPath, + initialTemplates, + ) + return strategyApplicationResult( [ duplicateElement('always', selectedElement, newUid), ...moveCommands, + ...midInteractionCommands, + ...onCompleteCommands, updateSelectedViews('always', [targetElement]), updateHighlightedViews('always', [targetElement]), setCursorCommand(CSSCursor.Duplicate), diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-move-rearrange-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-rearrange-strategy.ts new file mode 100644 index 000000000000..1d54d077cffb --- /dev/null +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-rearrange-strategy.ts @@ -0,0 +1,274 @@ +import type { ElementPath } from 'utopia-shared/src/types' +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import * as EP from '../../../../core/shared/element-path' +import type { + ElementInstanceMetadata, + ElementInstanceMetadataMap, + GridContainerProperties, +} from '../../../../core/shared/element-template' +import { gridPositionValue } from '../../../../core/shared/element-template' +import { isInfinityRectangle } from '../../../../core/shared/math-utils' +import { absolute } from '../../../../utils/utils' +import type { CanvasCommand } from '../../commands/commands' +import { reorderElement } from '../../commands/reorder-element-command' +import { showGridControls } from '../../commands/show-grid-controls-command' +import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies' +import type { CanvasStrategyFactory } from '../canvas-strategies' +import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' +import type { InteractionCanvasState } from '../canvas-strategy-types' +import { + emptyStrategyApplicationResult, + getTargetPathsFromInteractionTarget, + strategyApplicationResult, +} from '../canvas-strategy-types' +import type { DragInteractionData, InteractionSession } from '../interaction-state' +import type { GridCellGlobalFrames, SortableGridElementProperties } from './grid-helpers' +import { + findOriginalGrid, + getOriginalElementGridConfiguration, + getParentGridTemplatesFromChildMeasurements, + gridMoveStrategiesExtraCommands, + setGridPropsCommands, + sortElementsByGridPosition, +} from './grid-helpers' +import { getTargetGridCellData } from '../../../inspector/grid-helpers' + +export const gridMoveRearrangeStrategy: CanvasStrategyFactory = ( + canvasState: InteractionCanvasState, + interactionSession: InteractionSession | null, +) => { + const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) + if ( + selectedElements.length !== 1 || + interactionSession == null || + interactionSession.interactionData.type !== 'DRAG' || + interactionSession.interactionData.drag == null || + interactionSession.activeControl.type !== 'GRID_CELL_HANDLE' || + interactionSession.interactionData.modifiers.alt + ) { + return null + } + + const selectedElement = selectedElements[0] + if (!MetadataUtils.isGridCell(canvasState.startingMetadata, selectedElement)) { + return null + } + + const selectedElementMetadata = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + selectedElement, + ) + if (selectedElementMetadata == null) { + return null + } + const initialTemplates = getParentGridTemplatesFromChildMeasurements( + selectedElementMetadata.specialSizeMeasurements, + ) + if (initialTemplates == null) { + return null + } + + const parentGridPath = findOriginalGrid( + canvasState.startingMetadata, + EP.parentPath(selectedElement), + ) // TODO don't use EP.parentPath + if (parentGridPath == null) { + return null + } + + const gridFrame = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + parentGridPath, + )?.globalFrame + if (gridFrame == null || isInfinityRectangle(gridFrame)) { + return null + } + + if (MetadataUtils.isPositionAbsolute(selectedElementMetadata)) { + return null + } + + return { + id: 'rearrange-grid-move-strategy', + name: 'Grid rearrange', + descriptiveLabel: 'Grid rearrange', + icon: { + category: 'tools', + type: 'pointer', + }, + controlsToRender: [controlsForGridPlaceholders(parentGridPath, 'visible-only-while-active')], + 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 + } + + const { commands, elementsToRerender } = getCommandsAndPatchForGridRearrange( + canvasState, + interactionSession.interactionData, + selectedElement, + parentGridPath, + ) + if (commands.length === 0) { + return emptyStrategyApplicationResult + } + + const { midInteractionCommands, onCompleteCommands } = gridMoveStrategiesExtraCommands( + parentGridPath, + initialTemplates, + ) + return strategyApplicationResult( + [...midInteractionCommands, ...onCompleteCommands, ...commands], + elementsToRerender, + ) + }, + } +} + +function getCommandsAndPatchForGridRearrange( + canvasState: InteractionCanvasState, + interactionData: DragInteractionData, + selectedElement: ElementPath, + gridPath: ElementPath, +): { + commands: CanvasCommand[] + elementsToRerender: ElementPath[] +} { + if (interactionData.drag == null) { + return { commands: [], elementsToRerender: [] } + } + + const selectedElementMetadata = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + selectedElement, + ) + if (selectedElementMetadata == null) { + return { commands: [], elementsToRerender: [] } + } + + const { parentGridCellGlobalFrames, parentContainerGridProperties } = + selectedElementMetadata.specialSizeMeasurements + if (parentGridCellGlobalFrames == null) { + return { commands: [], elementsToRerender: [] } + } + + const commands = runGridMoveRearrange( + canvasState.startingMetadata, + interactionData, + selectedElementMetadata, + gridPath, + parentGridCellGlobalFrames, + parentContainerGridProperties, + null, + ) + + return { + commands: commands, + elementsToRerender: [gridPath, selectedElement], + } +} + +export function runGridMoveRearrange( + jsxMetadata: ElementInstanceMetadataMap, + interactionData: DragInteractionData, + selectedElementMetadata: ElementInstanceMetadata, + gridPath: ElementPath, + gridCellGlobalFrames: GridCellGlobalFrames, + gridTemplate: GridContainerProperties, + newPathAfterReparent: ElementPath | null, +): CanvasCommand[] { + if (interactionData.drag == null) { + return [] + } + + const isReparent = newPathAfterReparent != null + const pathForCommands = isReparent ? newPathAfterReparent : selectedElementMetadata.elementPath // when reparenting, we want to use the new path for commands + + const gridConfig = isReparent + ? { + originalCellBounds: { width: 1, height: 1 }, // when reparenting, we just put it in a single cell + mouseCellPosInOriginalElement: { row: 0, column: 0 }, + } + : getOriginalElementGridConfiguration( + gridCellGlobalFrames, + interactionData, + selectedElementMetadata, + ) + if (gridConfig == null) { + return [] + } + const { mouseCellPosInOriginalElement, originalCellBounds } = gridConfig + + const targetGridCellData = getTargetGridCellData( + interactionData, + gridCellGlobalFrames, + mouseCellPosInOriginalElement, + ) + if (targetGridCellData == null) { + return [] + } + const { targetCellCoords, targetRootCell } = targetGridCellData + + const gridCellMoveCommands = setGridPropsCommands(pathForCommands, gridTemplate, { + gridColumnStart: gridPositionValue(targetRootCell.column), + gridColumnEnd: gridPositionValue(targetRootCell.column + originalCellBounds.height), + gridRowStart: gridPositionValue(targetRootCell.row), + gridRowEnd: gridPositionValue(targetRootCell.row + originalCellBounds.width), + }) + + // The siblings of the grid element being moved + const siblings = MetadataUtils.getChildrenUnordered(jsxMetadata, gridPath) + .filter((s) => !EP.pathsEqual(s.elementPath, selectedElementMetadata.elementPath)) + .map( + (s, index): SortableGridElementProperties => ({ + ...s.specialSizeMeasurements.elementGridProperties, + index: index, + path: s.elementPath, + }), + ) + + // Sort the siblings and the cell under mouse ascending based on their grid coordinates, so that + // the indexes grow left-right, top-bottom. + const templateColumnsCount = + gridTemplate.gridTemplateColumns?.type === 'DIMENSIONS' + ? gridTemplate.gridTemplateColumns.dimensions.length + : 1 + const cellsSortedByPosition = siblings + .concat({ + ...{ + gridColumnStart: gridPositionValue(targetCellCoords.column), + gridColumnEnd: gridPositionValue(targetCellCoords.column), + gridRowStart: gridPositionValue(targetCellCoords.row), + gridRowEnd: gridPositionValue(targetCellCoords.row), + }, + path: selectedElementMetadata.elementPath, + index: siblings.length + 1, + }) + .sort(sortElementsByGridPosition(templateColumnsCount)) + + const indexInSortedCellsForRearrange = cellsSortedByPosition.findIndex((s) => + EP.pathsEqual(selectedElementMetadata.elementPath, s.path), + ) + + const updateGridControlsCommand = showGridControls( + 'mid-interaction', + gridPath, + targetCellCoords, + targetRootCell, + ) + + return [ + ...gridCellMoveCommands, + reorderElement( + 'always', + pathForCommands, + absolute(Math.max(indexInSortedCellsForRearrange, 0)), + ), + updateGridControlsCommand, + ] +} diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-move-reorder-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-reorder-strategy.ts new file mode 100644 index 000000000000..e603ba364f9f --- /dev/null +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-reorder-strategy.ts @@ -0,0 +1,256 @@ +import type { ElementPath } from 'utopia-shared/src/types' +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import * as EP from '../../../../core/shared/element-path' +import type { + ElementInstanceMetadata, + ElementInstanceMetadataMap, + GridContainerProperties, +} from '../../../../core/shared/element-template' +import { isInfinityRectangle } from '../../../../core/shared/math-utils' +import * as PP from '../../../../core/shared/property-path' +import { absolute } from '../../../../utils/utils' +import type { CanvasCommand } from '../../commands/commands' +import { deleteProperties } from '../../commands/delete-properties-command' +import { reorderElement } from '../../commands/reorder-element-command' +import { showGridControls } from '../../commands/show-grid-controls-command' +import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies' +import type { CanvasStrategyFactory } from '../canvas-strategies' +import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' +import type { InteractionCanvasState } from '../canvas-strategy-types' +import { + emptyStrategyApplicationResult, + getTargetPathsFromInteractionTarget, + strategyApplicationResult, +} from '../canvas-strategy-types' +import type { DragInteractionData, InteractionSession } from '../interaction-state' +import type { GridCellGlobalFrames } from './grid-helpers' +import { + findOriginalGrid, + getGridElementPinState, + getGridPositionIndex, + getOriginalElementGridConfiguration, + getParentGridTemplatesFromChildMeasurements, + gridMoveStrategiesExtraCommands, + isFlowGridChild, +} from './grid-helpers' +import { getTargetGridCellData } from '../../../inspector/grid-helpers' + +export const gridMoveReorderStrategy: CanvasStrategyFactory = ( + canvasState: InteractionCanvasState, + interactionSession: InteractionSession | null, +) => { + const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) + if ( + selectedElements.length !== 1 || + interactionSession == null || + interactionSession.interactionData.type !== 'DRAG' || + interactionSession.interactionData.drag == null || + interactionSession.activeControl.type !== 'GRID_CELL_HANDLE' || + interactionSession.interactionData.modifiers.alt || + interactionSession.interactionData.modifiers.cmd // disable reorder when reparenting, for now (TODO) + ) { + return null + } + + const selectedElement = selectedElements[0] + if (!MetadataUtils.isGridCell(canvasState.startingMetadata, selectedElement)) { + return null + } + + const selectedElementMetadata = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + selectedElement, + ) + if (selectedElementMetadata == null) { + return null + } + if (MetadataUtils.isPositionAbsolute(selectedElementMetadata)) { + return null + } + + const initialTemplates = getParentGridTemplatesFromChildMeasurements( + selectedElementMetadata.specialSizeMeasurements, + ) + if (initialTemplates == null) { + return null + } + + const parentGridPath = findOriginalGrid( + canvasState.startingMetadata, + EP.parentPath(selectedElement), + ) // TODO don't use EP.parentPath + if (parentGridPath == null) { + return null + } + + const gridFrame = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + parentGridPath, + )?.globalFrame + if (gridFrame == null || isInfinityRectangle(gridFrame)) { + return null + } + + const elementGridPropertiesFromProps = + selectedElementMetadata.specialSizeMeasurements.elementGridPropertiesFromProps + + const pinnedState = getGridElementPinState(elementGridPropertiesFromProps ?? null) + const fitnessModifier = pinnedState !== 'pinned' ? 1 : -1 + + return { + id: 'reorder-grid-move-strategy', + name: 'Grid Reorder', + descriptiveLabel: 'Grid Reorder', + icon: { + category: 'tools', + type: 'pointer', + }, + controlsToRender: [controlsForGridPlaceholders(parentGridPath, 'visible-only-while-active')], + fitness: onlyFitWhenDraggingThisControl( + interactionSession, + 'GRID_CELL_HANDLE', + 2 + fitnessModifier, + ), + apply: () => { + if ( + interactionSession == null || + interactionSession.interactionData.type !== 'DRAG' || + interactionSession.interactionData.drag == null || + interactionSession.activeControl.type !== 'GRID_CELL_HANDLE' + ) { + return emptyStrategyApplicationResult + } + + const { commands, elementsToRerender } = getCommandsAndPatchForGridReorder( + canvasState, + interactionSession.interactionData, + selectedElement, + parentGridPath, + ) + if (commands.length === 0) { + return emptyStrategyApplicationResult + } + + const { midInteractionCommands, onCompleteCommands } = gridMoveStrategiesExtraCommands( + parentGridPath, + initialTemplates, + ) + + return strategyApplicationResult( + [...midInteractionCommands, ...onCompleteCommands, ...commands], + elementsToRerender, + ) + }, + } +} + +function getCommandsAndPatchForGridReorder( + canvasState: InteractionCanvasState, + interactionData: DragInteractionData, + selectedElement: ElementPath, + gridPath: ElementPath, +): { + commands: CanvasCommand[] + elementsToRerender: ElementPath[] +} { + if (interactionData.drag == null) { + return { commands: [], elementsToRerender: [] } + } + + const selectedElementMetadata = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + selectedElement, + ) + if (selectedElementMetadata == null) { + return { commands: [], elementsToRerender: [] } + } + + const { parentGridCellGlobalFrames, parentContainerGridProperties } = + selectedElementMetadata.specialSizeMeasurements + if (parentGridCellGlobalFrames == null) { + return { commands: [], elementsToRerender: [] } + } + + const commands = runGridMoveReorder( + canvasState.startingMetadata, + interactionData, + selectedElementMetadata, + gridPath, + parentGridCellGlobalFrames, + parentContainerGridProperties, + ) + + return { + commands: commands, + elementsToRerender: [gridPath, selectedElement], + } +} + +function runGridMoveReorder( + jsxMetadata: ElementInstanceMetadataMap, + interactionData: DragInteractionData, + selectedElementMetadata: ElementInstanceMetadata, + gridPath: ElementPath, + gridCellGlobalFrames: GridCellGlobalFrames, + gridTemplate: GridContainerProperties, +): CanvasCommand[] { + if (interactionData.drag == null) { + return [] + } + + const mouseCellPosInOriginalElement = getOriginalElementGridConfiguration( + gridCellGlobalFrames, + interactionData, + selectedElementMetadata, + )?.mouseCellPosInOriginalElement + if (mouseCellPosInOriginalElement == null) { + return [] + } + + const targetGridCellData = getTargetGridCellData( + interactionData, + gridCellGlobalFrames, + mouseCellPosInOriginalElement, + ) + if (targetGridCellData == null) { + return [] + } + const { targetCellCoords, targetRootCell } = targetGridCellData + + const gridTemplateColumns = + gridTemplate.gridTemplateColumns?.type === 'DIMENSIONS' + ? gridTemplate.gridTemplateColumns.dimensions.length + : 1 + + const gridChildren = MetadataUtils.getChildrenUnordered(jsxMetadata, gridPath) + const gridFlowChildrenCount = gridChildren.filter(isFlowGridChild).length + + // The "pure" index in the grid children for the cell under mouse + const possiblyReorderIndex = getGridPositionIndex({ + row: targetCellCoords.row, + column: targetCellCoords.column, + gridTemplateColumns: gridTemplateColumns, + }) + + const canReorderToIndex = possiblyReorderIndex < gridFlowChildrenCount + + const updateGridControlsCommand = showGridControls( + 'mid-interaction', + gridPath, + targetCellCoords, + canReorderToIndex ? targetRootCell : null, + ) + + return [ + reorderElement('always', selectedElementMetadata.elementPath, absolute(possiblyReorderIndex)), + deleteProperties('always', selectedElementMetadata.elementPath, [ + PP.create('style', 'gridColumn'), + PP.create('style', 'gridRow'), + PP.create('style', 'gridColumnStart'), + PP.create('style', 'gridColumnEnd'), + PP.create('style', 'gridRowStart'), + PP.create('style', 'gridRowEnd'), + ]), + updateGridControlsCommand, + ] +} diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-keyboard-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-keyboard-strategy.ts index 8952cee616b8..9d5f8a20a1a5 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-keyboard-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-keyboard-strategy.ts @@ -10,7 +10,7 @@ import { strategyApplicationResult, } from '../canvas-strategy-types' import type { InteractionSession } from '../interaction-state' -import { setGridPropsCommands } from './grid-helpers' +import { findOriginalGrid, setGridPropsCommands } from './grid-helpers' import { getGridChildCellCoordBoundsFromCanvas } from './grid-cell-bounds' import { accumulatePresses } from './shared-keyboard-strategy-helpers' @@ -41,7 +41,10 @@ export function gridRearrangeResizeKeyboardStrategy( return null } - const parentGridPath = EP.parentPath(target) + const parentGridPath = findOriginalGrid(canvasState.startingMetadata, EP.parentPath(target)) // TODO don't use EP.parentPath + if (parentGridPath == null) { + return null + } const gridTemplate = cell.specialSizeMeasurements.parentContainerGridProperties const gridCellGlobalFrames = cell.specialSizeMeasurements.parentGridCellGlobalFrames diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.spec.browser2.tsx index e293d4cd572d..ace5540d545f 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.spec.browser2.tsx @@ -12,7 +12,12 @@ import { selectComponentsForTest } from '../../../../utils/utils.test-utils' import CanvasActions from '../../canvas-actions' import { GridCellTestId } from '../../controls/grid-controls-for-strategies' import { CanvasControlsContainerID } from '../../controls/new-canvas-controls' -import { mouseDragFromPointToPoint, mouseUpAtPoint } from '../../event-helpers.test-utils' +import { + keyDown, + mouseDownAtPoint, + mouseMoveToPoint, + mouseUpAtPoint, +} from '../../event-helpers.test-utils' import type { EditorRenderResult } from '../../ui-jsx.test-utils' import { renderTestEditorWithCode } from '../../ui-jsx.test-utils' import type { GridCellCoordinates } from './grid-cell-bounds' @@ -84,7 +89,9 @@ describe('grid rearrange move strategy', () => { scale: 1, pathString: `sb/scene/grid/${testId}`, testId: testId, + tab: true, }) + expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({ gridColumnEnd: 'auto', gridColumnStart: '3', @@ -214,24 +221,21 @@ export var storyboard = ( const sourceRect = sourceGridCell.getBoundingClientRect() const targetRect = targetGridCell.getBoundingClientRect() - await mouseDragFromPointToPoint( - sourceGridCell, - { - x: sourceRect.x + 10, - y: sourceRect.y + 10, - }, - getRectCenter( - localRectangle({ - x: targetRect.x, - y: targetRect.y, - width: targetRect.width, - height: targetRect.height, - }), - ), - { - moveBeforeMouseDown: true, - }, + const dragFrom = { + x: sourceRect.x + 10, + y: sourceRect.y + 10, + } + const dragTo = getRectCenter( + localRectangle({ + x: targetRect.x, + y: targetRect.y, + width: targetRect.width, + height: targetRect.height, + }), ) + await mouseDownAtPoint(sourceGridCell, dragFrom) + await mouseMoveToPoint(sourceGridCell, dragTo) + await mouseUpAtPoint(sourceGridCell, dragTo) const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } = editor.renderedDOM.getByTestId(testId).style @@ -275,8 +279,7 @@ export var storyboard = ( display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: '1fr 1fr', - border: '5px solid #000', - gridGap: 10, + gridGap: 0, }} >