-
Notifications
You must be signed in to change notification settings - Fork 172
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
**Problem:** We should be able to drag+alt to duplicate a grid element while moving it inside the grid. **Fix:** - Add a new `gridRearrangeMoveDuplicateStrategy` strategy - Extract the move logic from the move strategy into a helper, and use it for both the regular move strategy and the new duplication one - Tweak the logic to target a cell under the mouse so it can work recursively for when the duplication is happening https://github.com/concrete-utopia/utopia/assets/1081051/843a02fe-9935-45a0-80b1-80e2474e7bfe
- Loading branch information
Showing
5 changed files
with
297 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
162 changes: 146 additions & 16 deletions
162
editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,154 @@ | ||
import type { WindowPoint } from '../../../../core/shared/math-utils' | ||
import type { ElementPath } from 'utopia-shared/src/types' | ||
import { MetadataUtils } from '../../../../core/model/element-metadata-utils' | ||
import type { | ||
ElementInstanceMetadataMap, | ||
GridElementProperties, | ||
} from '../../../../core/shared/element-template' | ||
import type { CanvasVector } from '../../../../core/shared/math-utils' | ||
import { | ||
offsetPoint, | ||
rectContainsPoint, | ||
windowRectangle, | ||
type WindowPoint, | ||
} from '../../../../core/shared/math-utils' | ||
import { create } from '../../../../core/shared/property-path' | ||
import type { CanvasCommand } from '../../commands/commands' | ||
import { setProperty } from '../../commands/set-property-command' | ||
import type { GridCellCoordinates } from '../../controls/grid-controls' | ||
import { gridCellCoordinates } from '../../controls/grid-controls' | ||
import { canvasPointToWindowPoint } from '../../dom-lookup' | ||
import type { DragInteractionData } from '../interaction-state' | ||
|
||
export function getGridCellUnderMouse( | ||
export function getGridCellUnderMouse(mousePoint: WindowPoint) { | ||
return getGridCellAtPoint(mousePoint, false) | ||
} | ||
|
||
function getGridCellUnderMouseRecursive(mousePoint: WindowPoint) { | ||
return getGridCellAtPoint(mousePoint, true) | ||
} | ||
|
||
function getGridCellAtPoint( | ||
windowPoint: WindowPoint, | ||
duplicating: boolean, | ||
): { id: string; coordinates: GridCellCoordinates } | null { | ||
const cellsUnderMouse = document | ||
.elementsFromPoint(windowPoint.x, windowPoint.y) | ||
.filter((el) => el.id.startsWith(`gridcell-`)) | ||
if (cellsUnderMouse.length > 0) { | ||
const cellUnderMouse = cellsUnderMouse[0] | ||
const row = cellUnderMouse.getAttribute('data-grid-row') | ||
const column = cellUnderMouse.getAttribute('data-grid-column') | ||
return { | ||
id: cellsUnderMouse[0].id, | ||
coordinates: gridCellCoordinates( | ||
row == null ? 0 : parseInt(row), | ||
column == null ? 0 : parseInt(column), | ||
), | ||
function maybeRecursivelyFindCellAtPoint(elements: Element[]): Element | null { | ||
// If this used during duplication, the canvas controls will be in the way and we need to traverse the children too. | ||
for (const element of elements) { | ||
if (element.id.startsWith('gridcell-')) { | ||
const rect = element.getBoundingClientRect() | ||
if (rectContainsPoint(windowRectangle(rect), windowPoint)) { | ||
return element | ||
} | ||
} | ||
|
||
if (duplicating) { | ||
const child = maybeRecursivelyFindCellAtPoint(Array.from(element.children)) | ||
if (child != null) { | ||
return child | ||
} | ||
} | ||
} | ||
|
||
return null | ||
} | ||
|
||
const cellUnderMouse = maybeRecursivelyFindCellAtPoint( | ||
document.elementsFromPoint(windowPoint.x, windowPoint.y), | ||
) | ||
if (cellUnderMouse == null) { | ||
return null | ||
} | ||
|
||
const row = cellUnderMouse.getAttribute('data-grid-row') | ||
const column = cellUnderMouse.getAttribute('data-grid-column') | ||
return { | ||
id: cellUnderMouse.id, | ||
coordinates: gridCellCoordinates( | ||
row == null ? 0 : parseInt(row), | ||
column == null ? 0 : parseInt(column), | ||
), | ||
} | ||
} | ||
|
||
export function runGridRearrangeMove( | ||
targetElement: ElementPath, | ||
selectedElement: ElementPath, | ||
jsxMetadata: ElementInstanceMetadataMap, | ||
interactionData: DragInteractionData, | ||
canvasScale: number, | ||
canvasOffset: CanvasVector, | ||
targetGridCell: GridCellCoordinates | null, | ||
duplicating: boolean, | ||
): { commands: CanvasCommand[]; targetGridCell: GridCellCoordinates | null } { | ||
let commands: CanvasCommand[] = [] | ||
|
||
if (interactionData.drag == null) { | ||
return { commands: [], targetGridCell: null } | ||
} | ||
|
||
const mouseWindowPoint = canvasPointToWindowPoint( | ||
offsetPoint(interactionData.dragStart, interactionData.drag), | ||
canvasScale, | ||
canvasOffset, | ||
) | ||
|
||
let newTargetGridCell = targetGridCell ?? null | ||
const cellUnderMouse = duplicating | ||
? getGridCellUnderMouseRecursive(mouseWindowPoint) | ||
: getGridCellUnderMouse(mouseWindowPoint) | ||
if (cellUnderMouse != null) { | ||
newTargetGridCell = cellUnderMouse.coordinates | ||
} | ||
|
||
if (newTargetGridCell == null || newTargetGridCell.row < 1 || newTargetGridCell.column < 1) { | ||
return { commands: [], targetGridCell: null } | ||
} | ||
|
||
const originalElementMetadata = MetadataUtils.findElementByElementPath( | ||
jsxMetadata, | ||
selectedElement, | ||
) | ||
if (originalElementMetadata == null) { | ||
return { commands: [], targetGridCell: null } | ||
} | ||
|
||
function getGridProperty(field: keyof GridElementProperties, fallback: number) { | ||
const propValue = originalElementMetadata?.specialSizeMeasurements.elementGridProperties[field] | ||
return propValue == null || propValue === 'auto' ? 0 : propValue.numericalPosition ?? fallback | ||
} | ||
|
||
const gridColumnStart = getGridProperty('gridColumnStart', 0) | ||
const gridColumnEnd = getGridProperty('gridColumnEnd', 1) | ||
const gridRowStart = getGridProperty('gridRowStart', 0) | ||
const gridRowEnd = getGridProperty('gridRowEnd', 1) | ||
|
||
commands.push( | ||
setProperty( | ||
'always', | ||
targetElement, | ||
create('style', 'gridColumnStart'), | ||
newTargetGridCell.column, | ||
), | ||
setProperty( | ||
'always', | ||
targetElement, | ||
create('style', 'gridColumnEnd'), | ||
Math.max( | ||
newTargetGridCell.column, | ||
newTargetGridCell.column + (gridColumnEnd - gridColumnStart), | ||
), | ||
), | ||
setProperty('always', targetElement, create('style', 'gridRowStart'), newTargetGridCell.row), | ||
setProperty( | ||
'always', | ||
targetElement, | ||
create('style', 'gridRowEnd'), | ||
Math.max(newTargetGridCell.row, newTargetGridCell.row + (gridRowEnd - gridRowStart)), | ||
), | ||
) | ||
|
||
return { | ||
commands: commands, | ||
targetGridCell: newTargetGridCell, | ||
} | ||
return null | ||
} |
118 changes: 118 additions & 0 deletions
118
.../components/canvas/canvas-strategies/strategies/grid-rearrange-move-duplicate-strategy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
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 { CSSCursor } from '../../../../uuiui-deps' | ||
import { duplicateElement } from '../../commands/duplicate-element-command' | ||
import { setCursorCommand } from '../../commands/set-cursor-command' | ||
import { setElementsToRerenderCommand } from '../../commands/set-elements-to-rerender-command' | ||
import { updateHighlightedViews } from '../../commands/update-highlighted-views-command' | ||
import { updateSelectedViews } from '../../commands/update-selected-views-command' | ||
import { GridControls } from '../../controls/grid-controls' | ||
import type { CanvasStrategyFactory } from '../canvas-strategies' | ||
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' | ||
import type { CustomStrategyState, InteractionCanvasState } from '../canvas-strategy-types' | ||
import { | ||
getTargetPathsFromInteractionTarget, | ||
emptyStrategyApplicationResult, | ||
strategyApplicationResult, | ||
} from '../canvas-strategy-types' | ||
import type { InteractionSession } from '../interaction-state' | ||
import { runGridRearrangeMove } from './grid-helpers' | ||
|
||
export const gridRearrangeMoveDuplicateStrategy: CanvasStrategyFactory = ( | ||
canvasState: InteractionCanvasState, | ||
interactionSession: InteractionSession | null, | ||
customState: CustomStrategyState, | ||
) => { | ||
const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) | ||
if ( | ||
selectedElements.length === 0 || | ||
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] | ||
const ok = MetadataUtils.isGridLayoutedContainer( | ||
MetadataUtils.findElementByElementPath( | ||
canvasState.startingMetadata, | ||
EP.parentPath(selectedElement), | ||
), | ||
) | ||
if (!ok) { | ||
return null | ||
} | ||
|
||
return { | ||
id: 'rearrange-grid-move-duplicate-strategy', | ||
name: 'Rearrange Grid (Duplicate)', | ||
descriptiveLabel: 'Rearrange Grid (Duplicate)', | ||
icon: { | ||
category: 'tools', | ||
type: 'pointer', | ||
}, | ||
controlsToRender: [ | ||
{ | ||
control: GridControls, | ||
props: {}, | ||
key: `grid-controls-${EP.toString(selectedElement)}`, | ||
show: 'always-visible', | ||
}, | ||
], | ||
fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 3), | ||
apply: () => { | ||
if ( | ||
interactionSession == null || | ||
interactionSession.interactionData.type !== 'DRAG' || | ||
interactionSession.interactionData.drag == null || | ||
interactionSession.activeControl.type !== 'GRID_CELL_HANDLE' | ||
) { | ||
return emptyStrategyApplicationResult | ||
} | ||
|
||
const oldUid = EP.toUid(selectedElement) | ||
|
||
let duplicatedElementNewUids = { ...customState.duplicatedElementNewUids } | ||
let newUid = duplicatedElementNewUids[oldUid] | ||
if (newUid == null) { | ||
newUid = 'dup-' + generateUidWithExistingComponents(canvasState.projectContents) | ||
duplicatedElementNewUids[oldUid] = newUid | ||
} | ||
|
||
const targetElement = EP.appendToPath(EP.parentPath(selectedElement), newUid) | ||
|
||
const { commands: moveCommands, targetGridCell: newTargetGridCell } = runGridRearrangeMove( | ||
targetElement, | ||
selectedElement, | ||
canvasState.startingMetadata, | ||
interactionSession.interactionData, | ||
canvasState.scale, | ||
canvasState.canvasOffset, | ||
customState.targetGridCell, | ||
true, | ||
) | ||
if (moveCommands.length === 0) { | ||
return emptyStrategyApplicationResult | ||
} | ||
|
||
return strategyApplicationResult( | ||
[ | ||
duplicateElement('always', selectedElement, newUid), | ||
...moveCommands, | ||
setElementsToRerenderCommand([...selectedElements, targetElement]), | ||
updateSelectedViews('always', [targetElement]), | ||
updateHighlightedViews('always', [targetElement]), | ||
setCursorCommand(CSSCursor.Duplicate), | ||
], | ||
{ | ||
targetGridCell: newTargetGridCell, | ||
duplicatedElementNewUids: duplicatedElementNewUids, | ||
}, | ||
) | ||
}, | ||
} | ||
} |
Oops, something went wrong.