From 526e2a97d9969175fc53a4a051bfcab1d17e69c0 Mon Sep 17 00:00:00 2001 From: Federico Ruggi Date: Wed, 26 Jun 2024 14:29:34 -0400 Subject: [PATCH 01/29] wip wip hack days wip --- .../canvas-strategies/canvas-strategies.tsx | 2 + .../canvas-strategies/interaction-state.ts | 13 ++ .../strategies/rearrangeGridStrategy.ts | 98 ++++++++++ .../strategies/set-flex-gap-strategy.tsx | 8 +- .../canvas/controls/grid-controls.tsx | 172 ++++++++++++++++++ .../canvas/controls/new-canvas-controls.tsx | 1 + editor/src/core/shared/uid-utils.ts | 4 +- 7 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts create mode 100644 editor/src/components/canvas/controls/grid-controls.tsx diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index e6fe7a6c43b0..34c4085a9f3d 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -63,6 +63,7 @@ import type { InsertionSubject, InsertionSubjectWrapper } from '../../editor/edi import { generateUidWithExistingComponents } from '../../../core/model/element-template-utils' import { retargetStrategyToChildrenOfFragmentLikeElements } from './strategies/fragment-like-helpers' import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import { rearrangeGridStrategy } from './strategies/rearrangeGridStrategy' export type CanvasStrategyFactory = ( canvasState: InteractionCanvasState, @@ -90,6 +91,7 @@ const moveOrReorderStrategies: MetaCanvasStrategy = ( convertToAbsoluteAndMoveStrategy, convertToAbsoluteAndMoveAndSetParentFixedStrategy, reorderSliderStategy, + rearrangeGridStrategy, ], ) } diff --git a/editor/src/components/canvas/canvas-strategies/interaction-state.ts b/editor/src/components/canvas/canvas-strategies/interaction-state.ts index eb28af93f218..d4c943c74d4b 100644 --- a/editor/src/components/canvas/canvas-strategies/interaction-state.ts +++ b/editor/src/components/canvas/canvas-strategies/interaction-state.ts @@ -559,6 +559,18 @@ export function flexGapHandle(): FlexGapHandle { } } +export interface GridCellHandle { + type: 'GRID_CELL_HANDLE' + id: string +} + +export function gridCellHandle(id: string): GridCellHandle { + return { + type: 'GRID_CELL_HANDLE', + id: id, + } +} + export interface PaddingResizeHandle { type: 'PADDING_RESIZE_HANDLE' edgePiece: EdgePiece @@ -610,6 +622,7 @@ export type CanvasControlType = | KeyboardCatcherControl | ReorderSlider | BorderRadiusResizeHandle + | GridCellHandle export function isDragToPan( interaction: InteractionSession | null, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts new file mode 100644 index 000000000000..8c225ec72a77 --- /dev/null +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts @@ -0,0 +1,98 @@ +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import * as EP from '../../../../core/shared/element-path' +import { zeroRectIfNullOrInfinity } from '../../../../core/shared/math-utils' +import { printCSSNumber } from '../../../inspector/common/css-utils' +import { deleteProperties } from '../../commands/delete-properties-command' +import { setActiveFrames, activeFrameTargetPath } from '../../commands/set-active-frames-command' +import { setCursorCommand } from '../../commands/set-cursor-command' +import { setElementsToRerenderCommand } from '../../commands/set-elements-to-rerender-command' +import { setProperty } from '../../commands/set-property-command' +import { GridControls } from '../../controls/grid-controls' +import { recurseIntoChildrenOfMapOrFragment, cursorFromFlexDirection } from '../../gap-utils' +import type { CanvasStrategyFactory } from '../canvas-strategies' +import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' +import type { InteractionCanvasState } from '../canvas-strategy-types' +import { + getTargetPathsFromInteractionTarget, + emptyStrategyApplicationResult, + strategyApplicationResult, + controlForStrategyMemoized, +} from '../canvas-strategy-types' +import type { InteractionSession } from '../interaction-state' +import { SetFlexGapStrategyId } from './set-flex-gap-strategy' + +export const rearrangeGridStrategy: CanvasStrategyFactory = ( + canvasState: InteractionCanvasState, + interactionSession: InteractionSession | null, +) => { + const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) + if (selectedElements.length !== 1) { + return null + } + + const selectedElement = selectedElements[0] + if ( + !MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + EP.parentPath(selectedElement), + ), + ) + ) { + return null + } + + const children = recurseIntoChildrenOfMapOrFragment( + canvasState.startingMetadata, + canvasState.startingAllElementProps, + canvasState.startingElementPathTree, + selectedElement, + ) + + return { + id: SetFlexGapStrategyId, + name: 'Set flex gap', + descriptiveLabel: 'Changing Flex Gap', + icon: { + category: 'tools', + type: 'pointer', + }, + controlsToRender: [GridControls], + fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 1), + apply: () => { + if ( + interactionSession == null || + interactionSession.interactionData.type !== 'DRAG' || + interactionSession.activeControl.type !== 'GRID_CELL_HANDLE' + ) { + return emptyStrategyApplicationResult + } + + if (shouldTearOffGap) { + return strategyApplicationResult([ + deleteProperties('always', selectedElement, [StyleGapProp]), + ]) + } + + return strategyApplicationResult([ + setProperty( + 'always', + selectedElement, + StyleGapProp, + printCSSNumber(updatedFlexGapMeasurement.value, null), + ), + setCursorCommand(cursorFromFlexDirection(flexGap.direction)), + setElementsToRerenderCommand([...selectedElements, ...children.map((c) => c.elementPath)]), + setActiveFrames([ + { + action: 'set-gap', + target: activeFrameTargetPath(selectedElement), + source: zeroRectIfNullOrInfinity( + MetadataUtils.getFrameInCanvasCoords(selectedElement, canvasState.startingMetadata), + ), + }, + ]), + ]) + }, + } +} diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx index d7d87a7a0ed7..bb162b560a8a 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx @@ -199,7 +199,7 @@ export const setFlexGapStrategy: CanvasStrategyFactory = ( } } -function dragFromInteractionSession( +export function dragFromInteractionSession( interactionSession: InteractionSession | null, ): CanvasVector | null { if (interactionSession != null && interactionSession.interactionData.type === 'DRAG') { @@ -208,7 +208,7 @@ function dragFromInteractionSession( return null } -function modifiersFromInteractionSession( +export function modifiersFromInteractionSession( interactionSession: InteractionSession | null, ): Modifiers | null { if (interactionSession != null && interactionSession.interactionData.type === 'DRAG') { @@ -217,7 +217,7 @@ function modifiersFromInteractionSession( return null } -function isDragOverThreshold( +export function isDragOverThreshold( direction: FlexDirection, { gapPx, deltaPx }: { gapPx: number; deltaPx: number }, ): boolean { @@ -277,7 +277,7 @@ function flexGapValueIndicatorProps( } } -function isDragOngoing(interactionSession: InteractionSession | null): boolean { +export function isDragOngoing(interactionSession: InteractionSession | null): boolean { return ( interactionSession != null && interactionSession.activeControl.type === 'FLEX_GAP_HANDLE' && diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx new file mode 100644 index 000000000000..eaec786fc49c --- /dev/null +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -0,0 +1,172 @@ +import React from 'react' +import { CanvasOffsetWrapper } from './canvas-offset-wrapper' +import { Substores, useEditorState } from '../../editor/store/store-hook' +import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import * as EP from '../../../core/shared/element-path' +import { mapDropNulls } from '../../../core/shared/array-utils' +import { isFiniteRectangle } from '../../../core/shared/math-utils' +import { isRight } from '../../../core/shared/either' +import { isJSXElement } from '../../../core/shared/element-template' +import { controlForStrategyMemoized } from '../canvas-strategies/canvas-strategy-types' + +export const GridControls = controlForStrategyMemoized(() => { + const selectedViews = useEditorState( + Substores.selectedViews, + (store) => store.editor.selectedViews, + '', + ) + const grids = useEditorState( + Substores.metadataAndPropertyControlsInfo, + (store) => { + return mapDropNulls((view) => { + const element = MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, view) + const parent = MetadataUtils.findElementByElementPath( + store.editor.jsxMetadata, + EP.parentPath(view), + ) + + const target = MetadataUtils.isGridLayoutedContainer(element) + ? element + : MetadataUtils.isGridLayoutedContainer(parent) + ? parent + : null + + if ( + target == null || + target.globalFrame == null || + !isFiniteRectangle(target.globalFrame) + ) { + return null + } + + let rows = 0 + let columns = 0 + let gridTemplateColumns: any = undefined + let gridTemplateRows: any = undefined + let gap: any = 0 + let padding: any = 0 + // TODO well this whole logic piece is garbage, we should have these in the SpecialSizeMeasurements + if (isRight(target.element) && isJSXElement(target.element.value)) { + for (const prop of target.element.value.props) { + if ( + prop.type === 'JSX_ATTRIBUTES_ENTRY' && + prop.key === 'style' && + prop.value.type === 'ATTRIBUTE_NESTED_OBJECT' + ) { + for (const entry of prop.value.content) { + if ( + entry.type === 'PROPERTY_ASSIGNMENT' && + entry.value.type === 'ATTRIBUTE_VALUE' + ) { + if (typeof entry.value.value === 'string') { + // TODO do something about it… + const count = entry.value.value.trim().split(/\s+/).length + if (entry.key === 'gridTemplateColumns') { + columns = count + gridTemplateColumns = entry.value.value + } + if (entry.key === 'gridTemplateRows') { + rows = count + gridTemplateRows = entry.value.value + } + } + if (entry.key === 'gap' || entry.key === 'gridGap') { + gap = entry.value.value + } + if (entry.key === 'padding') { + padding = entry.value.value + } + } + } + } + } + } + return { + elementPath: target.elementPath, + frame: target.globalFrame, + gridTemplateColumns: gridTemplateColumns, + gridTemplateRows: gridTemplateRows, + gap: gap, + padding: padding, + cells: rows * columns, + } + }, store.editor.selectedViews) + }, + 'GridControls selectedGrids', + ) + + const jsxMetadata = useEditorState(Substores.metadata, (store) => store.editor.jsxMetadata, '') + + const cells = React.useMemo(() => { + return grids.flatMap((grid) => { + const children = MetadataUtils.getChildrenUnordered(jsxMetadata, grid.elementPath) + return mapDropNulls((cell) => { + if (cell == null || cell.globalFrame == null || !isFiniteRectangle(cell.globalFrame)) { + return null + } + return { elementPath: cell.elementPath, globalFrame: cell.globalFrame } + }, children) + }) + }, [grids, jsxMetadata]) + + if (grids.length === 0) { + return null + } + + return ( + + {/* grid lines */} + {grids.map((grid, index) => { + const placeholders = Array.from(Array(grid.cells).keys()) + return ( +
+ {placeholders.map((cell) => { + return ( +
+ ) + })} +
+ ) + })} + {/* cell targets */} + {cells.map((cell) => { + const isSelected = selectedViews.some((view) => EP.pathsEqual(cell.elementPath, view)) + return ( +
+ ) + })} + + ) +}) diff --git a/editor/src/components/canvas/controls/new-canvas-controls.tsx b/editor/src/components/canvas/controls/new-canvas-controls.tsx index 016d35e98cbd..dcaa1eb144ab 100644 --- a/editor/src/components/canvas/controls/new-canvas-controls.tsx +++ b/editor/src/components/canvas/controls/new-canvas-controls.tsx @@ -75,6 +75,7 @@ import { useStatus } from '../../../../liveblocks.config' import { MultiplayerWrapper } from '../../../utils/multiplayer-wrapper' import { MultiplayerPresence } from '../multiplayer-presence' import { isFeatureEnabled } from '../../../utils/feature-switches' +import { GridControls } from './grid-controls' export const CanvasControlsContainerID = 'new-canvas-controls-container' diff --git a/editor/src/core/shared/uid-utils.ts b/editor/src/core/shared/uid-utils.ts index f300f073e298..c5b276aa30ee 100644 --- a/editor/src/core/shared/uid-utils.ts +++ b/editor/src/core/shared/uid-utils.ts @@ -68,7 +68,7 @@ import { jsxSimpleAttributeToValue, setJSXValueAtPath, } from './jsx-attribute-utils' -import { IS_TEST_ENVIRONMENT } from '../../common/env-vars' +import { IS_TEST_ENVIRONMENT, PRODUCTION_ENV } from '../../common/env-vars' export const MOCK_NEXT_GENERATED_UIDS: { current: Array } = { current: [] } export const MOCK_NEXT_GENERATED_UIDS_IDX = { current: 0 } @@ -814,7 +814,7 @@ export function getUtopiaID(element: JSXElementChild | ElementInstanceMetadata): } // The length of element UIDs generated by the editor. -const UID_LENGTH = IS_TEST_ENVIRONMENT ? 3 : 32 // in characters +const UID_LENGTH = IS_TEST_ENVIRONMENT ? 3 : PRODUCTION_ENV ? 32 : 4 // in characters /** * Generate a new UID suitable for elements. From a68864e5baa1d0025e8c1120373b0eb2891bc25b Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Wed, 26 Jun 2024 20:33:49 +0200 Subject: [PATCH 02/29] strategy boilerplate --- .../strategies/rearrangeGridStrategy.ts | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts index 8c225ec72a77..a9debac1f20b 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts @@ -57,7 +57,14 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( category: 'tools', type: 'pointer', }, - controlsToRender: [GridControls], + controlsToRender: [ + { + control: GridControls, + props: {}, + key: `grid-controls-${EP.toString(selectedElement)}`, + show: 'always-visible', + }, + ], fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 1), apply: () => { if ( @@ -68,31 +75,8 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( return emptyStrategyApplicationResult } - if (shouldTearOffGap) { - return strategyApplicationResult([ - deleteProperties('always', selectedElement, [StyleGapProp]), - ]) - } - - return strategyApplicationResult([ - setProperty( - 'always', - selectedElement, - StyleGapProp, - printCSSNumber(updatedFlexGapMeasurement.value, null), - ), - setCursorCommand(cursorFromFlexDirection(flexGap.direction)), - setElementsToRerenderCommand([...selectedElements, ...children.map((c) => c.elementPath)]), - setActiveFrames([ - { - action: 'set-gap', - target: activeFrameTargetPath(selectedElement), - source: zeroRectIfNullOrInfinity( - MetadataUtils.getFrameInCanvasCoords(selectedElement, canvasState.startingMetadata), - ), - }, - ]), - ]) + // TODO + return emptyStrategyApplicationResult }, } } From 194e60451f436ddaabe5f7fcde49ed490635a450 Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Wed, 26 Jun 2024 14:51:21 -0400 Subject: [PATCH 03/29] spike(editor) Added some parsing for grid special measurements. --- editor/src/components/canvas/dom-walker.ts | 22 +++++ .../store-deep-equality-instances-3.spec.ts | 84 ++++++++++++++++++ .../store/store-deep-equality-instances.ts | 84 +++++++++++++++++- .../components/inspector/common/css-utils.ts | 16 ++++ editor/src/core/shared/element-template.ts | 85 +++++++++++++++++++ 5 files changed, 290 insertions(+), 1 deletion(-) diff --git a/editor/src/components/canvas/dom-walker.ts b/editor/src/components/canvas/dom-walker.ts index ff9ec1bd90cb..061551aa4c13 100644 --- a/editor/src/components/canvas/dom-walker.ts +++ b/editor/src/components/canvas/dom-walker.ts @@ -9,6 +9,8 @@ import type { SpecialSizeMeasurements, StyleAttributeMetadata, ElementInstanceMetadataMap, + GridContainerProperties, + GridElementProperties, } from '../../core/shared/element-template' import { elementInstanceMetadata, @@ -16,6 +18,8 @@ import { emptySpecialSizeMeasurements, emptyComputedStyle, emptyAttributeMetadata, + gridContainerProperties, + gridElementProperties, } from '../../core/shared/element-template' import type { ElementPath } from '../../core/shared/project-file-types' import { @@ -55,6 +59,7 @@ import { parseDirection, parseFlexDirection, parseCSSPx, + parseGridPosition, } from '../inspector/common/css-utils' import { camelCaseToDashed } from '../../core/shared/string-utils' import type { UtopiaStoreAPI } from '../editor/store/store-hook' @@ -881,6 +886,18 @@ function getComputedStyle( } } +function getGridContainerProperties(_elementStyle: CSSStyleDeclaration): GridContainerProperties { + return gridContainerProperties(null, null, null, null) +} + +function getGridElementProperties(elementStyle: CSSStyleDeclaration): GridElementProperties { + const gridColumnStart = defaultEither(null, parseGridPosition(elementStyle.gridColumnStart)) + const gridColumnEnd = defaultEither(null, parseGridPosition(elementStyle.gridColumnEnd)) + const gridRowStart = defaultEither(null, parseGridPosition(elementStyle.gridRowStart)) + const gridRowEnd = defaultEither(null, parseGridPosition(elementStyle.gridRowEnd)) + return gridElementProperties(gridColumnStart, gridColumnEnd, gridRowStart, gridRowEnd) +} + function getSpecialMeasurements( element: HTMLElement, closestOffsetParentPath: ElementPath, @@ -1074,6 +1091,9 @@ function getSpecialMeasurements( globalFrame, ) + const containerGridProperties = getGridContainerProperties(elementStyle) + const containerElementProperties = getGridElementProperties(elementStyle) + return specialSizeMeasurements( offset, coordinateSystemBounds, @@ -1118,6 +1138,8 @@ function getSpecialMeasurements( textDecorationLine, textBounds, computedHugProperty, + containerGridProperties, + containerElementProperties, ) } diff --git a/editor/src/components/editor/store/store-deep-equality-instances-3.spec.ts b/editor/src/components/editor/store/store-deep-equality-instances-3.spec.ts index 6525041dbf94..007cac1c922b 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances-3.spec.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances-3.spec.ts @@ -293,6 +293,18 @@ describe('SpecialSizeMeasurementsKeepDeepEquality', () => { width: null, height: null, }, + containerGridProperties: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridProperties: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, } const newDifferentValue: SpecialSizeMeasurements = { @@ -383,6 +395,18 @@ describe('SpecialSizeMeasurementsKeepDeepEquality', () => { width: null, height: null, }, + containerGridProperties: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridProperties: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, } it('same reference returns the same reference', () => { @@ -534,6 +558,18 @@ describe('ElementInstanceMetadataKeepDeepEquality', () => { width: null, height: null, }, + containerGridProperties: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridProperties: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', @@ -662,6 +698,18 @@ describe('ElementInstanceMetadataKeepDeepEquality', () => { width: null, height: null, }, + containerGridProperties: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridProperties: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', @@ -816,6 +864,18 @@ describe('ElementInstanceMetadataMapKeepDeepEquality', () => { width: null, height: null, }, + containerGridProperties: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridProperties: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', @@ -946,6 +1006,18 @@ describe('ElementInstanceMetadataMapKeepDeepEquality', () => { width: null, height: null, }, + containerGridProperties: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridProperties: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', @@ -1076,6 +1148,18 @@ describe('ElementInstanceMetadataMapKeepDeepEquality', () => { width: null, height: null, }, + containerGridProperties: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridProperties: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index f6720c2a230c..cf699f8be0d1 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -143,6 +143,12 @@ import type { JSOpaqueArbitraryStatement, JSAssignmentStatement, JSAssignment, + GridContainerProperties, + GridTemplate, + GridAuto, + GridElementProperties, + GridPositionValue, + GridPosition, } from '../../../core/shared/element-template' import { elementInstanceMetadata, @@ -214,6 +220,9 @@ import { jsAssignmentStatement, jsAssignment, jsxMapExpression, + gridContainerProperties, + gridElementProperties, + gridPositionValue, } from '../../../core/shared/element-template' import type { CanvasRectangle, @@ -1936,6 +1945,66 @@ export const ImportInfoKeepDeepEquality: KeepDeepEqualityCall = ( return keepDeepEqualityResult(newValue, false) } +export const GridTemplateKeepDeepEquality: KeepDeepEqualityCall = + createCallWithTripleEquals() + +export const GridAutoKeepDeepEquality: KeepDeepEqualityCall = + createCallWithTripleEquals() + +export function GridContainerPropertiesKeepDeepEquality(): KeepDeepEqualityCall { + return combine4EqualityCalls( + (properties) => properties.gridTemplateColumns, + GridTemplateKeepDeepEquality, + (properties) => properties.gridTemplateRows, + GridTemplateKeepDeepEquality, + (properties) => properties.gridAutoColumns, + GridAutoKeepDeepEquality, + (properties) => properties.gridAutoRows, + GridAutoKeepDeepEquality, + gridContainerProperties, + ) +} + +export const GridPositionValueKeepDeepEquality: KeepDeepEqualityCall = + combine1EqualityCall( + (value) => value.numericalPosition, + nullableDeepEquality(createCallWithTripleEquals()), + gridPositionValue, + ) + +export const GridPositionKeepDeepEquality: KeepDeepEqualityCall = ( + oldValue, + newValue, +) => { + if (typeof oldValue === 'string') { + if (typeof newValue === 'string') { + return createCallWithTripleEquals()(oldValue, newValue) + } else { + return keepDeepEqualityResult(newValue, false) + } + } else { + if (typeof newValue === 'string') { + return keepDeepEqualityResult(newValue, false) + } else { + return GridPositionValueKeepDeepEquality(oldValue, newValue) + } + } +} + +export function GridElementPropertiesKeepDeepEquality(): KeepDeepEqualityCall { + return combine4EqualityCalls( + (properties) => properties.gridColumnStart, + nullableDeepEquality(GridPositionKeepDeepEquality), + (properties) => properties.gridColumnEnd, + nullableDeepEquality(GridPositionKeepDeepEquality), + (properties) => properties.gridRowStart, + nullableDeepEquality(GridPositionKeepDeepEquality), + (properties) => properties.gridRowEnd, + nullableDeepEquality(GridPositionKeepDeepEquality), + gridElementProperties, + ) +} + export function SpecialSizeMeasurementsKeepDeepEquality(): KeepDeepEqualityCall { return (oldSize, newSize) => { const offsetResult = LocalPointKeepDeepEquality(oldSize.offset, newSize.offset) @@ -2012,6 +2081,15 @@ export function SpecialSizeMeasurementsKeepDeepEquality(): KeepDeepEqualityCall< oldSize.computedHugProperty.width === newSize.computedHugProperty.width && oldSize.computedHugProperty.height === newSize.computedHugProperty.height + const gridContainerPropertiesEqual = GridContainerPropertiesKeepDeepEquality()( + oldSize.containerGridProperties, + newSize.containerGridProperties, + ).areEqual + const gridElementPropertiesEqual = GridElementPropertiesKeepDeepEquality()( + oldSize.elementGridProperties, + newSize.elementGridProperties, + ).areEqual + const areEqual = offsetResult.areEqual && coordinateSystemBoundsResult.areEqual && @@ -2053,7 +2131,9 @@ export function SpecialSizeMeasurementsKeepDeepEquality(): KeepDeepEqualityCall< fontStyleEquals && textDecorationLineEquals && textBoundsEqual && - computedHugPropertyEqual + computedHugPropertyEqual && + gridContainerPropertiesEqual && + gridElementPropertiesEqual if (areEqual) { return keepDeepEqualityResult(oldSize, true) } else { @@ -2099,6 +2179,8 @@ export function SpecialSizeMeasurementsKeepDeepEquality(): KeepDeepEqualityCall< newSize.textDecorationLine, newSize.textBounds, newSize.computedHugProperty, + newSize.containerGridProperties, + newSize.elementGridProperties, ) return keepDeepEqualityResult(sizeMeasurements, false) } diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index de5692da4e91..79416f70ab4b 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -16,6 +16,7 @@ import type { LayoutPropertyTypes, StyleLayoutProp } from '../../../core/layout/ import { findLastIndex } from '../../../core/shared/array-utils' import type { Either, Right as EitherRight } from '../../../core/shared/either' import { + alternativeEither, bimapEither, eitherToMaybe, flatMapEither, @@ -31,6 +32,7 @@ import type { JSXAttributes, JSExpressionValue, JSXElement, + GridPosition, } from '../../../core/shared/element-template' import { emptyComments, @@ -40,6 +42,7 @@ import { isRegularJSXAttribute, jsExpressionFunctionCall, jsExpressionValue, + gridPositionValue, } from '../../../core/shared/element-template' import type { ModifiableAttribute } from '../../../core/shared/jsx-attributes' import { @@ -797,6 +800,19 @@ export const parseCSSNumber = ( } } +export function parseGridPosition(input: unknown): Either { + if (input === 'auto') { + return right('auto') + } else if (typeof input === 'string') { + const asNumber = parseNumber(input) + return mapEither(gridPositionValue, asNumber) + } else if (typeof input === 'number') { + return right(gridPositionValue(input)) + } else { + return left('Not a valid grid position.') + } +} + export function parseDisplay(input: unknown): Either { if (typeof input === 'string') { return right(input) diff --git a/editor/src/core/shared/element-template.ts b/editor/src/core/shared/element-template.ts index d41793bfbc15..55cf5ad505c3 100644 --- a/editor/src/core/shared/element-template.ts +++ b/editor/src/core/shared/element-template.ts @@ -2556,6 +2556,73 @@ export function elementInstanceMetadata( export type SettableLayoutSystem = 'flex' | 'flow' | 'grid' | LayoutSystem +export interface GridPositionValue { + numericalPosition: number | null +} + +export function gridPositionValue(numericalPosition: number | null): GridPositionValue { + return { + numericalPosition: numericalPosition, + } +} + +export type GridPosition = GridPositionValue | 'auto' + +export type GridColumnStart = GridPosition +export type GridColumnEnd = GridPosition +export type GridRowStart = GridPosition +export type GridRowEnd = GridPosition + +export type GridAuto = null +export type GridTemplate = null + +export type GridTemplateColumns = GridTemplate +export type GridTemplateRows = GridTemplate +export type GridAutoColumns = GridAuto +export type GridAutoRows = GridAuto + +export interface GridContainerProperties { + gridTemplateColumns: GridTemplateColumns | null + gridTemplateRows: GridTemplateRows | null + gridAutoColumns: GridAutoColumns | null + gridAutoRows: GridAutoRows | null +} + +export function gridContainerProperties( + gridTemplateColumns: GridTemplateColumns | null, + gridTemplateRows: GridTemplateRows | null, + gridAutoColumns: GridAutoColumns | null, + gridAutoRows: GridAutoRows | null, +): GridContainerProperties { + return { + gridTemplateColumns: gridTemplateColumns, + gridTemplateRows: gridTemplateRows, + gridAutoColumns: gridAutoColumns, + gridAutoRows: gridAutoRows, + } +} + +export interface GridElementProperties { + gridColumnStart: GridColumnStart | null + gridColumnEnd: GridColumnEnd | null + gridRowStart: GridRowStart | null + gridRowEnd: GridRowEnd | null +} + +export function gridElementProperties( + gridColumnStart: GridColumnStart | null, + gridColumnEnd: GridColumnEnd | null, + gridRowStart: GridRowStart | null, + gridRowEnd: GridRowEnd | null, +): GridElementProperties { + return { + gridColumnStart: gridColumnStart, + gridColumnEnd: gridColumnEnd, + gridRowStart: gridRowStart, + gridRowEnd: gridRowEnd, + } +} + export interface SpecialSizeMeasurements { offset: LocalPoint coordinateSystemBounds: CanvasRectangle | null @@ -2598,6 +2665,8 @@ export interface SpecialSizeMeasurements { fontStyle: string | null textDecorationLine: string | null computedHugProperty: HugPropertyWidthHeight + containerGridProperties: GridContainerProperties + elementGridProperties: GridElementProperties } export function specialSizeMeasurements( @@ -2642,6 +2711,8 @@ export function specialSizeMeasurements( textDecorationLine: string | null, textBounds: CanvasRectangle | null, computedHugProperty: HugPropertyWidthHeight, + containerGridProperties: GridContainerProperties, + elementGridProperties: GridElementProperties, ): SpecialSizeMeasurements { return { offset, @@ -2685,6 +2756,8 @@ export function specialSizeMeasurements( fontStyle, textDecorationLine, computedHugProperty, + containerGridProperties, + elementGridProperties, } } @@ -2733,6 +2806,18 @@ export const emptySpecialSizeMeasurements = specialSizeMeasurements( null, null, { width: null, height: null }, + { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, ) export function walkElement( From 253fe62855c0d1cc7632540fd2d140f57042ad8d Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Wed, 26 Jun 2024 15:18:45 -0400 Subject: [PATCH 04/29] spike(editor) Now simply parse the container values. --- editor/src/components/canvas/dom-walker.ts | 14 ++++++++++-- .../store/store-deep-equality-instances.ts | 22 ++++++++++++++----- editor/src/core/shared/element-template.ts | 4 ++-- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/editor/src/components/canvas/dom-walker.ts b/editor/src/components/canvas/dom-walker.ts index 061551aa4c13..2f235e40d22b 100644 --- a/editor/src/components/canvas/dom-walker.ts +++ b/editor/src/components/canvas/dom-walker.ts @@ -91,6 +91,7 @@ import { pick } from '../../core/shared/object-utils' import { getFlexAlignment, getFlexJustifyContent, MaxContent } from '../inspector/inspector-common' import type { EditorDispatch } from '../editor/action-types' import { runDOMWalker } from '../editor/actions/action-creators' +import { parseString } from '../../utils/value-parser-utils' export const ResizeObserver = window.ResizeObserver ?? ResizeObserverSyntheticDefault.default ?? ResizeObserverSyntheticDefault @@ -886,8 +887,17 @@ function getComputedStyle( } } -function getGridContainerProperties(_elementStyle: CSSStyleDeclaration): GridContainerProperties { - return gridContainerProperties(null, null, null, null) +function getGridContainerProperties(elementStyle: CSSStyleDeclaration): GridContainerProperties { + const gridTemplateColumns = defaultEither(null, parseString(elementStyle.gridTemplateColumns)) + const gridTemplateRows = defaultEither(null, parseString(elementStyle.gridTemplateRows)) + const gridAutoColumns = defaultEither(null, parseString(elementStyle.gridAutoColumns)) + const gridAutoRows = defaultEither(null, parseString(elementStyle.gridAutoRows)) + return gridContainerProperties( + gridTemplateColumns, + gridTemplateRows, + gridAutoColumns, + gridAutoRows, + ) } function getGridElementProperties(elementStyle: CSSStyleDeclaration): GridElementProperties { diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index cf699f8be0d1..866d486482a5 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -448,6 +448,7 @@ import type { ResizeHandle, BorderRadiusResizeHandle, ZeroDragPermitted, + GridCellHandle, } from '../../canvas/canvas-strategies/interaction-state' import { boundingArea, @@ -456,6 +457,7 @@ import { interactionSession, keyboardCatcherControl, resizeHandle, + gridCellHandle, } from '../../canvas/canvas-strategies/interaction-state' import type { Modifiers } from '../../../utils/modifiers' import type { @@ -1946,21 +1948,21 @@ export const ImportInfoKeepDeepEquality: KeepDeepEqualityCall = ( } export const GridTemplateKeepDeepEquality: KeepDeepEqualityCall = - createCallWithTripleEquals() + createCallWithTripleEquals() export const GridAutoKeepDeepEquality: KeepDeepEqualityCall = - createCallWithTripleEquals() + createCallWithTripleEquals() export function GridContainerPropertiesKeepDeepEquality(): KeepDeepEqualityCall { return combine4EqualityCalls( (properties) => properties.gridTemplateColumns, - GridTemplateKeepDeepEquality, + nullableDeepEquality(GridTemplateKeepDeepEquality), (properties) => properties.gridTemplateRows, - GridTemplateKeepDeepEquality, + nullableDeepEquality(GridTemplateKeepDeepEquality), (properties) => properties.gridAutoColumns, - GridAutoKeepDeepEquality, + nullableDeepEquality(GridAutoKeepDeepEquality), (properties) => properties.gridAutoRows, - GridAutoKeepDeepEquality, + nullableDeepEquality(GridAutoKeepDeepEquality), gridContainerProperties, ) } @@ -2777,6 +2779,9 @@ export const BorderRadiusResizeHandleKeepDeepEquality: KeepDeepEqualityCall< return keepDeepEqualityResult(oldValue, true) } +export const GridCellHandleKeepDeepEquality: KeepDeepEqualityCall = + combine1EqualityCall((handle) => handle.id, createCallWithTripleEquals(), gridCellHandle) + export const CanvasControlTypeKeepDeepEquality: KeepDeepEqualityCall = ( oldValue, newValue, @@ -2817,6 +2822,11 @@ export const CanvasControlTypeKeepDeepEquality: KeepDeepEqualityCall Date: Wed, 26 Jun 2024 21:34:36 +0200 Subject: [PATCH 05/29] wip --- .../absolute-resize-bounding-box-strategy.tsx | 7 +- .../strategies/basic-resize-strategy.tsx | 19 +-- .../keyboard-absolute-resize-strategy.tsx | 8 +- .../strategies/rearrangeGridStrategy.ts | 115 ++++++++++++++---- .../canvas/controls/grid-controls.tsx | 39 +++++- 5 files changed, 151 insertions(+), 37 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx index e5340b153d4e..43f6bb3a71e4 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx @@ -83,7 +83,12 @@ export function absoluteResizeBoundingBoxStrategy( retargetedTargets.length === 0 || !retargetedTargets.every((element) => { return supportsAbsoluteResize(canvasState.startingMetadata, element, canvasState) - }) + }) || + retargetedTargets.some((t) => + MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath(canvasState.startingMetadata, EP.parentPath(t)), + ), + ) ) { return null } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx index 81a3e65479eb..6ea2a94dcd6d 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx @@ -54,6 +54,7 @@ import { pushIntendedBoundsAndUpdateGroups } from '../../commands/push-intended- import { queueTrueUpElement } from '../../commands/queue-true-up-command' import { treatElementAsGroupLike } from './group-helpers' import { trueUpGroupElementChanged } from '../../../editor/store/editor-state' +import { parentPath } from '../../../../core/shared/element-path' export const BASIC_RESIZE_STRATEGY_ID = 'BASIC_RESIZE' @@ -63,7 +64,15 @@ export function basicResizeStrategy( ): CanvasStrategy | null { const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) - if (selectedElements.length !== 1 || !honoursPropsSize(canvasState, selectedElements[0])) { + if ( + selectedElements.length !== 1 || + !honoursPropsSize(canvasState, selectedElements[0]) || + selectedElements.some((t) => + MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath(canvasState.startingMetadata, parentPath(t)), + ), + ) + ) { return null } const metadata = MetadataUtils.findElementByElementPath( @@ -73,14 +82,6 @@ export function basicResizeStrategy( const elementDimensionsProps = metadata != null ? getElementDimensions(metadata) : null const elementParentBounds = metadata?.specialSizeMeasurements.immediateParentBounds ?? null - const elementDimensions = - elementDimensionsProps == null - ? null - : { - width: elementDimensionsProps.width, - height: elementDimensionsProps.height, - } - return { id: BASIC_RESIZE_STRATEGY_ID, name: 'Resize (Basic)', diff --git a/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx index 536ab0cb0f9e..60df531900ae 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx @@ -43,6 +43,7 @@ import * as EP from '../../../../core/shared/element-path' import type { ElementInstanceMetadataMap } from '../../../../core/shared/element-template' import type { AllElementProps } from '../../../editor/store/editor-state' import { getDescriptiveStrategyLabelWithRetargetedPaths } from '../canvas-strategies' +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' interface VectorAndEdge { movement: CanvasVector @@ -127,7 +128,12 @@ export function keyboardAbsoluteResizeStrategy( selectedElements.length === 0 || !selectedElements.every((element) => { return supportsAbsoluteResize(canvasState.startingMetadata, element, canvasState) - }) + }) || + selectedElements.some((t) => + MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath(canvasState.startingMetadata, EP.parentPath(t)), + ), + ) ) { return null } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts index a9debac1f20b..2ee5c0016487 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts @@ -1,14 +1,17 @@ import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import * as EP from '../../../../core/shared/element-path' -import { zeroRectIfNullOrInfinity } from '../../../../core/shared/math-utils' -import { printCSSNumber } from '../../../inspector/common/css-utils' -import { deleteProperties } from '../../commands/delete-properties-command' -import { setActiveFrames, activeFrameTargetPath } from '../../commands/set-active-frames-command' -import { setCursorCommand } from '../../commands/set-cursor-command' -import { setElementsToRerenderCommand } from '../../commands/set-elements-to-rerender-command' -import { setProperty } from '../../commands/set-property-command' +import type { ElementInstanceMetadata } from '../../../../core/shared/element-template' +import { + isFiniteRectangle, + offsetPoint, + rectContainsPointInclusive, +} from '../../../../core/shared/math-utils' +import { optionalMap } from '../../../../core/shared/optional-utils' +import type { ElementPath } from '../../../../core/shared/project-file-types' +import type { CanvasCommand } from '../../commands/commands' +import { rearrangeChildren } from '../../commands/rearrange-children-command' import { GridControls } from '../../controls/grid-controls' -import { recurseIntoChildrenOfMapOrFragment, cursorFromFlexDirection } from '../../gap-utils' +import { recurseIntoChildrenOfMapOrFragment } from '../../gap-utils' import type { CanvasStrategyFactory } from '../canvas-strategies' import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' import type { InteractionCanvasState } from '../canvas-strategy-types' @@ -16,10 +19,8 @@ import { getTargetPathsFromInteractionTarget, emptyStrategyApplicationResult, strategyApplicationResult, - controlForStrategyMemoized, } from '../canvas-strategy-types' import type { InteractionSession } from '../interaction-state' -import { SetFlexGapStrategyId } from './set-flex-gap-strategy' export const rearrangeGridStrategy: CanvasStrategyFactory = ( canvasState: InteractionCanvasState, @@ -31,14 +32,13 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( } const selectedElement = selectedElements[0] - if ( - !MetadataUtils.isGridLayoutedContainer( - MetadataUtils.findElementByElementPath( - canvasState.startingMetadata, - EP.parentPath(selectedElement), - ), - ) - ) { + const ok = MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + EP.parentPath(selectedElement), + ), + ) + if (!ok) { return null } @@ -50,9 +50,9 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( ) return { - id: SetFlexGapStrategyId, - name: 'Set flex gap', - descriptiveLabel: 'Changing Flex Gap', + id: 'rearrange-grid-strategy', + name: 'Rearrange Grid', + descriptiveLabel: 'Rearrange Grid', icon: { category: 'tools', type: 'pointer', @@ -65,18 +65,85 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( show: 'always-visible', }, ], - fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 1), + fitness: 1, // onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 1), apply: () => { if ( interactionSession == null || interactionSession.interactionData.type !== 'DRAG' || + interactionSession.interactionData.drag == null || interactionSession.activeControl.type !== 'GRID_CELL_HANDLE' ) { return emptyStrategyApplicationResult } - // TODO - return emptyStrategyApplicationResult + const pointOnCanvas = offsetPoint( + interactionSession.interactionData.dragStart, + interactionSession.interactionData.drag, + ) + + const pointerOverChild = children.find( + (c) => + c.globalFrame != null && + isFiniteRectangle(c.globalFrame) && + rectContainsPointInclusive(c.globalFrame, pointOnCanvas), + ) + + if ( + pointerOverChild == null || + EP.toUid(pointerOverChild.elementPath) === interactionSession.activeControl.id + ) { + return emptyStrategyApplicationResult + } + + const commands = swapChildrenCommands({ + grabbedElementUid: interactionSession.activeControl.id, + swapToElementUid: EP.toUid(pointerOverChild.elementPath), + children: children, + parentPath: EP.parentPath(selectedElement), + }) + + if (commands == null) { + return emptyStrategyApplicationResult + } + + return strategyApplicationResult(commands) }, } } + +function swapChildrenCommands({ + grabbedElementUid, + swapToElementUid, + children, + parentPath, +}: { + grabbedElementUid: string + swapToElementUid: string + children: ElementInstanceMetadata[] + parentPath: ElementPath +}): CanvasCommand[] | null { + const grabbedElement = children.find((c) => EP.toUid(c.elementPath) === grabbedElementUid) + const swapToElement = children.find((c) => EP.toUid(c.elementPath) === swapToElementUid) + + if (grabbedElement == null || swapToElement == null) { + return null + } + + /** + * - update child props + */ + + const rearrangedChildren = children + .map((c) => { + if (EP.pathsEqual(c.elementPath, grabbedElement.elementPath)) { + return swapToElement.elementPath + } + if (EP.pathsEqual(c.elementPath, swapToElement.elementPath)) { + return grabbedElement.elementPath + } + return c.elementPath + }) + .map((path) => EP.dynamicPathToStaticPath(path)) + + return [rearrangeChildren('always', parentPath, rearrangedChildren)] +} diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index eaec786fc49c..ba9b68feed02 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -1,13 +1,18 @@ import React from 'react' import { CanvasOffsetWrapper } from './canvas-offset-wrapper' -import { Substores, useEditorState } from '../../editor/store/store-hook' +import { Substores, useEditorState, useRefEditorState } from '../../editor/store/store-hook' import { MetadataUtils } from '../../../core/model/element-metadata-utils' import * as EP from '../../../core/shared/element-path' import { mapDropNulls } from '../../../core/shared/array-utils' -import { isFiniteRectangle } from '../../../core/shared/math-utils' +import { isFiniteRectangle, windowPoint } from '../../../core/shared/math-utils' import { isRight } from '../../../core/shared/either' import { isJSXElement } from '../../../core/shared/element-template' import { controlForStrategyMemoized } from '../canvas-strategies/canvas-strategy-types' +import { useDispatch } from '../../editor/store/dispatch-context' +import { createInteractionViaMouse, gridCellHandle } from '../canvas-strategies/interaction-state' +import CanvasActions from '../canvas-actions' +import { Modifier } from '../../../utils/modifiers' +import { windowToCanvasCoordinates } from '../dom-lookup' export const GridControls = controlForStrategyMemoized(() => { const selectedViews = useEditorState( @@ -15,6 +20,7 @@ export const GridControls = controlForStrategyMemoized(() => { (store) => store.editor.selectedViews, '', ) + const grids = useEditorState( Substores.metadataAndPropertyControlsInfo, (store) => { @@ -109,6 +115,34 @@ export const GridControls = controlForStrategyMemoized(() => { }) }, [grids, jsxMetadata]) + const dispatch = useDispatch() + + const canvasOffsetRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) + const scaleRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) + + const startInteractionWithUid = React.useCallback( + (uid: string) => (event: React.MouseEvent) => { + event.stopPropagation() + const start = windowToCanvasCoordinates( + scaleRef.current, + canvasOffsetRef.current, + windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + ) + + dispatch([ + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start.canvasPositionRounded, + Modifier.modifiersForEvent(event), + gridCellHandle(uid), + 'zero-drag-not-permitted', + ), + ), + ]) + }, + [canvasOffsetRef, dispatch, scaleRef], + ) + if (grids.length === 0) { return null } @@ -154,6 +188,7 @@ export const GridControls = controlForStrategyMemoized(() => { const isSelected = selectedViews.some((view) => EP.pathsEqual(cell.elementPath, view)) return (
Date: Wed, 26 Jun 2024 21:41:26 +0200 Subject: [PATCH 06/29] remove event listener --- editor/src/components/canvas/controls/grid-controls.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index ba9b68feed02..0395d955e434 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -122,7 +122,6 @@ export const GridControls = controlForStrategyMemoized(() => { const startInteractionWithUid = React.useCallback( (uid: string) => (event: React.MouseEvent) => { - event.stopPropagation() const start = windowToCanvasCoordinates( scaleRef.current, canvasOffsetRef.current, From 2f461be1b883b08cfdfaaa3dd06459fb9305460e Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Wed, 26 Jun 2024 21:41:52 +0200 Subject: [PATCH 07/29] rename strategy --- .../components/canvas/canvas-strategies/canvas-strategies.tsx | 2 +- .../{rearrangeGridStrategy.ts => rearrange-grid-strategy.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename editor/src/components/canvas/canvas-strategies/strategies/{rearrangeGridStrategy.ts => rearrange-grid-strategy.ts} (100%) diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index 34c4085a9f3d..7b331472ef37 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -63,7 +63,7 @@ import type { InsertionSubject, InsertionSubjectWrapper } from '../../editor/edi import { generateUidWithExistingComponents } from '../../../core/model/element-template-utils' import { retargetStrategyToChildrenOfFragmentLikeElements } from './strategies/fragment-like-helpers' import { MetadataUtils } from '../../../core/model/element-metadata-utils' -import { rearrangeGridStrategy } from './strategies/rearrangeGridStrategy' +import { rearrangeGridStrategy } from './strategies/rearrange-grid-strategy' export type CanvasStrategyFactory = ( canvasState: InteractionCanvasState, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts similarity index 100% rename from editor/src/components/canvas/canvas-strategies/strategies/rearrangeGridStrategy.ts rename to editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts From 87e54395992a3151fc341fc48b9248dfce489f82 Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Wed, 26 Jun 2024 15:43:49 -0400 Subject: [PATCH 08/29] spike(editor) Grid range support. --- editor/src/components/canvas/dom-walker.ts | 17 +++++++++++++---- .../components/inspector/common/css-utils.ts | 19 +++++++++++++++++++ editor/src/core/shared/element-template.ts | 12 ++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/editor/src/components/canvas/dom-walker.ts b/editor/src/components/canvas/dom-walker.ts index 2f235e40d22b..aa28f88bb7a7 100644 --- a/editor/src/components/canvas/dom-walker.ts +++ b/editor/src/components/canvas/dom-walker.ts @@ -60,6 +60,7 @@ import { parseFlexDirection, parseCSSPx, parseGridPosition, + parseGridRange, } from '../inspector/common/css-utils' import { camelCaseToDashed } from '../../core/shared/string-utils' import type { UtopiaStoreAPI } from '../editor/store/store-hook' @@ -901,10 +902,18 @@ function getGridContainerProperties(elementStyle: CSSStyleDeclaration): GridCont } function getGridElementProperties(elementStyle: CSSStyleDeclaration): GridElementProperties { - const gridColumnStart = defaultEither(null, parseGridPosition(elementStyle.gridColumnStart)) - const gridColumnEnd = defaultEither(null, parseGridPosition(elementStyle.gridColumnEnd)) - const gridRowStart = defaultEither(null, parseGridPosition(elementStyle.gridRowStart)) - const gridRowEnd = defaultEither(null, parseGridPosition(elementStyle.gridRowEnd)) + const gridColumn = defaultEither(null, parseGridRange(elementStyle.gridColumn)) + const gridColumnStart = + defaultEither(null, parseGridPosition(elementStyle.gridColumnStart)) ?? + gridColumn?.start ?? + null + const gridColumnEnd = + defaultEither(null, parseGridPosition(elementStyle.gridColumnEnd)) ?? gridColumn?.end ?? null + const gridRow = defaultEither(null, parseGridRange(elementStyle.gridRow)) + const gridRowStart = + defaultEither(null, parseGridPosition(elementStyle.gridRowStart)) ?? gridRow?.start ?? null + const gridRowEnd = + defaultEither(null, parseGridPosition(elementStyle.gridRowEnd)) ?? gridRow?.end ?? null return gridElementProperties(gridColumnStart, gridColumnEnd, gridRowStart, gridRowEnd) } diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index 79416f70ab4b..2e27619b77fd 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -17,6 +17,7 @@ import { findLastIndex } from '../../../core/shared/array-utils' import type { Either, Right as EitherRight } from '../../../core/shared/either' import { alternativeEither, + applicative2Either, bimapEither, eitherToMaybe, flatMapEither, @@ -33,6 +34,7 @@ import type { JSExpressionValue, JSXElement, GridPosition, + GridRange, } from '../../../core/shared/element-template' import { emptyComments, @@ -43,6 +45,7 @@ import { jsExpressionFunctionCall, jsExpressionValue, gridPositionValue, + gridRange, } from '../../../core/shared/element-template' import type { ModifiableAttribute } from '../../../core/shared/jsx-attributes' import { @@ -813,6 +816,22 @@ export function parseGridPosition(input: unknown): Either } } +export function parseGridRange(input: unknown): Either { + if (typeof input === 'string') { + if (input.includes('/')) { + const splitInput = input.split('/') + const startParsed = parseGridPosition(splitInput[0]) + const endParsed = parseGridPosition(splitInput[1]) + return applicative2Either(gridRange, startParsed, endParsed) + } else { + const startParsed = parseGridPosition(input) + return mapEither((start) => gridRange(start, null), startParsed) + } + } else { + return left('Not a valid grid range.') + } +} + export function parseDisplay(input: unknown): Either { if (typeof input === 'string') { return right(input) diff --git a/editor/src/core/shared/element-template.ts b/editor/src/core/shared/element-template.ts index a2540222a391..cf3286956bd9 100644 --- a/editor/src/core/shared/element-template.ts +++ b/editor/src/core/shared/element-template.ts @@ -2568,6 +2568,18 @@ export function gridPositionValue(numericalPosition: number | null): GridPositio export type GridPosition = GridPositionValue | 'auto' +export interface GridRange { + start: GridPosition + end: GridPosition | null +} + +export function gridRange(start: GridPosition, end: GridPosition | null): GridRange { + return { + start: start, + end: end, + } +} + export type GridColumnStart = GridPosition export type GridColumnEnd = GridPosition export type GridRowStart = GridPosition From 419daceac7df2c539c162e5638abf86eab9458a1 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Wed, 26 Jun 2024 21:53:14 +0200 Subject: [PATCH 09/29] wip --- .../canvas-strategies/canvas-strategies.tsx | 2 +- .../strategies/drag-to-move-metastrategy.tsx | 19 +++++++++++++++++-- .../strategies/rearrange-grid-strategy.ts | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index 7b331472ef37..350d106356ce 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -584,7 +584,7 @@ export function useGetApplicableStrategyControls(): Array + MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath(canvasState.startingMetadata, EP.parentPath(t)), + ), + ) + ) { + return null + } + return { id: DoNothingStrategyID, name: 'No Default Available', diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts index 2ee5c0016487..93132ad3361f 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts @@ -65,7 +65,7 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( show: 'always-visible', }, ], - fitness: 1, // onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 1), + fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 1), apply: () => { if ( interactionSession == null || From 34144f9ba1a0dcf002ceb4c6de036f89f2d95274 Mon Sep 17 00:00:00 2001 From: Federico Ruggi Date: Wed, 26 Jun 2024 16:25:08 -0400 Subject: [PATCH 10/29] use special size measurements new cool stuff --- .../canvas/controls/grid-controls.tsx | 82 +++++++------------ 1 file changed, 29 insertions(+), 53 deletions(-) diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index 0395d955e434..c3afaa266903 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -5,8 +5,6 @@ import { MetadataUtils } from '../../../core/model/element-metadata-utils' import * as EP from '../../../core/shared/element-path' import { mapDropNulls } from '../../../core/shared/array-utils' import { isFiniteRectangle, windowPoint } from '../../../core/shared/math-utils' -import { isRight } from '../../../core/shared/either' -import { isJSXElement } from '../../../core/shared/element-template' import { controlForStrategyMemoized } from '../canvas-strategies/canvas-strategy-types' import { useDispatch } from '../../editor/store/dispatch-context' import { createInteractionViaMouse, gridCellHandle } from '../canvas-strategies/interaction-state' @@ -31,65 +29,40 @@ export const GridControls = controlForStrategyMemoized(() => { EP.parentPath(view), ) - const target = MetadataUtils.isGridLayoutedContainer(element) + const targetGridContainer = MetadataUtils.isGridLayoutedContainer(element) ? element : MetadataUtils.isGridLayoutedContainer(parent) ? parent : null if ( - target == null || - target.globalFrame == null || - !isFiniteRectangle(target.globalFrame) + targetGridContainer == null || + targetGridContainer.globalFrame == null || + !isFiniteRectangle(targetGridContainer.globalFrame) ) { return null } - let rows = 0 - let columns = 0 - let gridTemplateColumns: any = undefined - let gridTemplateRows: any = undefined - let gap: any = 0 - let padding: any = 0 - // TODO well this whole logic piece is garbage, we should have these in the SpecialSizeMeasurements - if (isRight(target.element) && isJSXElement(target.element.value)) { - for (const prop of target.element.value.props) { - if ( - prop.type === 'JSX_ATTRIBUTES_ENTRY' && - prop.key === 'style' && - prop.value.type === 'ATTRIBUTE_NESTED_OBJECT' - ) { - for (const entry of prop.value.content) { - if ( - entry.type === 'PROPERTY_ASSIGNMENT' && - entry.value.type === 'ATTRIBUTE_VALUE' - ) { - if (typeof entry.value.value === 'string') { - // TODO do something about it… - const count = entry.value.value.trim().split(/\s+/).length - if (entry.key === 'gridTemplateColumns') { - columns = count - gridTemplateColumns = entry.value.value - } - if (entry.key === 'gridTemplateRows') { - rows = count - gridTemplateRows = entry.value.value - } - } - if (entry.key === 'gap' || entry.key === 'gridGap') { - gap = entry.value.value - } - if (entry.key === 'padding') { - padding = entry.value.value - } - } - } - } - } + const gap = targetGridContainer.specialSizeMeasurements.gap + const padding = targetGridContainer.specialSizeMeasurements.padding + const gridTemplateColumns = + targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns + const gridTemplateRows = + targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows + + function getSillyCellsCount(template: string | null) { + return template == null ? 0 : template.trim().split(/\s+/).length } + const columns = getSillyCellsCount( + targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns, + ) + const rows = getSillyCellsCount( + targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows, + ) + return { - elementPath: target.elementPath, - frame: target.globalFrame, + elementPath: targetGridContainer.elementPath, + frame: targetGridContainer.globalFrame, gridTemplateColumns: gridTemplateColumns, gridTemplateRows: gridTemplateRows, gap: gap, @@ -163,10 +136,13 @@ export const GridControls = controlForStrategyMemoized(() => { height: grid.frame.height, backgroundColor: '#ff00ff0a', display: 'grid', - gridTemplateColumns: grid.gridTemplateColumns, - gridTemplateRows: grid.gridTemplateRows, - gap: grid.gap, - padding: grid.padding, + gridTemplateColumns: grid.gridTemplateColumns ?? undefined, + gridTemplateRows: grid.gridTemplateRows ?? undefined, + gap: grid.gap ?? 0, + padding: + grid.padding == null + ? 0 + : `${grid.padding.top} ${grid.padding.right} ${grid.padding.bottom} ${grid.padding.left}`, }} > {placeholders.map((cell) => { From 3a6187181d276f7989267ae7a1235ccf3f659cea Mon Sep 17 00:00:00 2001 From: Federico Ruggi Date: Wed, 26 Jun 2024 16:45:45 -0400 Subject: [PATCH 11/29] wip wip --- .../canvas/controls/grid-controls.tsx | 2 +- .../select-mode/select-mode-hooks.tsx | 36 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index c3afaa266903..451d4368f44e 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -91,7 +91,7 @@ export const GridControls = controlForStrategyMemoized(() => { const dispatch = useDispatch() const canvasOffsetRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) - const scaleRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) + const scaleRef = useRefEditorState((store) => store.editor.canvas.scale) const startInteractionWithUid = React.useCallback( (uid: string) => (event: React.MouseEvent) => { diff --git a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx index 8e2991ea4caa..9ba0b35fd181 100644 --- a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx +++ b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx @@ -656,23 +656,23 @@ function useSelectOrLiveModeSelectAndHover( let editorActions: Array = [] if (foundTarget != null || isDeselect) { - if (foundTarget != null && draggingAllowed) { - const start = windowToCanvasCoordinates( - windowPoint(point(event.clientX, event.clientY)), - ).canvasPositionRounded - if (event.button !== 2 && event.type !== 'mouseup') { - editorActions.push( - CanvasActions.createInteractionSession( - createInteractionViaMouse( - start, - Modifier.modifiersForEvent(event), - boundingArea(), - 'zero-drag-not-permitted', - ), - ), - ) - } - } + // if (foundTarget != null && draggingAllowed) { + // const start = windowToCanvasCoordinates( + // windowPoint(point(event.clientX, event.clientY)), + // ).canvasPositionRounded + // if (event.button !== 2 && event.type !== 'mouseup') { + // editorActions.push( + // CanvasActions.createInteractionSession( + // createInteractionViaMouse( + // start, + // Modifier.modifiersForEvent(event), + // boundingArea(), + // 'zero-drag-not-permitted', + // ), + // ), + // ) + // } + // } let updatedSelection: Array if (isMultiselect) { @@ -739,8 +739,6 @@ function useSelectOrLiveModeSelectAndHover( setSelectedViewsForCanvasControlsOnly, getSelectableViewsForSelectMode, editorStoreRef, - draggingAllowed, - windowToCanvasCoordinates, active, ], ) From aa9c8bc43334bd8f43d8823554591ee118114ce5 Mon Sep 17 00:00:00 2001 From: Federico Ruggi Date: Wed, 26 Jun 2024 16:51:31 -0400 Subject: [PATCH 12/29] fixerino --- .../canvas-strategies/strategies/rearrange-grid-strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts index 93132ad3361f..1e23a5a3162f 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts @@ -46,7 +46,7 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( canvasState.startingMetadata, canvasState.startingAllElementProps, canvasState.startingElementPathTree, - selectedElement, + EP.parentPath(selectedElement), ) return { From e93af72b30d8e34a551f2f695bc46535da2bf865 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Wed, 26 Jun 2024 23:18:47 +0200 Subject: [PATCH 13/29] props --- .../strategies/rearrange-grid-strategy.ts | 77 +++++++++++++++++-- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts index 1e23a5a3162f..9a87b2f30559 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts @@ -1,6 +1,11 @@ import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import { stripNulls } from '../../../../core/shared/array-utils' import * as EP from '../../../../core/shared/element-path' -import type { ElementInstanceMetadata } from '../../../../core/shared/element-template' +import type { + ElementInstanceMetadata, + GridElementProperties, + GridPosition, +} from '../../../../core/shared/element-template' import { isFiniteRectangle, offsetPoint, @@ -8,8 +13,11 @@ import { } from '../../../../core/shared/math-utils' import { optionalMap } from '../../../../core/shared/optional-utils' import type { ElementPath } from '../../../../core/shared/project-file-types' +import { create } from '../../../../core/shared/property-path' import type { CanvasCommand } from '../../commands/commands' +import { deleteProperties } from '../../commands/delete-properties-command' import { rearrangeChildren } from '../../commands/rearrange-children-command' +import { setProperty } from '../../commands/set-property-command' import { GridControls } from '../../controls/grid-controls' import { recurseIntoChildrenOfMapOrFragment } from '../../gap-utils' import type { CanvasStrategyFactory } from '../canvas-strategies' @@ -111,6 +119,47 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( } } +const GridPositioningProps: Array = [ + 'gridColumn', + 'gridRow', + 'gridColumnStart', + 'gridColumnEnd', + 'gridRowStart', + 'gridRowEnd', +] + +function gridPositionToValue(p: GridPosition | null): string | number | null { + if (p == null) { + return null + } + if (p === 'auto') { + return 'auto' + } + + return p.numericalPosition +} + +function setGridProps(elementPath: ElementPath, gridProps: GridElementProperties): CanvasCommand[] { + return stripNulls([ + optionalMap( + (s) => setProperty('always', elementPath, create('style', 'gridColumnStart'), s), + gridPositionToValue(gridProps.gridColumnStart), + ), + optionalMap( + (s) => setProperty('always', elementPath, create('style', 'gridColumnEnd'), s), + gridPositionToValue(gridProps.gridColumnEnd), + ), + optionalMap( + (s) => setProperty('always', elementPath, create('style', 'gridRowStart'), s), + gridPositionToValue(gridProps.gridRowStart), + ), + optionalMap( + (s) => setProperty('always', elementPath, create('style', 'gridRowEnd'), s), + gridPositionToValue(gridProps.gridRowEnd), + ), + ]) +} + function swapChildrenCommands({ grabbedElementUid, swapToElementUid, @@ -129,10 +178,6 @@ function swapChildrenCommands({ return null } - /** - * - update child props - */ - const rearrangedChildren = children .map((c) => { if (EP.pathsEqual(c.elementPath, grabbedElement.elementPath)) { @@ -145,5 +190,25 @@ function swapChildrenCommands({ }) .map((path) => EP.dynamicPathToStaticPath(path)) - return [rearrangeChildren('always', parentPath, rearrangedChildren)] + return [ + rearrangeChildren('always', parentPath, rearrangedChildren), + deleteProperties( + 'always', + swapToElement.elementPath, + GridPositioningProps.map((p) => create('style', p)), + ), + deleteProperties( + 'always', + grabbedElement.elementPath, + GridPositioningProps.map((p) => create('style', p)), + ), + ...setGridProps( + grabbedElement.elementPath, + swapToElement.specialSizeMeasurements.elementGridProperties, + ), + ...setGridProps( + swapToElement.elementPath, + grabbedElement.specialSizeMeasurements.elementGridProperties, + ), + ] } From 4e95caabccedca9b8552327458552329517dc3a4 Mon Sep 17 00:00:00 2001 From: Federico Ruggi Date: Wed, 26 Jun 2024 17:23:07 -0400 Subject: [PATCH 14/29] dragging zones --- .../strategies/rearrange-grid-strategy.ts | 1 - .../canvas/controls/grid-controls.tsx | 42 +++++++++++++++---- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts index 9a87b2f30559..f5884258b5b6 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts @@ -11,7 +11,6 @@ import { offsetPoint, rectContainsPointInclusive, } from '../../../../core/shared/math-utils' -import { optionalMap } from '../../../../core/shared/optional-utils' import type { ElementPath } from '../../../../core/shared/project-file-types' import { create } from '../../../../core/shared/property-path' import type { CanvasCommand } from '../../commands/commands' diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index 451d4368f44e..c8591d12defb 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -11,14 +11,29 @@ import { createInteractionViaMouse, gridCellHandle } from '../canvas-strategies/ import CanvasActions from '../canvas-actions' import { Modifier } from '../../../utils/modifiers' import { windowToCanvasCoordinates } from '../dom-lookup' +import { motion } from 'framer-motion' export const GridControls = controlForStrategyMemoized(() => { const selectedViews = useEditorState( Substores.selectedViews, (store) => store.editor.selectedViews, + 'GridControls selectedViews', + ) + + const isActivelyDraggingCell = useEditorState( + Substores.canvas, + (store) => + store.editor.canvas.interactionSession != null && + store.editor.canvas.interactionSession.activeControl.type === 'GRID_CELL_HANDLE', '', ) + const jsxMetadata = useEditorState( + Substores.metadata, + (store) => store.editor.jsxMetadata, + 'GridControls jsxMetadata', + ) + const grids = useEditorState( Substores.metadataAndPropertyControlsInfo, (store) => { @@ -71,11 +86,9 @@ export const GridControls = controlForStrategyMemoized(() => { } }, store.editor.selectedViews) }, - 'GridControls selectedGrids', + 'GridControls grids', ) - const jsxMetadata = useEditorState(Substores.metadata, (store) => store.editor.jsxMetadata, '') - const cells = React.useMemo(() => { return grids.flatMap((grid) => { const children = MetadataUtils.getChildrenUnordered(jsxMetadata, grid.elementPath) @@ -134,7 +147,7 @@ export const GridControls = controlForStrategyMemoized(() => { left: grid.frame.x, width: grid.frame.width, height: grid.frame.height, - backgroundColor: '#ff00ff0a', + // backgroundColor: '#ff00ff09', display: 'grid', gridTemplateColumns: grid.gridTemplateColumns ?? undefined, gridTemplateRows: grid.gridTemplateRows ?? undefined, @@ -142,7 +155,7 @@ export const GridControls = controlForStrategyMemoized(() => { padding: grid.padding == null ? 0 - : `${grid.padding.top} ${grid.padding.right} ${grid.padding.bottom} ${grid.padding.left}`, + : `${grid.padding.top}px ${grid.padding.right}px ${grid.padding.bottom}px ${grid.padding.left}px`, }} > {placeholders.map((cell) => { @@ -151,6 +164,7 @@ export const GridControls = controlForStrategyMemoized(() => { key={`grid-${index}-cell-${cell}`} style={{ border: '1px solid #ff00ff66', + background: '#ff00ff06', }} /> ) @@ -162,7 +176,19 @@ export const GridControls = controlForStrategyMemoized(() => { {cells.map((cell) => { const isSelected = selectedViews.some((view) => EP.pathsEqual(cell.elementPath, view)) return ( -
{ left: cell.globalFrame.x, width: cell.globalFrame.width, height: cell.globalFrame.height, - backgroundColor: '#09f', - opacity: !isSelected ? 0.3 : 0, + backgroundColor: '#f0f', + opacity: !isActivelyDraggingCell || isSelected ? 0 : 0.2, }} /> ) From 7e42fb7ba7bc73c82c20cdb83782508b87863551 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Wed, 26 Jun 2024 23:55:38 +0200 Subject: [PATCH 15/29] phix --- .../canvas-strategies/strategies/rearrange-grid-strategy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts index f5884258b5b6..9a87b2f30559 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts @@ -11,6 +11,7 @@ import { offsetPoint, rectContainsPointInclusive, } from '../../../../core/shared/math-utils' +import { optionalMap } from '../../../../core/shared/optional-utils' import type { ElementPath } from '../../../../core/shared/project-file-types' import { create } from '../../../../core/shared/property-path' import type { CanvasCommand } from '../../commands/commands' From 754e74f75064bb50f31f9c666c158dee1384f699 Mon Sep 17 00:00:00 2001 From: Federico Ruggi Date: Wed, 26 Jun 2024 17:55:41 -0400 Subject: [PATCH 16/29] whatev --- editor/src/components/canvas/controls/grid-controls.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index c8591d12defb..a6cb8f927582 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -178,10 +178,10 @@ export const GridControls = controlForStrategyMemoized(() => { return ( Date: Wed, 26 Jun 2024 18:48:34 -0400 Subject: [PATCH 17/29] spike(editor) Parse the grid template columns and rows. --- .../canvas/controls/grid-controls.tsx | 35 ++++++++++-- editor/src/components/canvas/dom-walker.ts | 22 ++++++-- .../store/store-deep-equality-instances.ts | 55 ++++++++++++++++++- .../components/inspector/common/css-utils.ts | 49 ++++++++++++++++- editor/src/core/shared/element-template.ts | 38 ++++++++++++- 5 files changed, 183 insertions(+), 16 deletions(-) diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index a6cb8f927582..d2506103e05b 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -12,6 +12,34 @@ import CanvasActions from '../canvas-actions' import { Modifier } from '../../../utils/modifiers' import { windowToCanvasCoordinates } from '../dom-lookup' import { motion } from 'framer-motion' +import type { GridAutoOrTemplateBase } from '../../../core/shared/element-template' +import { assertNever } from '../../../core/shared/utils' +import { printGridAutoOrTemplateBase } from '../../../components/inspector/common/css-utils' + +function getSillyCellsCount(template: GridAutoOrTemplateBase | null): number { + if (template == null) { + return 0 + } else { + switch (template.type) { + case 'DIMENSIONS': + return template.dimensions.length + case 'FALLBACK': + return 0 + default: + assertNever(template) + } + } +} + +function getNullableAutoOrTemplateBaeString( + template: GridAutoOrTemplateBase | null, +): string | undefined { + if (template == null) { + return undefined + } else { + return printGridAutoOrTemplateBase(template) + } +} export const GridControls = controlForStrategyMemoized(() => { const selectedViews = useEditorState( @@ -65,9 +93,6 @@ export const GridControls = controlForStrategyMemoized(() => { const gridTemplateRows = targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows - function getSillyCellsCount(template: string | null) { - return template == null ? 0 : template.trim().split(/\s+/).length - } const columns = getSillyCellsCount( targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns, ) @@ -149,8 +174,8 @@ export const GridControls = controlForStrategyMemoized(() => { height: grid.frame.height, // backgroundColor: '#ff00ff09', display: 'grid', - gridTemplateColumns: grid.gridTemplateColumns ?? undefined, - gridTemplateRows: grid.gridTemplateRows ?? undefined, + gridTemplateColumns: getNullableAutoOrTemplateBaeString(grid.gridTemplateColumns), + gridTemplateRows: getNullableAutoOrTemplateBaeString(grid.gridTemplateRows), gap: grid.gap ?? 0, padding: grid.padding == null diff --git a/editor/src/components/canvas/dom-walker.ts b/editor/src/components/canvas/dom-walker.ts index aa28f88bb7a7..50973a259cb8 100644 --- a/editor/src/components/canvas/dom-walker.ts +++ b/editor/src/components/canvas/dom-walker.ts @@ -20,6 +20,7 @@ import { emptyAttributeMetadata, gridContainerProperties, gridElementProperties, + gridAutoOrTemplateFallback, } from '../../core/shared/element-template' import type { ElementPath } from '../../core/shared/project-file-types' import { @@ -61,6 +62,7 @@ import { parseCSSPx, parseGridPosition, parseGridRange, + parseGridAutoOrTemplateBase, } from '../inspector/common/css-utils' import { camelCaseToDashed } from '../../core/shared/string-utils' import type { UtopiaStoreAPI } from '../editor/store/store-hook' @@ -889,10 +891,22 @@ function getComputedStyle( } function getGridContainerProperties(elementStyle: CSSStyleDeclaration): GridContainerProperties { - const gridTemplateColumns = defaultEither(null, parseString(elementStyle.gridTemplateColumns)) - const gridTemplateRows = defaultEither(null, parseString(elementStyle.gridTemplateRows)) - const gridAutoColumns = defaultEither(null, parseString(elementStyle.gridAutoColumns)) - const gridAutoRows = defaultEither(null, parseString(elementStyle.gridAutoRows)) + const gridTemplateColumns = defaultEither( + gridAutoOrTemplateFallback(elementStyle.gridTemplateColumns), + parseGridAutoOrTemplateBase(elementStyle.gridTemplateColumns), + ) + const gridTemplateRows = defaultEither( + gridAutoOrTemplateFallback(elementStyle.gridTemplateRows), + parseGridAutoOrTemplateBase(elementStyle.gridTemplateRows), + ) + const gridAutoColumns = defaultEither( + gridAutoOrTemplateFallback(elementStyle.gridAutoColumns), + parseGridAutoOrTemplateBase(elementStyle.gridAutoColumns), + ) + const gridAutoRows = defaultEither( + gridAutoOrTemplateFallback(elementStyle.gridAutoRows), + parseGridAutoOrTemplateBase(elementStyle.gridAutoRows), + ) return gridContainerProperties( gridTemplateColumns, gridTemplateRows, diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index 866d486482a5..235b8a7db53b 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -149,6 +149,9 @@ import type { GridElementProperties, GridPositionValue, GridPosition, + GridAutoOrTemplateBase, + GridAutoOrTemplateDimensions, + GridAutoOrTemplateFallback, } from '../../../core/shared/element-template' import { elementInstanceMetadata, @@ -223,6 +226,8 @@ import { gridContainerProperties, gridElementProperties, gridPositionValue, + gridAutoOrTemplateFallback, + gridAutoOrTemplateDimensions, } from '../../../core/shared/element-template' import type { CanvasRectangle, @@ -552,11 +557,13 @@ import type { CSSFontWeightAndStyle, CSSLetterSpacing, CSSLineHeight, + CSSNumber, + CSSNumberUnit, CSSTextAlign, CSSTextDecorationLine, FontSettings, } from '../../inspector/common/css-utils' -import { fontSettings } from '../../inspector/common/css-utils' +import { cssNumber, fontSettings } from '../../inspector/common/css-utils' import type { ElementPaste, ProjectListing } from '../action-types' import { projectListing } from '../action-types' import type { Bounds, UtopiaVSCodeConfig } from 'utopia-vscode-common' @@ -1947,11 +1954,53 @@ export const ImportInfoKeepDeepEquality: KeepDeepEqualityCall = ( return keepDeepEqualityResult(newValue, false) } +export const CSSNumberKeepDeepEquality: KeepDeepEqualityCall = combine2EqualityCalls( + (cssNum) => cssNum.value, + createCallWithTripleEquals(), + (cssNum) => cssNum.unit, + undefinableDeepEquality(nullableDeepEquality(createCallWithTripleEquals())), + cssNumber, +) + +export const GridAutoOrTemplateDimensionsKeepDeepEquality: KeepDeepEqualityCall = + combine1EqualityCall( + (value) => value.dimensions, + arrayDeepEquality(CSSNumberKeepDeepEquality), + gridAutoOrTemplateDimensions, + ) + +export const GridAutoOrTemplateFallbackKeepDeepEquality: KeepDeepEqualityCall = + combine1EqualityCall( + (value) => value.value, + createCallWithTripleEquals(), + gridAutoOrTemplateFallback, + ) + +export const GridAutoOrTemplateBaseKeepDeepEquality: KeepDeepEqualityCall< + GridAutoOrTemplateBase +> = (oldValue, newValue) => { + switch (oldValue.type) { + case 'DIMENSIONS': + if (newValue.type === oldValue.type) { + return GridAutoOrTemplateDimensionsKeepDeepEquality(oldValue, newValue) + } + break + case 'FALLBACK': + if (newValue.type === oldValue.type) { + return GridAutoOrTemplateFallbackKeepDeepEquality(oldValue, newValue) + } + break + default: + assertNever(oldValue) + } + return keepDeepEqualityResult(newValue, false) +} + export const GridTemplateKeepDeepEquality: KeepDeepEqualityCall = - createCallWithTripleEquals() + GridAutoOrTemplateBaseKeepDeepEquality export const GridAutoKeepDeepEquality: KeepDeepEqualityCall = - createCallWithTripleEquals() + GridAutoOrTemplateBaseKeepDeepEquality export function GridContainerPropertiesKeepDeepEquality(): KeepDeepEqualityCall { return combine4EqualityCalls( diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index 2e27619b77fd..d307f73c2aab 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -24,6 +24,7 @@ import { isLeft, isRight, left, + leftMapEither, mapEither, right, traverseEither, @@ -35,6 +36,7 @@ import type { JSXElement, GridPosition, GridRange, + GridAutoOrTemplateBase, } from '../../../core/shared/element-template' import { emptyComments, @@ -46,6 +48,7 @@ import { jsExpressionValue, gridPositionValue, gridRange, + gridAutoOrTemplateDimensions, } from '../../../core/shared/element-template' import type { ModifiableAttribute } from '../../../core/shared/jsx-attributes' import { @@ -67,7 +70,7 @@ import { } from '../../../core/shared/math-utils' import type { PropertyPath } from '../../../core/shared/project-file-types' import * as PP from '../../../core/shared/property-path' -import type { PrimitiveType, ValueOf } from '../../../core/shared/utils' +import { assertNever, type PrimitiveType, type ValueOf } from '../../../core/shared/utils' import { parseBackgroundSize } from '../../../printer-parsers/css/css-parser-background-size' import { parseBorder } from '../../../printer-parsers/css/css-parser-border' import Utils from '../../../utils/utils' @@ -82,6 +85,9 @@ import { } from '../../../printer-parsers/css/css-parser-margin' import { parseFlex, printFlexAsAttributeValue } from '../../../printer-parsers/css/css-parser-flex' import { memoize } from '../../../core/shared/memoize' +import { parseCSSArray } from '../../../printer-parsers/css/css-parser-utils' +import type { ParseError } from '../../../utils/value-parser-utils' +import { descriptionParseError } from '../../../utils/value-parser-utils' var combineRegExp = function (regexpList: Array, flags?: string) { let source: string = '' @@ -733,6 +739,22 @@ export function printCSSNumber( } } +export function printGridAutoOrTemplateBase(input: GridAutoOrTemplateBase): string { + switch (input.type) { + case 'DIMENSIONS': + return input.dimensions + .map((dimension) => { + const printed = printCSSNumber(dimension, null) + return typeof printed === 'string' ? printed : `${printed}` + }) + .join(' ') + case 'FALLBACK': + return input.value + default: + assertNever(input) + } +} + export function printCSSNumberOrKeyword( input: CSSNumber | CSSKeyword, defaultUnitToSkip: string | null, @@ -832,6 +854,31 @@ export function parseGridRange(input: unknown): Either { } } +export function parseGridAutoOrTemplateBase( + input: unknown, +): Either { + function numberParse(inputToParse: unknown): Either { + const result = parseCSSAnyValidNumber(inputToParse) + return leftMapEither(descriptionParseError, result) + } + if (typeof input === 'string') { + const parsedCSSArray = parseCSSArray([numberParse])(input.split(/ +/)) + return bimapEither( + (error) => { + if (error.type === 'DESCRIPTION_PARSE_ERROR') { + return error.description + } else { + return error.toString() + } + }, + gridAutoOrTemplateDimensions, + parsedCSSArray, + ) + } else { + return left('Unknown input.') + } +} + export function parseDisplay(input: unknown): Either { if (typeof input === 'string') { return right(input) diff --git a/editor/src/core/shared/element-template.ts b/editor/src/core/shared/element-template.ts index cf3286956bd9..6bf495b9a7d3 100644 --- a/editor/src/core/shared/element-template.ts +++ b/editor/src/core/shared/element-template.ts @@ -21,7 +21,11 @@ import { sides } from 'utopia-api/core' import { assertNever, fastForEach, unknownObjectProperty } from './utils' import { addAllUniquely, mapDropNulls } from './array-utils' import { objectMap } from './object-utils' -import type { CSSPosition, FlexDirection } from '../../components/inspector/common/css-utils' +import type { + CSSNumber, + CSSPosition, + FlexDirection, +} from '../../components/inspector/common/css-utils' import type { ModifiableAttribute } from './jsx-attributes' import * as EP from './element-path' import { firstLetterIsLowerCase } from './string-utils' @@ -2585,8 +2589,36 @@ export type GridColumnEnd = GridPosition export type GridRowStart = GridPosition export type GridRowEnd = GridPosition -export type GridAuto = string -export type GridTemplate = string +export interface GridAutoOrTemplateFallback { + type: 'FALLBACK' + value: string +} + +export function gridAutoOrTemplateFallback(value: string): GridAutoOrTemplateFallback { + return { + type: 'FALLBACK', + value: value, + } +} + +export interface GridAutoOrTemplateDimensions { + type: 'DIMENSIONS' + dimensions: Array +} + +export function gridAutoOrTemplateDimensions( + dimensions: Array, +): GridAutoOrTemplateDimensions { + return { + type: 'DIMENSIONS', + dimensions: dimensions, + } +} + +export type GridAutoOrTemplateBase = GridAutoOrTemplateDimensions | GridAutoOrTemplateFallback + +export type GridAuto = GridAutoOrTemplateBase +export type GridTemplate = GridAutoOrTemplateBase export type GridTemplateColumns = GridTemplate export type GridTemplateRows = GridTemplate From 7bc27a546d4b9d302c95bced7e3ea97e57a81be3 Mon Sep 17 00:00:00 2001 From: Federico Ruggi Date: Thu, 27 Jun 2024 10:51:41 -0400 Subject: [PATCH 18/29] grid move strat --- .../canvas-strategies/canvas-strategies.tsx | 6 +- .../canvas-strategies/interaction-state.ts | 4 +- .../rearrange-grid-move-strategy.ts | 128 +++++++++++++ ...egy.ts => rearrange-grid-swap-strategy.ts} | 34 ++-- .../canvas/controls/grid-controls.tsx | 171 ++++++++++++++---- 5 files changed, 289 insertions(+), 54 deletions(-) create mode 100644 editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-move-strategy.ts rename editor/src/components/canvas/canvas-strategies/strategies/{rearrange-grid-strategy.ts => rearrange-grid-swap-strategy.ts} (90%) diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index 350d106356ce..8dafee7fae9a 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -63,7 +63,8 @@ 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 { rearrangeGridStrategy } from './strategies/rearrange-grid-strategy' +import { rearrangeGridSwapStrategy } from './strategies/rearrange-grid-swap-strategy' +import { rearrangeGridMoveStrategy } from './strategies/rearrange-grid-move-strategy' export type CanvasStrategyFactory = ( canvasState: InteractionCanvasState, @@ -91,7 +92,8 @@ const moveOrReorderStrategies: MetaCanvasStrategy = ( convertToAbsoluteAndMoveStrategy, convertToAbsoluteAndMoveAndSetParentFixedStrategy, reorderSliderStategy, - rearrangeGridStrategy, + rearrangeGridSwapStrategy, + rearrangeGridMoveStrategy, ], ) } diff --git a/editor/src/components/canvas/canvas-strategies/interaction-state.ts b/editor/src/components/canvas/canvas-strategies/interaction-state.ts index d4c943c74d4b..11d8b20855ea 100644 --- a/editor/src/components/canvas/canvas-strategies/interaction-state.ts +++ b/editor/src/components/canvas/canvas-strategies/interaction-state.ts @@ -564,10 +564,10 @@ export interface GridCellHandle { id: string } -export function gridCellHandle(id: string): GridCellHandle { +export function gridCellHandle(params: { id: string }): GridCellHandle { return { type: 'GRID_CELL_HANDLE', - id: id, + id: params.id, } } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-move-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-move-strategy.ts new file mode 100644 index 000000000000..52a2cd3ed756 --- /dev/null +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-move-strategy.ts @@ -0,0 +1,128 @@ +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import * as EP from '../../../../core/shared/element-path' +import type { GridElementProperties } from '../../../../core/shared/element-template' +import { create } from '../../../../core/shared/property-path' +import type { CanvasCommand } from '../../commands/commands' +import { setProperty } from '../../commands/set-property-command' +import { TargetGridCell, GridControls } from '../../controls/grid-controls' +import type { CanvasStrategyFactory } from '../canvas-strategies' +import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' +import type { InteractionCanvasState } from '../canvas-strategy-types' +import { + getTargetPathsFromInteractionTarget, + emptyStrategyApplicationResult, + strategyApplicationResult, +} from '../canvas-strategy-types' +import type { InteractionSession } from '../interaction-state' + +export const rearrangeGridMoveStrategy: CanvasStrategyFactory = ( + canvasState: InteractionCanvasState, + interactionSession: InteractionSession | null, +) => { + const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) + if (selectedElements.length !== 1) { + return null + } + + const selectedElement = selectedElements[0] + const ok = MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + EP.parentPath(selectedElement), + ), + ) + if (!ok) { + return null + } + + return { + id: 'rearrange-grid-move-strategy', + name: 'Rearrange Grid (Move)', + descriptiveLabel: 'Rearrange Grid (Move)', + icon: { + category: 'tools', + type: 'pointer', + }, + controlsToRender: [ + { + control: GridControls, + props: {}, + key: `grid-controls-${EP.toString(selectedElement)}`, + show: 'always-visible', + }, + ], + fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 1) + 1, // add one so it wins over the swap + apply: () => { + if ( + interactionSession == null || + interactionSession.interactionData.type !== 'DRAG' || + interactionSession.interactionData.drag == null || + interactionSession.activeControl.type !== 'GRID_CELL_HANDLE' + ) { + return emptyStrategyApplicationResult + } + + let commands: CanvasCommand[] = [] + + if (TargetGridCell.current.row > 0 && TargetGridCell.current.column > 0) { + const metadata = MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + selectedElement, + ) + + function getGridProperty(field: keyof GridElementProperties, fallback: number) { + const propValue = metadata?.specialSizeMeasurements.elementGridProperties[field] + return propValue == null || propValue === 'auto' + ? 0 + : propValue.numericalPosition ?? fallback + } + + const gridColumnStart = getGridProperty('gridColumnStart', 0) + const gridColumnEnd = getGridProperty('gridColumnEnd', 1) + const gridRowStart = getGridProperty('gridRowStart', 0) + const gridRowEnd = getGridProperty('gridRowEnd', 1) + + if (metadata != null) { + commands.push( + setProperty( + 'always', + selectedElement, + create('style', 'gridColumnStart'), + TargetGridCell.current.column, + ), + setProperty( + 'always', + selectedElement, + create('style', 'gridColumnEnd'), + Math.max( + TargetGridCell.current.column, + TargetGridCell.current.column + (gridColumnEnd - gridColumnStart), + ), + ), + setProperty( + 'always', + selectedElement, + create('style', 'gridRowStart'), + TargetGridCell.current.row, + ), + setProperty( + 'always', + selectedElement, + create('style', 'gridRowEnd'), + Math.max( + TargetGridCell.current.row, + TargetGridCell.current.row + (gridRowEnd - gridRowStart), + ), + ), + ) + } + } + + if (commands == null) { + return emptyStrategyApplicationResult + } + + return strategyApplicationResult(commands) + }, + } +} diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-swap-strategy.ts similarity index 90% rename from editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts rename to editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-swap-strategy.ts index 9a87b2f30559..a9179e482166 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-swap-strategy.ts @@ -30,7 +30,7 @@ import { } from '../canvas-strategy-types' import type { InteractionSession } from '../interaction-state' -export const rearrangeGridStrategy: CanvasStrategyFactory = ( +export const rearrangeGridSwapStrategy: CanvasStrategyFactory = ( canvasState: InteractionCanvasState, interactionSession: InteractionSession | null, ) => { @@ -58,9 +58,9 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( ) return { - id: 'rearrange-grid-strategy', - name: 'Rearrange Grid', - descriptiveLabel: 'Rearrange Grid', + id: 'rearrange-grid-swap-strategy', + name: 'Rearrange Grid (Swap)', + descriptiveLabel: 'Rearrange Grid (Swap)', icon: { category: 'tools', type: 'pointer', @@ -96,20 +96,22 @@ export const rearrangeGridStrategy: CanvasStrategyFactory = ( rectContainsPointInclusive(c.globalFrame, pointOnCanvas), ) + let commands: CanvasCommand[] = [] + if ( - pointerOverChild == null || - EP.toUid(pointerOverChild.elementPath) === interactionSession.activeControl.id + pointerOverChild != null && + EP.toUid(pointerOverChild.elementPath) !== interactionSession.activeControl.id ) { - return emptyStrategyApplicationResult + commands.push( + ...swapChildrenCommands({ + grabbedElementUid: interactionSession.activeControl.id, + swapToElementUid: EP.toUid(pointerOverChild.elementPath), + children: children, + parentPath: EP.parentPath(selectedElement), + }), + ) } - const commands = swapChildrenCommands({ - grabbedElementUid: interactionSession.activeControl.id, - swapToElementUid: EP.toUid(pointerOverChild.elementPath), - children: children, - parentPath: EP.parentPath(selectedElement), - }) - if (commands == null) { return emptyStrategyApplicationResult } @@ -170,12 +172,12 @@ function swapChildrenCommands({ swapToElementUid: string children: ElementInstanceMetadata[] parentPath: ElementPath -}): CanvasCommand[] | null { +}): CanvasCommand[] { const grabbedElement = children.find((c) => EP.toUid(c.elementPath) === grabbedElementUid) const swapToElement = children.find((c) => EP.toUid(c.elementPath) === swapToElementUid) if (grabbedElement == null || swapToElement == null) { - return null + return [] } const rearrangedChildren = children diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index d2506103e05b..8ed24a278586 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -11,11 +11,19 @@ import { createInteractionViaMouse, gridCellHandle } from '../canvas-strategies/ import CanvasActions from '../canvas-actions' import { Modifier } from '../../../utils/modifiers' import { windowToCanvasCoordinates } from '../dom-lookup' -import { motion } from 'framer-motion' import type { GridAutoOrTemplateBase } from '../../../core/shared/element-template' import { assertNever } from '../../../core/shared/utils' import { printGridAutoOrTemplateBase } from '../../../components/inspector/common/css-utils' +type GridCellCoordinates = { row: number; column: number } + +function emptyGridCellCoordinates(): GridCellCoordinates { + return { row: 0, column: 0 } +} + +// TODO please forgive me (hackathon code) +export let TargetGridCell = { current: emptyGridCellCoordinates() } + function getSillyCellsCount(template: GridAutoOrTemplateBase | null): number { if (template == null) { return 0 @@ -42,12 +50,6 @@ function getNullableAutoOrTemplateBaeString( } export const GridControls = controlForStrategyMemoized(() => { - const selectedViews = useEditorState( - Substores.selectedViews, - (store) => store.editor.selectedViews, - 'GridControls selectedViews', - ) - const isActivelyDraggingCell = useEditorState( Substores.canvas, (store) => @@ -107,6 +109,8 @@ export const GridControls = controlForStrategyMemoized(() => { gridTemplateRows: gridTemplateRows, gap: gap, padding: padding, + rows: rows, + columns: columns, cells: rows * columns, } }, store.editor.selectedViews) @@ -117,11 +121,32 @@ export const GridControls = controlForStrategyMemoized(() => { const cells = React.useMemo(() => { return grids.flatMap((grid) => { const children = MetadataUtils.getChildrenUnordered(jsxMetadata, grid.elementPath) - return mapDropNulls((cell) => { + return mapDropNulls((cell, index) => { if (cell == null || cell.globalFrame == null || !isFiniteRectangle(cell.globalFrame)) { return null } - return { elementPath: cell.elementPath, globalFrame: cell.globalFrame } + const countedRow = Math.floor(index / grid.columns) + 1 + const countedColumn = Math.floor(index % grid.columns) + 1 + + const columnFromProps = cell.specialSizeMeasurements.elementGridProperties.gridColumnStart + const rowFromProps = cell.specialSizeMeasurements.elementGridProperties.gridRowStart + return { + elementPath: cell.elementPath, + globalFrame: cell.globalFrame, + column: + columnFromProps == null + ? countedColumn + : columnFromProps === 'auto' + ? countedColumn + : columnFromProps.numericalPosition ?? countedColumn, + row: + rowFromProps == null + ? countedRow + : rowFromProps === 'auto' + ? countedRow + : rowFromProps.numericalPosition ?? countedRow, + index: index, + } }, children) }) }, [grids, jsxMetadata]) @@ -132,7 +157,9 @@ export const GridControls = controlForStrategyMemoized(() => { const scaleRef = useRefEditorState((store) => store.editor.canvas.scale) const startInteractionWithUid = React.useCallback( - (uid: string) => (event: React.MouseEvent) => { + (params: { uid: string; row: number; column: number }) => (event: React.MouseEvent) => { + TargetGridCell.current = emptyGridCellCoordinates() + const start = windowToCanvasCoordinates( scaleRef.current, canvasOffsetRef.current, @@ -144,7 +171,7 @@ export const GridControls = controlForStrategyMemoized(() => { createInteractionViaMouse( start.canvasPositionRounded, Modifier.modifiersForEvent(event), - gridCellHandle(uid), + gridCellHandle({ id: params.uid }), 'zero-drag-not-permitted', ), ), @@ -153,6 +180,30 @@ export const GridControls = controlForStrategyMemoized(() => { [canvasOffsetRef, dispatch, scaleRef], ) + React.useEffect(() => { + function h(e: MouseEvent) { + if (!isActivelyDraggingCell) { + return + } + const cellsUnderMouse = document + .elementsFromPoint(e.clientX, e.clientY) + .filter((el) => el.id.startsWith(`gridcell-`)) + + // TODO this sucks! + if (cellsUnderMouse.length > 0) { + const cellUnderMouse = cellsUnderMouse[0] + const row = cellUnderMouse.getAttribute('data-grid-row') + const column = cellUnderMouse.getAttribute('data-grid-column') + TargetGridCell.current.row = row == null ? 0 : parseInt(row) + TargetGridCell.current.column = column == null ? 0 : parseInt(column) + } + } + window.addEventListener('mousemove', h) + return function () { + window.removeEventListener('mousemove', h) + } + }, [isActivelyDraggingCell]) + if (grids.length === 0) { return null } @@ -162,11 +213,11 @@ export const GridControls = controlForStrategyMemoized(() => { {/* grid lines */} {grids.map((grid, index) => { const placeholders = Array.from(Array(grid.cells).keys()) + return (
{ : `${grid.padding.top}px ${grid.padding.right}px ${grid.padding.bottom}px ${grid.padding.left}px`, }} > - {placeholders.map((cell) => { + {placeholders.map((cell, cellIndex) => { + const countedRow = Math.floor(cellIndex / grid.columns) + 1 + const countedColumn = Math.floor(cellIndex % grid.columns) + 1 + const id = `gridcell-${index}-${cell}` + const edgeColor = isActivelyDraggingCell ? '#00000033' : 'transparent' + const borderColor = isActivelyDraggingCell ? '#00000022' : '#0000000a' return (
+ data-grid-row={countedRow} + data-grid-column={countedColumn} + > +
+
+
+
+
+
+
+
) })}
@@ -199,22 +313,13 @@ export const GridControls = controlForStrategyMemoized(() => { })} {/* cell targets */} {cells.map((cell) => { - const isSelected = selectedViews.some((view) => EP.pathsEqual(cell.elementPath, view)) return ( - { left: cell.globalFrame.x, width: cell.globalFrame.width, height: cell.globalFrame.height, - backgroundColor: '#f0f', - opacity: !isActivelyDraggingCell || isSelected ? 0 : 0.2, }} /> ) From 81f5a7a4cb8ec3340d41989982458fbe43156a96 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 27 Jun 2024 16:46:43 +0200 Subject: [PATCH 19/29] wip - resize strategy --- .../canvas-strategies/canvas-strategies.tsx | 4 +- .../canvas-strategies/interaction-state.ts | 13 ++ .../strategies/grid-resize-strategy.ts | 220 ++++++++++++++++++ .../canvas/controls/grid-controls.tsx | 35 ++- .../canvas/controls/grid-strategy-utils.ts | 51 ++++ .../store/store-deep-equality-instances.ts | 14 ++ 6 files changed, 332 insertions(+), 5 deletions(-) create mode 100644 editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts create mode 100644 editor/src/components/canvas/controls/grid-strategy-utils.ts diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index 8dafee7fae9a..f1b08e82639e 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -65,6 +65,7 @@ import { retargetStrategyToChildrenOfFragmentLikeElements } from './strategies/f import { MetadataUtils } from '../../../core/model/element-metadata-utils' import { rearrangeGridSwapStrategy } from './strategies/rearrange-grid-swap-strategy' import { rearrangeGridMoveStrategy } from './strategies/rearrange-grid-move-strategy' +import { gridResizeStrategy } from './strategies/grid-resize-strategy' export type CanvasStrategyFactory = ( canvasState: InteractionCanvasState, @@ -104,13 +105,14 @@ const resizeStrategies: MetaCanvasStrategy = ( customStrategyState: CustomStrategyState, ): Array => { return mapDropNulls( - (factory) => factory(canvasState, interactionSession), + (factory) => factory(canvasState, interactionSession, customStrategyState), [ keyboardAbsoluteResizeStrategy, absoluteResizeBoundingBoxStrategy, flexResizeBasicStrategy, flexResizeStrategy, basicResizeStrategy, + gridResizeStrategy, ], ) } diff --git a/editor/src/components/canvas/canvas-strategies/interaction-state.ts b/editor/src/components/canvas/canvas-strategies/interaction-state.ts index 11d8b20855ea..7615cc1b4a69 100644 --- a/editor/src/components/canvas/canvas-strategies/interaction-state.ts +++ b/editor/src/components/canvas/canvas-strategies/interaction-state.ts @@ -571,6 +571,18 @@ export function gridCellHandle(params: { id: string }): GridCellHandle { } } +export interface GridResizeHandle { + type: 'GRID_RESIZE_HANDLE' + id: string +} + +export function gridResizeHandle(id: string): GridResizeHandle { + return { + type: 'GRID_RESIZE_HANDLE', + id: id, + } +} + export interface PaddingResizeHandle { type: 'PADDING_RESIZE_HANDLE' edgePiece: EdgePiece @@ -623,6 +635,7 @@ export type CanvasControlType = | ReorderSlider | BorderRadiusResizeHandle | GridCellHandle + | GridResizeHandle export function isDragToPan( interaction: InteractionSession | null, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts new file mode 100644 index 000000000000..d9753bcece90 --- /dev/null +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts @@ -0,0 +1,220 @@ +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import { stripNulls } from '../../../../core/shared/array-utils' +import * as EP from '../../../../core/shared/element-path' +import type { + ElementInstanceMetadata, + GridElementProperties, + GridPosition, +} from '../../../../core/shared/element-template' +import { + isFiniteRectangle, + offsetPoint, + rectContainsPointInclusive, +} from '../../../../core/shared/math-utils' +import { optionalMap } from '../../../../core/shared/optional-utils' +import type { ElementPath } from '../../../../core/shared/project-file-types' +import { create } from '../../../../core/shared/property-path' +import type { CanvasCommand } from '../../commands/commands' +import { deleteProperties } from '../../commands/delete-properties-command' +import { rearrangeChildren } from '../../commands/rearrange-children-command' +import { setProperty } from '../../commands/set-property-command' +import { GridControls } from '../../controls/grid-controls' +import { getGridPegs } from '../../controls/grid-strategy-utils' +import { recurseIntoChildrenOfMapOrFragment } from '../../gap-utils' +import type { CanvasStrategyFactory } from '../canvas-strategies' +import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' +import type { InteractionCanvasState } from '../canvas-strategy-types' +import { + getTargetPathsFromInteractionTarget, + emptyStrategyApplicationResult, + strategyApplicationResult, +} from '../canvas-strategy-types' +import type { InteractionSession } from '../interaction-state' + +export const gridResizeStrategy: CanvasStrategyFactory = ( + canvasState: InteractionCanvasState, + interactionSession: InteractionSession | null, +) => { + const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) + if (selectedElements.length !== 1) { + return null + } + + const selectedElement = selectedElements[0] + const ok = MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath( + canvasState.startingMetadata, + EP.parentPath(selectedElement), + ), + ) + if (!ok) { + return null + } + + return { + id: 'grid-resize-strategy', + name: 'Resize Grid', + descriptiveLabel: 'Resize Grid', + icon: { + category: 'tools', + type: 'pointer', + }, + controlsToRender: [ + { + control: GridControls, + props: {}, + key: `grid-controls-${EP.toString(selectedElement)}`, + show: 'always-visible', + }, + ], + fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_RESIZE_HANDLE', 1), + apply: () => { + if ( + interactionSession == null || + interactionSession.interactionData.type !== 'DRAG' || + interactionSession.interactionData.drag == null || + interactionSession.activeControl.type !== 'GRID_RESIZE_HANDLE' + ) { + return emptyStrategyApplicationResult + } + + const gridPegs = getGridPegs(selectedElement, canvasState.startingMetadata) + if (gridPegs == null) { + return emptyStrategyApplicationResult + } + + const children = recurseIntoChildrenOfMapOrFragment( + canvasState.startingMetadata, + canvasState.startingAllElementProps, + canvasState.startingElementPathTree, + EP.parentPath(selectedElement), + ) + + const pointOnCanvas = offsetPoint( + interactionSession.interactionData.dragStart, + interactionSession.interactionData.drag, + ) + + const pointerOverChild = children.find( + (c) => + c.globalFrame != null && + isFiniteRectangle(c.globalFrame) && + rectContainsPointInclusive(c.globalFrame, pointOnCanvas), + ) + + if ( + pointerOverChild == null || + EP.toUid(pointerOverChild.elementPath) === interactionSession.activeControl.id + ) { + return emptyStrategyApplicationResult + } + + const commands = swapChildrenCommands({ + grabbedElementUid: interactionSession.activeControl.id, + swapToElementUid: EP.toUid(pointerOverChild.elementPath), + children: children, + parentPath: EP.parentPath(selectedElement), + }) + + if (commands == null) { + return emptyStrategyApplicationResult + } + + return strategyApplicationResult(commands) + }, + } +} + +const GridPositioningProps: Array = [ + 'gridColumn', + 'gridRow', + 'gridColumnStart', + 'gridColumnEnd', + 'gridRowStart', + 'gridRowEnd', +] + +function gridPositionToValue(p: GridPosition | null): string | number | null { + if (p == null) { + return null + } + if (p === 'auto') { + return 'auto' + } + + return p.numericalPosition +} + +function setGridProps(elementPath: ElementPath, gridProps: GridElementProperties): CanvasCommand[] { + return stripNulls([ + optionalMap( + (s) => setProperty('always', elementPath, create('style', 'gridColumnStart'), s), + gridPositionToValue(gridProps.gridColumnStart), + ), + optionalMap( + (s) => setProperty('always', elementPath, create('style', 'gridColumnEnd'), s), + gridPositionToValue(gridProps.gridColumnEnd), + ), + optionalMap( + (s) => setProperty('always', elementPath, create('style', 'gridRowStart'), s), + gridPositionToValue(gridProps.gridRowStart), + ), + optionalMap( + (s) => setProperty('always', elementPath, create('style', 'gridRowEnd'), s), + gridPositionToValue(gridProps.gridRowEnd), + ), + ]) +} + +function swapChildrenCommands({ + grabbedElementUid, + swapToElementUid, + children, + parentPath, +}: { + grabbedElementUid: string + swapToElementUid: string + children: ElementInstanceMetadata[] + parentPath: ElementPath +}): CanvasCommand[] | null { + const grabbedElement = children.find((c) => EP.toUid(c.elementPath) === grabbedElementUid) + const swapToElement = children.find((c) => EP.toUid(c.elementPath) === swapToElementUid) + + if (grabbedElement == null || swapToElement == null) { + return null + } + + const rearrangedChildren = children + .map((c) => { + if (EP.pathsEqual(c.elementPath, grabbedElement.elementPath)) { + return swapToElement.elementPath + } + if (EP.pathsEqual(c.elementPath, swapToElement.elementPath)) { + return grabbedElement.elementPath + } + return c.elementPath + }) + .map((path) => EP.dynamicPathToStaticPath(path)) + + return [ + rearrangeChildren('always', parentPath, rearrangedChildren), + deleteProperties( + 'always', + swapToElement.elementPath, + GridPositioningProps.map((p) => create('style', p)), + ), + deleteProperties( + 'always', + grabbedElement.elementPath, + GridPositioningProps.map((p) => create('style', p)), + ), + ...setGridProps( + grabbedElement.elementPath, + swapToElement.specialSizeMeasurements.elementGridProperties, + ), + ...setGridProps( + swapToElement.elementPath, + grabbedElement.specialSizeMeasurements.elementGridProperties, + ), + ] +} diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index 8ed24a278586..bd9d5720b8d7 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -7,7 +7,11 @@ import { mapDropNulls } from '../../../core/shared/array-utils' import { isFiniteRectangle, windowPoint } from '../../../core/shared/math-utils' import { controlForStrategyMemoized } from '../canvas-strategies/canvas-strategy-types' import { useDispatch } from '../../editor/store/dispatch-context' -import { createInteractionViaMouse, gridCellHandle } from '../canvas-strategies/interaction-state' +import { + createInteractionViaMouse, + gridCellHandle, + gridResizeHandle, +} from '../canvas-strategies/interaction-state' import CanvasActions from '../canvas-actions' import { Modifier } from '../../../utils/modifiers' import { windowToCanvasCoordinates } from '../dom-lookup' @@ -39,7 +43,7 @@ function getSillyCellsCount(template: GridAutoOrTemplateBase | null): number { } } -function getNullableAutoOrTemplateBaeString( +function getNullableAutoOrTemplateBaseString( template: GridAutoOrTemplateBase | null, ): string | undefined { if (template == null) { @@ -204,6 +208,29 @@ export const GridControls = controlForStrategyMemoized(() => { } }, [isActivelyDraggingCell]) + const startResizeInteractionWithUid = React.useCallback( + (uid: string) => (event: React.MouseEvent) => { + event.stopPropagation() + const start = windowToCanvasCoordinates( + scaleRef.current, + canvasOffsetRef.current, + windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + ) + + dispatch([ + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start.canvasPositionRounded, + Modifier.modifiersForEvent(event), + gridResizeHandle(uid), + 'zero-drag-not-permitted', + ), + ), + ]) + }, + [canvasOffsetRef, dispatch, scaleRef], + ) + if (grids.length === 0) { return null } @@ -225,8 +252,8 @@ export const GridControls = controlForStrategyMemoized(() => { height: grid.frame.height, // backgroundColor: '#ff00ff09', display: 'grid', - gridTemplateColumns: getNullableAutoOrTemplateBaeString(grid.gridTemplateColumns), - gridTemplateRows: getNullableAutoOrTemplateBaeString(grid.gridTemplateRows), + gridTemplateColumns: getNullableAutoOrTemplateBaseString(grid.gridTemplateColumns), + gridTemplateRows: getNullableAutoOrTemplateBaseString(grid.gridTemplateRows), gap: grid.gap ?? 0, padding: grid.padding == null diff --git a/editor/src/components/canvas/controls/grid-strategy-utils.ts b/editor/src/components/canvas/controls/grid-strategy-utils.ts new file mode 100644 index 000000000000..91764e8ad288 --- /dev/null +++ b/editor/src/components/canvas/controls/grid-strategy-utils.ts @@ -0,0 +1,51 @@ +import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import * as EP from '../../../core/shared/element-path' +import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' +import { isFiniteRectangle } from '../../../core/shared/math-utils' +import type { ElementPath } from '../../../core/shared/project-file-types' + +export function getGridPegs(view: ElementPath, metadata: ElementInstanceMetadataMap) { + const element = MetadataUtils.findElementByElementPath(metadata, view) + const parent = MetadataUtils.findElementByElementPath(metadata, EP.parentPath(view)) + + const targetGridContainer = MetadataUtils.isGridLayoutedContainer(element) + ? element + : MetadataUtils.isGridLayoutedContainer(parent) + ? parent + : null + + if ( + targetGridContainer == null || + targetGridContainer.globalFrame == null || + !isFiniteRectangle(targetGridContainer.globalFrame) + ) { + return null + } + + const gap = targetGridContainer.specialSizeMeasurements.gap + const padding = targetGridContainer.specialSizeMeasurements.padding + const gridTemplateColumns = + targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns + const gridTemplateRows = + targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows + + function getSillyCellsCount(template: string | null) { + return template == null ? 0 : template.trim().split(/\s+/).length + } + const columns = getSillyCellsCount( + targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns, + ) + const rows = getSillyCellsCount( + targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows, + ) + + return { + elementPath: targetGridContainer.elementPath, + frame: targetGridContainer.globalFrame, + gridTemplateColumns: gridTemplateColumns, + gridTemplateRows: gridTemplateRows, + gap: gap, + padding: padding, + cells: rows * columns, + } +} diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index 235b8a7db53b..c09d073c95f9 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -454,6 +454,7 @@ import type { BorderRadiusResizeHandle, ZeroDragPermitted, GridCellHandle, + GridResizeHandle, } from '../../canvas/canvas-strategies/interaction-state' import { boundingArea, @@ -463,6 +464,7 @@ import { keyboardCatcherControl, resizeHandle, gridCellHandle, + gridResizeHandle, } from '../../canvas/canvas-strategies/interaction-state' import type { Modifiers } from '../../../utils/modifiers' import type { @@ -2831,6 +2833,13 @@ export const BorderRadiusResizeHandleKeepDeepEquality: KeepDeepEqualityCall< export const GridCellHandleKeepDeepEquality: KeepDeepEqualityCall = combine1EqualityCall((handle) => handle.id, createCallWithTripleEquals(), gridCellHandle) +export const GridResizeHandleKeepDeepEquality: KeepDeepEqualityCall = + combine1EqualityCall( + (handle) => handle.id, + createCallWithTripleEquals(), + gridResizeHandle, + ) + export const CanvasControlTypeKeepDeepEquality: KeepDeepEqualityCall = ( oldValue, newValue, @@ -2876,6 +2885,11 @@ export const CanvasControlTypeKeepDeepEquality: KeepDeepEqualityCall Date: Thu, 27 Jun 2024 17:17:35 +0200 Subject: [PATCH 20/29] rebase --- .../strategies/grid-resize-strategy.ts | 6 --- .../canvas/controls/grid-strategy-utils.ts | 51 ------------------- .../store/store-deep-equality-instances.ts | 6 ++- 3 files changed, 5 insertions(+), 58 deletions(-) delete mode 100644 editor/src/components/canvas/controls/grid-strategy-utils.ts diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts index d9753bcece90..244e0d7196a5 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts @@ -19,7 +19,6 @@ import { deleteProperties } from '../../commands/delete-properties-command' import { rearrangeChildren } from '../../commands/rearrange-children-command' import { setProperty } from '../../commands/set-property-command' import { GridControls } from '../../controls/grid-controls' -import { getGridPegs } from '../../controls/grid-strategy-utils' import { recurseIntoChildrenOfMapOrFragment } from '../../gap-utils' import type { CanvasStrategyFactory } from '../canvas-strategies' import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' @@ -78,11 +77,6 @@ export const gridResizeStrategy: CanvasStrategyFactory = ( return emptyStrategyApplicationResult } - const gridPegs = getGridPegs(selectedElement, canvasState.startingMetadata) - if (gridPegs == null) { - return emptyStrategyApplicationResult - } - const children = recurseIntoChildrenOfMapOrFragment( canvasState.startingMetadata, canvasState.startingAllElementProps, diff --git a/editor/src/components/canvas/controls/grid-strategy-utils.ts b/editor/src/components/canvas/controls/grid-strategy-utils.ts deleted file mode 100644 index 91764e8ad288..000000000000 --- a/editor/src/components/canvas/controls/grid-strategy-utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { MetadataUtils } from '../../../core/model/element-metadata-utils' -import * as EP from '../../../core/shared/element-path' -import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' -import { isFiniteRectangle } from '../../../core/shared/math-utils' -import type { ElementPath } from '../../../core/shared/project-file-types' - -export function getGridPegs(view: ElementPath, metadata: ElementInstanceMetadataMap) { - const element = MetadataUtils.findElementByElementPath(metadata, view) - const parent = MetadataUtils.findElementByElementPath(metadata, EP.parentPath(view)) - - const targetGridContainer = MetadataUtils.isGridLayoutedContainer(element) - ? element - : MetadataUtils.isGridLayoutedContainer(parent) - ? parent - : null - - if ( - targetGridContainer == null || - targetGridContainer.globalFrame == null || - !isFiniteRectangle(targetGridContainer.globalFrame) - ) { - return null - } - - const gap = targetGridContainer.specialSizeMeasurements.gap - const padding = targetGridContainer.specialSizeMeasurements.padding - const gridTemplateColumns = - targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns - const gridTemplateRows = - targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows - - function getSillyCellsCount(template: string | null) { - return template == null ? 0 : template.trim().split(/\s+/).length - } - const columns = getSillyCellsCount( - targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns, - ) - const rows = getSillyCellsCount( - targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows, - ) - - return { - elementPath: targetGridContainer.elementPath, - frame: targetGridContainer.globalFrame, - gridTemplateColumns: gridTemplateColumns, - gridTemplateRows: gridTemplateRows, - gap: gap, - padding: padding, - cells: rows * columns, - } -} diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index c09d073c95f9..9c66e31fb116 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -2831,7 +2831,11 @@ export const BorderRadiusResizeHandleKeepDeepEquality: KeepDeepEqualityCall< } export const GridCellHandleKeepDeepEquality: KeepDeepEqualityCall = - combine1EqualityCall((handle) => handle.id, createCallWithTripleEquals(), gridCellHandle) + combine1EqualityCall( + (handle) => handle.id, + createCallWithTripleEquals(), + (id) => gridCellHandle({ id }), + ) export const GridResizeHandleKeepDeepEquality: KeepDeepEqualityCall = combine1EqualityCall( From f678c48de7dcf4e47ee9f4818275a5882d9b92d9 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 27 Jun 2024 17:36:48 +0200 Subject: [PATCH 21/29] reize --- .../strategies/grid-resize-strategy.ts | 155 ++---------------- .../rearrange-grid-move-strategy.ts | 2 +- .../canvas/controls/grid-controls.tsx | 26 ++- 3 files changed, 37 insertions(+), 146 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts index 244e0d7196a5..8d5b39d122bc 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts @@ -1,25 +1,10 @@ import { MetadataUtils } from '../../../../core/model/element-metadata-utils' -import { stripNulls } from '../../../../core/shared/array-utils' import * as EP from '../../../../core/shared/element-path' -import type { - ElementInstanceMetadata, - GridElementProperties, - GridPosition, -} from '../../../../core/shared/element-template' -import { - isFiniteRectangle, - offsetPoint, - rectContainsPointInclusive, -} from '../../../../core/shared/math-utils' -import { optionalMap } from '../../../../core/shared/optional-utils' import type { ElementPath } from '../../../../core/shared/project-file-types' import { create } from '../../../../core/shared/property-path' import type { CanvasCommand } from '../../commands/commands' -import { deleteProperties } from '../../commands/delete-properties-command' -import { rearrangeChildren } from '../../commands/rearrange-children-command' import { setProperty } from '../../commands/set-property-command' -import { GridControls } from '../../controls/grid-controls' -import { recurseIntoChildrenOfMapOrFragment } from '../../gap-utils' +import { GridControls, TargetGridCell } from '../../controls/grid-controls' import type { CanvasStrategyFactory } from '../canvas-strategies' import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' import type { InteractionCanvasState } from '../canvas-strategy-types' @@ -77,138 +62,22 @@ export const gridResizeStrategy: CanvasStrategyFactory = ( return emptyStrategyApplicationResult } - const children = recurseIntoChildrenOfMapOrFragment( - canvasState.startingMetadata, - canvasState.startingAllElementProps, - canvasState.startingElementPathTree, - EP.parentPath(selectedElement), - ) - - const pointOnCanvas = offsetPoint( - interactionSession.interactionData.dragStart, - interactionSession.interactionData.drag, - ) - - const pointerOverChild = children.find( - (c) => - c.globalFrame != null && - isFiniteRectangle(c.globalFrame) && - rectContainsPointInclusive(c.globalFrame, pointOnCanvas), + return strategyApplicationResult( + resizeGridCellCommands(selectedElement, { + columnEnd: TargetGridCell.current.column + 1, + rowEnd: TargetGridCell.current.row + 1, + }), ) - - if ( - pointerOverChild == null || - EP.toUid(pointerOverChild.elementPath) === interactionSession.activeControl.id - ) { - return emptyStrategyApplicationResult - } - - const commands = swapChildrenCommands({ - grabbedElementUid: interactionSession.activeControl.id, - swapToElementUid: EP.toUid(pointerOverChild.elementPath), - children: children, - parentPath: EP.parentPath(selectedElement), - }) - - if (commands == null) { - return emptyStrategyApplicationResult - } - - return strategyApplicationResult(commands) }, } } -const GridPositioningProps: Array = [ - 'gridColumn', - 'gridRow', - 'gridColumnStart', - 'gridColumnEnd', - 'gridRowStart', - 'gridRowEnd', -] - -function gridPositionToValue(p: GridPosition | null): string | number | null { - if (p == null) { - return null - } - if (p === 'auto') { - return 'auto' - } - - return p.numericalPosition -} - -function setGridProps(elementPath: ElementPath, gridProps: GridElementProperties): CanvasCommand[] { - return stripNulls([ - optionalMap( - (s) => setProperty('always', elementPath, create('style', 'gridColumnStart'), s), - gridPositionToValue(gridProps.gridColumnStart), - ), - optionalMap( - (s) => setProperty('always', elementPath, create('style', 'gridColumnEnd'), s), - gridPositionToValue(gridProps.gridColumnEnd), - ), - optionalMap( - (s) => setProperty('always', elementPath, create('style', 'gridRowStart'), s), - gridPositionToValue(gridProps.gridRowStart), - ), - optionalMap( - (s) => setProperty('always', elementPath, create('style', 'gridRowEnd'), s), - gridPositionToValue(gridProps.gridRowEnd), - ), - ]) -} - -function swapChildrenCommands({ - grabbedElementUid, - swapToElementUid, - children, - parentPath, -}: { - grabbedElementUid: string - swapToElementUid: string - children: ElementInstanceMetadata[] - parentPath: ElementPath -}): CanvasCommand[] | null { - const grabbedElement = children.find((c) => EP.toUid(c.elementPath) === grabbedElementUid) - const swapToElement = children.find((c) => EP.toUid(c.elementPath) === swapToElementUid) - - if (grabbedElement == null || swapToElement == null) { - return null - } - - const rearrangedChildren = children - .map((c) => { - if (EP.pathsEqual(c.elementPath, grabbedElement.elementPath)) { - return swapToElement.elementPath - } - if (EP.pathsEqual(c.elementPath, swapToElement.elementPath)) { - return grabbedElement.elementPath - } - return c.elementPath - }) - .map((path) => EP.dynamicPathToStaticPath(path)) - +function resizeGridCellCommands( + elementPath: ElementPath, + { columnEnd, rowEnd }: { columnEnd: number; rowEnd: number }, +): CanvasCommand[] { return [ - rearrangeChildren('always', parentPath, rearrangedChildren), - deleteProperties( - 'always', - swapToElement.elementPath, - GridPositioningProps.map((p) => create('style', p)), - ), - deleteProperties( - 'always', - grabbedElement.elementPath, - GridPositioningProps.map((p) => create('style', p)), - ), - ...setGridProps( - grabbedElement.elementPath, - swapToElement.specialSizeMeasurements.elementGridProperties, - ), - ...setGridProps( - swapToElement.elementPath, - grabbedElement.specialSizeMeasurements.elementGridProperties, - ), + setProperty('always', elementPath, create('style', 'gridColumnEnd'), columnEnd), + setProperty('always', elementPath, create('style', 'gridRowEnd'), rowEnd), ] } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-move-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-move-strategy.ts index 52a2cd3ed756..abc2ed0da727 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-move-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/rearrange-grid-move-strategy.ts @@ -51,7 +51,7 @@ export const rearrangeGridMoveStrategy: CanvasStrategyFactory = ( show: 'always-visible', }, ], - fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 1) + 1, // add one so it wins over the swap + fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 2), apply: () => { if ( interactionSession == null || diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index bd9d5720b8d7..10ffd2a1e96a 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -54,11 +54,17 @@ function getNullableAutoOrTemplateBaseString( } export const GridControls = controlForStrategyMemoized(() => { + const selectedElement = useEditorState( + Substores.selectedViews, + (store) => store.editor.selectedViews.at(0) ?? null, + 'GridControls selectedElement', + ) const isActivelyDraggingCell = useEditorState( Substores.canvas, (store) => store.editor.canvas.interactionSession != null && - store.editor.canvas.interactionSession.activeControl.type === 'GRID_CELL_HANDLE', + (store.editor.canvas.interactionSession.activeControl.type === 'GRID_CELL_HANDLE' || + store.editor.canvas.interactionSession.activeControl.type === 'GRID_RESIZE_HANDLE'), '', ) @@ -354,8 +360,24 @@ export const GridControls = controlForStrategyMemoized(() => { left: cell.globalFrame.x, width: cell.globalFrame.width, height: cell.globalFrame.height, + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'flex-end', }} - /> + > +
+
) })} From 2044a98f50791eeecdf526f6b013e7fac25cb1f2 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 27 Jun 2024 18:52:56 +0200 Subject: [PATCH 22/29] resize with shadows --- .../strategies/grid-resize-strategy.ts | 8 +- .../canvas/controls/grid-controls.tsx | 173 +++++++++++++----- 2 files changed, 135 insertions(+), 46 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts index 8d5b39d122bc..1854c4deea01 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts @@ -4,7 +4,7 @@ import type { ElementPath } from '../../../../core/shared/project-file-types' import { create } from '../../../../core/shared/property-path' import type { CanvasCommand } from '../../commands/commands' import { setProperty } from '../../commands/set-property-command' -import { GridControls, TargetGridCell } from '../../controls/grid-controls' +import { GridControls, GridResizeShadow, TargetGridCell } from '../../controls/grid-controls' import type { CanvasStrategyFactory } from '../canvas-strategies' import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' import type { InteractionCanvasState } from '../canvas-strategy-types' @@ -50,6 +50,12 @@ export const gridResizeStrategy: CanvasStrategyFactory = ( key: `grid-controls-${EP.toString(selectedElement)}`, show: 'always-visible', }, + { + control: GridResizeShadow, + props: { elementPath: selectedElement }, + key: `grid-resize-shadow-${EP.toString(selectedElement)}`, + show: 'always-visible', + }, ], fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_RESIZE_HANDLE', 1), apply: () => { diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index 10ffd2a1e96a..e5236c03de57 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -4,7 +4,12 @@ import { Substores, useEditorState, useRefEditorState } from '../../editor/store import { MetadataUtils } from '../../../core/model/element-metadata-utils' import * as EP from '../../../core/shared/element-path' import { mapDropNulls } from '../../../core/shared/array-utils' -import { isFiniteRectangle, windowPoint } from '../../../core/shared/math-utils' +import { + isFiniteRectangle, + isInfinityRectangle, + windowPoint, + zeroRectIfNullOrInfinity, +} from '../../../core/shared/math-utils' import { controlForStrategyMemoized } from '../canvas-strategies/canvas-strategy-types' import { useDispatch } from '../../editor/store/dispatch-context' import { @@ -15,9 +20,14 @@ import { import CanvasActions from '../canvas-actions' import { Modifier } from '../../../utils/modifiers' import { windowToCanvasCoordinates } from '../dom-lookup' -import type { GridAutoOrTemplateBase } from '../../../core/shared/element-template' +import type { + ElementInstanceMetadata, + GridAutoOrTemplateBase, +} from '../../../core/shared/element-template' import { assertNever } from '../../../core/shared/utils' import { printGridAutoOrTemplateBase } from '../../../components/inspector/common/css-utils' +import type { ElementPath } from '../../../core/shared/project-file-types' +import { useColorTheme } from '../../../uuiui' type GridCellCoordinates = { row: number; column: number } @@ -54,18 +64,13 @@ function getNullableAutoOrTemplateBaseString( } export const GridControls = controlForStrategyMemoized(() => { - const selectedElement = useEditorState( - Substores.selectedViews, - (store) => store.editor.selectedViews.at(0) ?? null, - 'GridControls selectedElement', - ) const isActivelyDraggingCell = useEditorState( Substores.canvas, (store) => store.editor.canvas.interactionSession != null && (store.editor.canvas.interactionSession.activeControl.type === 'GRID_CELL_HANDLE' || store.editor.canvas.interactionSession.activeControl.type === 'GRID_RESIZE_HANDLE'), - '', + 'GridControls isActivelyDraggingCell', ) const jsxMetadata = useEditorState( @@ -214,29 +219,6 @@ export const GridControls = controlForStrategyMemoized(() => { } }, [isActivelyDraggingCell]) - const startResizeInteractionWithUid = React.useCallback( - (uid: string) => (event: React.MouseEvent) => { - event.stopPropagation() - const start = windowToCanvasCoordinates( - scaleRef.current, - canvasOffsetRef.current, - windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), - ) - - dispatch([ - CanvasActions.createInteractionSession( - createInteractionViaMouse( - start.canvasPositionRounded, - Modifier.modifiersForEvent(event), - gridResizeHandle(uid), - 'zero-drag-not-permitted', - ), - ), - ]) - }, - [canvasOffsetRef, dispatch, scaleRef], - ) - if (grids.length === 0) { return null } @@ -364,22 +346,123 @@ export const GridControls = controlForStrategyMemoized(() => { justifyContent: 'flex-end', alignItems: 'flex-end', }} - > -
-
+ /> ) })} ) }) + +export const GridResizeShadow = controlForStrategyMemoized( + ({ elementPath }: { elementPath: ElementPath }) => { + const element = useEditorState( + Substores.metadata, + (store) => MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, elementPath), + 'GridResizeShadow element', + ) + + const dispatch = useDispatch() + const canvasOffsetRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) + const scaleRef = useRefEditorState((store) => store.editor.canvas.scale) + + const dragging = useEditorState( + Substores.canvas, + (store) => + store.editor.canvas.interactionSession != null && + store.editor.canvas.interactionSession.activeControl.type === 'GRID_RESIZE_HANDLE', + '', + ) + const [offset, setOffset] = React.useState<{ width: number; height: number } | null>(null) + const onMouseMove = React.useCallback( + (e: MouseEvent) => { + if (!dragging) { + return + } + + setOffset((o) => + o == null ? null : { width: o.width + e.movementX, height: o.height + e.movementY }, + ) + }, + [dragging], + ) + + const onMouseUp = React.useCallback(() => setOffset(null), []) + + React.useEffect(() => { + window.addEventListener('mousemove', onMouseMove) + window.addEventListener('mouseup', onMouseUp) + return () => { + window.removeEventListener('mousemove', onMouseMove) + window.removeEventListener('mouseup', onMouseUp) + } + }, [onMouseMove, onMouseUp]) + + const startResizeInteractionWithUid = React.useCallback( + (uid: string) => (event: React.MouseEvent) => { + event.stopPropagation() + const frame = zeroRectIfNullOrInfinity(element?.globalFrame ?? null) + setOffset({ width: frame.width, height: frame.height }) + const start = windowToCanvasCoordinates( + scaleRef.current, + canvasOffsetRef.current, + windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + ) + dispatch([ + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start.canvasPositionRounded, + Modifier.modifiersForEvent(event), + gridResizeHandle(uid), + 'zero-drag-not-permitted', + ), + ), + ]) + }, + [canvasOffsetRef, dispatch, element?.globalFrame, scaleRef], + ) + + const colorTheme = useColorTheme() + + if ( + element == null || + element.globalFrame == null || + isInfinityRectangle(element.globalFrame) + ) { + return null + } + + return ( + +
+
+
+ + ) + }, +) From 737ee578f557f3536fdb4e267800fea4550c1a6b Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Thu, 27 Jun 2024 15:00:36 -0400 Subject: [PATCH 23/29] spike(canvas) Added grid column and row resizing. --- .../canvas-strategies/canvas-strategies.tsx | 6 +- .../canvas-strategies/interaction-state.ts | 15 ++ ...rategy.ts => grid-cell-resize-strategy.ts} | 8 +- .../strategies/resize-grid-strategy.ts | 100 +++++++++++++ .../canvas/controls/grid-controls.tsx | 136 +++++++++++++++++- .../components/inspector/common/css-utils.ts | 16 ++- editor/src/core/shared/element-template.ts | 6 + 7 files changed, 268 insertions(+), 19 deletions(-) rename editor/src/components/canvas/canvas-strategies/strategies/{grid-resize-strategy.ts => grid-cell-resize-strategy.ts} (94%) create mode 100644 editor/src/components/canvas/canvas-strategies/strategies/resize-grid-strategy.ts diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index f1b08e82639e..f4ab583b5c8c 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -65,7 +65,8 @@ import { retargetStrategyToChildrenOfFragmentLikeElements } from './strategies/f import { MetadataUtils } from '../../../core/model/element-metadata-utils' import { rearrangeGridSwapStrategy } from './strategies/rearrange-grid-swap-strategy' import { rearrangeGridMoveStrategy } from './strategies/rearrange-grid-move-strategy' -import { gridResizeStrategy } from './strategies/grid-resize-strategy' +import { gridCellResizeStrategy } from './strategies/grid-cell-resize-strategy' +import { resizeGridStrategy } from './strategies/resize-grid-strategy' export type CanvasStrategyFactory = ( canvasState: InteractionCanvasState, @@ -95,6 +96,7 @@ const moveOrReorderStrategies: MetaCanvasStrategy = ( reorderSliderStategy, rearrangeGridSwapStrategy, rearrangeGridMoveStrategy, + gridCellResizeStrategy, ], ) } @@ -112,7 +114,7 @@ const resizeStrategies: MetaCanvasStrategy = ( flexResizeBasicStrategy, flexResizeStrategy, basicResizeStrategy, - gridResizeStrategy, + resizeGridStrategy, ], ) } diff --git a/editor/src/components/canvas/canvas-strategies/interaction-state.ts b/editor/src/components/canvas/canvas-strategies/interaction-state.ts index 7615cc1b4a69..ebd54df0be16 100644 --- a/editor/src/components/canvas/canvas-strategies/interaction-state.ts +++ b/editor/src/components/canvas/canvas-strategies/interaction-state.ts @@ -583,6 +583,20 @@ export function gridResizeHandle(id: string): GridResizeHandle { } } +export interface GridAxisHandle { + type: 'GRID_AXIS_HANDLE' + axis: 'column' | 'row' + columnOrRow: number +} + +export function gridAxisHandle(axis: 'column' | 'row', columnOrRow: number): GridAxisHandle { + return { + type: 'GRID_AXIS_HANDLE', + axis: axis, + columnOrRow: columnOrRow, + } +} + export interface PaddingResizeHandle { type: 'PADDING_RESIZE_HANDLE' edgePiece: EdgePiece @@ -636,6 +650,7 @@ export type CanvasControlType = | BorderRadiusResizeHandle | GridCellHandle | GridResizeHandle + | GridAxisHandle export function isDragToPan( interaction: InteractionSession | null, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-cell-resize-strategy.ts similarity index 94% rename from editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts rename to editor/src/components/canvas/canvas-strategies/strategies/grid-cell-resize-strategy.ts index 1854c4deea01..1d2c4585c6c9 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-cell-resize-strategy.ts @@ -15,7 +15,7 @@ import { } from '../canvas-strategy-types' import type { InteractionSession } from '../interaction-state' -export const gridResizeStrategy: CanvasStrategyFactory = ( +export const gridCellResizeStrategy: CanvasStrategyFactory = ( canvasState: InteractionCanvasState, interactionSession: InteractionSession | null, ) => { @@ -36,9 +36,9 @@ export const gridResizeStrategy: CanvasStrategyFactory = ( } return { - id: 'grid-resize-strategy', - name: 'Resize Grid', - descriptiveLabel: 'Resize Grid', + id: 'grid-cell-resize-strategy', + name: 'Resize Grid Cell', + descriptiveLabel: 'Resize Grid Cell', icon: { category: 'tools', type: 'pointer', diff --git a/editor/src/components/canvas/canvas-strategies/strategies/resize-grid-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/resize-grid-strategy.ts new file mode 100644 index 000000000000..96ea5a7c7aee --- /dev/null +++ b/editor/src/components/canvas/canvas-strategies/strategies/resize-grid-strategy.ts @@ -0,0 +1,100 @@ +import { fromArrayIndex, fromField } from '../../../../core/shared/optics/optic-creators' +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import * as EP from '../../../../core/shared/element-path' +import * as PP from '../../../../core/shared/property-path' +import { setProperty } from '../../commands/set-property-command' +import { GridControls } from '../../controls/grid-controls' +import type { CanvasStrategyFactory } from '../canvas-strategies' +import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' +import type { InteractionCanvasState } from '../canvas-strategy-types' +import { + getTargetPathsFromInteractionTarget, + emptyStrategyApplicationResult, + strategyApplicationResult, +} from '../canvas-strategy-types' +import type { InteractionSession } from '../interaction-state' +import type { CSSNumber } from '../../../../components/inspector/common/css-utils' +import { printArrayCSSNumber } from '../../../../components/inspector/common/css-utils' +import { modify } from '../../../../core/shared/optics/optic-utilities' +import { setElementsToRerenderCommand } from '../../commands/set-elements-to-rerender-command' + +export const resizeGridStrategy: CanvasStrategyFactory = ( + canvasState: InteractionCanvasState, + interactionSession: InteractionSession | null, +) => { + const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) + if (selectedElements.length !== 1) { + return null + } + + const selectedElement = selectedElements[0] + const parentPath = EP.parentPath(selectedElement) + const ok = MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath(canvasState.startingMetadata, parentPath), + ) + if (!ok) { + return null + } + + return { + id: 'resize-grid-strategy', + name: 'Resize Grid', + descriptiveLabel: 'Resize Grid', + icon: { + category: 'tools', + type: 'pointer', + }, + controlsToRender: [ + { + control: GridControls, + props: {}, + key: `grid-controls-${EP.toString(selectedElement)}`, + show: 'always-visible', + }, + ], + fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_AXIS_HANDLE', 1), + apply: () => { + if ( + interactionSession == null || + interactionSession.interactionData.type !== 'DRAG' || + interactionSession.interactionData.drag == null || + interactionSession.activeControl.type !== 'GRID_AXIS_HANDLE' + ) { + return emptyStrategyApplicationResult + } + + const control = interactionSession.activeControl + const drag = interactionSession.interactionData.drag + const dragAmount = control.axis === 'column' ? drag.x : drag.y + const parentSpecialSizeMeasurements = + canvasState.startingMetadata[EP.toString(parentPath)].specialSizeMeasurements + const originalValues = + control.axis === 'column' + ? parentSpecialSizeMeasurements.containerGridProperties.gridTemplateColumns + : parentSpecialSizeMeasurements.containerGridProperties.gridTemplateRows + + if (originalValues == null || originalValues.type !== 'DIMENSIONS') { + return emptyStrategyApplicationResult + } + const originalDimensions = originalValues.dimensions + const updateOptic = fromArrayIndex(control.columnOrRow).compose(fromField('value')) + const newSetting = modify(updateOptic, (current) => current + dragAmount, originalDimensions) + const propertyValueAsString = printArrayCSSNumber(newSetting) + + const commands = [ + setProperty( + 'always', + parentPath, + PP.create( + 'style', + control.axis === 'column' ? 'gridTemplateColumns' : 'gridTemplateRows', + ), + propertyValueAsString, + ), + setElementsToRerenderCommand([parentPath]), + ] + + return strategyApplicationResult(commands) + }, + } +} diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index e5236c03de57..f0027729fe7a 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -16,6 +16,7 @@ import { createInteractionViaMouse, gridCellHandle, gridResizeHandle, + gridAxisHandle, } from '../canvas-strategies/interaction-state' import CanvasActions from '../canvas-actions' import { Modifier } from '../../../utils/modifiers' @@ -74,8 +75,8 @@ export const GridControls = controlForStrategyMemoized(() => { ) const jsxMetadata = useEditorState( - Substores.metadata, - (store) => store.editor.jsxMetadata, + Substores.fullStore, + (store) => store.editor.canvas.interactionSession?.latestMetadata ?? store.editor.jsxMetadata, 'GridControls jsxMetadata', ) @@ -83,11 +84,8 @@ export const GridControls = controlForStrategyMemoized(() => { Substores.metadataAndPropertyControlsInfo, (store) => { return mapDropNulls((view) => { - const element = MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, view) - const parent = MetadataUtils.findElementByElementPath( - store.editor.jsxMetadata, - EP.parentPath(view), - ) + const element = MetadataUtils.findElementByElementPath(jsxMetadata, view) + const parent = MetadataUtils.findElementByElementPath(jsxMetadata, EP.parentPath(view)) const targetGridContainer = MetadataUtils.isGridLayoutedContainer(element) ? element @@ -349,6 +347,130 @@ export const GridControls = controlForStrategyMemoized(() => { /> ) })} + {grids.flatMap((grid) => { + if (grid.gridTemplateColumns == null) { + return [] + } else { + switch (grid.gridTemplateColumns.type) { + case 'DIMENSIONS': + let workingPrefix: number = grid.frame.x + return grid.gridTemplateColumns.dimensions.flatMap((dimension, dimensionIndex) => { + // Assumes pixels currently. + workingPrefix += dimension.value + if (dimensionIndex !== 0) { + workingPrefix += grid.gap ?? 0 + } + function mouseDownHandler(event: React.MouseEvent): void { + const start = windowToCanvasCoordinates( + scaleRef.current, + canvasOffsetRef.current, + windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + ) + + dispatch([ + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start.canvasPositionRounded, + Modifier.modifiersForEvent(event), + gridAxisHandle('column', dimensionIndex), + 'zero-drag-not-permitted', + ), + ), + ]) + event.stopPropagation() + event.preventDefault() + } + return ( +
+ {`${dimension.value}${dimension.unit ?? ''}`} +
+ ) + }) + case 'FALLBACK': + return [] + default: + assertNever(grid.gridTemplateColumns) + return [] + } + } + })} + {grids.flatMap((grid) => { + if (grid.gridTemplateRows == null) { + return [] + } else { + switch (grid.gridTemplateRows.type) { + case 'DIMENSIONS': + let workingPrefix: number = grid.frame.y + return grid.gridTemplateRows.dimensions.flatMap((dimension, dimensionIndex) => { + // Assumes pixels currently. + workingPrefix += dimension.value + if (dimensionIndex !== 0) { + workingPrefix += grid.gap ?? 0 + } + function mouseDownHandler(event: React.MouseEvent): void { + const start = windowToCanvasCoordinates( + scaleRef.current, + canvasOffsetRef.current, + windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + ) + + dispatch([ + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start.canvasPositionRounded, + Modifier.modifiersForEvent(event), + gridAxisHandle('row', dimensionIndex), + 'zero-drag-not-permitted', + ), + ), + ]) + event.stopPropagation() + event.preventDefault() + } + return ( +
+ {`${dimension.value}${dimension.unit ?? ''}`} +
+ ) + }) + case 'FALLBACK': + return [] + default: + assertNever(grid.gridTemplateRows) + return [] + } + } + })} ) }) diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index d307f73c2aab..8f57a794fb15 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -739,15 +739,19 @@ export function printCSSNumber( } } +export function printArrayCSSNumber(array: Array): string { + return array + .map((dimension) => { + const printed = printCSSNumber(dimension, null) + return typeof printed === 'string' ? printed : `${printed}` + }) + .join(' ') +} + export function printGridAutoOrTemplateBase(input: GridAutoOrTemplateBase): string { switch (input.type) { case 'DIMENSIONS': - return input.dimensions - .map((dimension) => { - const printed = printCSSNumber(dimension, null) - return typeof printed === 'string' ? printed : `${printed}` - }) - .join(' ') + return printArrayCSSNumber(input.dimensions) case 'FALLBACK': return input.value default: diff --git a/editor/src/core/shared/element-template.ts b/editor/src/core/shared/element-template.ts index 6bf495b9a7d3..ccc3a99e8900 100644 --- a/editor/src/core/shared/element-template.ts +++ b/editor/src/core/shared/element-template.ts @@ -2617,6 +2617,12 @@ export function gridAutoOrTemplateDimensions( export type GridAutoOrTemplateBase = GridAutoOrTemplateDimensions | GridAutoOrTemplateFallback +export function isGridAutoOrTemplateDimensions( + value: GridAutoOrTemplateBase, +): value is GridAutoOrTemplateDimensions { + return value.type === 'DIMENSIONS' +} + export type GridAuto = GridAutoOrTemplateBase export type GridTemplate = GridAutoOrTemplateBase From b637a51af2720cfbaa11efc60cbe0353858f2a68 Mon Sep 17 00:00:00 2001 From: Federico Ruggi Date: Thu, 27 Jun 2024 17:52:02 -0400 Subject: [PATCH 24/29] roll your own grid --- .../canvas/controls/grid-controls.tsx | 760 ++++++++++++------ .../canvas/controls/new-canvas-controls.tsx | 121 ++- editor/src/utils/feature-switches.ts | 24 + 3 files changed, 637 insertions(+), 268 deletions(-) diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index f0027729fe7a..bfa09c035f28 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -4,9 +4,15 @@ import { Substores, useEditorState, useRefEditorState } from '../../editor/store import { MetadataUtils } from '../../../core/model/element-metadata-utils' import * as EP from '../../../core/shared/element-path' import { mapDropNulls } from '../../../core/shared/array-utils' +import type { CanvasPoint, CanvasRectangle } from '../../../core/shared/math-utils' import { + canvasPoint, + distance, + getRectCenter, isFiniteRectangle, isInfinityRectangle, + offsetPoint, + pointDifference, windowPoint, zeroRectIfNullOrInfinity, } from '../../../core/shared/math-utils' @@ -27,8 +33,13 @@ import type { } from '../../../core/shared/element-template' import { assertNever } from '../../../core/shared/utils' import { printGridAutoOrTemplateBase } from '../../../components/inspector/common/css-utils' -import type { ElementPath } from '../../../core/shared/project-file-types' +import { when } from '../../../utils/react-conditionals' +import { CanvasMousePositionRaw } from '../../../utils/global-positions' +import { motion, useAnimationControls } from 'framer-motion' +import { atom, useAtom } from 'jotai' +import { isFeatureEnabled } from '../../../utils/feature-switches' import { useColorTheme } from '../../../uuiui' +import type { ElementPath } from 'utopia-shared/src/types' type GridCellCoordinates = { row: number; column: number } @@ -64,14 +75,83 @@ function getNullableAutoOrTemplateBaseString( } } +export const defaultExperimentalGridFeatures = { + dragLockedToCenter: false, + dragVerbatim: false, + dragMagnetic: false, + dragRatio: true, + animateSnap: true, + dotgrid: true, + shadow: true, + adaptiveOpacity: true, + activeGridColor: '#0099ff77', + dotgridColor: '#0099ffaa', + inactiveGridColor: '#0000000a', + opacityBaseline: 0.25, +} + +export const gridFeaturesExplained: Record = { + adaptiveOpacity: 'shadow opacity is proportional to the drag distance', + dragLockedToCenter: 'drag will keep the shadow centered', + dragVerbatim: 'drag will be verbatim', + dragMagnetic: 'drag will magnetize to the snap regions', + dragRatio: 'drag will keep the shadow positioned based on the drag start', + animateSnap: 'the shadow goes *boop* when snapping', + dotgrid: 'show dotgrid', + shadow: 'show the shadow during drag', + activeGridColor: 'grid lines color during drag', + dotgridColor: 'dotgrid items color', + inactiveGridColor: 'grid lines color when not dragging', + opacityBaseline: 'maximum shadow opacity', +} + +export const experimentalGridFeatures = atom(defaultExperimentalGridFeatures) + export const GridControls = controlForStrategyMemoized(() => { - const isActivelyDraggingCell = useEditorState( + const [features, setFeatures] = useAtom(experimentalGridFeatures) + + React.useEffect(() => { + setFeatures((old) => ({ + ...old, + adaptiveOpacity: isFeatureEnabled('Grid move - adaptiveOpacity'), + dragLockedToCenter: isFeatureEnabled('Grid move - dragLockedToCenter'), + dragVerbatim: isFeatureEnabled('Grid move - dragVerbatim'), + dragMagnetic: isFeatureEnabled('Grid move - dragMagnetic'), + dragRatio: isFeatureEnabled('Grid move - dragRatio'), + animateSnap: isFeatureEnabled('Grid move - animateSnap'), + dotgrid: isFeatureEnabled('Grid move - dotgrid'), + shadow: isFeatureEnabled('Grid move - shadow'), + })) + }, [setFeatures]) + + const activelyDraggingOrResizingCell = useEditorState( Substores.canvas, (store) => store.editor.canvas.interactionSession != null && (store.editor.canvas.interactionSession.activeControl.type === 'GRID_CELL_HANDLE' || - store.editor.canvas.interactionSession.activeControl.type === 'GRID_RESIZE_HANDLE'), - 'GridControls isActivelyDraggingCell', + store.editor.canvas.interactionSession.activeControl.type === 'GRID_RESIZE_HANDLE') + ? store.editor.canvas.interactionSession.activeControl.id + : null, + '', + ) + + const dragging = useEditorState( + Substores.canvas, + (store) => + store.editor.canvas.interactionSession != null && + store.editor.canvas.interactionSession.activeControl.type === 'GRID_CELL_HANDLE' + ? store.editor.canvas.interactionSession.activeControl.id + : null, + '', + ) + + const interactionSession = useEditorState( + Substores.canvas, + (store) => + store.editor.canvas.interactionSession?.interactionData.type === 'DRAG' + ? store.editor.canvas.interactionSession.interactionData + : null, + '', ) const jsxMetadata = useEditorState( @@ -146,6 +226,7 @@ export const GridControls = controlForStrategyMemoized(() => { return { elementPath: cell.elementPath, globalFrame: cell.globalFrame, + borderRadius: cell.specialSizeMeasurements.borderRadius, column: columnFromProps == null ? countedColumn @@ -169,33 +250,62 @@ export const GridControls = controlForStrategyMemoized(() => { const canvasOffsetRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) const scaleRef = useRefEditorState((store) => store.editor.canvas.scale) + const shadow = React.useMemo(() => { + return cells.find((cell) => EP.toUid(cell.elementPath) === dragging) + }, [cells, dragging]) + + const [shadowFrame, setShadowFrame] = React.useState( + shadow?.globalFrame ?? null, + ) + const startInteractionWithUid = React.useCallback( - (params: { uid: string; row: number; column: number }) => (event: React.MouseEvent) => { - TargetGridCell.current = emptyGridCellCoordinates() - - const start = windowToCanvasCoordinates( - scaleRef.current, - canvasOffsetRef.current, - windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), - ) - - dispatch([ - CanvasActions.createInteractionSession( - createInteractionViaMouse( - start.canvasPositionRounded, - Modifier.modifiersForEvent(event), - gridCellHandle({ id: params.uid }), - 'zero-drag-not-permitted', + (params: { uid: string; row: number; column: number; frame: CanvasRectangle }) => + (event: React.MouseEvent) => { + TargetGridCell.current = emptyGridCellCoordinates() + + setShadowFrame(params.frame) + + const start = windowToCanvasCoordinates( + scaleRef.current, + canvasOffsetRef.current, + windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + ) + + dispatch([ + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start.canvasPositionRounded, + Modifier.modifiersForEvent(event), + gridCellHandle({ id: params.uid }), + 'zero-drag-not-permitted', + ), ), - ), - ]) - }, + ]) + }, [canvasOffsetRef, dispatch, scaleRef], ) + const [hoveringCell, setHoveringCell] = React.useState(null) + const [hoveringStart, setHoveringStart] = React.useState<{ + id: string + point: CanvasPoint + } | null>(null) + const controls = useAnimationControls() + + React.useEffect(() => { + if (hoveringCell == null) { + return + } + if (!features.animateSnap) { + return + } + void controls.start('boop') + }, [hoveringCell, controls, features]) + React.useEffect(() => { function h(e: MouseEvent) { - if (!isActivelyDraggingCell) { + if (activelyDraggingOrResizingCell == null) { + setHoveringStart(null) return } const cellsUnderMouse = document @@ -209,269 +319,389 @@ export const GridControls = controlForStrategyMemoized(() => { const column = cellUnderMouse.getAttribute('data-grid-column') TargetGridCell.current.row = row == null ? 0 : parseInt(row) TargetGridCell.current.column = column == null ? 0 : parseInt(column) + setHoveringCell(cellUnderMouse.id) + + setHoveringStart((start) => { + if (start == null || start.id !== cellUnderMouse.id) { + return { id: cellUnderMouse.id, point: canvasPoint(CanvasMousePositionRaw!) } + } + return start + }) } } window.addEventListener('mousemove', h) return function () { window.removeEventListener('mousemove', h) } - }, [isActivelyDraggingCell]) + }, [activelyDraggingOrResizingCell]) if (grids.length === 0) { return null } return ( - - {/* grid lines */} - {grids.map((grid, index) => { - const placeholders = Array.from(Array(grid.cells).keys()) - - return ( -
- {placeholders.map((cell, cellIndex) => { - const countedRow = Math.floor(cellIndex / grid.columns) + 1 - const countedColumn = Math.floor(cellIndex % grid.columns) + 1 - const id = `gridcell-${index}-${cell}` - const edgeColor = isActivelyDraggingCell ? '#00000033' : 'transparent' - const borderColor = isActivelyDraggingCell ? '#00000022' : '#0000000a' - return ( -
-
-
-
-
-
-
-
-
- ) - })} -
- ) - })} - {/* cell targets */} - {cells.map((cell) => { - return ( -
- ) - })} - {grids.flatMap((grid) => { - if (grid.gridTemplateColumns == null) { - return [] - } else { - switch (grid.gridTemplateColumns.type) { - case 'DIMENSIONS': - let workingPrefix: number = grid.frame.x - return grid.gridTemplateColumns.dimensions.flatMap((dimension, dimensionIndex) => { - // Assumes pixels currently. - workingPrefix += dimension.value - if (dimensionIndex !== 0) { - workingPrefix += grid.gap ?? 0 - } - function mouseDownHandler(event: React.MouseEvent): void { - const start = windowToCanvasCoordinates( - scaleRef.current, - canvasOffsetRef.current, - windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), - ) - - dispatch([ - CanvasActions.createInteractionSession( - createInteractionViaMouse( - start.canvasPositionRounded, - Modifier.modifiersForEvent(event), - gridAxisHandle('column', dimensionIndex), - 'zero-drag-not-permitted', - ), - ), - ]) - event.stopPropagation() - event.preventDefault() - } + + + {/* grid lines */} + {grids.map((grid, index) => { + const placeholders = Array.from(Array(grid.cells).keys()) + + return ( +
+ {placeholders.map((cell, cellIndex) => { + const countedRow = Math.floor(cellIndex / grid.columns) + 1 + const countedColumn = Math.floor(cellIndex % grid.columns) + 1 + const id = `gridcell-${index}-${cell}` + const dotgridColor = + activelyDraggingOrResizingCell != null ? features.dotgridColor : 'transparent' + const borderColor = + activelyDraggingOrResizingCell != null + ? features.activeGridColor + : features.inactiveGridColor return (
- {`${dimension.value}${dimension.unit ?? ''}`} + {when( + features.dotgrid, + <> +
+
+
+
+
+
+
+ , + )}
) - }) - case 'FALLBACK': - return [] - default: - assertNever(grid.gridTemplateColumns) - return [] - } - } - })} - {grids.flatMap((grid) => { - if (grid.gridTemplateRows == null) { - return [] - } else { - switch (grid.gridTemplateRows.type) { - case 'DIMENSIONS': - let workingPrefix: number = grid.frame.y - return grid.gridTemplateRows.dimensions.flatMap((dimension, dimensionIndex) => { - // Assumes pixels currently. - workingPrefix += dimension.value - if (dimensionIndex !== 0) { - workingPrefix += grid.gap ?? 0 - } - function mouseDownHandler(event: React.MouseEvent): void { - const start = windowToCanvasCoordinates( - scaleRef.current, - canvasOffsetRef.current, - windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + })} +
+ ) + })} + {grids.flatMap((grid) => { + if (grid.gridTemplateColumns == null) { + return [] + } else { + switch (grid.gridTemplateColumns.type) { + case 'DIMENSIONS': + let workingPrefix: number = grid.frame.x + return grid.gridTemplateColumns.dimensions.flatMap((dimension, dimensionIndex) => { + // Assumes pixels currently. + workingPrefix += dimension.value + if (dimensionIndex !== 0) { + workingPrefix += grid.gap ?? 0 + } + function mouseDownHandler(event: React.MouseEvent): void { + const start = windowToCanvasCoordinates( + scaleRef.current, + canvasOffsetRef.current, + windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + ) + + dispatch([ + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start.canvasPositionRounded, + Modifier.modifiersForEvent(event), + gridAxisHandle('column', dimensionIndex), + 'zero-drag-not-permitted', + ), + ), + ]) + event.stopPropagation() + event.preventDefault() + } + return ( +
+ {`${dimension.value}${dimension.unit ?? ''}`} +
) - - dispatch([ - CanvasActions.createInteractionSession( - createInteractionViaMouse( - start.canvasPositionRounded, - Modifier.modifiersForEvent(event), - gridAxisHandle('row', dimensionIndex), - 'zero-drag-not-permitted', + }) + case 'FALLBACK': + return [] + default: + assertNever(grid.gridTemplateColumns) + return [] + } + } + })} + {grids.flatMap((grid) => { + if (grid.gridTemplateRows == null) { + return [] + } else { + switch (grid.gridTemplateRows.type) { + case 'DIMENSIONS': + let workingPrefix: number = grid.frame.y + return grid.gridTemplateRows.dimensions.flatMap((dimension, dimensionIndex) => { + // Assumes pixels currently. + workingPrefix += dimension.value + if (dimensionIndex !== 0) { + workingPrefix += grid.gap ?? 0 + } + function mouseDownHandler(event: React.MouseEvent): void { + const start = windowToCanvasCoordinates( + scaleRef.current, + canvasOffsetRef.current, + windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + ) + + dispatch([ + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start.canvasPositionRounded, + Modifier.modifiersForEvent(event), + gridAxisHandle('row', dimensionIndex), + 'zero-drag-not-permitted', + ), ), - ), - ]) - event.stopPropagation() - event.preventDefault() - } - return ( -
- {`${dimension.value}${dimension.unit ?? ''}`} -
- ) - }) - case 'FALLBACK': - return [] - default: - assertNever(grid.gridTemplateRows) - return [] + ]) + event.stopPropagation() + event.preventDefault() + } + return ( +
+ {`${dimension.value}${dimension.unit ?? ''}`} +
+ ) + }) + case 'FALLBACK': + return [] + default: + assertNever(grid.gridTemplateRows) + return [] + } } - } - })} - + })} + {/* cell targets */} + {cells.map((cell) => { + return ( +
+ ) + })} + {/* shadow */} + {features.shadow && + shadow != null && + shadowFrame != null && + interactionSession?.dragStart != null && + interactionSession?.drag != null && + hoveringStart != null && ( + + )} + + ) }) diff --git a/editor/src/components/canvas/controls/new-canvas-controls.tsx b/editor/src/components/canvas/controls/new-canvas-controls.tsx index dcaa1eb144ab..d522f9ef21f9 100644 --- a/editor/src/components/canvas/controls/new-canvas-controls.tsx +++ b/editor/src/components/canvas/controls/new-canvas-controls.tsx @@ -71,11 +71,15 @@ import { useSelectionArea } from './selection-area-hooks' import { RemixSceneLabelControl } from './select-mode/remix-scene-label' import { NO_OP } from '../../../core/shared/utils' import { useIsMyProject } from '../../editor/store/collaborative-editing' -import { useStatus } from '../../../../liveblocks.config' import { MultiplayerWrapper } from '../../../utils/multiplayer-wrapper' import { MultiplayerPresence } from '../multiplayer-presence' -import { isFeatureEnabled } from '../../../utils/feature-switches' -import { GridControls } from './grid-controls' +import { setFeatureEnabled } from '../../../utils/feature-switches' +import { + defaultExperimentalGridFeatures, + experimentalGridFeatures, + gridFeaturesExplained, +} from './grid-controls' +import { motion } from 'framer-motion' export const CanvasControlsContainerID = 'new-canvas-controls-container' @@ -519,6 +523,8 @@ const NewCanvasControlsInner = (props: NewCanvasControlsInnerProps) => { const resizeStatus = getResizeStatus() + const [gridFeatures, setGridFeatures] = useAtom(experimentalGridFeatures) + return ( <>
{ )} {when(isSelectMode(editorMode), )} +
+

+ + Roll your own damn grid + Roll your own damn grid + +

+ + {Object.entries(gridFeatures).map(([feat, value]) => { + const isDefault = (defaultExperimentalGridFeatures as any)[feat] === true + return ( +
+
+
+ {feat} {isDefault ? '(default)' : ''} +
+
+ {gridFeaturesExplained[feat]} +
+
+ {typeof value === 'boolean' && ( + { + e.stopPropagation() + setGridFeatures({ ...gridFeatures, [feat]: e.target.checked }) + setFeatureEnabled(`Grid move - ${feat}` as any, e.target.checked) // terrible hacks on top of terrible hacks + }} + /> + )} + {typeof value === 'string' && ( + { + e.stopPropagation() + setGridFeatures({ ...gridFeatures, [feat]: e.target.value }) + }} + /> + )} + {typeof value === 'number' && ( + { + e.stopPropagation() + setGridFeatures({ + ...gridFeatures, + [feat]: parseFloat(e.target.value), + }) + }} + /> + )} +
+ ) + })} +
, )} , diff --git a/editor/src/utils/feature-switches.ts b/editor/src/utils/feature-switches.ts index 88d2b5b74c45..18917b4bbd8b 100644 --- a/editor/src/utils/feature-switches.ts +++ b/editor/src/utils/feature-switches.ts @@ -15,6 +15,14 @@ export type FeatureName = | 'Debug - Print UIDs' | 'Debug – Connections' | 'Condensed Navigator Entries' + | 'Grid move - adaptiveOpacity' + | 'Grid move - dragLockedToCenter' + | 'Grid move - dragVerbatim' + | 'Grid move - animateSnap' + | 'Grid move - dotgrid' + | 'Grid move - shadow' + | 'Grid move - dragMagnetic' + | 'Grid move - dragRatio' export const AllFeatureNames: FeatureName[] = [ // 'Dragging Reparents By Default', // Removing this option so that we can experiment on this later @@ -30,6 +38,14 @@ export const AllFeatureNames: FeatureName[] = [ 'Debug - Print UIDs', 'Debug – Connections', 'Condensed Navigator Entries', + 'Grid move - adaptiveOpacity', + 'Grid move - dragLockedToCenter', + 'Grid move - dragVerbatim', + 'Grid move - animateSnap', + 'Grid move - dotgrid', + 'Grid move - shadow', + 'Grid move - dragMagnetic', + 'Grid move - dragRatio', ] let FeatureSwitches: { [feature in FeatureName]: boolean } = { @@ -45,6 +61,14 @@ let FeatureSwitches: { [feature in FeatureName]: boolean } = { 'Debug - Print UIDs': false, 'Debug – Connections': false, 'Condensed Navigator Entries': !IS_TEST_ENVIRONMENT, + 'Grid move - adaptiveOpacity': true, + 'Grid move - dragLockedToCenter': false, + 'Grid move - dragVerbatim': false, + 'Grid move - animateSnap': true, + 'Grid move - dotgrid': true, + 'Grid move - shadow': true, + 'Grid move - dragMagnetic': false, + 'Grid move - dragRatio': true, } export const STEGANOGRAPHY_ENABLED = false From 8575ab1908fad5a78d080a866e50fbffb74a870d Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Thu, 27 Jun 2024 18:17:51 -0400 Subject: [PATCH 25/29] spike(canvas) Some basic handling of fractional values. --- .../strategies/resize-grid-strategy.ts | 50 ++- .../canvas/controls/grid-controls.tsx | 380 +++++++++++------- editor/src/components/canvas/dom-walker.ts | 4 + .../store-deep-equality-instances-3.spec.ts | 84 ++++ .../store/store-deep-equality-instances.ts | 46 ++- .../components/inspector/common/css-utils.ts | 37 +- editor/src/core/shared/element-template.ts | 23 +- 7 files changed, 453 insertions(+), 171 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/resize-grid-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/resize-grid-strategy.ts index 96ea5a7c7aee..914bf68d1894 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/resize-grid-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/resize-grid-strategy.ts @@ -1,4 +1,9 @@ -import { fromArrayIndex, fromField } from '../../../../core/shared/optics/optic-creators' +import { + filtered, + fromArrayIndex, + fromField, + notNull, +} from '../../../../core/shared/optics/optic-creators' import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import * as EP from '../../../../core/shared/element-path' import * as PP from '../../../../core/shared/property-path' @@ -13,10 +18,12 @@ import { strategyApplicationResult, } from '../canvas-strategy-types' import type { InteractionSession } from '../interaction-state' -import type { CSSNumber } from '../../../../components/inspector/common/css-utils' +import type { CSSNumber, GridCSSNumber } from '../../../../components/inspector/common/css-utils' import { printArrayCSSNumber } from '../../../../components/inspector/common/css-utils' -import { modify } from '../../../../core/shared/optics/optic-utilities' +import { any, anyBy, modify, toFirst } from '../../../../core/shared/optics/optic-utilities' import { setElementsToRerenderCommand } from '../../commands/set-elements-to-rerender-command' +import { isRight } from '../../../../core/shared/either' +import { roundToNearestWhole } from '../../../../core/shared/math-utils' export const resizeGridStrategy: CanvasStrategyFactory = ( canvasState: InteractionCanvasState, @@ -69,16 +76,47 @@ export const resizeGridStrategy: CanvasStrategyFactory = ( const parentSpecialSizeMeasurements = canvasState.startingMetadata[EP.toString(parentPath)].specialSizeMeasurements const originalValues = + control.axis === 'column' + ? parentSpecialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateColumns + : parentSpecialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateRows + const calculatedValues = control.axis === 'column' ? parentSpecialSizeMeasurements.containerGridProperties.gridTemplateColumns : parentSpecialSizeMeasurements.containerGridProperties.gridTemplateRows - if (originalValues == null || originalValues.type !== 'DIMENSIONS') { + if ( + calculatedValues == null || + calculatedValues.type !== 'DIMENSIONS' || + originalValues == null || + originalValues.type !== 'DIMENSIONS' + ) { return emptyStrategyApplicationResult } + const unitOptic = fromArrayIndex(control.columnOrRow) + .compose(fromField('unit')) + .compose(notNull()) + const valueOptic = fromArrayIndex(control.columnOrRow).compose( + fromField('value'), + ) + const isFractional = anyBy(unitOptic, (unit) => unit === 'fr', originalValues.dimensions) + let newSetting: Array const originalDimensions = originalValues.dimensions - const updateOptic = fromArrayIndex(control.columnOrRow).compose(fromField('value')) - const newSetting = modify(updateOptic, (current) => current + dragAmount, originalDimensions) + if (isFractional) { + const possibleOriginalFractionalValue = toFirst(valueOptic, originalValues.dimensions) + const possibleCalculatedValue = toFirst(valueOptic, calculatedValues.dimensions) + if (isRight(possibleOriginalFractionalValue) && isRight(possibleCalculatedValue)) { + const originalFractionalValue = possibleOriginalFractionalValue.value + const calculatedValue = possibleCalculatedValue.value + const perPointOne = + originalFractionalValue == 0 ? 10 : (calculatedValue / originalFractionalValue) * 0.1 + const newValue = roundToNearestWhole((dragAmount / perPointOne) * 10) / 10 + newSetting = modify(valueOptic, (current) => current + newValue, originalDimensions) + } else { + throw new Error(`Somehow we cannot identify the right dimensions.`) + } + } else { + newSetting = modify(valueOptic, (current) => current + dragAmount, originalDimensions) + } const propertyValueAsString = printArrayCSSNumber(newSetting) const commands = [ diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index bfa09c035f28..36be2b5182aa 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -1,7 +1,7 @@ import React from 'react' import { CanvasOffsetWrapper } from './canvas-offset-wrapper' import { Substores, useEditorState, useRefEditorState } from '../../editor/store/store-hook' -import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import { MetadataUtils, getSimpleAttributeAtPath } from '../../../core/model/element-metadata-utils' import * as EP from '../../../core/shared/element-path' import { mapDropNulls } from '../../../core/shared/array-utils' import type { CanvasPoint, CanvasRectangle } from '../../../core/shared/math-utils' @@ -27,11 +27,13 @@ import { import CanvasActions from '../canvas-actions' import { Modifier } from '../../../utils/modifiers' import { windowToCanvasCoordinates } from '../dom-lookup' -import type { - ElementInstanceMetadata, - GridAutoOrTemplateBase, +import { + isGridAutoOrTemplateDimensions, + type ElementInstanceMetadata, + type GridAutoOrTemplateBase, } from '../../../core/shared/element-template' import { assertNever } from '../../../core/shared/utils' +import type { GridCSSNumber } from '../../../components/inspector/common/css-utils' import { printGridAutoOrTemplateBase } from '../../../components/inspector/common/css-utils' import { when } from '../../../utils/react-conditionals' import { CanvasMousePositionRaw } from '../../../utils/global-positions' @@ -39,7 +41,15 @@ import { motion, useAnimationControls } from 'framer-motion' import { atom, useAtom } from 'jotai' import { isFeatureEnabled } from '../../../utils/feature-switches' import { useColorTheme } from '../../../uuiui' -import type { ElementPath } from 'utopia-shared/src/types' +import type { Optic } from '../../../core/shared/optics/optics' +import { + fromArrayIndex, + fromField, + fromTypeGuard, + notNull, +} from '../../../core/shared/optics/optic-creators' +import { toFirst } from '../../../core/shared/optics/optic-utilities' +import { defaultEither } from '../../../core/shared/either' type GridCellCoordinates = { row: number; column: number } @@ -75,37 +85,25 @@ function getNullableAutoOrTemplateBaseString( } } -export const defaultExperimentalGridFeatures = { - dragLockedToCenter: false, - dragVerbatim: false, - dragMagnetic: false, - dragRatio: true, - animateSnap: true, - dotgrid: true, - shadow: true, - adaptiveOpacity: true, - activeGridColor: '#0099ff77', - dotgridColor: '#0099ffaa', - inactiveGridColor: '#0000000a', - opacityBaseline: 0.25, +function getFromPropsOptic(index: number): Optic { + return notNull() + .compose(fromTypeGuard(isGridAutoOrTemplateDimensions)) + .compose(fromField('dimensions')) + .compose(fromArrayIndex(index)) } -export const gridFeaturesExplained: Record = { - adaptiveOpacity: 'shadow opacity is proportional to the drag distance', - dragLockedToCenter: 'drag will keep the shadow centered', - dragVerbatim: 'drag will be verbatim', - dragMagnetic: 'drag will magnetize to the snap regions', - dragRatio: 'drag will keep the shadow positioned based on the drag start', - animateSnap: 'the shadow goes *boop* when snapping', - dotgrid: 'show dotgrid', - shadow: 'show the shadow during drag', - activeGridColor: 'grid lines color during drag', - dotgridColor: 'dotgrid items color', - inactiveGridColor: 'grid lines color when not dragging', - opacityBaseline: 'maximum shadow opacity', +function gridCSSNumberToLabel(gridCSSNumber: GridCSSNumber): string { + return `${gridCSSNumber.value}${gridCSSNumber.unit ?? ''}` } -export const experimentalGridFeatures = atom(defaultExperimentalGridFeatures) +function getLabelForAxis( + fromDOM: GridCSSNumber, + index: number, + fromProps: GridAutoOrTemplateBase | null, +): string { + const fromPropsAtIndex = toFirst(getFromPropsOptic(index), fromProps) + return gridCSSNumberToLabel(defaultEither(fromDOM, fromPropsAtIndex)) +} export const GridControls = controlForStrategyMemoized(() => { const [features, setFeatures] = useAtom(experimentalGridFeatures) @@ -187,6 +185,12 @@ export const GridControls = controlForStrategyMemoized(() => { targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns const gridTemplateRows = targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows + const gridTemplateColumnsFromProps = + targetGridContainer.specialSizeMeasurements.containerGridPropertiesFromProps + .gridTemplateColumns + const gridTemplateRowsFromProps = + targetGridContainer.specialSizeMeasurements.containerGridPropertiesFromProps + .gridTemplateRows const columns = getSillyCellsCount( targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns, @@ -200,6 +204,8 @@ export const GridControls = controlForStrategyMemoized(() => { frame: targetGridContainer.globalFrame, gridTemplateColumns: gridTemplateColumns, gridTemplateRows: gridTemplateRows, + gridTemplateColumnsFromProps: gridTemplateColumnsFromProps, + gridTemplateRowsFromProps: gridTemplateRowsFromProps, gap: gap, padding: padding, rows: rows, @@ -346,40 +352,160 @@ export const GridControls = controlForStrategyMemoized(() => { {grids.map((grid, index) => { const placeholders = Array.from(Array(grid.cells).keys()) - return ( -
- {placeholders.map((cell, cellIndex) => { - const countedRow = Math.floor(cellIndex / grid.columns) + 1 - const countedColumn = Math.floor(cellIndex % grid.columns) + 1 - const id = `gridcell-${index}-${cell}` - const dotgridColor = - activelyDraggingOrResizingCell != null ? features.dotgridColor : 'transparent' - const borderColor = - activelyDraggingOrResizingCell != null - ? features.activeGridColor - : features.inactiveGridColor + return ( +
+ {placeholders.map((cell, cellIndex) => { + const countedRow = Math.floor(cellIndex / grid.columns) + 1 + const countedColumn = Math.floor(cellIndex % grid.columns) + 1 + const id = `gridcell-${index}-${cell}` + const edgeColor = isActivelyDraggingCell ? '#00000033' : 'transparent' + const borderColor = isActivelyDraggingCell ? '#00000022' : '#0000000a' + return ( +
+
+
+
+
+
+
+
+
+ ) + })} +
+ ) + })} + {/* cell targets */} + {cells.map((cell) => { + return ( +
+ ) + })} + {grids.flatMap((grid) => { + if (grid.gridTemplateColumns == null) { + return [] + } else { + switch (grid.gridTemplateColumns.type) { + case 'DIMENSIONS': + let workingPrefix: number = grid.frame.x + return grid.gridTemplateColumns.dimensions.flatMap((dimension, dimensionIndex) => { + // Assumes pixels currently. + workingPrefix += dimension.value + if (dimensionIndex !== 0) { + workingPrefix += grid.gap ?? 0 + } + function mouseDownHandler(event: React.MouseEvent): void { + const start = windowToCanvasCoordinates( + scaleRef.current, + canvasOffsetRef.current, + windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), + ) + + dispatch([ + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start.canvasPositionRounded, + Modifier.modifiersForEvent(event), + gridAxisHandle('column', dimensionIndex), + 'zero-drag-not-permitted', + ), + ), + ]) + event.stopPropagation() + event.preventDefault() + } + return (
{ data-grid-row={countedRow} data-grid-column={countedColumn} > - {when( - features.dotgrid, - <> -
-
-
-
-
-
-
- , - )} + {getLabelForAxis(dimension, dimensionIndex, grid.gridTemplateColumnsFromProps)}
) })} @@ -485,37 +553,37 @@ export const GridControls = controlForStrategyMemoized(() => { 'zero-drag-not-permitted', ), ), - ]) - event.stopPropagation() - event.preventDefault() - } - return ( -
- {`${dimension.value}${dimension.unit ?? ''}`} -
- ) - }) - case 'FALLBACK': - return [] - default: - assertNever(grid.gridTemplateColumns) - return [] - } + ), + ]) + event.stopPropagation() + event.preventDefault() + } + return ( +
+ {getLabelForAxis(dimension, dimensionIndex, grid.gridTemplateRowsFromProps)} +
+ ) + }) + case 'FALLBACK': + return [] + default: + assertNever(grid.gridTemplateRows) + return [] } })} {grids.flatMap((grid) => { diff --git a/editor/src/components/canvas/dom-walker.ts b/editor/src/components/canvas/dom-walker.ts index 50973a259cb8..24a0d35061ae 100644 --- a/editor/src/components/canvas/dom-walker.ts +++ b/editor/src/components/canvas/dom-walker.ts @@ -1126,6 +1126,8 @@ function getSpecialMeasurements( const containerGridProperties = getGridContainerProperties(elementStyle) const containerElementProperties = getGridElementProperties(elementStyle) + const containerGridPropertiesFromProps = getGridContainerProperties(element.style) + const containerElementPropertyiesFromProps = getGridElementProperties(element.style) return specialSizeMeasurements( offset, @@ -1173,6 +1175,8 @@ function getSpecialMeasurements( computedHugProperty, containerGridProperties, containerElementProperties, + containerGridPropertiesFromProps, + containerElementPropertyiesFromProps, ) } diff --git a/editor/src/components/editor/store/store-deep-equality-instances-3.spec.ts b/editor/src/components/editor/store/store-deep-equality-instances-3.spec.ts index 007cac1c922b..a65a41b3ed8d 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances-3.spec.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances-3.spec.ts @@ -305,6 +305,18 @@ describe('SpecialSizeMeasurementsKeepDeepEquality', () => { gridRowStart: null, gridRowEnd: null, }, + containerGridPropertiesFromProps: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridPropertiesFromProps: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, } const newDifferentValue: SpecialSizeMeasurements = { @@ -407,6 +419,18 @@ describe('SpecialSizeMeasurementsKeepDeepEquality', () => { gridRowStart: null, gridRowEnd: null, }, + containerGridPropertiesFromProps: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridPropertiesFromProps: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, } it('same reference returns the same reference', () => { @@ -570,6 +594,18 @@ describe('ElementInstanceMetadataKeepDeepEquality', () => { gridRowStart: null, gridRowEnd: null, }, + containerGridPropertiesFromProps: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridPropertiesFromProps: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', @@ -710,6 +746,18 @@ describe('ElementInstanceMetadataKeepDeepEquality', () => { gridRowStart: null, gridRowEnd: null, }, + containerGridPropertiesFromProps: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridPropertiesFromProps: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', @@ -876,6 +924,18 @@ describe('ElementInstanceMetadataMapKeepDeepEquality', () => { gridRowStart: null, gridRowEnd: null, }, + containerGridPropertiesFromProps: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridPropertiesFromProps: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', @@ -1018,6 +1078,18 @@ describe('ElementInstanceMetadataMapKeepDeepEquality', () => { gridRowStart: null, gridRowEnd: null, }, + containerGridPropertiesFromProps: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridPropertiesFromProps: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', @@ -1160,6 +1232,18 @@ describe('ElementInstanceMetadataMapKeepDeepEquality', () => { gridRowStart: null, gridRowEnd: null, }, + containerGridPropertiesFromProps: { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + elementGridPropertiesFromProps: { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, }, computedStyle: { a: 'a', diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index 9c66e31fb116..561a293e7ad4 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -455,6 +455,7 @@ import type { ZeroDragPermitted, GridCellHandle, GridResizeHandle, + GridAxisHandle, } from '../../canvas/canvas-strategies/interaction-state' import { boundingArea, @@ -465,6 +466,7 @@ import { resizeHandle, gridCellHandle, gridResizeHandle, + gridAxisHandle, } from '../../canvas/canvas-strategies/interaction-state' import type { Modifiers } from '../../../utils/modifiers' import type { @@ -564,8 +566,10 @@ import type { CSSTextAlign, CSSTextDecorationLine, FontSettings, + GridCSSNumber, + GridCSSNumberUnit, } from '../../inspector/common/css-utils' -import { cssNumber, fontSettings } from '../../inspector/common/css-utils' +import { cssNumber, fontSettings, gridCSSNumber } from '../../inspector/common/css-utils' import type { ElementPaste, ProjectListing } from '../action-types' import { projectListing } from '../action-types' import type { Bounds, UtopiaVSCodeConfig } from 'utopia-vscode-common' @@ -1964,10 +1968,19 @@ export const CSSNumberKeepDeepEquality: KeepDeepEqualityCall = combin cssNumber, ) +export const GridCSSNumberKeepDeepEquality: KeepDeepEqualityCall = + combine2EqualityCalls( + (cssNum) => cssNum.value, + createCallWithTripleEquals(), + (cssNum) => cssNum.unit, + nullableDeepEquality(createCallWithTripleEquals()), + gridCSSNumber, + ) + export const GridAutoOrTemplateDimensionsKeepDeepEquality: KeepDeepEqualityCall = combine1EqualityCall( (value) => value.dimensions, - arrayDeepEquality(CSSNumberKeepDeepEquality), + arrayDeepEquality(GridCSSNumberKeepDeepEquality), gridAutoOrTemplateDimensions, ) @@ -2143,6 +2156,15 @@ export function SpecialSizeMeasurementsKeepDeepEquality(): KeepDeepEqualityCall< newSize.elementGridProperties, ).areEqual + const gridContainerPropertiesFromPropsEqual = GridContainerPropertiesKeepDeepEquality()( + oldSize.containerGridPropertiesFromProps, + newSize.containerGridPropertiesFromProps, + ).areEqual + const gridElementPropertiesFromPropsEqual = GridElementPropertiesKeepDeepEquality()( + oldSize.elementGridPropertiesFromProps, + newSize.elementGridPropertiesFromProps, + ).areEqual + const areEqual = offsetResult.areEqual && coordinateSystemBoundsResult.areEqual && @@ -2186,7 +2208,9 @@ export function SpecialSizeMeasurementsKeepDeepEquality(): KeepDeepEqualityCall< textBoundsEqual && computedHugPropertyEqual && gridContainerPropertiesEqual && - gridElementPropertiesEqual + gridElementPropertiesEqual && + gridContainerPropertiesFromPropsEqual && + gridElementPropertiesFromPropsEqual if (areEqual) { return keepDeepEqualityResult(oldSize, true) } else { @@ -2234,6 +2258,8 @@ export function SpecialSizeMeasurementsKeepDeepEquality(): KeepDeepEqualityCall< newSize.computedHugProperty, newSize.containerGridProperties, newSize.elementGridProperties, + newSize.containerGridPropertiesFromProps, + newSize.elementGridPropertiesFromProps, ) return keepDeepEqualityResult(sizeMeasurements, false) } @@ -2844,6 +2870,15 @@ export const GridResizeHandleKeepDeepEquality: KeepDeepEqualityCall = + combine2EqualityCalls( + (handle) => handle.axis, + createCallWithTripleEquals(), + (handle) => handle.columnOrRow, + createCallWithTripleEquals(), + gridAxisHandle, + ) + export const CanvasControlTypeKeepDeepEquality: KeepDeepEqualityCall = ( oldValue, newValue, @@ -2894,6 +2929,11 @@ export const CanvasControlTypeKeepDeepEquality: KeepDeepEqualityCall = [...LengthUnits, ...ResolutionUnits, '%', 'fr'] + +export interface GridCSSNumber { + value: number + unit: GridCSSNumberUnit | null +} + +export function gridCSSNumber(value: number, unit: GridCSSNumberUnit | null): GridCSSNumber { + return { + value, + unit, + } +} + export function cssNumber(value: number, unit: CSSNumberUnit | null = null): CSSNumber { return { value, unit } } @@ -659,6 +675,7 @@ const parseCSSResolutionUnit = (input: string) => parseCSSNumberUnit(input, Reso const parseCSSUnitlessUnit = (_: string) => left(`No unit expected`) const parseCSSUnitlessPercentUnit = (input: string) => parseCSSNumberUnit(input, ['%']) const parseCSSAnyValidNumberUnit = (input: string) => parseCSSNumberUnit(input, CSSNumberUnits) +const parseCSSGridUnit = (input: string) => parseCSSNumberUnit(input, GridCSSNumberUnits) function unitParseFnForType( numberType: CSSNumberType, @@ -688,6 +705,8 @@ function unitParseFnForType( return parseCSSUnitlessPercentUnit case 'AnyValid': return parseCSSAnyValidNumberUnit + case 'Grid': + return parseCSSGridUnit default: const _exhaustiveCheck: never = numberType throw new Error(`Unable to parse CSSNumber of type ${numberType}`) @@ -739,7 +758,7 @@ export function printCSSNumber( } } -export function printArrayCSSNumber(array: Array): string { +export function printArrayCSSNumber(array: Array): string { return array .map((dimension) => { const printed = printCSSNumber(dimension, null) @@ -805,6 +824,7 @@ export const parseCSSTimePercent = (input: unknown) => parseCSSNumber(input, 'Ti export const parseCSSUnitless = (input: unknown) => parseCSSNumber(input, 'Unitless') export const parseCSSUnitlessPercent = (input: unknown) => parseCSSNumber(input, 'UnitlessPercent') export const parseCSSAnyValidNumber = (input: unknown) => parseCSSNumber(input, 'AnyValid') +export const parseCSSGrid = (input: unknown) => parseCSSNumber(input, 'Grid') export const parseCSSUnitlessAsNumber = (input: unknown): Either => { const parsed = parseCSSNumber(input, 'Unitless') if (isRight(parsed)) { @@ -814,6 +834,15 @@ export const parseCSSUnitlessAsNumber = (input: unknown): Either } } +export function parseToCSSGridNumber(input: unknown): Either { + return mapEither((value) => { + return { + value: value.value, + unit: value.unit as GridCSSNumberUnit | null, + } + }, parseCSSGrid(input)) +} + export const parseCSSNumber = ( input: unknown, numberType: CSSNumberType, @@ -861,9 +890,9 @@ export function parseGridRange(input: unknown): Either { export function parseGridAutoOrTemplateBase( input: unknown, ): Either { - function numberParse(inputToParse: unknown): Either { - const result = parseCSSAnyValidNumber(inputToParse) - return leftMapEither(descriptionParseError, result) + function numberParse(inputToParse: unknown): Either { + const result = parseToCSSGridNumber(inputToParse) + return leftMapEither(descriptionParseError, result) } if (typeof input === 'string') { const parsedCSSArray = parseCSSArray([numberParse])(input.split(/ +/)) diff --git a/editor/src/core/shared/element-template.ts b/editor/src/core/shared/element-template.ts index ccc3a99e8900..852c4052acf4 100644 --- a/editor/src/core/shared/element-template.ts +++ b/editor/src/core/shared/element-template.ts @@ -25,6 +25,7 @@ import type { CSSNumber, CSSPosition, FlexDirection, + GridCSSNumber, } from '../../components/inspector/common/css-utils' import type { ModifiableAttribute } from './jsx-attributes' import * as EP from './element-path' @@ -2603,11 +2604,11 @@ export function gridAutoOrTemplateFallback(value: string): GridAutoOrTemplateFal export interface GridAutoOrTemplateDimensions { type: 'DIMENSIONS' - dimensions: Array + dimensions: Array } export function gridAutoOrTemplateDimensions( - dimensions: Array, + dimensions: Array, ): GridAutoOrTemplateDimensions { return { type: 'DIMENSIONS', @@ -2717,6 +2718,8 @@ export interface SpecialSizeMeasurements { computedHugProperty: HugPropertyWidthHeight containerGridProperties: GridContainerProperties elementGridProperties: GridElementProperties + containerGridPropertiesFromProps: GridContainerProperties + elementGridPropertiesFromProps: GridElementProperties } export function specialSizeMeasurements( @@ -2763,6 +2766,8 @@ export function specialSizeMeasurements( computedHugProperty: HugPropertyWidthHeight, containerGridProperties: GridContainerProperties, elementGridProperties: GridElementProperties, + containerGridPropertiesFromProps: GridContainerProperties, + elementGridPropertiesFromProps: GridElementProperties, ): SpecialSizeMeasurements { return { offset, @@ -2808,6 +2813,8 @@ export function specialSizeMeasurements( computedHugProperty, containerGridProperties, elementGridProperties, + containerGridPropertiesFromProps, + elementGridPropertiesFromProps, } } @@ -2868,6 +2875,18 @@ export const emptySpecialSizeMeasurements = specialSizeMeasurements( gridRowStart: null, gridRowEnd: null, }, + { + gridTemplateColumns: null, + gridTemplateRows: null, + gridAutoColumns: null, + gridAutoRows: null, + }, + { + gridColumnStart: null, + gridColumnEnd: null, + gridRowStart: null, + gridRowEnd: null, + }, ) export function walkElement( From abe2b6d12a328dea0f8ad3b38e72eba378ce720e Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Thu, 27 Jun 2024 18:56:19 -0400 Subject: [PATCH 26/29] spike(canvas) Fixing horrifying merge conflicts. --- .../canvas/controls/grid-controls.tsx | 374 +++++++++--------- 1 file changed, 188 insertions(+), 186 deletions(-) diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index 36be2b5182aa..a361adb63dbd 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -50,6 +50,8 @@ import { } from '../../../core/shared/optics/optic-creators' import { toFirst } from '../../../core/shared/optics/optic-utilities' import { defaultEither } from '../../../core/shared/either' +import { assertNode } from '@babel/types' +import type { ElementPath } from 'utopia-shared/src/types' type GridCellCoordinates = { row: number; column: number } @@ -75,6 +77,38 @@ function getSillyCellsCount(template: GridAutoOrTemplateBase | null): number { } } +export const defaultExperimentalGridFeatures = { + dragLockedToCenter: false, + dragVerbatim: false, + dragMagnetic: false, + dragRatio: true, + animateSnap: true, + dotgrid: true, + shadow: true, + adaptiveOpacity: true, + activeGridColor: '#0099ff77', + dotgridColor: '#0099ffaa', + inactiveGridColor: '#0000000a', + opacityBaseline: 0.25, +} + +export const gridFeaturesExplained: Record = { + adaptiveOpacity: 'shadow opacity is proportional to the drag distance', + dragLockedToCenter: 'drag will keep the shadow centered', + dragVerbatim: 'drag will be verbatim', + dragMagnetic: 'drag will magnetize to the snap regions', + dragRatio: 'drag will keep the shadow positioned based on the drag start', + animateSnap: 'the shadow goes *boop* when snapping', + dotgrid: 'show dotgrid', + shadow: 'show the shadow during drag', + activeGridColor: 'grid lines color during drag', + dotgridColor: 'dotgrid items color', + inactiveGridColor: 'grid lines color when not dragging', + opacityBaseline: 'maximum shadow opacity', +} + +export const experimentalGridFeatures = atom(defaultExperimentalGridFeatures) + function getNullableAutoOrTemplateBaseString( template: GridAutoOrTemplateBase | null, ): string | undefined { @@ -352,159 +386,40 @@ export const GridControls = controlForStrategyMemoized(() => { {grids.map((grid, index) => { const placeholders = Array.from(Array(grid.cells).keys()) - return ( -
- {placeholders.map((cell, cellIndex) => { - const countedRow = Math.floor(cellIndex / grid.columns) + 1 - const countedColumn = Math.floor(cellIndex % grid.columns) + 1 - const id = `gridcell-${index}-${cell}` - const edgeColor = isActivelyDraggingCell ? '#00000033' : 'transparent' - const borderColor = isActivelyDraggingCell ? '#00000022' : '#0000000a' - return ( -
-
-
-
-
-
-
-
-
- ) - })} -
- ) - })} - {/* cell targets */} - {cells.map((cell) => { - return ( -
- ) - })} - {grids.flatMap((grid) => { - if (grid.gridTemplateColumns == null) { - return [] - } else { - switch (grid.gridTemplateColumns.type) { - case 'DIMENSIONS': - let workingPrefix: number = grid.frame.x - return grid.gridTemplateColumns.dimensions.flatMap((dimension, dimensionIndex) => { - // Assumes pixels currently. - workingPrefix += dimension.value - if (dimensionIndex !== 0) { - workingPrefix += grid.gap ?? 0 - } - function mouseDownHandler(event: React.MouseEvent): void { - const start = windowToCanvasCoordinates( - scaleRef.current, - canvasOffsetRef.current, - windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }), - ) - - dispatch([ - CanvasActions.createInteractionSession( - createInteractionViaMouse( - start.canvasPositionRounded, - Modifier.modifiersForEvent(event), - gridAxisHandle('column', dimensionIndex), - 'zero-drag-not-permitted', - ), - ), - ]) - event.stopPropagation() - event.preventDefault() - } + return ( +
+ {placeholders.map((cell, cellIndex) => { + const countedRow = Math.floor(cellIndex / grid.columns) + 1 + const countedColumn = Math.floor(cellIndex % grid.columns) + 1 + const id = `gridcell-${index}-${cell}` + const dotgridColor = + activelyDraggingOrResizingCell != null ? features.dotgridColor : 'transparent' + const borderColor = + activelyDraggingOrResizingCell != null + ? features.activeGridColor + : features.inactiveGridColor return (
{ data-grid-row={countedRow} data-grid-column={countedColumn} > - {getLabelForAxis(dimension, dimensionIndex, grid.gridTemplateColumnsFromProps)} + {when( + features.dotgrid, + <> +
+
+
+
+
+
+
+ , + )}
) })}
) })} + {/* cell targets */} + {cells.map((cell) => { + return ( +
+ ) + })} {grids.flatMap((grid) => { if (grid.gridTemplateColumns == null) { return [] @@ -553,37 +550,42 @@ export const GridControls = controlForStrategyMemoized(() => { 'zero-drag-not-permitted', ), ), - ), - ]) - event.stopPropagation() - event.preventDefault() - } - return ( -
- {getLabelForAxis(dimension, dimensionIndex, grid.gridTemplateRowsFromProps)} -
- ) - }) - case 'FALLBACK': - return [] - default: - assertNever(grid.gridTemplateRows) - return [] + ]) + event.stopPropagation() + event.preventDefault() + } + + return ( +
+ {getLabelForAxis( + dimension, + dimensionIndex, + grid.gridTemplateColumnsFromProps, + )} +
+ ) + }) + case 'FALLBACK': + return [] + default: + assertNever(grid.gridTemplateColumns) + return [] + } } })} {grids.flatMap((grid) => { @@ -636,7 +638,7 @@ export const GridControls = controlForStrategyMemoized(() => { }} onMouseDown={mouseDownHandler} > - {`${dimension.value}${dimension.unit ?? ''}`} + {getLabelForAxis(dimension, dimensionIndex, grid.gridTemplateRowsFromProps)}
) }) From b8297b49731ca82b30f3403435ec7df67043a84c Mon Sep 17 00:00:00 2001 From: Federico Ruggi Date: Thu, 27 Jun 2024 19:12:38 -0400 Subject: [PATCH 27/29] i mean, might as well make it look good --- .../src/components/canvas/controls/new-canvas-controls.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/editor/src/components/canvas/controls/new-canvas-controls.tsx b/editor/src/components/canvas/controls/new-canvas-controls.tsx index d522f9ef21f9..0cc251b8446e 100644 --- a/editor/src/components/canvas/controls/new-canvas-controls.tsx +++ b/editor/src/components/canvas/controls/new-canvas-controls.tsx @@ -614,8 +614,10 @@ const NewCanvasControlsInner = (props: NewCanvasControlsInnerProps) => { bottom: 10, right: 10, background: 'white', + color: 'black', borderRadius: 2, - border: '1px double black', + border: '1px solid black', + boxShadow: '3px 3px 0px black', display: 'flex', flexDirection: 'column', gap: 2, @@ -645,7 +647,7 @@ const NewCanvasControlsInner = (props: NewCanvasControlsInnerProps) => { ease: 'linear', }, }} - style={{ display: 'flex', gap: 100 }} + style={{ display: 'flex', gap: 100, padding: 10 }} > Roll your own damn grid Roll your own damn grid From 0bfc43f55add321f412d3743be190c8e5f45c67d Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 1 Jul 2024 17:32:05 +0200 Subject: [PATCH 28/29] fix grid checks --- .../absolute-resize-bounding-box-strategy.tsx | 7 +-- .../strategies/basic-resize-strategy.tsx | 11 +--- .../keyboard-absolute-resize-strategy.tsx | 7 +-- .../select-mode/absolute-resize-control.tsx | 1 + .../select-mode/select-mode-hooks.tsx | 54 +++++++++++-------- 5 files changed, 37 insertions(+), 43 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx index 43f6bb3a71e4..e5340b153d4e 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx @@ -83,12 +83,7 @@ export function absoluteResizeBoundingBoxStrategy( retargetedTargets.length === 0 || !retargetedTargets.every((element) => { return supportsAbsoluteResize(canvasState.startingMetadata, element, canvasState) - }) || - retargetedTargets.some((t) => - MetadataUtils.isGridLayoutedContainer( - MetadataUtils.findElementByElementPath(canvasState.startingMetadata, EP.parentPath(t)), - ), - ) + }) ) { return null } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx index 6ea2a94dcd6d..f96032b76411 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx @@ -54,7 +54,6 @@ import { pushIntendedBoundsAndUpdateGroups } from '../../commands/push-intended- import { queueTrueUpElement } from '../../commands/queue-true-up-command' import { treatElementAsGroupLike } from './group-helpers' import { trueUpGroupElementChanged } from '../../../editor/store/editor-state' -import { parentPath } from '../../../../core/shared/element-path' export const BASIC_RESIZE_STRATEGY_ID = 'BASIC_RESIZE' @@ -64,15 +63,7 @@ export function basicResizeStrategy( ): CanvasStrategy | null { const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget) - if ( - selectedElements.length !== 1 || - !honoursPropsSize(canvasState, selectedElements[0]) || - selectedElements.some((t) => - MetadataUtils.isGridLayoutedContainer( - MetadataUtils.findElementByElementPath(canvasState.startingMetadata, parentPath(t)), - ), - ) - ) { + if (selectedElements.length !== 1 || !honoursPropsSize(canvasState, selectedElements[0])) { return null } const metadata = MetadataUtils.findElementByElementPath( diff --git a/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx index 60df531900ae..02a6871d401b 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx @@ -128,12 +128,7 @@ export function keyboardAbsoluteResizeStrategy( selectedElements.length === 0 || !selectedElements.every((element) => { return supportsAbsoluteResize(canvasState.startingMetadata, element, canvasState) - }) || - selectedElements.some((t) => - MetadataUtils.isGridLayoutedContainer( - MetadataUtils.findElementByElementPath(canvasState.startingMetadata, EP.parentPath(t)), - ), - ) + }) ) { return null } diff --git a/editor/src/components/canvas/controls/select-mode/absolute-resize-control.tsx b/editor/src/components/canvas/controls/select-mode/absolute-resize-control.tsx index 8a7e00aba726..38f456abf349 100644 --- a/editor/src/components/canvas/controls/select-mode/absolute-resize-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/absolute-resize-control.tsx @@ -151,6 +151,7 @@ export const AbsoluteResizeControl = controlForStrategyMemoized( ref={controlRef} style={{ position: 'absolute', + pointerEvents: 'none', }} > = [] if (foundTarget != null || isDeselect) { - // if (foundTarget != null && draggingAllowed) { - // const start = windowToCanvasCoordinates( - // windowPoint(point(event.clientX, event.clientY)), - // ).canvasPositionRounded - // if (event.button !== 2 && event.type !== 'mouseup') { - // editorActions.push( - // CanvasActions.createInteractionSession( - // createInteractionViaMouse( - // start, - // Modifier.modifiersForEvent(event), - // boundingArea(), - // 'zero-drag-not-permitted', - // ), - // ), - // ) - // } - // } + if ( + foundTarget != null && + draggingAllowed && + !MetadataUtils.isGridLayoutedContainer( + // grid has its own drag handling + MetadataUtils.findElementByElementPath( + editorStoreRef.current.editor.jsxMetadata, + EP.parentPath(foundTarget.elementPath), + ), + ) + ) { + const start = windowToCanvasCoordinates( + windowPoint(point(event.clientX, event.clientY)), + ).canvasPositionRounded + if (event.button !== 2 && event.type !== 'mouseup') { + editorActions.push( + CanvasActions.createInteractionSession( + createInteractionViaMouse( + start, + Modifier.modifiersForEvent(event), + boundingArea(), + 'zero-drag-not-permitted', + ), + ), + ) + } + } let updatedSelection: Array if (isMultiselect) { @@ -733,13 +743,15 @@ function useSelectOrLiveModeSelectAndHover( dispatch(editorActions) }, [ + editorStoreRef, + active, + getSelectableViewsForSelectMode, + findValidTarget, dispatch, + draggingAllowed, + windowToCanvasCoordinates, selectedViewsRef, - findValidTarget, setSelectedViewsForCanvasControlsOnly, - getSelectableViewsForSelectMode, - editorStoreRef, - active, ], ) From 251d8f198a246957989253d7c4c1deb4224f3884 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:30:30 +0200 Subject: [PATCH 29/29] update usage --- .../canvas/controls/grid-controls.tsx | 95 ++++---------- .../canvas/controls/new-canvas-controls.tsx | 120 ------------------ .../left-pane/roll-your-own-pane.tsx | 95 ++++++++++---- 3 files changed, 96 insertions(+), 214 deletions(-) diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index a361adb63dbd..22959408732b 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -1,7 +1,7 @@ import React from 'react' import { CanvasOffsetWrapper } from './canvas-offset-wrapper' import { Substores, useEditorState, useRefEditorState } from '../../editor/store/store-hook' -import { MetadataUtils, getSimpleAttributeAtPath } from '../../../core/model/element-metadata-utils' +import { MetadataUtils } from '../../../core/model/element-metadata-utils' import * as EP from '../../../core/shared/element-path' import { mapDropNulls } from '../../../core/shared/array-utils' import type { CanvasPoint, CanvasRectangle } from '../../../core/shared/math-utils' @@ -29,7 +29,6 @@ import { Modifier } from '../../../utils/modifiers' import { windowToCanvasCoordinates } from '../dom-lookup' import { isGridAutoOrTemplateDimensions, - type ElementInstanceMetadata, type GridAutoOrTemplateBase, } from '../../../core/shared/element-template' import { assertNever } from '../../../core/shared/utils' @@ -38,8 +37,6 @@ import { printGridAutoOrTemplateBase } from '../../../components/inspector/commo import { when } from '../../../utils/react-conditionals' import { CanvasMousePositionRaw } from '../../../utils/global-positions' import { motion, useAnimationControls } from 'framer-motion' -import { atom, useAtom } from 'jotai' -import { isFeatureEnabled } from '../../../utils/feature-switches' import { useColorTheme } from '../../../uuiui' import type { Optic } from '../../../core/shared/optics/optics' import { @@ -50,8 +47,8 @@ import { } from '../../../core/shared/optics/optic-creators' import { toFirst } from '../../../core/shared/optics/optic-utilities' import { defaultEither } from '../../../core/shared/either' -import { assertNode } from '@babel/types' import type { ElementPath } from 'utopia-shared/src/types' +import { useRollYourOwnFeatures } from '../../navigator/left-pane/roll-your-own-pane' type GridCellCoordinates = { row: number; column: number } @@ -77,38 +74,6 @@ function getSillyCellsCount(template: GridAutoOrTemplateBase | null): number { } } -export const defaultExperimentalGridFeatures = { - dragLockedToCenter: false, - dragVerbatim: false, - dragMagnetic: false, - dragRatio: true, - animateSnap: true, - dotgrid: true, - shadow: true, - adaptiveOpacity: true, - activeGridColor: '#0099ff77', - dotgridColor: '#0099ffaa', - inactiveGridColor: '#0000000a', - opacityBaseline: 0.25, -} - -export const gridFeaturesExplained: Record = { - adaptiveOpacity: 'shadow opacity is proportional to the drag distance', - dragLockedToCenter: 'drag will keep the shadow centered', - dragVerbatim: 'drag will be verbatim', - dragMagnetic: 'drag will magnetize to the snap regions', - dragRatio: 'drag will keep the shadow positioned based on the drag start', - animateSnap: 'the shadow goes *boop* when snapping', - dotgrid: 'show dotgrid', - shadow: 'show the shadow during drag', - activeGridColor: 'grid lines color during drag', - dotgridColor: 'dotgrid items color', - inactiveGridColor: 'grid lines color when not dragging', - opacityBaseline: 'maximum shadow opacity', -} - -export const experimentalGridFeatures = atom(defaultExperimentalGridFeatures) - function getNullableAutoOrTemplateBaseString( template: GridAutoOrTemplateBase | null, ): string | undefined { @@ -140,21 +105,7 @@ function getLabelForAxis( } export const GridControls = controlForStrategyMemoized(() => { - const [features, setFeatures] = useAtom(experimentalGridFeatures) - - React.useEffect(() => { - setFeatures((old) => ({ - ...old, - adaptiveOpacity: isFeatureEnabled('Grid move - adaptiveOpacity'), - dragLockedToCenter: isFeatureEnabled('Grid move - dragLockedToCenter'), - dragVerbatim: isFeatureEnabled('Grid move - dragVerbatim'), - dragMagnetic: isFeatureEnabled('Grid move - dragMagnetic'), - dragRatio: isFeatureEnabled('Grid move - dragRatio'), - animateSnap: isFeatureEnabled('Grid move - animateSnap'), - dotgrid: isFeatureEnabled('Grid move - dotgrid'), - shadow: isFeatureEnabled('Grid move - shadow'), - })) - }, [setFeatures]) + const features = useRollYourOwnFeatures() const activelyDraggingOrResizingCell = useEditorState( Substores.canvas, @@ -336,7 +287,7 @@ export const GridControls = controlForStrategyMemoized(() => { if (hoveringCell == null) { return } - if (!features.animateSnap) { + if (!features.Grid.animateSnap) { return } void controls.start('boop') @@ -415,11 +366,13 @@ export const GridControls = controlForStrategyMemoized(() => { const countedColumn = Math.floor(cellIndex % grid.columns) + 1 const id = `gridcell-${index}-${cell}` const dotgridColor = - activelyDraggingOrResizingCell != null ? features.dotgridColor : 'transparent' + activelyDraggingOrResizingCell != null + ? features.Grid.dotgridColor + : 'transparent' const borderColor = activelyDraggingOrResizingCell != null - ? features.activeGridColor - : features.inactiveGridColor + ? features.Grid.activeGridColor + : features.Grid.inactiveGridColor return (
{ data-grid-column={countedColumn} > {when( - features.dotgrid, + features.Grid.dotgrid, <>
{ ) })} {/* shadow */} - {features.shadow && + {features.Grid.shadow && shadow != null && shadowFrame != null && interactionSession?.dragStart != null && @@ -711,14 +664,14 @@ export const GridControls = controlForStrategyMemoized(() => { ? `${shadow.borderRadius.top}px ${shadow.borderRadius.right}px ${shadow.borderRadius.bottom}px ${shadow.borderRadius.left}px` : 0, backgroundColor: 'black', - opacity: features.adaptiveOpacity - ? features.dragLockedToCenter + opacity: features.Grid.adaptiveOpacity + ? features.Grid.dragLockedToCenter ? Math.min( (0.2 * distance(getRectCenter(shadow.globalFrame), CanvasMousePositionRaw!)) / Math.min(shadow.globalFrame.height, shadow.globalFrame.width) + 0.05, - features.opacityBaseline, + features.Grid.opacityBaseline, ) : Math.min( (0.2 * @@ -731,36 +684,36 @@ export const GridControls = controlForStrategyMemoized(() => { )) / Math.min(shadow.globalFrame.height, shadow.globalFrame.width) + 0.05, - features.opacityBaseline, + features.Grid.opacityBaseline, ) - : features.opacityBaseline, + : features.Grid.opacityBaseline, border: '1px solid white', - top: features.dragVerbatim + top: features.Grid.dragVerbatim ? shadowFrame.y + interactionSession.drag.y - : features.dragLockedToCenter + : features.Grid.dragLockedToCenter ? shadow.globalFrame.y + interactionSession.drag.y - (shadow.globalFrame.y - interactionSession.dragStart.y) - shadow.globalFrame.height / 2 - : features.dragMagnetic + : features.Grid.dragMagnetic ? shadow.globalFrame.y + (CanvasMousePositionRaw!.y - hoveringStart.point.y) - : features.dragRatio + : features.Grid.dragRatio ? shadow.globalFrame.y + interactionSession.drag.y - (shadow.globalFrame.y - interactionSession.dragStart.y) - shadow.globalFrame.height * ((interactionSession.dragStart.y - shadowFrame.y) / shadowFrame.height) : undefined, - left: features.dragVerbatim + left: features.Grid.dragVerbatim ? shadowFrame.x + interactionSession.drag.x - : features.dragLockedToCenter + : features.Grid.dragLockedToCenter ? shadow.globalFrame.x + interactionSession.drag.x - (shadow.globalFrame.x - interactionSession.dragStart.x) - shadow.globalFrame.width / 2 - : features.dragMagnetic + : features.Grid.dragMagnetic ? shadow.globalFrame.x + (CanvasMousePositionRaw!.x - hoveringStart.point.x) - : features.dragRatio + : features.Grid.dragRatio ? shadow.globalFrame.x + interactionSession.drag.x - (shadow.globalFrame.x - interactionSession.dragStart.x) - diff --git a/editor/src/components/canvas/controls/new-canvas-controls.tsx b/editor/src/components/canvas/controls/new-canvas-controls.tsx index 0cc251b8446e..1b6e033b07a0 100644 --- a/editor/src/components/canvas/controls/new-canvas-controls.tsx +++ b/editor/src/components/canvas/controls/new-canvas-controls.tsx @@ -73,13 +73,6 @@ import { NO_OP } from '../../../core/shared/utils' import { useIsMyProject } from '../../editor/store/collaborative-editing' import { MultiplayerWrapper } from '../../../utils/multiplayer-wrapper' import { MultiplayerPresence } from '../multiplayer-presence' -import { setFeatureEnabled } from '../../../utils/feature-switches' -import { - defaultExperimentalGridFeatures, - experimentalGridFeatures, - gridFeaturesExplained, -} from './grid-controls' -import { motion } from 'framer-motion' export const CanvasControlsContainerID = 'new-canvas-controls-container' @@ -523,8 +516,6 @@ const NewCanvasControlsInner = (props: NewCanvasControlsInnerProps) => { const resizeStatus = getResizeStatus() - const [gridFeatures, setGridFeatures] = useAtom(experimentalGridFeatures) - return ( <>
{ )} {when(isSelectMode(editorMode), )} -
-

- - Roll your own damn grid - Roll your own damn grid - -

- - {Object.entries(gridFeatures).map(([feat, value]) => { - const isDefault = (defaultExperimentalGridFeatures as any)[feat] === true - return ( -
-
-
- {feat} {isDefault ? '(default)' : ''} -
-
- {gridFeaturesExplained[feat]} -
-
- {typeof value === 'boolean' && ( - { - e.stopPropagation() - setGridFeatures({ ...gridFeatures, [feat]: e.target.checked }) - setFeatureEnabled(`Grid move - ${feat}` as any, e.target.checked) // terrible hacks on top of terrible hacks - }} - /> - )} - {typeof value === 'string' && ( - { - e.stopPropagation() - setGridFeatures({ ...gridFeatures, [feat]: e.target.value }) - }} - /> - )} - {typeof value === 'number' && ( - { - e.stopPropagation() - setGridFeatures({ - ...gridFeatures, - [feat]: parseFloat(e.target.value), - }) - }} - /> - )} -
- ) - })} -
, )} , diff --git a/editor/src/components/navigator/left-pane/roll-your-own-pane.tsx b/editor/src/components/navigator/left-pane/roll-your-own-pane.tsx index 5a29e338b287..0a6a9f4436f6 100644 --- a/editor/src/components/navigator/left-pane/roll-your-own-pane.tsx +++ b/editor/src/components/navigator/left-pane/roll-your-own-pane.tsx @@ -1,7 +1,7 @@ import React from 'react' import { FlexColumn, FlexRow, Section } from '../../../uuiui' import { when } from '../../../utils/react-conditionals' -import { atom, useAtom } from 'jotai' +import { atom, useAtom, useSetAtom } from 'jotai' import { UIGridRow } from '../../inspector/widgets/ui-grid-row' import { atomWithStorage } from 'jotai/utils' import { IS_TEST_ENVIRONMENT } from '../../../common/env-vars' @@ -10,7 +10,18 @@ const sections = ['Grid'] as const type Section = (typeof sections)[number] type GridFeatures = { - foo: boolean + dragLockedToCenter: boolean + dragVerbatim: boolean + dragMagnetic: boolean + dragRatio: boolean + animateSnap: boolean + dotgrid: boolean + shadow: boolean + adaptiveOpacity: boolean + activeGridColor: string + dotgridColor: string + inactiveGridColor: string + opacityBaseline: number } type RollYourOwnFeaturesTypes = { @@ -21,18 +32,40 @@ type RollYourOwnFeatures = { [K in Section]: RollYourOwnFeaturesTypes[K] } -let defaultRollYourOwnFeatures: RollYourOwnFeatures = { +const defaultRollYourOwnFeatures: RollYourOwnFeatures = { Grid: { - foo: true, + dragLockedToCenter: false, + dragVerbatim: false, + dragMagnetic: false, + dragRatio: true, + animateSnap: true, + dotgrid: true, + shadow: true, + adaptiveOpacity: true, + activeGridColor: '#0099ff77', + dotgridColor: '#0099ffaa', + inactiveGridColor: '#0000000a', + opacityBaseline: 0.25, }, } const ROLL_YOUR_OWN_FEATURES_KEY: string = 'roll-your-own-features' -export const rollYourOwnFeatures = IS_TEST_ENVIRONMENT +const rollYourOwnFeaturesAtom = IS_TEST_ENVIRONMENT ? atom(defaultRollYourOwnFeatures) : atomWithStorage(ROLL_YOUR_OWN_FEATURES_KEY, defaultRollYourOwnFeatures) +export function useRollYourOwnFeatures() { + const [features] = useAtom(rollYourOwnFeaturesAtom) + const merged: RollYourOwnFeatures = { + Grid: { + ...defaultRollYourOwnFeatures.Grid, + ...features.Grid, + }, + } + return merged +} + export const RollYourOwnFeaturesPane = React.memo(() => { const [currentSection, setCurrentSection] = React.useState
(null) @@ -91,38 +124,54 @@ export const RollYourOwnFeaturesPane = React.memo(() => { }) RollYourOwnFeaturesPane.displayName = 'RollYourOwnFeaturesPane' +function getNewFeatureValueOrNull(currentValue: any, e: React.ChangeEvent) { + switch (typeof currentValue) { + case 'boolean': + return e.target.checked + case 'string': + return e.target.value + case 'number': + return parseFloat(e.target.value) + default: + return null + } +} + const GridSection = React.memo(() => { - const [features, setFeatures] = useAtom(rollYourOwnFeatures) + const features = useRollYourOwnFeatures() + const setFeatures = useSetAtom(rollYourOwnFeaturesAtom) const onChange = React.useCallback( (feat: keyof GridFeatures) => (e: React.ChangeEvent) => { - setFeatures((existing) => { - return { - ...existing, + const newValue = getNewFeatureValueOrNull(features.Grid[feat], e) + if (newValue != null) { + setFeatures({ + ...features, Grid: { - ...existing.Grid, - [feat]: e.target.checked, + ...features.Grid, + [feat]: newValue, }, - } - }) + }) + } }, - [setFeatures], + [features, setFeatures], ) return ( - {Object.entries(features.Grid).map(([feat, value]) => { + {Object.keys(defaultRollYourOwnFeatures.Grid).map((key) => { + const feat = key as keyof GridFeatures + const value = features.Grid[feat] ?? defaultRollYourOwnFeatures.Grid[feat] return (
{feat}
- {when( - typeof value === 'boolean', - , - )} + {typeof value === 'boolean' ? ( + + ) : typeof value === 'string' ? ( + + ) : typeof value === 'number' ? ( + + ) : null}
) })}