From 3c31ac6f9c4952d907b45eb9a9ec24742fdd5c5e Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:43:33 +0200 Subject: [PATCH] Reparent out of grid (#6264) **Problem:** It's currently not possible to reparent out of a grid. **Fix:** Allow reparenting out of a grid while holding `cmd`, like all other reparenting scenarios. https://github.com/user-attachments/assets/df75b0ee-5098-4cba-b66c-507697cf03c0 **Notes:** The grid rearrange strategy now needs to act as a sort of meta strat, because 1) it needs a custom mouse drag handling (due to the grid placeholder controls) and 2) cells don't have positioning props so absolute reparenting needs some massaging in order to work Fixes #6263 --- .../strategies/absolute-reparent-strategy.tsx | 245 ++++++++------- .../grid-rearrange-move-strategy.ts | 284 +++++++++++++++--- .../strategies/grid-reparent-strategy.tsx | 245 ++++++++------- .../rearrange-grid-swap-strategy.ts | 3 +- .../reparent-as-static-strategy.tsx | 70 +++-- .../canvas/controls/grid-controls.tsx | 1 + 6 files changed, 550 insertions(+), 298 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.tsx index 87771a51e3f9..557f1c8724cf 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.tsx @@ -24,6 +24,7 @@ import type { CanvasStrategy, CustomStrategyState, InteractionCanvasState, + InteractionLifecycle, } from '../canvas-strategy-types' import { controlWithProps, @@ -61,7 +62,6 @@ export function baseAbsoluteReparentStrategy( return null } - const dragInteractionData = interactionSession.interactionData // Why TypeScript?! const filteredSelectedElements = flattenSelection(selectedElements) const isApplicable = replaceFragmentLikePathsWithTheirChildrenRecursive( canvasState.startingMetadata, @@ -90,26 +90,7 @@ export function baseAbsoluteReparentStrategy( category: 'modalities', type: 'reparent-large', }, - controlsToRender: [ - controlWithProps({ - control: ParentOutlines, - props: { targetParent: reparentTarget.newParent.intendedParentPath }, - key: 'parent-outlines-control', - show: 'visible-only-while-active', - }), - controlWithProps({ - control: ParentBounds, - props: { targetParent: reparentTarget.newParent.intendedParentPath }, - key: 'parent-bounds-control', - show: 'visible-only-while-active', - }), - controlWithProps({ - control: ZeroSizedElementControls, - props: { showAllPossibleElements: true }, - key: 'zero-size-control', - show: 'visible-only-while-active', - }), - ], + controlsToRender: controlsForAbsoluteReparent(reparentTarget), fitness: shouldKeepMovingDraggedGroupChildren( canvasState.startingMetadata, selectedElements, @@ -117,100 +98,148 @@ export function baseAbsoluteReparentStrategy( ) ? 1 : fitness, - apply: (strategyLifecycle) => { - const { projectContents, nodeModules } = canvasState - const newParent = reparentTarget.newParent - return ifAllowedToReparent( - canvasState, - canvasState.startingMetadata, - filteredSelectedElements, - newParent.intendedParentPath, - () => { - if (dragInteractionData.drag == null) { - return emptyStrategyApplicationResult - } + apply: applyAbsoluteReparent( + canvasState, + interactionSession, + customStrategyState, + reparentTarget, + filteredSelectedElements, + ), + } + } +} - const allowedToReparent = filteredSelectedElements.every((selectedElement) => { - return isAllowedToReparent( - canvasState.projectContents, - canvasState.startingMetadata, - selectedElement, - newParent.intendedParentPath, - ) - }) +export function controlsForAbsoluteReparent(reparentTarget: ReparentTarget) { + return [ + controlWithProps({ + control: ParentOutlines, + props: { targetParent: reparentTarget.newParent.intendedParentPath }, + key: 'parent-outlines-control', + show: 'visible-only-while-active', + }), + controlWithProps({ + control: ParentBounds, + props: { targetParent: reparentTarget.newParent.intendedParentPath }, + key: 'parent-bounds-control', + show: 'visible-only-while-active', + }), + controlWithProps({ + control: ZeroSizedElementControls, + props: { showAllPossibleElements: true }, + key: 'zero-size-control', + show: 'visible-only-while-active', + }), + ] +} - if (reparentTarget.shouldReparent && allowedToReparent) { - const commands = mapDropNulls( - (selectedElement) => - createAbsoluteReparentAndOffsetCommands( - selectedElement, - newParent, - null, - canvasState.startingMetadata, - canvasState.startingElementPathTree, - canvasState.startingAllElementProps, - canvasState.builtInDependencies, - projectContents, - nodeModules, - 'force-pins', - ), - selectedElements, - ) +export function applyAbsoluteReparent( + canvasState: InteractionCanvasState, + interactionSession: InteractionSession, + customStrategyState: CustomStrategyState, + reparentTarget: ReparentTarget, + selectedElements: ElementPath[], +) { + return (strategyLifecycle: InteractionLifecycle) => { + if ( + selectedElements.length === 0 || + interactionSession == null || + interactionSession.interactionData.type !== 'DRAG' + ) { + return emptyStrategyApplicationResult + } + const dragInteractionData = interactionSession.interactionData + + const { projectContents, nodeModules } = canvasState + const newParent = reparentTarget.newParent + return ifAllowedToReparent( + canvasState, + canvasState.startingMetadata, + selectedElements, + newParent.intendedParentPath, + () => { + if (dragInteractionData.drag == null) { + return emptyStrategyApplicationResult + } + + const allowedToReparent = selectedElements.every((selectedElement) => { + return isAllowedToReparent( + canvasState.projectContents, + canvasState.startingMetadata, + selectedElement, + newParent.intendedParentPath, + ) + }) - let newPaths: Array = [] - let updatedTargetPaths: UpdatedPathMap = {} + if (reparentTarget.shouldReparent && allowedToReparent) { + const commands = mapDropNulls( + (selectedElement) => + createAbsoluteReparentAndOffsetCommands( + selectedElement, + newParent, + null, + canvasState.startingMetadata, + canvasState.startingElementPathTree, + canvasState.startingAllElementProps, + canvasState.builtInDependencies, + projectContents, + nodeModules, + 'force-pins', + ), + selectedElements, + ) + + let newPaths: Array = [] + let updatedTargetPaths: UpdatedPathMap = {} - commands.forEach((c) => { - newPaths.push(c.newPath) - updatedTargetPaths[EP.toString(c.oldPath)] = c.newPath - }) + commands.forEach((c) => { + newPaths.push(c.newPath) + updatedTargetPaths[EP.toString(c.oldPath)] = c.newPath + }) - const moveCommands = - absoluteMoveStrategy( - canvasState, - { - ...interactionSession, - updatedTargetPaths: updatedTargetPaths, - }, - { ...defaultCustomStrategyState(), action: 'reparent' }, - )?.strategy.apply(strategyLifecycle).commands ?? [] + const moveCommands = + absoluteMoveStrategy( + canvasState, + { + ...interactionSession, + updatedTargetPaths: updatedTargetPaths, + }, + { ...defaultCustomStrategyState(), action: 'reparent' }, + )?.strategy.apply(strategyLifecycle).commands ?? [] - const elementsToRerender = EP.uniqueElementPaths([ - ...customStrategyState.elementsToRerender, - ...newPaths, - ...newPaths.map(EP.parentPath), - ...filteredSelectedElements.map(EP.parentPath), - ]) - return strategyApplicationResult( - [ - ...moveCommands, - ...commands.flatMap((c) => c.commands), - updateSelectedViews('always', newPaths), - setElementsToRerenderCommand(elementsToRerender), - ...maybeAddContainLayout( - canvasState.startingMetadata, - canvasState.startingAllElementProps, - canvasState.startingElementPathTree, - newParent.intendedParentPath, - ), - setCursorCommand(CSSCursor.Reparent), - ], - { - elementsToRerender, - }, - ) - } else { - const moveCommands = - absoluteMoveStrategy(canvasState, interactionSession, { - ...defaultCustomStrategyState(), - action: 'reparent', - })?.strategy.apply(strategyLifecycle).commands ?? [] - return strategyApplicationResult(moveCommands) - } - }, - ) + const elementsToRerender = EP.uniqueElementPaths([ + ...customStrategyState.elementsToRerender, + ...newPaths, + ...newPaths.map(EP.parentPath), + ...selectedElements.map(EP.parentPath), + ]) + return strategyApplicationResult( + [ + ...moveCommands, + ...commands.flatMap((c) => c.commands), + updateSelectedViews('always', newPaths), + setElementsToRerenderCommand(elementsToRerender), + ...maybeAddContainLayout( + canvasState.startingMetadata, + canvasState.startingAllElementProps, + canvasState.startingElementPathTree, + newParent.intendedParentPath, + ), + setCursorCommand(CSSCursor.Reparent), + ], + { + elementsToRerender, + }, + ) + } else { + const moveCommands = + absoluteMoveStrategy(canvasState, interactionSession, { + ...defaultCustomStrategyState(), + action: 'reparent', + })?.strategy.apply(strategyLifecycle).commands ?? [] + return strategyApplicationResult(moveCommands) + } }, - } + ) } } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.ts index 67586f33faef..b0d5b80df555 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.ts @@ -16,14 +16,29 @@ import { } from '../../commands/set-property-command' import type { CanvasStrategyFactory } from '../canvas-strategies' import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' -import type { CustomStrategyState, InteractionCanvasState } from '../canvas-strategy-types' +import type { + ControlWithProps, + CustomStrategyState, + CustomStrategyStatePatch, + InteractionCanvasState, + InteractionLifecycle, +} from '../canvas-strategy-types' import { getTargetPathsFromInteractionTarget, emptyStrategyApplicationResult, strategyApplicationResult, } from '../canvas-strategy-types' -import type { InteractionSession } from '../interaction-state' +import type { DragInteractionData, InteractionSession } from '../interaction-state' import { runGridRearrangeMove } from './grid-helpers' +import type { CanvasRectangle } from '../../../../core/shared/math-utils' +import { isInfinityRectangle, offsetPoint } from '../../../../core/shared/math-utils' +import { findReparentStrategies } from './reparent-helpers/reparent-strategy-helpers' +import { applyAbsoluteReparent, controlsForAbsoluteReparent } from './absolute-reparent-strategy' +import type { CanvasCommand } from '../../commands/commands' +import { applyStaticReparent, controlsForStaticReparent } from './reparent-as-static-strategy' +import type { FindReparentStrategyResult } from './reparent-helpers/reparent-strategy-parent-lookup' +import { applyGridReparent, controlsForGridReparent } from './grid-reparent-strategy' +import { assertNever } from '../../../../core/shared/utils' export const gridRearrangeMoveStrategy: CanvasStrategyFactory = ( canvasState: InteractionCanvasState, @@ -48,31 +63,39 @@ export const gridRearrangeMoveStrategy: CanvasStrategyFactory = ( } const parentGridPath = EP.parentPath(selectedElement) + const gridFrame = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + parentGridPath, + )?.globalFrame + if (gridFrame == null || isInfinityRectangle(gridFrame)) { + return null + } const initialTemplates = getGridTemplates(canvasState.startingMetadata, parentGridPath) if (initialTemplates == null) { return null } + const strategyToApply = getStrategyToApply( + canvasState, + interactionSession.interactionData, + parentGridPath, + ) + if (strategyToApply == null) { + return null + } + return { id: 'rearrange-grid-move-strategy', - name: 'Rearrange Grid (Move)', - descriptiveLabel: 'Rearrange Grid (Move)', + name: strategyToApply.name, + descriptiveLabel: strategyToApply.name, icon: { category: 'tools', type: 'pointer', }, - controlsToRender: [ - { - control: GridControls, - props: { targets: [parentGridPath] }, - key: GridControlsKey(parentGridPath), - show: 'always-visible', - priority: 'bottom', - }, - ], + controlsToRender: strategyToApply.controlsToRender, fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 2), - apply: () => { + apply: (strategyLifecycle) => { if ( interactionSession == null || interactionSession.interactionData.type !== 'DRAG' || @@ -82,28 +105,6 @@ export const gridRearrangeMoveStrategy: CanvasStrategyFactory = ( return emptyStrategyApplicationResult } - const targetElement = selectedElement - - const { - commands: moveCommands, - targetCell: targetGridCell, - draggingFromCell, - originalRootCell, - targetRootCell, - } = runGridRearrangeMove( - targetElement, - selectedElement, - canvasState.startingMetadata, - interactionSession.interactionData, - canvasState.scale, - canvasState.canvasOffset, - customState.grid, - false, - ) - if (moveCommands.length === 0) { - return emptyStrategyApplicationResult - } - const midInteractionCommands = [ // during the interaction, freeze the template with the calculated values… updateBulkProperties('mid-interaction', parentGridPath, [ @@ -127,21 +128,137 @@ export const gridRearrangeMoveStrategy: CanvasStrategyFactory = ( ), ] + const { commands, patch } = + strategyToApply.type === 'GRID_REARRANGE' + ? getCommandsAndPatchForGridRearrange( + canvasState, + interactionSession.interactionData, + customState, + selectedElement, + ) + : getCommandsAndPatchForReparent( + strategyToApply.strategy, + canvasState, + interactionSession.interactionData, + interactionSession, + customState, + selectedElement, + strategyLifecycle, + gridFrame, + ) + + if (commands.length === 0) { + return emptyStrategyApplicationResult + } + return strategyApplicationResult( - [...moveCommands, ...midInteractionCommands, ...onCompleteCommands], - { - grid: { - targetCell: targetGridCell, - draggingFromCell: draggingFromCell, - originalRootCell: originalRootCell, - currentRootCell: targetRootCell, - }, - }, + [...midInteractionCommands, ...onCompleteCommands, ...commands], + patch, ) }, } } +function getCommandsAndPatchForGridRearrange( + canvasState: InteractionCanvasState, + interactionData: DragInteractionData, + customState: CustomStrategyState, + selectedElement: ElementPath, +): { commands: CanvasCommand[]; patch: CustomStrategyStatePatch } { + if (interactionData.drag == null) { + return { commands: [], patch: {} } + } + + const { + commands, + targetCell: targetGridCell, + draggingFromCell, + originalRootCell, + targetRootCell, + } = runGridRearrangeMove( + selectedElement, + selectedElement, + canvasState.startingMetadata, + interactionData, + canvasState.scale, + canvasState.canvasOffset, + customState.grid, + false, + ) + + return { + commands: commands, + patch: { + grid: { + targetCell: targetGridCell, + draggingFromCell: draggingFromCell, + originalRootCell: originalRootCell, + currentRootCell: targetRootCell, + }, + }, + } +} + +function getCommandsAndPatchForReparent( + strategy: FindReparentStrategyResult, + canvasState: InteractionCanvasState, + interactionData: DragInteractionData, + interactionSession: InteractionSession, + customState: CustomStrategyState, + targetElement: ElementPath, + strategyLifecycle: InteractionLifecycle, + gridFrame: CanvasRectangle, +): { commands: CanvasCommand[]; patch: CustomStrategyStatePatch } { + if (interactionData.drag == null) { + return { commands: [], patch: {} } + } + + function applyReparent() { + switch (strategy.strategy) { + case 'REPARENT_AS_ABSOLUTE': + return applyAbsoluteReparent( + canvasState, + interactionSession, + customState, + strategy.target, + [targetElement], + )(strategyLifecycle) + case 'REPARENT_AS_STATIC': + return applyStaticReparent(canvasState, interactionSession, customState, strategy.target) + case 'REPARENT_INTO_GRID': + return applyGridReparent( + canvasState, + interactionData, + customState, + strategy.target, + [targetElement], + gridFrame, + )() + default: + assertNever(strategy.strategy) + } + } + const result = applyReparent() + + let commands: CanvasCommand[] = [] + if (strategy.strategy === 'REPARENT_AS_ABSOLUTE') { + const frame = MetadataUtils.getFrameOrZeroRect(targetElement, canvasState.startingMetadata) + commands.push( + updateBulkProperties('always', targetElement, [ + propertyToSet(PP.create('style', 'position'), 'absolute'), + propertyToSet(PP.create('style', 'top'), frame.y + interactionData.drag.y), + propertyToSet(PP.create('style', 'left'), frame.x + interactionData.drag.x), + ]), + ) + } + commands.push(...result.commands) + + return { + commands: commands, + patch: result.customStatePatch, + } +} + function restoreGridTemplateFromProps(params: { columns: GridAutoOrTemplateBase rows: GridAutoOrTemplateBase @@ -199,3 +316,80 @@ function getGridTemplates(jsxMetadata: ElementInstanceMetadataMap, gridPath: Ele }, } } + +type StrategyToApply = + | { + type: 'GRID_REARRANGE' + controlsToRender: ControlWithProps[] + name: string + } + | { + type: 'REPARENT' + controlsToRender: ControlWithProps[] + name: string + strategy: FindReparentStrategyResult + } + +function getStrategyToApply( + canvasState: InteractionCanvasState, + interactionData: DragInteractionData, + parentGridPath: ElementPath, +): StrategyToApply | null { + if (interactionData.drag == null) { + return null + } + + const shouldReparent = interactionData.modifiers.cmd + if (shouldReparent) { + const pointOnCanvas = offsetPoint(interactionData.originalDragStart, interactionData.drag) + const reparentStrategies = findReparentStrategies( + canvasState, + true, + pointOnCanvas, + 'allow-smaller-parent', + ) + + const strategy = reparentStrategies[0] + if (strategy != null) { + switch (strategy.strategy) { + case 'REPARENT_AS_ABSOLUTE': + return { + type: 'REPARENT', + name: 'Reparent (Abs)', + controlsToRender: controlsForAbsoluteReparent(strategy.target), + strategy: strategy, + } + case 'REPARENT_AS_STATIC': + return { + type: 'REPARENT', + name: 'Reparent (Flex)', + controlsToRender: controlsForStaticReparent(strategy.target), + strategy: strategy, + } + case 'REPARENT_INTO_GRID': + return { + type: 'REPARENT', + name: 'Reparent (Grid)', + controlsToRender: controlsForGridReparent(strategy.target), + strategy: strategy, + } + default: + assertNever(strategy.strategy) + } + } + } + + return { + type: 'GRID_REARRANGE', + name: 'Rearrange Grid (Move)', + controlsToRender: [ + { + control: GridControls, + props: { targets: [parentGridPath] }, + key: GridControlsKey(parentGridPath), + show: 'always-visible', + priority: 'bottom', + }, + ], + } +} diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategy.tsx index 29b1ffb84615..8402c8aa91e4 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategy.tsx @@ -5,11 +5,7 @@ import * as PP from '../../../../core/shared/property-path' import type { ElementPath } from '../../../../core/shared/project-file-types' import { CSSCursor } from '../../canvas-types' import { setCursorCommand } from '../../commands/set-cursor-command' -import { - propertyToDelete, - propertyToSet, - updateBulkProperties, -} from '../../commands/set-property-command' +import { propertyToSet, updateBulkProperties } from '../../commands/set-property-command' import { updateSelectedViews } from '../../commands/update-selected-views-command' import { ParentBounds } from '../../controls/parent-bounds' import { ParentOutlines } from '../../controls/parent-outlines' @@ -17,6 +13,7 @@ import { ZeroSizedElementControls } from '../../controls/zero-sized-element-cont import type { CanvasStrategyFactory } from '../canvas-strategies' import type { CanvasStrategy, + ControlWithProps, CustomStrategyState, InteractionCanvasState, } from '../canvas-strategy-types' @@ -26,13 +23,14 @@ import { getTargetPathsFromInteractionTarget, strategyApplicationResult, } from '../canvas-strategy-types' -import type { InteractionSession, UpdatedPathMap } from '../interaction-state' +import type { DragInteractionData, InteractionSession, UpdatedPathMap } from '../interaction-state' import { honoursPropsPosition, shouldKeepMovingDraggedGroupChildren } from './absolute-utils' import { replaceFragmentLikePathsWithTheirChildrenRecursive } from './fragment-like-helpers' import { ifAllowedToReparent, isAllowedToReparent } from './reparent-helpers/reparent-helpers' import type { ReparentTarget } from './reparent-helpers/reparent-strategy-helpers' import { getReparentOutcome, pathToReparent } from './reparent-utils' import { flattenSelection } from './shared-move-strategies-helpers' +import type { CanvasRectangle } from '../../../../core/shared/math-utils' import { isInfinityRectangle } from '../../../../core/shared/math-utils' import { showGridControls } from '../../commands/show-grid-controls-command' import { GridControls } from '../../controls/grid-controls' @@ -42,7 +40,6 @@ import type { AllElementProps } from '../../../editor/store/editor-state' import type { BuiltInDependencies } from '../../../../core/es-modules/package-manager/built-in-dependencies-list' import type { NodeModules, ProjectContentTreeRoot } from 'utopia-shared/src/types' import type { InsertionPath } from '../../../editor/store/insertion-path' -import type { WhenToRun } from '../../commands/commands' import { removeAbsolutePositioningProps } from './reparent-helpers/reparent-property-changes' export function gridReparentStrategy( @@ -92,33 +89,7 @@ export function gridReparentStrategy( category: 'modalities', type: 'reparent-large', }, - controlsToRender: [ - controlWithProps({ - control: ParentOutlines, - props: { targetParent: reparentTarget.newParent.intendedParentPath }, - key: 'parent-outlines-control', - show: 'visible-only-while-active', - }), - controlWithProps({ - control: ParentBounds, - props: { targetParent: reparentTarget.newParent.intendedParentPath }, - key: 'parent-bounds-control', - show: 'visible-only-while-active', - }), - controlWithProps({ - control: ZeroSizedElementControls, - props: { showAllPossibleElements: true }, - key: 'zero-size-control', - show: 'visible-only-while-active', - }), - { - control: GridControls, - props: { targets: [reparentTarget.newParent.intendedParentPath] }, - key: `draw-into-grid-strategy-controls`, - show: 'always-visible', - priority: 'bottom', - }, - ], + controlsToRender: controlsForGridReparent(reparentTarget), fitness: shouldKeepMovingDraggedGroupChildren( canvasState.startingMetadata, selectedElements, @@ -126,86 +97,138 @@ export function gridReparentStrategy( ) ? 1 : fitness, - apply: () => { - const { projectContents, nodeModules } = canvasState - const newParent = reparentTarget.newParent - return ifAllowedToReparent( - canvasState, - canvasState.startingMetadata, - filteredSelectedElements, - newParent.intendedParentPath, - () => { - if (dragInteractionData.drag == null) { - return emptyStrategyApplicationResult - } - - const allowedToReparent = filteredSelectedElements.every((selectedElement) => { - return isAllowedToReparent( - canvasState.projectContents, - canvasState.startingMetadata, - selectedElement, - newParent.intendedParentPath, - ) - }) - - if (!(reparentTarget.shouldReparent && allowedToReparent)) { - return emptyStrategyApplicationResult - } - const outcomes = mapDropNulls( - (selectedElement) => - gridReparentCommands( - canvasState.startingMetadata, - canvasState.startingElementPathTree, - canvasState.startingAllElementProps, - canvasState.builtInDependencies, - projectContents, - nodeModules, - selectedElement, - newParent, - ), - selectedElements, - ) - - let newPaths: Array = [] - let updatedTargetPaths: UpdatedPathMap = {} - - outcomes.forEach((c) => { - newPaths.push(c.newPath) - updatedTargetPaths[EP.toString(c.oldPath)] = c.newPath - }) - - const gridContainerCommands = updateBulkProperties( - 'mid-interaction', - reparentTarget.newParent.intendedParentPath, - [ - propertyToSet(PP.create('style', 'width'), gridFrame.width), - propertyToSet(PP.create('style', 'height'), gridFrame.height), - ], - ) - - const elementsToRerender = EP.uniqueElementPaths([ - ...customStrategyState.elementsToRerender, - ...newPaths, - ...newPaths.map(EP.parentPath), - ...filteredSelectedElements.map(EP.parentPath), - ]) - - return strategyApplicationResult( - [ - ...outcomes.flatMap((c) => c.commands), - gridContainerCommands, - updateSelectedViews('always', newPaths), - setCursorCommand(CSSCursor.Reparent), - showGridControls('mid-interaction', reparentTarget.newParent.intendedParentPath), - ], - { - elementsToRerender, - }, - ) + apply: applyGridReparent( + canvasState, + dragInteractionData, + customStrategyState, + reparentTarget, + filteredSelectedElements, + gridFrame, + ), + } + } +} + +export function controlsForGridReparent(reparentTarget: ReparentTarget): ControlWithProps[] { + return [ + controlWithProps({ + control: ParentOutlines, + props: { targetParent: reparentTarget.newParent.intendedParentPath }, + key: 'parent-outlines-control', + show: 'visible-only-while-active', + }), + controlWithProps({ + control: ParentBounds, + props: { targetParent: reparentTarget.newParent.intendedParentPath }, + key: 'parent-bounds-control', + show: 'visible-only-while-active', + }), + controlWithProps({ + control: ZeroSizedElementControls, + props: { showAllPossibleElements: true }, + key: 'zero-size-control', + show: 'visible-only-while-active', + }), + { + control: GridControls, + props: { targets: [reparentTarget.newParent.intendedParentPath] }, + key: `draw-into-grid-strategy-controls`, + show: 'always-visible', + priority: 'bottom', + }, + ] +} + +export function applyGridReparent( + canvasState: InteractionCanvasState, + interactionData: DragInteractionData, + customStrategyState: CustomStrategyState, + reparentTarget: ReparentTarget, + selectedElements: ElementPath[], + gridFrame: CanvasRectangle, +) { + return () => { + if (interactionData.drag == null) { + return emptyStrategyApplicationResult + } + + const { projectContents, nodeModules } = canvasState + const newParent = reparentTarget.newParent + return ifAllowedToReparent( + canvasState, + canvasState.startingMetadata, + selectedElements, + newParent.intendedParentPath, + () => { + if (interactionData.drag == null) { + return emptyStrategyApplicationResult + } + + const allowedToReparent = selectedElements.every((selectedElement) => { + return isAllowedToReparent( + canvasState.projectContents, + canvasState.startingMetadata, + selectedElement, + newParent.intendedParentPath, + ) + }) + + if (!(reparentTarget.shouldReparent && allowedToReparent)) { + return emptyStrategyApplicationResult + } + const outcomes = mapDropNulls( + (selectedElement) => + gridReparentCommands( + canvasState.startingMetadata, + canvasState.startingElementPathTree, + canvasState.startingAllElementProps, + canvasState.builtInDependencies, + projectContents, + nodeModules, + selectedElement, + newParent, + ), + selectedElements, + ) + + let newPaths: Array = [] + let updatedTargetPaths: UpdatedPathMap = {} + + outcomes.forEach((c) => { + newPaths.push(c.newPath) + updatedTargetPaths[EP.toString(c.oldPath)] = c.newPath + }) + + const gridContainerCommands = updateBulkProperties( + 'mid-interaction', + reparentTarget.newParent.intendedParentPath, + [ + propertyToSet(PP.create('style', 'width'), gridFrame.width), + propertyToSet(PP.create('style', 'height'), gridFrame.height), + ], + ) + + const elementsToRerender = EP.uniqueElementPaths([ + ...customStrategyState.elementsToRerender, + ...newPaths, + ...newPaths.map(EP.parentPath), + ...selectedElements.map(EP.parentPath), + ]) + + return strategyApplicationResult( + [ + ...outcomes.flatMap((c) => c.commands), + gridContainerCommands, + updateSelectedViews('always', newPaths), + setCursorCommand(CSSCursor.Reparent), + showGridControls('mid-interaction', reparentTarget.newParent.intendedParentPath), + ], + { + elementsToRerender, }, ) }, - } + ) } } 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 index 898f19f319e1..85688d7c2441 100644 --- 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 @@ -38,7 +38,8 @@ export const rearrangeGridSwapStrategy: CanvasStrategyFactory = ( interactionSession.interactionData.type !== 'DRAG' || interactionSession.interactionData.drag == null || interactionSession.activeControl.type !== 'GRID_CELL_HANDLE' || - interactionSession.interactionData.modifiers.alt + interactionSession.interactionData.modifiers.alt || + interactionSession.interactionData.modifiers.cmd ) { return null } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-as-static-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/reparent-as-static-strategy.tsx index 39e2e674deb6..a932268764d4 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-as-static-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-as-static-strategy.tsx @@ -66,38 +66,7 @@ export function baseReparentAsStaticStrategy( return { ...getDescriptivePropertiesOfReparentToStaticStrategy(targetLayout), - controlsToRender: [ - controlWithProps({ - control: ParentOutlines, - props: { targetParent: reparentTarget.newParent.intendedParentPath }, - key: 'parent-outlines-control', - show: 'visible-only-while-active', - }), - controlWithProps({ - control: ParentBounds, - props: { targetParent: reparentTarget.newParent.intendedParentPath }, - key: 'parent-bounds-control', - show: 'visible-only-while-active', - }), - controlWithProps({ - control: FlexReparentTargetIndicator, - props: {}, - key: 'flex-reparent-target-indicator', - show: 'visible-only-while-active', - }), - controlWithProps({ - control: ZeroSizedElementControls, - props: { showAllPossibleElements: true }, - key: 'zero-size-control', - show: 'visible-only-while-active', - }), - controlWithProps({ - control: StaticReparentTargetOutlineIndicator, - props: {}, - key: 'parent-outline-highlight', - show: 'visible-only-while-active', - }), - ], + controlsToRender: controlsForStaticReparent(reparentTarget), fitness: shouldKeepMovingDraggedGroupChildren( canvasState.startingMetadata, selectedElements, @@ -117,6 +86,41 @@ export function baseReparentAsStaticStrategy( } } +export function controlsForStaticReparent(reparentTarget: ReparentTarget) { + return [ + controlWithProps({ + control: ParentOutlines, + props: { targetParent: reparentTarget.newParent.intendedParentPath }, + key: 'parent-outlines-control', + show: 'visible-only-while-active', + }), + controlWithProps({ + control: ParentBounds, + props: { targetParent: reparentTarget.newParent.intendedParentPath }, + key: 'parent-bounds-control', + show: 'visible-only-while-active', + }), + controlWithProps({ + control: FlexReparentTargetIndicator, + props: {}, + key: 'flex-reparent-target-indicator', + show: 'visible-only-while-active', + }), + controlWithProps({ + control: ZeroSizedElementControls, + props: { showAllPossibleElements: true }, + key: 'zero-size-control', + show: 'visible-only-while-active', + }), + controlWithProps({ + control: StaticReparentTargetOutlineIndicator, + props: {}, + key: 'parent-outline-highlight', + show: 'visible-only-while-active', + }), + ] +} + function getDescriptivePropertiesOfReparentToStaticStrategy( targetLayout: 'flex' | 'flow', ): Pick { @@ -146,7 +150,7 @@ function getDescriptivePropertiesOfReparentToStaticStrategy( } } -function applyStaticReparent( +export function applyStaticReparent( canvasState: InteractionCanvasState, interactionSession: InteractionSession, customStrategyState: CustomStrategyState, diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index 0c93131c230c..9feca16c3c13 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -486,6 +486,7 @@ export const GridControls = controlForStrategyMemoized(({ tar store.editor.canvas.interactionSession != null && store.editor.canvas.interactionSession.activeControl.type === 'GRID_CELL_HANDLE' && store.editor.canvas.interactionSession?.interactionData.type === 'DRAG' && + store.editor.canvas.interactionSession?.interactionData.modifiers.cmd !== true && store.editor.canvas.interactionSession?.interactionData.drag != null ? store.editor.canvas.interactionSession.activeControl.id : null,