From 7ebe63a42cdc2e10fdef456dc2f7688284b218f8 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Fri, 6 Oct 2023 16:03:33 +0200 Subject: [PATCH] Fix canvas padding controls going out of sync on undo (#4336) --- .../select-mode/padding-resize-control.tsx | 366 ++++++++++-------- 1 file changed, 194 insertions(+), 172 deletions(-) diff --git a/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx b/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx index aa421d9fda13..ebf322c6a537 100644 --- a/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx @@ -1,10 +1,16 @@ import React from 'react' import type { CanvasSubstate } from '../../../../components/editor/store/store-hook-substore-types' -import type { CanvasVector, Size } from '../../../../core/shared/math-utils' -import { size, windowPoint } from '../../../../core/shared/math-utils' +import type { CanvasRectangle, CanvasVector, Size } from '../../../../core/shared/math-utils' +import { + boundingRectangleArray, + canvasRectangle, + isInfinityRectangle, + size, + windowPoint, + zeroRectangle, +} from '../../../../core/shared/math-utils' import type { ElementPath } from '../../../../core/shared/project-file-types' import { assertNever } from '../../../../core/shared/utils' -import { isFeatureEnabled } from '../../../../utils/feature-switches' import type { Modifiers } from '../../../../utils/modifiers' import { emptyModifiers, Modifier } from '../../../../utils/modifiers' import { useColorTheme, UtopiaStyles } from '../../../../uuiui' @@ -35,6 +41,8 @@ import { CanvasOffsetWrapper } from '../canvas-offset-wrapper' import { isZeroSizedElement } from '../outline-utils' import type { CSSNumberWithRenderedValue } from './controls-common' import { CanvasLabel, PillHandle, useHoverWithDelay } from './controls-common' +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import { mapDropNulls } from '../../../../core/shared/array-utils' export const paddingControlTestId = (edge: EdgePiece): string => `padding-control-${edge}` export const paddingControlHandleTestId = (edge: EdgePiece): string => @@ -46,6 +54,7 @@ type Orientation = 'vertical' | 'horizontal' interface ResizeContolProps { edge: EdgePiece + boundingBox: CanvasRectangle shownByParent: boolean setShownByParent: (_: boolean) => void paddingValue: CSSNumberWithRenderedValue @@ -117,152 +126,182 @@ const isDraggingSelector = (store: EditorStorePatched, edge: EdgePiece): boolean store.editor.canvas.interactionSession.activeControl.edgePiece, ).includes(edge) -const PaddingResizeControlI = React.memo( - React.forwardRef((props, ref) => { - const { setShownByParent } = props - const dispatch = useDispatch() - const { scale, isDragging } = useEditorState( - Substores.fullStore, - (store) => ({ - scale: scaleSelector(store), - isDragging: isDraggingSelector(store, props.edge), - }), - 'PaddingResizeControl scale isDragging', - ) +const PaddingResizeControlI = React.memo((props: ResizeContolProps) => { + const { setShownByParent } = props + const dispatch = useDispatch() + const { scale, isDragging } = useEditorState( + Substores.fullStore, + (store) => ({ + scale: scaleSelector(store), + isDragging: isDraggingSelector(store, props.edge), + }), + 'PaddingResizeControl scale isDragging', + ) - const canvasOffsetRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) - const [indicatorShown, setIndicatorShown] = React.useState(false) - const [stripesShown, setStripesShown] = React.useState(false) + const canvasOffsetRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) + const [indicatorShown, setIndicatorShown] = React.useState(false) + const [stripesShown, setStripesShown] = React.useState(false) - const colorTheme = useColorTheme() + const colorTheme = useColorTheme() - const [hoverStartDelayed, hoverEndDelayed] = useHoverWithDelay( - PaddingResizeControlHoverTimeout, - (h) => setShownByParent(h), - ) + const [hoverStartDelayed, hoverEndDelayed] = useHoverWithDelay( + PaddingResizeControlHoverTimeout, + (h) => setShownByParent(h), + ) - const backgroundHoverEnd = React.useCallback( - (e: React.MouseEvent) => { - setStripesShown(false) - hoverEndDelayed(e) - }, - [hoverEndDelayed], - ) + const backgroundHoverEnd = React.useCallback( + (e: React.MouseEvent) => { + setStripesShown(false) + hoverEndDelayed(e) + }, + [hoverEndDelayed], + ) - const hoverStart = React.useCallback((e: React.MouseEvent) => { - setStripesShown(true) - setIndicatorShown(true) - }, []) - - const hoverEnd = React.useCallback((e: React.MouseEvent) => { - setIndicatorShown(false) - }, []) - - const onEdgeMouseDown = React.useCallback( - (event: React.MouseEvent) => { - setShownByParent(true) - startResizeInteraction(event, dispatch, props.edge, canvasOffsetRef.current, scale) - }, - [setShownByParent, dispatch, props.edge, canvasOffsetRef, scale], - ) + const hoverStart = React.useCallback((e: React.MouseEvent) => { + setStripesShown(true) + setIndicatorShown(true) + }, []) - const { cursor, orientation } = edgePieceDerivedProps(props.edge) + const hoverEnd = React.useCallback((e: React.MouseEvent) => { + setIndicatorShown(false) + }, []) - const shown = !isDragging && props.shownByParent - const backgroundShown = props.shownByParent && !isDragging && stripesShown + const onEdgeMouseDown = React.useCallback( + (event: React.MouseEvent) => { + setShownByParent(true) + startResizeInteraction(event, dispatch, props.edge, canvasOffsetRef.current, scale) + }, + [setShownByParent, dispatch, props.edge, canvasOffsetRef, scale], + ) - const { width, height } = sizeFromOrientation( - orientation, - size(PaddingResizeControlWidth / scale, PaddingResizeControlHeight / scale), - ) + const { cursor, orientation } = edgePieceDerivedProps(props.edge) - const [hitAreaPaddingMainSide, hitAreaPaddingUnaffectedSide, borderWidth, dragBorderWidth] = [ - PaddingResizeControlHitAreaPaddingMainSide, - PaddingResizeControlHitAreaPaddingUnaffectedSide, - PaddingResizeControlBorder, - PaddingResizeDragBorder, - ].map((v) => v / scale) - - // We only want the mouse catchment area to be inside the element, so to prevent it from - // overflowing we apply padding on all sides of the pill control that are inside the element, - // and then use a margin to top up the difference so that the pill is always in the correct place - const controlPaddingForEdge = Math.min( - hitAreaPaddingMainSide, - props.paddingValue.renderedValuePx / 2 / scale, - ) - const controlMarginForEdge = hitAreaPaddingMainSide - controlPaddingForEdge - const horizontalPadding = isHorizontalEdgePiece(props.edge) - ? hitAreaPaddingMainSide - : hitAreaPaddingUnaffectedSide - const verticalPadding = isHorizontalEdgePiece(props.edge) - ? hitAreaPaddingUnaffectedSide - : hitAreaPaddingMainSide - - const stripeColor = colorTheme.brandNeonPink.value - const color = colorTheme.brandNeonPink.value - - return ( + const shown = !isDragging && props.shownByParent + const backgroundShown = props.shownByParent && !isDragging && stripesShown + + const { width, height } = sizeFromOrientation( + orientation, + size(PaddingResizeControlWidth / scale, PaddingResizeControlHeight / scale), + ) + + const [hitAreaPaddingMainSide, hitAreaPaddingUnaffectedSide, borderWidth, dragBorderWidth] = [ + PaddingResizeControlHitAreaPaddingMainSide, + PaddingResizeControlHitAreaPaddingUnaffectedSide, + PaddingResizeControlBorder, + PaddingResizeDragBorder, + ].map((v) => v / scale) + + // We only want the mouse catchment area to be inside the element, so to prevent it from + // overflowing we apply padding on all sides of the pill control that are inside the element, + // and then use a margin to top up the difference so that the pill is always in the correct place + const controlPaddingForEdge = React.useMemo( + () => Math.min(hitAreaPaddingMainSide, props.paddingValue.renderedValuePx / 2 / scale), + [props.paddingValue, hitAreaPaddingMainSide, scale], + ) + const controlMarginForEdge = React.useMemo( + () => hitAreaPaddingMainSide - controlPaddingForEdge, + [hitAreaPaddingMainSide, controlPaddingForEdge], + ) + const horizontalPadding = React.useMemo( + () => + isHorizontalEdgePiece(props.edge) ? hitAreaPaddingMainSide : hitAreaPaddingUnaffectedSide, + [props.edge, hitAreaPaddingMainSide, hitAreaPaddingUnaffectedSide], + ) + const verticalPadding = React.useMemo( + () => + isHorizontalEdgePiece(props.edge) ? hitAreaPaddingUnaffectedSide : hitAreaPaddingMainSide, + [props.edge, hitAreaPaddingMainSide, hitAreaPaddingUnaffectedSide], + ) + + const stripeColor = colorTheme.brandNeonPink.value + const color = colorTheme.brandNeonPink.value + + const points = React.useMemo((): { + top: number + left: number + width: number | string + height: number | string + } => { + return { + top: + props.edge === 'bottom' ? props.boundingBox.height - props.paddingValue.renderedValuePx : 0, + left: + props.edge === 'right' ? props.boundingBox.width - props.paddingValue.renderedValuePx : 0, + width: + props.edge === 'top' || props.edge === 'bottom' + ? '100%' + : props.paddingValue.renderedValuePx, + height: + props.edge === 'left' || props.edge === 'right' + ? '100%' + : props.paddingValue.renderedValuePx, + } + }, [props.edge, props.boundingBox, props.paddingValue]) + + return ( +
-
- {!isDragging && indicatorShown && ( -
- -
- )} - -
+ {!isDragging && indicatorShown && ( +
+ +
+ )} +
- ) - }), -) +
+ ) +}) + +PaddingResizeControlI.displayName = 'PaddingResizeControlI' interface PaddingControlProps { targets: Array @@ -282,8 +321,6 @@ export const PaddingResizeControl = controlForStrategyMemoized((props: PaddingCo 'PaddingResizeControl hoveredViews', ) - const numberToPxValue = (n: number) => n + 'px' - const controlRef = useBoundingBox( selectedElements, (ref, safeGappedBoundingBox, realBoundingBox) => { @@ -321,43 +358,28 @@ export const PaddingResizeControl = controlForStrategyMemoized((props: PaddingCo } }, [hoveredViews, selectedElements]) - const currentPadding = combinePaddings( - paddingFromSpecialSizeMeasurements(elementMetadata, selectedElements[0]), - simplePaddingFromMetadata(elementMetadata, selectedElements[0]), - ) - - const leftRef = useBoundingBox(selectedElements, (ref, boundingBox) => { - const padding = simplePaddingFromMetadata(elementMetadata, selectedElements[0]) - ref.current.style.height = numberToPxValue(boundingBox.height) - ref.current.style.width = numberToPxValue(padding.paddingLeft?.renderedValuePx ?? 0) - }) - - const topRef = useBoundingBox(selectedElements, (ref, boundingBox) => { - const padding = simplePaddingFromMetadata(elementMetadata, selectedElements[0]) - ref.current.style.width = numberToPxValue(boundingBox.width) - ref.current.style.height = numberToPxValue(padding.paddingTop?.renderedValuePx ?? 0) - }) - - const rightRef = useBoundingBox(selectedElements, (ref, boundingBox) => { - const padding = simplePaddingFromMetadata(elementMetadata, selectedElements[0]) - ref.current.style.left = numberToPxValue( - boundingBox.width - (padding.paddingRight?.renderedValuePx ?? 0), + const currentPadding = React.useMemo(() => { + return combinePaddings( + paddingFromSpecialSizeMeasurements(elementMetadata, selectedElements[0]), + simplePaddingFromMetadata(elementMetadata, selectedElements[0]), ) - ref.current.style.height = numberToPxValue(boundingBox.height) - ref.current.style.width = numberToPxValue(padding.paddingRight?.renderedValuePx ?? 0) - }) - - const bottomRef = useBoundingBox(selectedElements, (ref, boundingBox) => { - const padding = simplePaddingFromMetadata(elementMetadata, selectedElements[0]) - ref.current.style.top = numberToPxValue( - boundingBox.height - (padding.paddingBottom?.renderedValuePx ?? 0), - ) - ref.current.style.width = numberToPxValue(boundingBox.width) - ref.current.style.height = numberToPxValue(padding.paddingBottom?.renderedValuePx ?? 0) - }) + }, [elementMetadata, selectedElements]) const shownByParent = selectedElementHovered || anyControlHovered + const boundingBox = useEditorState( + Substores.metadata, + (store) => { + const selectedFrames = mapDropNulls((view) => { + const frame = MetadataUtils.getFrameInCanvasCoords(view, store.editor.jsxMetadata) + return frame == null || isInfinityRectangle(frame) ? null : frame + }, selectedElements) + + return boundingRectangleArray(selectedFrames) ?? canvasRectangle(zeroRectangle) + }, + 'PaddingResizeControl boundingBox', + ) + return (