From ad390875dc166296e4f8f3f6da29d4672d35d578 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:42:29 +0200 Subject: [PATCH] Grid cell resize from the edges (#6529) **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 --- .../strategies/basic-resize-strategy.tsx | 7 +- ...-resize-element-strategy.spec.browser2.tsx | 20 +- .../grid-resize-element-strategy.ts | 7 +- .../controls/grid-controls-for-strategies.tsx | 20 +- .../canvas/controls/grid-controls.tsx | 107 ++++----- .../select-mode/absolute-resize-control.tsx | 208 +++++------------- .../controls/select-mode/resize-edge.tsx | 33 +++ .../controls/select-mode/use-resize-edges.tsx | 133 +++++++++++ .../components/inspector/inspector-common.ts | 10 + ...performance-regression-tests.spec.tsx.snap | 32 +-- 10 files changed, 314 insertions(+), 263 deletions(-) create mode 100644 editor/src/components/canvas/controls/select-mode/resize-edge.tsx create mode 100644 editor/src/components/canvas/controls/select-mode/use-resize-edges.tsx 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 9c38e0755315..5135bee92379 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 @@ -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 { @@ -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 } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx index a5c28b4d6af5..94ec2157177d 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx @@ -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, @@ -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( @@ -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({ @@ -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') diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts index 29b20a5ab66e..fbd27c0778ba 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts @@ -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, @@ -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 } diff --git a/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx b/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx index 41e56da794a5..c1e6beda11d3 100644 --- a/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx +++ b/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx @@ -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, @@ -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)}` @@ -167,8 +167,6 @@ export interface GridControlsProps { export const GridControls = controlForStrategyMemoized(GridControlsComponent) -export const GridResizeEdgeTestId = (edge: GridResizeEdge) => `grid-resize-edge-${edge}` - interface GridResizeControlProps { target: ElementPath } @@ -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', diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index 1fee7f42f12e..c3f9486c031a 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -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' @@ -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' @@ -70,6 +65,7 @@ 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' @@ -77,13 +73,14 @@ 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 @@ -820,8 +817,6 @@ const GridControl = React.memo(({ 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 @@ -1455,12 +1450,9 @@ export const GridResizeControlsComponent = ({ target }: GridResizeControlProps) const isResizing = bounds != null - const [resizingEdge, setResizingEdge] = React.useState(null) - const onMouseUp = React.useCallback(() => { setBounds(null) setStartingBounds(null) - setResizingEdge(null) }, []) React.useEffect(() => { @@ -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( @@ -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) => { + 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 || @@ -1541,57 +1560,16 @@ export const GridResizeControlsComponent = ({ target }: GridResizeControlProps) pointerEvents: 'none', }} > - {GridResizeEdges.map((edge) => { - const properties = gridResizeEdgeProperties(edge) - const visible = !isResizing || resizingEdge === edge - return ( -
-
-
- ) - })} + {resizeEdges.top} + {resizeEdges.left} + {resizeEdges.bottom} + {resizeEdges.right}
) } -const GRID_RESIZE_HANDLE_SIZES = { - long: 24, - short: 4, -} - function gridEdgeToCSSCursor(edge: GridResizeEdge): CSSCursor { switch (edge) { case 'column-end': @@ -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)}` } 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 c8db573bd4af..366453bbf8ad 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 @@ -18,7 +18,7 @@ import { useColorTheme } from '../../../../uuiui' import type { EditorDispatch } from '../../../editor/action-types' import { applyCommandsAction } from '../../../editor/actions/action-creators' import { useDispatch } from '../../../editor/store/dispatch-context' -import { AllElementProps, getMetadata } from '../../../editor/store/editor-state' +import { getMetadata } from '../../../editor/store/editor-state' import { Substores, useEditorState, useRefEditorState } from '../../../editor/store/store-hook' import type { FixedHugFill } from '../../../inspector/inspector-common' import { @@ -34,14 +34,13 @@ import { createInteractionViaMouse } from '../../canvas-strategies/interaction-s import type { EdgePosition } from '../../canvas-types' import { CSSCursor } from '../../canvas-types' import { getAllTargetsUnderAreaAABB, windowToCanvasCoordinates } from '../../dom-lookup' -import { SmallElementSize, useBoundingBox } from '../bounding-box-hooks' +import { useBoundingBox } from '../bounding-box-hooks' import { CanvasOffsetWrapper } from '../canvas-offset-wrapper' import { isZeroSizedElement } from '../outline-utils' import { useMaybeHighlightElement } from './select-mode-hooks' import { isEdgePositionEqualTo } from '../../canvas-utils' import { treatElementAsGroupLike } from '../../canvas-strategies/strategies/group-helpers' -import { treatElementAsFragmentLike } from '../../canvas-strategies/strategies/fragment-like-helpers' -import { ElementPathTrees } from '../../../../core/shared/element-path-tree' +import { useResizeEdges } from './use-resize-edges' export const AbsoluteResizeControlTestId = (targets: Array): string => `${targets.map(EP.toString).sort()}-absolute-resize-control` @@ -54,17 +53,16 @@ interface AbsoluteResizeControlProps { export const SizeLabelID = 'SizeLabel' export const SizeLabelTestId = 'SizeLabelTestId' -function shouldUseSmallElementResizeControl(size: number, scale: number): boolean { - return size <= SmallElementSize / scale -} - export const AbsoluteResizeControl = controlForStrategyMemoized( ({ targets, pathsWereReplaced }: AbsoluteResizeControlProps) => { - const scale = useEditorState( - Substores.canvasOffset, - (store) => store.editor.canvas.scale, - 'AbsoluteResizeControl scale', - ) + const dispatch = useDispatch() + + const canvasOffsetRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) + const metadataRef = useRefEditorState((store) => store.editor.jsxMetadata) + const selectedElementsRef = useRefEditorState((store) => store.editor.selectedViews) + const elementPathTreeRef = useRefEditorState((store) => store.editor.elementPathTree) + + const { maybeClearHighlightsOnHoverEnd } = useMaybeHighlightElement() const controlRef = useBoundingBox(targets, (ref, safeGappedBoundingBox, realBoundingBox) => { if (isZeroSizedElement(realBoundingBox)) { @@ -78,54 +76,6 @@ export const AbsoluteResizeControl = controlForStrategyMemoized( } }) - const leftRef = useBoundingBox(targets, (ref, boundingBox) => { - const isSmallElement = shouldUseSmallElementResizeControl(boundingBox.width, scale) - const lineSize = ResizeMouseAreaSize / scale - const width = isSmallElement ? lineSize / 2 : lineSize - const offsetLeft = `${-lineSize / 2}px` - const offsetTop = `0px` - - ref.current.style.width = `${width}px` - ref.current.style.transform = `translate(${offsetLeft}, ${offsetTop})` - ref.current.style.height = boundingBox.height + 'px' - }) - const topRef = useBoundingBox(targets, (ref, boundingBox) => { - const isSmallElement = shouldUseSmallElementResizeControl(boundingBox.height, scale) - const lineSize = ResizeMouseAreaSize / scale - const height = isSmallElement ? lineSize / 2 : lineSize - const offsetLeft = `0px` - const offsetTop = `${-lineSize / 2}px` - - ref.current.style.width = boundingBox.width + 'px' - ref.current.style.height = height + 'px' - ref.current.style.transform = `translate(${offsetLeft}, ${offsetTop})` - }) - const rightRef = useBoundingBox(targets, (ref, boundingBox) => { - const isSmallElement = shouldUseSmallElementResizeControl(boundingBox.width, scale) - const lineSize = ResizeMouseAreaSize / scale - const width = isSmallElement ? lineSize / 2 : lineSize - const offsetLeft = isSmallElement ? `0px` : `${-lineSize / 2}px` - const offsetTop = `0px` - - ref.current.style.transform = `translate(${offsetLeft}, ${offsetTop})` - ref.current.style.left = boundingBox.width + 'px' - ref.current.style.width = width + 'px' - ref.current.style.height = boundingBox.height + 'px' - }) - - const bottomRef = useBoundingBox(targets, (ref, boundingBox) => { - const isSmallElement = shouldUseSmallElementResizeControl(boundingBox.height, scale) - const lineSize = ResizeMouseAreaSize / scale - const height = isSmallElement ? lineSize / 2 : lineSize - const offsetLeft = `0px` - const offsetTop = isSmallElement ? `0px` : `${-lineSize / 2}px` - - ref.current.style.transform = `translate(${offsetLeft}, ${offsetTop})` - ref.current.style.top = boundingBox.height + 'px' - ref.current.style.width = boundingBox.width + 'px' - ref.current.style.height = height + 'px' - }) - const topLeftRef = useBoundingBox(targets, NO_OP) const topRightRef = useBoundingBox(targets, (ref, boundingBox) => { ref.current.style.left = boundingBox.width + 'px' @@ -144,6 +94,47 @@ export const AbsoluteResizeControl = controlForStrategyMemoized( ref.current.style.width = boundingBox.width + 'px' }) + const scale = useEditorState( + Substores.canvasOffset, + (store) => store.editor.canvas.scale, + 'AbsoluteResizeControl scale', + ) + const onEdgeMouseDown = React.useCallback( + (position: EdgePosition) => (event: React.MouseEvent) => { + startResizeInteraction(event, dispatch, position, canvasOffsetRef.current, scale) + }, + [dispatch, canvasOffsetRef, scale], + ) + + const onEdgeMouseMove = React.useCallback( + (event: React.MouseEvent) => { + maybeClearHighlightsOnHoverEnd() + event.stopPropagation() + }, + [maybeClearHighlightsOnHoverEnd], + ) + + const onEdgeDoubleClick = React.useCallback( + (direction: 'horizontal' | 'vertical') => () => { + executeFirstApplicableStrategy( + dispatch, + setPropHugStrategies( + metadataRef.current, + selectedElementsRef.current, + elementPathTreeRef.current, + invert(direction), + ), + ) + }, + [dispatch, metadataRef, elementPathTreeRef, selectedElementsRef], + ) + + const resizeEdges = useResizeEdges(targets, { + onEdgeMouseDown, + onEdgeMouseMove, + onEdgeDoubleClick, + }) + return (
- - - - + {resizeEdges.top} + {resizeEdges.left} + {resizeEdges.bottom} + {resizeEdges.right} ((props, ref) => { - const scale = useEditorState( - Substores.canvasOffset, - (store) => store.editor.canvas.scale, - 'ResizeEdge scale', - ) - const dispatch = useDispatch() - const canvasOffsetRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) - const metadataRef = useRefEditorState((store) => store.editor.jsxMetadata) - const selectedElementsRef = useRefEditorState((store) => store.editor.selectedViews) - const elementPathTreeRef = useRefEditorState((store) => store.editor.elementPathTree) - - const { maybeClearHighlightsOnHoverEnd } = useMaybeHighlightElement() - - const onEdgeMouseDown = React.useCallback( - (event: React.MouseEvent) => { - startResizeInteraction(event, dispatch, props.position, canvasOffsetRef.current, scale) - }, - [dispatch, props.position, canvasOffsetRef, scale], - ) - - const onMouseMove = React.useCallback( - (event: React.MouseEvent) => { - maybeClearHighlightsOnHoverEnd() - event.stopPropagation() - }, - [maybeClearHighlightsOnHoverEnd], - ) - - const onEdgeDblClick = React.useCallback(() => { - executeFirstApplicableStrategy( - dispatch, - setPropHugStrategies( - metadataRef.current, - selectedElementsRef.current, - elementPathTreeRef.current, - invert(props.direction), - ), - ) - }, [dispatch, metadataRef, props.direction, elementPathTreeRef, selectedElementsRef]) - - return ( -
- ) - }), -) - const sizeLabel = (state: FixedHugFill['type'], actualSize: number): string => { switch (state) { case 'fill': diff --git a/editor/src/components/canvas/controls/select-mode/resize-edge.tsx b/editor/src/components/canvas/controls/select-mode/resize-edge.tsx new file mode 100644 index 000000000000..6e913ffe61f2 --- /dev/null +++ b/editor/src/components/canvas/controls/select-mode/resize-edge.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import type { CSSCursor, EdgePosition } from '../../canvas-types' +import { ResizePointTestId } from './absolute-resize-control' + +interface ResizeEdgeProps { + cursor: CSSCursor + direction: 'horizontal' | 'vertical' + position: EdgePosition + onMouseDown: (e: React.MouseEvent) => void + onMouseMove: (e: React.MouseEvent) => void + onDoubleClick: (e: React.MouseEvent) => void +} + +export const ResizeEdge = React.memo( + React.forwardRef((props, ref) => { + return ( +
+ ) + }), +) +ResizeEdge.displayName = 'ResizeEdge' diff --git a/editor/src/components/canvas/controls/select-mode/use-resize-edges.tsx b/editor/src/components/canvas/controls/select-mode/use-resize-edges.tsx new file mode 100644 index 000000000000..34831303a362 --- /dev/null +++ b/editor/src/components/canvas/controls/select-mode/use-resize-edges.tsx @@ -0,0 +1,133 @@ +import React from 'react' +import type { ElementPath } from '../../../../core/shared/project-file-types' +import { Substores, useEditorState } from '../../../editor/store/store-hook' +import type { EdgePosition } from '../../canvas-types' +import { CSSCursor } from '../../canvas-types' +import { SmallElementSize, useBoundingBox } from '../bounding-box-hooks' +import { ResizeEdge } from './resize-edge' + +const RESIZE_MOUSE_AREA_SIZE = 10 + +export function useResizeEdges( + targets: ElementPath[], + params: { + onEdgeMouseDown: (position: EdgePosition) => (e: React.MouseEvent) => void + onEdgeMouseMove: (e: React.MouseEvent) => void + onEdgeDoubleClick: ( + direction: 'horizontal' | 'vertical', + ) => (e: React.MouseEvent) => void + cursors?: { + top?: CSSCursor + left?: CSSCursor + bottom?: CSSCursor + right?: CSSCursor + } + }, +) { + const scale = useEditorState( + Substores.canvasOffset, + (store) => store.editor.canvas.scale, + 'useResizeEdges scale', + ) + + const topRef = useBoundingBox(targets, (ref, boundingBox) => { + const isSmallElement = shouldUseSmallElementResizeControl(boundingBox.height, scale) + const lineSize = RESIZE_MOUSE_AREA_SIZE / scale + const height = isSmallElement ? lineSize / 2 : lineSize + const offsetLeft = `0px` + const offsetTop = `${-lineSize / 2}px` + + ref.current.style.width = boundingBox.width + 'px' + ref.current.style.height = height + 'px' + ref.current.style.transform = `translate(${offsetLeft}, ${offsetTop})` + }) + + const leftRef = useBoundingBox(targets, (ref, boundingBox) => { + const isSmallElement = shouldUseSmallElementResizeControl(boundingBox.width, scale) + const lineSize = RESIZE_MOUSE_AREA_SIZE / scale + const width = isSmallElement ? lineSize / 2 : lineSize + const offsetLeft = `${-lineSize / 2}px` + const offsetTop = `0px` + + ref.current.style.width = `${width}px` + ref.current.style.transform = `translate(${offsetLeft}, ${offsetTop})` + ref.current.style.height = boundingBox.height + 'px' + }) + + const bottomRef = useBoundingBox(targets, (ref, boundingBox) => { + const isSmallElement = shouldUseSmallElementResizeControl(boundingBox.height, scale) + const lineSize = RESIZE_MOUSE_AREA_SIZE / scale + const height = isSmallElement ? lineSize / 2 : lineSize + const offsetLeft = `0px` + const offsetTop = isSmallElement ? `0px` : `${-lineSize / 2}px` + + ref.current.style.transform = `translate(${offsetLeft}, ${offsetTop})` + ref.current.style.top = boundingBox.height + 'px' + ref.current.style.width = boundingBox.width + 'px' + ref.current.style.height = height + 'px' + }) + + const rightRef = useBoundingBox(targets, (ref, boundingBox) => { + const isSmallElement = shouldUseSmallElementResizeControl(boundingBox.width, scale) + const lineSize = RESIZE_MOUSE_AREA_SIZE / scale + const width = isSmallElement ? lineSize / 2 : lineSize + const offsetLeft = isSmallElement ? `0px` : `${-lineSize / 2}px` + const offsetTop = `0px` + + ref.current.style.transform = `translate(${offsetLeft}, ${offsetTop})` + ref.current.style.left = boundingBox.width + 'px' + ref.current.style.width = width + 'px' + ref.current.style.height = boundingBox.height + 'px' + }) + + return { + top: ( + + ), + left: ( + + ), + bottom: ( + + ), + right: ( + + ), + } +} + +function shouldUseSmallElementResizeControl(size: number, scale: number): boolean { + return size <= SmallElementSize / scale +} diff --git a/editor/src/components/inspector/inspector-common.ts b/editor/src/components/inspector/inspector-common.ts index 32eeadabddf6..2d6cadf9f718 100644 --- a/editor/src/components/inspector/inspector-common.ts +++ b/editor/src/components/inspector/inspector-common.ts @@ -911,6 +911,16 @@ export function isFixedHugFillModeApplied( ) } +export function isFillOrStretchModeApplied( + metadata: ElementInstanceMetadataMap, + element: ElementPath, +): boolean { + return ( + isFixedHugFillModeApplied(metadata, element, 'fill') || + isFixedHugFillModeApplied(metadata, element, 'stretch') + ) +} + export function isFixedHugFillModeAppliedOnAnySide( metadata: ElementInstanceMetadataMap, element: ElementPath, diff --git a/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap b/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap index 4635c7a78aab..18108e135a96 100644 --- a/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap +++ b/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap @@ -54,10 +54,10 @@ Array [ "/UtopiaSpiedExoticType(Symbol(react.fragment))/UtopiaSpiedExoticType(Symbol(react.fragment))//Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.fragment))/UtopiaSpiedExoticType(Symbol(react.fragment))//UtopiaSpiedExoticType(Symbol(react.fragment))", "/UtopiaSpiedExoticType(Symbol(react.fragment))/Symbol(react.memo)()//Symbol(react.memo)()", - "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)())", - "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)())", - "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)())", - "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)())", + "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))", + "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))", + "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))", + "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))", "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))", "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))", "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))", @@ -66,10 +66,10 @@ Array [ "/Symbol(react.memo)()///div:data-testid='utopia-storyboard-uid/scene-aaa/app-entity:parent/ccc-absolute-resize-control'", "/Symbol(react.memo)()///Symbol(react.memo)()", "////div", - "/div/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/div:data-testid='resize-control-1-0.5'", - "/div/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/div:data-testid='resize-control-0.5-1'", - "/div/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/div:data-testid='resize-control-0-0.5'", - "/div/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/div:data-testid='resize-control-0.5-0'", + "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))/Symbol(react.forward_ref)(ResizeEdge)/div:data-testid='resize-control-0.5-0'", + "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))/Symbol(react.forward_ref)(ResizeEdge)/div:data-testid='resize-control-0-0.5'", + "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))/Symbol(react.forward_ref)(ResizeEdge)/div:data-testid='resize-control-0.5-1'", + "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))/Symbol(react.forward_ref)(ResizeEdge)/div:data-testid='resize-control-1-0.5'", "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))/Symbol(react.forward_ref)(ResizePoint)/div", "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))/Symbol(react.forward_ref)(ResizePoint)/div:data-testid='resize-control-0-0'", "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))/Symbol(react.forward_ref)(ResizePoint)/div", @@ -834,10 +834,10 @@ Array [ "/UtopiaSpiedExoticType(Symbol(react.fragment))/UtopiaSpiedExoticType(Symbol(react.fragment))//Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.fragment))/UtopiaSpiedExoticType(Symbol(react.fragment))//UtopiaSpiedExoticType(Symbol(react.fragment))", "/UtopiaSpiedExoticType(Symbol(react.fragment))/Symbol(react.memo)()//Symbol(react.memo)()", - "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)())", - "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)())", - "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)())", - "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)())", + "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))", + "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))", + "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))", + "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))", "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))", "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))", "/Symbol(react.memo)()///Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))", @@ -846,10 +846,10 @@ Array [ "/Symbol(react.memo)()///div:data-testid='utopia-storyboard-uid/scene-aaa/app-entity:parent/ccc-absolute-resize-control'", "/Symbol(react.memo)()///Symbol(react.memo)()", "////div", - "/div/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/div:data-testid='resize-control-1-0.5'", - "/div/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/div:data-testid='resize-control-0.5-1'", - "/div/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/div:data-testid='resize-control-0-0.5'", - "/div/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/div:data-testid='resize-control-0.5-0'", + "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))/Symbol(react.forward_ref)(ResizeEdge)/div:data-testid='resize-control-0.5-0'", + "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))/Symbol(react.forward_ref)(ResizeEdge)/div:data-testid='resize-control-0-0.5'", + "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))/Symbol(react.forward_ref)(ResizeEdge)/div:data-testid='resize-control-0.5-1'", + "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizeEdge))/Symbol(react.forward_ref)(ResizeEdge)/div:data-testid='resize-control-1-0.5'", "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))/Symbol(react.forward_ref)(ResizePoint)/div", "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))/Symbol(react.forward_ref)(ResizePoint)/div:data-testid='resize-control-0-0'", "/div/Symbol(react.memo)(Symbol(react.forward_ref)(ResizePoint))/Symbol(react.forward_ref)(ResizePoint)/div",