Skip to content

Commit

Permalink
Grid cell resize from the edges (#6529)
Browse files Browse the repository at this point in the history
**Problem:**

Instead of having dedicated control handles for resizing a grid cell
(when the child is filling/stretching the parent), we should use instead
the same edge controls used for absolute move.

**Fix:**

1. Factor out the edge components from the absolute resize controls into
a hook `useResizeEdges`, and make it so it accepts custom event
callbacks for the mouse interactions and which cursors to display while
hovering
2. Replace the old grid edge handles in the grid controls with the new
edges, hooking them up with the existing resize logic
3. Show the `ResizeCol`/`ResizeRow` cursors


https://github.com/user-attachments/assets/8ae7eec6-ea33-4b4a-8445-f60390605302

Fixes #6528
  • Loading branch information
ruggi authored Oct 14, 2024
1 parent ddd55e0 commit ad39087
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 263 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as PP from '../../../../core/shared/property-path'
import { styleStringInArray } from '../../../../utils/common-constants'
import { trueUpGroupElementChanged } from '../../../editor/store/editor-state'
import { stylePropPathMappingFn } from '../../../inspector/common/property-path-hooks'
import { isFixedHugFillModeApplied } from '../../../inspector/inspector-common'
import { isFillOrStretchModeApplied } from '../../../inspector/inspector-common'
import type { EdgePosition } from '../../canvas-types'
import { oppositeEdgePosition } from '../../canvas-types'
import {
Expand Down Expand Up @@ -84,10 +84,7 @@ export function basicResizeStrategy(
const elementParentBounds = metadata?.specialSizeMeasurements.immediateParentBounds ?? null

const isGridCell = MetadataUtils.isGridCell(canvasState.startingMetadata, selectedElement)
if (
isGridCell &&
isFixedHugFillModeApplied(canvasState.startingMetadata, selectedElement, 'fill')
) {
if (isGridCell && isFillOrStretchModeApplied(canvasState.startingMetadata, selectedElement)) {
return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import {
offsetPoint,
} from '../../../../core/shared/math-utils'
import { selectComponentsForTest } from '../../../../utils/utils.test-utils'
import { GridResizeEdgeTestId } from '../../controls/grid-controls-for-strategies'
import { mouseDragFromPointToPoint } from '../../event-helpers.test-utils'
import type { EditorRenderResult } from '../../ui-jsx.test-utils'
import { renderTestEditorWithCode } from '../../ui-jsx.test-utils'
import type { GridResizeEdge } from '../interaction-state'
import { gridCellTargetId } from './grid-cell-bounds'
import { ResizePointTestId } from '../../controls/select-mode/absolute-resize-control'
import { gridEdgeToEdgePosition } from '../../controls/grid-controls-for-strategies'

async function runCellResizeTest(
editor: EditorRenderResult,
Expand All @@ -30,7 +31,9 @@ async function runCellResizeTest(
) {
await selectComponentsForTest(editor, [elementPathToDrag])

const resizeControl = editor.renderedDOM.getByTestId(GridResizeEdgeTestId(edge))
const resizeControl = editor.renderedDOM.getByTestId(
ResizePointTestId(gridEdgeToEdgePosition(edge)),
)
const targetGridCell = editor.renderedDOM.getByTestId(dragToCellTestId)

await mouseDragFromPointToPoint(
Expand Down Expand Up @@ -61,7 +64,9 @@ async function runCellResizeTestWithDragVector(
) {
await selectComponentsForTest(editor, [elementPathToDrag])

const resizeControl = editor.renderedDOM.getByTestId(GridResizeEdgeTestId(edge))
const resizeControl = editor.renderedDOM.getByTestId(
ResizePointTestId(gridEdgeToEdgePosition(edge)),
)

const resizeControlCenter = getRectCenter(
localRectangle({
Expand All @@ -82,15 +87,6 @@ async function runCellResizeTestWithDragVector(
}

describe('grid resize element strategy', () => {
it('cannot resize non-filling cells', async () => {
const editor = await renderTestEditorWithCode(ProjectCode, 'await-first-dom-report')

await selectComponentsForTest(editor, [EP.fromString('sb/scene/grid/grid-child-not-filling')])

const resizeControl = editor.renderedDOM.queryByTestId(GridResizeEdgeTestId('column-end'))
expect(resizeControl).toBeNull()
})

describe('column-end', () => {
it('can enlarge element', async () => {
const editor = await renderTestEditorWithCode(ProjectCode, 'await-first-dom-report')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
rectangleIntersection,
} from '../../../../core/shared/math-utils'
import { isCSSKeyword } from '../../../inspector/common/css-utils'
import { isFixedHugFillModeApplied } from '../../../inspector/inspector-common'
import { isFillOrStretchModeApplied } from '../../../inspector/inspector-common'
import {
controlsForGridPlaceholders,
gridEdgeToEdgePosition,
Expand Down Expand Up @@ -52,10 +52,7 @@ export const gridResizeElementStrategy: CanvasStrategyFactory = (
return null
}

const isFillOrStretchContainer =
isFixedHugFillModeApplied(canvasState.startingMetadata, selectedElement, 'fill') ||
isFixedHugFillModeApplied(canvasState.startingMetadata, selectedElement, 'stretch')
if (!isFillOrStretchContainer) {
if (!isFillOrStretchModeApplied(canvasState.startingMetadata, selectedElement)) {
return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { controlForStrategyMemoized } from '../canvas-strategies/canvas-strategy
import type { GridResizeEdge } from '../canvas-strategies/interaction-state'
import type { EdgePosition } from '../canvas-types'
import {
CSSCursor,
EdgePositionBottom,
EdgePositionLeft,
EdgePositionRight,
Expand All @@ -31,6 +30,7 @@ import {
GridResizeControlsComponent,
GridRowColumnResizingControlsComponent,
} from './grid-controls'
import { isEdgePositionOnSide } from '../canvas-utils'

export const GridCellTestId = (elementPath: ElementPath) => `grid-cell-${EP.toString(elementPath)}`

Expand Down Expand Up @@ -167,8 +167,6 @@ export interface GridControlsProps {

export const GridControls = controlForStrategyMemoized<GridControlsProps>(GridControlsComponent)

export const GridResizeEdgeTestId = (edge: GridResizeEdge) => `grid-resize-edge-${edge}`

interface GridResizeControlProps {
target: ElementPath
}
Expand All @@ -192,6 +190,22 @@ export function gridEdgeToEdgePosition(edge: GridResizeEdge): EdgePosition {
}
}

export function edgePositionToGridResizeEdge(position: EdgePosition): GridResizeEdge | null {
if (!isEdgePositionOnSide(position)) {
return null
} else if (position.x === 0) {
return 'column-start'
} else if (position.x === 1) {
return 'column-end'
} else if (position.y === 0) {
return 'row-start'
} else if (position.y === 1) {
return 'row-end'
} else {
return null
}
}

export function controlsForGridPlaceholders(
gridPath: ElementPath,
whenToShow: WhenToShowControl = 'always-visible',
Expand Down
107 changes: 37 additions & 70 deletions editor/src/components/canvas/controls/grid-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
import { toFirst } from '../../../core/shared/optics/optic-utilities'
import type { Optic } from '../../../core/shared/optics/optics'
import { optionalMap } from '../../../core/shared/optional-utils'
import { assertNever } from '../../../core/shared/utils'
import { assertNever, NO_OP } from '../../../core/shared/utils'
import { Modifier } from '../../../utils/modifiers'
import { when } from '../../../utils/react-conditionals'
import { useColorTheme, UtopiaStyles } from '../../../uuiui'
Expand All @@ -50,16 +50,11 @@ import {
printGridCSSNumber,
} from '../../inspector/common/css-utils'
import CanvasActions from '../canvas-actions'
import type {
GridResizeEdge,
GridResizeEdgeProperties,
} from '../canvas-strategies/interaction-state'
import type { GridResizeEdge } from '../canvas-strategies/interaction-state'
import {
createInteractionViaMouse,
gridAxisHandle,
gridCellHandle,
gridResizeEdgeProperties,
GridResizeEdges,
gridResizeHandle,
} from '../canvas-strategies/interaction-state'
import type { GridCellCoordinates } from '../canvas-strategies/strategies/grid-cell-bounds'
Expand All @@ -70,20 +65,22 @@ import {
} from '../canvas-strategies/strategies/grid-helpers'
import { canResizeGridTemplate } from '../canvas-strategies/strategies/resize-grid-strategy'
import { resizeBoundingBoxFromSide } from '../canvas-strategies/strategies/resize-helpers'
import type { EdgePosition } from '../canvas-types'
import { CSSCursor } from '../canvas-types'
import { windowToCanvasCoordinates } from '../dom-lookup'
import type { Axis } from '../gap-utils'
import { useCanvasAnimation } from '../ui-jsx-canvas-renderer/animation-context'
import { CanvasOffsetWrapper } from './canvas-offset-wrapper'
import type { GridControlsProps, GridData } from './grid-controls-for-strategies'
import {
edgePositionToGridResizeEdge,
getNullableAutoOrTemplateBaseString,
GridCellTestId,
gridEdgeToEdgePosition,
GridResizeEdgeTestId,
useGridData,
} from './grid-controls-for-strategies'
import { useMaybeHighlightElement } from './select-mode/select-mode-hooks'
import { useResizeEdges } from './select-mode/use-resize-edges'

const CELL_ANIMATION_DURATION = 0.15 // seconds

Expand Down Expand Up @@ -820,8 +817,6 @@ const GridControl = React.memo<GridControlProps>(({ grid }) => {
const countedColumn = Math.floor(cell % grid.columns) + 1
const id = gridCellTargetId(grid.elementPath, countedRow, countedColumn)
const borderID = `${id}-border`
const dotgridColor =
activelyDraggingOrResizingCell != null ? colorTheme.blackOpacity35.value : 'transparent'

const isActiveCell =
countedColumn === currentHoveredCell?.column && countedRow === currentHoveredCell?.row
Expand Down Expand Up @@ -1455,12 +1450,9 @@ export const GridResizeControlsComponent = ({ target }: GridResizeControlProps)

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 @@ -1476,7 +1468,6 @@ export const GridResizeControlsComponent = ({ target }: GridResizeControlProps)
(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 Down Expand Up @@ -1509,6 +1500,34 @@ export const GridResizeControlsComponent = ({ target }: GridResizeControlProps)
return scaledFrame.width * scale > 30 && scaledFrame.height > 30
}, [element, scale, isResizing])

const onEdgeMouseDown = React.useCallback(
(position: EdgePosition) => (e: React.MouseEvent<HTMLDivElement>) => {
if (element == null) {
return
}

const edge = edgePositionToGridResizeEdge(position)
if (edge == null) {
return
}

startResizeInteraction(EP.toUid(element.elementPath), edge)(e)
},
[element, startResizeInteraction],
)

const resizeEdges = useResizeEdges([target], {
onEdgeDoubleClick: () => NO_OP,
onEdgeMouseMove: NO_OP,
onEdgeMouseDown: onEdgeMouseDown,
cursors: {
top: CSSCursor.RowResize,
bottom: CSSCursor.RowResize,
left: CSSCursor.ColResize,
right: CSSCursor.ColResize,
},
})

if (
element == null ||
element.globalFrame == null ||
Expand Down Expand Up @@ -1541,57 +1560,16 @@ export const GridResizeControlsComponent = ({ target }: GridResizeControlProps)
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>
)
})}
{resizeEdges.top}
{resizeEdges.left}
{resizeEdges.bottom}
{resizeEdges.right}
</div>
</div>
</CanvasOffsetWrapper>
)
}

const GRID_RESIZE_HANDLE_SIZES = {
long: 24,
short: 4,
}

function gridEdgeToCSSCursor(edge: GridResizeEdge): CSSCursor {
switch (edge) {
case 'column-end':
Expand All @@ -1605,17 +1583,6 @@ function gridEdgeToCSSCursor(edge: GridResizeEdge): CSSCursor {
}
}

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,
}
}

function gridKeyFromPath(path: ElementPath): string {
return `grid-${EP.toString(path)}`
}
Expand Down
Loading

0 comments on commit ad39087

Please sign in to comment.