Skip to content

Commit

Permalink
Tweak cell resize handles (#6082)
Browse files Browse the repository at this point in the history
**Problem:**

The grid cell resize handles don't scale correctly with zoom < 1, and
they get off-center if the element frame is too small.

**Fix:**

1. Use a different rendering strategy for the elements (position:
absolute instead of grid) so they can scale more easily when the
relative container frame gets tiny
2. Hide the handles if the frame is too tiny (relative to the zoom too)
3. Hide the other three inactive edge handles when resizing is in
progress, so they don't look off

| Before | After |
|--------|------------|
| ![Kapture 2024-07-15 at 18 04
36](https://github.com/user-attachments/assets/6f919b88-4bd7-4592-b788-a0939eb96520)
| ![Kapture 2024-07-15 at 18 03
10](https://github.com/user-attachments/assets/3778cebb-1ab0-409f-b256-21d7ed3300ef)
|
|
https://github.com/user-attachments/assets/d6ba7995-c73e-4715-b4a1-1efb92ae3d5d
|
https://github.com/user-attachments/assets/a889a07b-60fa-4519-bd37-8001ca583f35
|

_I have no idea why GH is truncating those GIFs, see the mp4 attachments
for reference_



Fixes #6081
  • Loading branch information
ruggi authored Jul 16, 2024
1 parent bc45f62 commit 2052444
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,22 @@ export function gridCellHandle(params: { id: string }): GridCellHandle {
export const GridResizeEdges = ['row-start', 'row-end', 'column-start', 'column-end'] as const
export type GridResizeEdge = (typeof GridResizeEdges)[number]

export type GridResizeEdgeProperties = {
isRow: boolean
isColumn: boolean
isStart: boolean
isEnd: boolean
}

export function gridResizeEdgeProperties(edge: GridResizeEdge): GridResizeEdgeProperties {
return {
isRow: edge === 'row-start' || edge === 'row-end',
isColumn: edge === 'column-start' || edge === 'column-end',
isStart: edge === 'row-start' || edge === 'column-start',
isEnd: edge === 'row-end' || edge === 'column-end',
}
}

export interface GridResizeHandle {
type: 'GRID_RESIZE_HANDLE'
id: string
Expand Down
153 changes: 84 additions & 69 deletions editor/src/components/canvas/controls/grid-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { jsx } from '@emotion/react'
import type { AnimationControls } from 'framer-motion'
import { motion, useAnimationControls } from 'framer-motion'
import type { CSSProperties } from 'react'
import React from 'react'
import type { ElementPath } from 'utopia-shared/src/types'
import type { GridCSSNumber } from '../../../components/inspector/common/css-utils'
Expand All @@ -25,6 +26,7 @@ import {
offsetPoint,
pointDifference,
pointsEqual,
scaleRect,
windowPoint,
zeroRectIfNullOrInfinity,
} from '../../../core/shared/math-utils'
Expand All @@ -44,12 +46,16 @@ import { Substores, useEditorState, useRefEditorState } from '../../editor/store
import { useRollYourOwnFeatures } from '../../navigator/left-pane/roll-your-own-pane'
import CanvasActions from '../canvas-actions'
import { controlForStrategyMemoized } from '../canvas-strategies/canvas-strategy-types'
import type { GridResizeEdge } from '../canvas-strategies/interaction-state'
import type {
GridResizeEdge,
GridResizeEdgeProperties,
} from '../canvas-strategies/interaction-state'
import {
GridResizeEdges,
createInteractionViaMouse,
gridAxisHandle,
gridCellHandle,
gridResizeEdgeProperties,
gridResizeHandle,
} from '../canvas-strategies/interaction-state'
import { windowToCanvasCoordinates } from '../dom-lookup'
Expand Down Expand Up @@ -1054,9 +1060,12 @@ export const GridResizeControls = controlForStrategyMemoized<GridResizeControlPr

const isResizing = bounds != null

const [resizingEdge, setResizingEdge] = React.useState<GridResizeEdge | null>(null)

const onMouseUp = React.useCallback(() => {
setBounds(null)
setStartingBounds(null)
setResizingEdge(null)
}, [])

React.useEffect(() => {
Expand All @@ -1072,6 +1081,7 @@ export const GridResizeControls = controlForStrategyMemoized<GridResizeControlPr
(uid: string, edge: GridResizeEdge) => (event: React.MouseEvent) => {
event.stopPropagation()
const frame = zeroRectIfNullOrInfinity(element?.globalFrame ?? null)
setResizingEdge(edge)
setBounds(frame)
setStartingBounds(frame)
const start = windowToCanvasCoordinates(
Expand All @@ -1093,10 +1103,22 @@ export const GridResizeControls = controlForStrategyMemoized<GridResizeControlPr
[canvasOffsetRef, dispatch, element?.globalFrame, scale],
)

const canShowHandles = React.useMemo(() => {
if (isResizing) {
return true
}
if (element?.globalFrame == null || isInfinityRectangle(element.globalFrame)) {
return false
}
const scaledFrame = scaleRect(element.globalFrame, scale)
return scaledFrame.width * scale > 30 && scaledFrame.height > 30
}, [element, scale, isResizing])

if (
element == null ||
element.globalFrame == null ||
isInfinityRectangle(element.globalFrame)
isInfinityRectangle(element.globalFrame) ||
!canShowHandles
) {
return null
}
Expand All @@ -1113,62 +1135,67 @@ export const GridResizeControls = controlForStrategyMemoized<GridResizeControlPr
left: bounds?.x ?? element.globalFrame.x,
width: bounds?.width ?? element.globalFrame.width,
height: bounds?.height ?? element.globalFrame.height,
display: 'grid',
gridTemplateRows: '10px 1fr 10px',
gridTemplateColumns: '10px 1fr 10px',
gridTemplateAreas: "'empty1 rs empty2' 'cs empty3 ce' 'empty4 re empty5'",
backgroundColor: isResizing ? colorTheme.whiteOpacity30.value : 'transparent',
}}
>
{GridResizeEdges.map((edge) => (
<div
key={edge}
style={{
pointerEvents: 'none',
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: 2 / scale,
gridArea: gridEdgeToGridArea(edge),
cursor: gridEdgeToCSSCursor(edge),
}}
>
<div
data-testid={GridResizeEdgeTestId(edge)}
onMouseDown={startResizeInteraction(EP.toUid(element.elementPath), edge)}
style={{
pointerEvents: 'initial',
...gridEdgeToWidthHeight(edge, scale),
backgroundColor: colorTheme.white.value,
boxShadow: `${colorTheme.canvasControlsSizeBoxShadowColor50.value} 0px 0px
${1 / scale}px, ${colorTheme.canvasControlsSizeBoxShadowColor20.value} 0px ${1 / scale}px ${
2 / scale
}px ${1 / scale}px`,
}}
/>
</div>
))}
<div
style={{
position: 'relative',
width: '100%',
height: '100%',
pointerEvents: 'none',
}}
>
{GridResizeEdges.map((edge) => {
const properties = gridResizeEdgeProperties(edge)
const visible = !isResizing || resizingEdge === edge
return (
<div
key={edge}
style={{
visibility: visible ? 'visible' : 'hidden',
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
...gridEdgeToWidthHeight(properties, scale),
}}
>
<div
data-testid={GridResizeEdgeTestId(edge)}
onMouseDown={startResizeInteraction(EP.toUid(element.elementPath), edge)}
style={{
width: properties.isRow
? GRID_RESIZE_HANDLE_SIZES.long
: GRID_RESIZE_HANDLE_SIZES.short,
height: properties.isColumn
? GRID_RESIZE_HANDLE_SIZES.long
: GRID_RESIZE_HANDLE_SIZES.short,
borderRadius: 4,
cursor: gridEdgeToCSSCursor(edge),
pointerEvents: 'initial',
backgroundColor: colorTheme.white.value,
boxShadow: `${colorTheme.canvasControlsSizeBoxShadowColor50.value} 0px 0px
${1 / scale}px, ${
colorTheme.canvasControlsSizeBoxShadowColor20.value
} 0px ${1 / scale}px ${2 / scale}px ${1 / scale}px`,
zoom: 1 / scale,
}}
/>
</div>
)
})}
</div>
</div>
</CanvasOffsetWrapper>
)
},
)

function gridEdgeToGridArea(edge: GridResizeEdge): string {
switch (edge) {
case 'column-end':
return 'ce'
case 'column-start':
return 'cs'
case 'row-end':
return 're'
case 'row-start':
return 'rs'
default:
assertNever(edge)
}
const GRID_RESIZE_HANDLE_SIZES = {
long: 24,
short: 4,
}

function gridEdgeToEdgePosition(edge: GridResizeEdge): EdgePosition {
Expand Down Expand Up @@ -1199,25 +1226,13 @@ function gridEdgeToCSSCursor(edge: GridResizeEdge): CSSCursor {
}
}

function gridEdgeToWidthHeight(
edge: GridResizeEdge,
scale: number,
): {
width: number
height: number
borderRadius: number
} {
const LONG_EDGE = 24 / scale
const SHORT_EDGE = 4 / scale

switch (edge) {
case 'column-end':
case 'column-start':
return { width: SHORT_EDGE, height: LONG_EDGE, borderRadius: SHORT_EDGE / 2 }
case 'row-end':
case 'row-start':
return { width: LONG_EDGE, height: SHORT_EDGE, borderRadius: SHORT_EDGE / 2 }
default:
assertNever(edge)
function gridEdgeToWidthHeight(props: GridResizeEdgeProperties, scale: number): CSSProperties {
return {
width: props.isColumn ? (GRID_RESIZE_HANDLE_SIZES.short * 4) / scale : '100%',
height: props.isRow ? (GRID_RESIZE_HANDLE_SIZES.short * 4) / scale : '100%',
top: props.isStart ? 0 : undefined,
left: props.isStart ? 0 : undefined,
right: props.isEnd ? 0 : undefined,
bottom: props.isEnd ? 0 : undefined,
}
}

0 comments on commit 2052444

Please sign in to comment.