Skip to content

Commit

Permalink
Grid resize better handles (#6058)
Browse files Browse the repository at this point in the history
**Problem:**

The resize grid strategy has some UI issues:

1. the handles are only shown when a grid cell is selected, not the grid
itself
2. the handles are not positioned correctly relatively to the grid
itself
3. it's hard to discern precisely the resize effects

**Fix:**

1. Enable the grid resizing if the grid is selected too
2. Position the handles centered relatively to the grid rows/columns
3. Replace the "numbered handles" with more subtle draggable circles
4. During drag, show a striped background for the affected row or
column, with the measurements centered in the striped area

**Note**
The handles icons are placeholders/ideas.


| Before | After |
|--------|-----------|
| ![Kapture 2024-07-10 at 14 19
33](https://github.com/concrete-utopia/utopia/assets/1081051/0b310d2d-fda5-443e-b685-299556b90ef6)
| ![Kapture 2024-07-10 at 14 17
58](https://github.com/concrete-utopia/utopia/assets/1081051/d6b005df-8e9b-4219-a5b6-8c976e85cc0a)
|
  • Loading branch information
ruggi authored Jul 10, 2024
1 parent 9ee92d4 commit 9bdeed7
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ export const resizeGridStrategy: CanvasStrategyFactory = (

const selectedElement = selectedElements[0]

if (!MetadataUtils.isGridCell(canvasState.startingMetadata, selectedElement)) {
const isGridCell = MetadataUtils.isGridCell(canvasState.startingMetadata, selectedElement)
const isGrid = MetadataUtils.isGridLayoutedContainer(
MetadataUtils.findElementByElementPath(canvasState.startingMetadata, selectedElement),
)
const isGridOrGridCell = isGridCell || isGrid

if (!isGridOrGridCell) {
return null
}

Expand Down Expand Up @@ -65,17 +71,19 @@ export const resizeGridStrategy: CanvasStrategyFactory = (
const control = interactionSession.activeControl
const drag = interactionSession.interactionData.drag
const dragAmount = control.axis === 'column' ? drag.x : drag.y
const parentPath = EP.parentPath(selectedElement)
const parentSpecialSizeMeasurements =
canvasState.startingMetadata[EP.toString(parentPath)].specialSizeMeasurements

const gridPath = isGrid ? selectedElement : EP.parentPath(selectedElement)

const gridSpecialSizeMeasurements =
canvasState.startingMetadata[EP.toString(gridPath)].specialSizeMeasurements
const originalValues =
control.axis === 'column'
? parentSpecialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateColumns
: parentSpecialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateRows
? gridSpecialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateColumns
: gridSpecialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateRows
const calculatedValues =
control.axis === 'column'
? parentSpecialSizeMeasurements.containerGridProperties.gridTemplateColumns
: parentSpecialSizeMeasurements.containerGridProperties.gridTemplateRows
? gridSpecialSizeMeasurements.containerGridProperties.gridTemplateColumns
: gridSpecialSizeMeasurements.containerGridProperties.gridTemplateRows

if (
calculatedValues == null ||
Expand Down Expand Up @@ -115,14 +123,14 @@ export const resizeGridStrategy: CanvasStrategyFactory = (
const commands = [
setProperty(
'always',
parentPath,
gridPath,
PP.create(
'style',
control.axis === 'column' ? 'gridTemplateColumns' : 'gridTemplateRows',
),
propertyValueAsString,
),
setElementsToRerenderCommand([parentPath]),
setElementsToRerenderCommand([gridPath]),
]

return strategyApplicationResult(commands)
Expand Down
218 changes: 154 additions & 64 deletions editor/src/components/canvas/controls/grid-controls.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import { jsx } from '@emotion/react'
import type { AnimationControls } from 'framer-motion'
import { motion, useAnimationControls } from 'framer-motion'
import React from 'react'
Expand Down Expand Up @@ -51,7 +54,7 @@ import {
} from '../canvas-strategies/interaction-state'
import { windowToCanvasCoordinates } from '../dom-lookup'
import { CanvasOffsetWrapper } from './canvas-offset-wrapper'
import { useColorTheme } from '../../../uuiui'
import { useColorTheme, UtopiaStyles } from '../../../uuiui'
import { gridCellTargetId } from '../canvas-strategies/strategies/grid-helpers'
import { resizeBoundingBoxFromSide } from '../canvas-strategies/strategies/resize-helpers'
import type { EdgePosition } from '../canvas-types'
Expand All @@ -65,6 +68,7 @@ import {
import { useCanvasAnimation } from '../ui-jsx-canvas-renderer/animation-context'
import { CanvasLabel } from './select-mode/controls-common'
import { optionalMap } from '../../../core/shared/optional-utils'
import type { Sides } from 'utopia-api/core'

const CELL_ANIMATION_DURATION = 0.15 // seconds

Expand Down Expand Up @@ -122,16 +126,16 @@ function getLabelForAxis(
}

const SHADOW_SNAP_ANIMATION = 'shadow-snap'

const GridResizingContainerSize = 100
const GRID_RESIZE_HANDLE_CONTAINER_SIZE = 30 // px
const GRID_RESIZE_HANDLE_SIZE = 15 // px

export interface GridResizingControlProps {
dimension: GridCSSNumber
dimensionIndex: number
axis: 'row' | 'column'
containingFrame: CanvasRectangle
workingPrefix: number
fromPropsAxisValues: GridAutoOrTemplateBase | null
padding: number | null
}

export const GridResizingControl = React.memo((props: GridResizingControlProps) => {
Expand All @@ -148,13 +152,22 @@ export const GridResizingControl = React.memo((props: GridResizingControlProps)
const dispatch = useDispatch()
const colorTheme = useColorTheme()

const [resizing, setResizing] = React.useState(false)

const mouseDownHandler = React.useCallback(
(event: React.MouseEvent): void => {
function mouseUpHandler() {
setResizing(false)
window.removeEventListener('mouseup', mouseUpHandler)
}
window.addEventListener('mouseup', mouseUpHandler)

const start = windowToCanvasCoordinates(
scale,
canvasOffset,
windowPoint({ x: event.nativeEvent.x, y: event.nativeEvent.y }),
)
setResizing(true)

dispatch([
CanvasActions.createInteractionSession(
Expand All @@ -175,34 +188,84 @@ export const GridResizingControl = React.memo((props: GridResizingControlProps)
const labelId = `grid-${props.axis}-handle-${props.dimensionIndex}`
const containerId = `${labelId}-container`

const shadowSize = React.useMemo(() => {
return props.axis === 'column'
? props.containingFrame.height + GRID_RESIZE_HANDLE_CONTAINER_SIZE
: props.containingFrame.width + GRID_RESIZE_HANDLE_CONTAINER_SIZE
}, [props.containingFrame, props.axis])

return (
<div
key={containerId}
data-testid={containerId}
style={{
position: 'absolute',
left:
props.axis === 'column' ? props.workingPrefix - GridResizingContainerSize / 2 : undefined,
top:
props.axis === 'row'
? props.workingPrefix - GridResizingContainerSize / 2
: props.containingFrame.y - 30 / scale,
right: props.axis === 'row' ? 10 / scale - props.containingFrame.x : undefined,
width: props.axis === 'column' ? GridResizingContainerSize : `max-content`,
height: props.axis === 'row' ? GridResizingContainerSize : `max-content`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
alignItems: props.axis === 'column' ? 'flex-start' : 'center',
justifyContent: props.axis === 'column' ? 'center' : 'flex-start',
border: `1px solid ${resizing ? colorTheme.brandNeonPink.value : 'transparent'}`,
height: props.axis === 'column' && resizing ? shadowSize : '100%',
width: props.axis === 'row' && resizing ? shadowSize : '100%',
position: 'relative',
...(resizing
? UtopiaStyles.backgrounds.stripedBackground(colorTheme.brandNeonPink60.value, scale)
: {}),
}}
>
<CanvasLabel
testId={labelId}
value={getLabelForAxis(props.dimension, props.dimensionIndex, props.fromPropsAxisValues)}
scale={scale}
color={colorTheme.brandNeonPink.value}
textColor={colorTheme.white.value}
<div
data-testid={labelId}
style={{
zoom: 1 / scale,
width: GRID_RESIZE_HANDLE_SIZE,
height: GRID_RESIZE_HANDLE_SIZE,
borderRadius: '100%',
border: `1px solid ${colorTheme.border0.value}`,
boxShadow: `${colorTheme.canvasControlsSizeBoxShadowColor50.value} 0px 0px
${1 / scale}px, ${colorTheme.canvasControlsSizeBoxShadowColor20.value} 0px ${
1 / scale
}px ${2 / scale}px ${1 / scale}px`,
background: colorTheme.white.value,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: gridEdgeToCSSCursor(props.axis === 'column' ? 'column-start' : 'row-start'),
fontSize: 8,
}}
css={{
opacity: resizing ? 1 : 0.5,
':hover': {
opacity: 1,
},
}}
onMouseDown={mouseDownHandler}
/>
>
{props.axis === 'row' ? '↕' : '↔'}
</div>
{when(
resizing,
<div
style={{
position: 'absolute',
top: props.axis === 'column' ? GRID_RESIZE_HANDLE_CONTAINER_SIZE : 0,
left: props.axis === 'row' ? GRID_RESIZE_HANDLE_CONTAINER_SIZE : 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<CanvasLabel
value={getLabelForAxis(
props.dimension,
props.dimensionIndex,
props.fromPropsAxisValues,
)}
scale={scale}
color={colorTheme.brandNeonPink.value}
textColor={colorTheme.white.value}
/>
</div>,
)}
</div>
)
})
Expand All @@ -214,50 +277,75 @@ export interface GridResizingProps {
containingFrame: CanvasRectangle
axis: 'row' | 'column'
gap: number | null
padding: Sides | null
}

export const GridResizing = React.memo((props: GridResizingProps) => {
const canvasScale = useEditorState(
Substores.canvasOffset,
(store) => store.editor.canvas.scale,
'GridResizing canvasScale',
)
if (props.axisValues == null) {
return null
} else {
switch (props.axisValues.type) {
case 'DIMENSIONS':
let workingPrefix: number =
props.axis === 'column' ? props.containingFrame.x : props.containingFrame.y
return (
<>
{props.axisValues.dimensions.flatMap((dimension, dimensionIndex) => {
// Assumes pixels currently.
workingPrefix += dimension.value
if (dimensionIndex === 0) {
// Shift by half the gap initially...
workingPrefix += (props.gap ?? 0) / 2
} else {
// ...Then by the full gap, as it would be half from the prior entry
// and half from the current one.
workingPrefix += props.gap ?? 0
}

return (
<GridResizingControl
key={`grid-resizing-control-${dimensionIndex}`}
dimensionIndex={dimensionIndex}
dimension={dimension}
fromPropsAxisValues={props.fromPropsAxisValues}
axis={props.axis}
containingFrame={props.containingFrame}
workingPrefix={workingPrefix}
/>
)
})}
</>
)
case 'FALLBACK':
return null
default:
assertNever(props.axisValues)
return null
}
}
switch (props.axisValues.type) {
case 'DIMENSIONS':
const size = GRID_RESIZE_HANDLE_CONTAINER_SIZE / canvasScale
return (
<div
style={{
position: 'absolute',
top: props.containingFrame.y - (props.axis === 'column' ? size : 0),
left: props.containingFrame.x - (props.axis === 'row' ? size : 0),
width:
props.axis === 'column'
? props.containingFrame.width
: size + props.containingFrame.width,
height: props.axis === 'row' ? props.containingFrame.height : size,
display: 'grid',
gridTemplateColumns:
props.axis === 'column'
? props.axisValues.dimensions.map((dim) => `${dim.value}${dim.unit}`).join(' ')
: undefined,
gridTemplateRows:
props.axis === 'row'
? props.axisValues.dimensions.map((dim) => `${dim.value}${dim.unit}`).join(' ')
: undefined,
gap: props.gap ?? 0,
paddingLeft:
props.axis === 'column' && props.padding != null
? `${props.padding.left}px`
: undefined,
paddingTop:
props.axis === 'row' && props.padding != null ? `${props.padding.top}px` : undefined,
}}
>
{props.axisValues.dimensions.flatMap((dimension, dimensionIndex) => {
return (
<GridResizingControl
key={`grid-resizing-control-${dimensionIndex}`}
dimensionIndex={dimensionIndex}
dimension={dimension}
fromPropsAxisValues={props.fromPropsAxisValues}
axis={props.axis}
containingFrame={props.containingFrame}
padding={
props.padding == null
? 0
: props.axis === 'column'
? props.padding.left ?? 0
: props.padding.top ?? 0
}
/>
)
})}
</div>
)
case 'FALLBACK':
return null
default:
assertNever(props.axisValues)
}
})
GridResizing.displayName = 'GridResizing'
Expand Down Expand Up @@ -602,7 +690,7 @@ export const GridControls = controlForStrategyMemoized(() => {
>
{when(
features.Grid.dotgrid,
<>
<React.Fragment>
<div
style={{
position: 'absolute',
Expand Down Expand Up @@ -657,7 +745,7 @@ export const GridControls = controlForStrategyMemoized(() => {
right: -1,
}}
/>
</>,
</React.Fragment>,
)}
</div>
)
Expand Down Expand Up @@ -742,6 +830,7 @@ export const GridControls = controlForStrategyMemoized(() => {
containingFrame={grid.frame}
axis={'column'}
gap={grid.gap}
padding={grid.padding}
/>
)
})}
Expand All @@ -754,6 +843,7 @@ export const GridControls = controlForStrategyMemoized(() => {
containingFrame={grid.frame}
axis={'row'}
gap={grid.gap}
padding={grid.padding}
/>
)
})}
Expand Down

0 comments on commit 9bdeed7

Please sign in to comment.