diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx index ee582c73eb9b..b8c839125179 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx @@ -66,6 +66,7 @@ export function absoluteResizeBoundingBoxStrategy( const originalTargets = flattenSelection( getTargetPathsFromInteractionTarget(canvasState.interactionTarget), ) + const retargetedTargets = flattenSelection( retargetStrategyToChildrenOfFragmentLikeElements(canvasState), ) diff --git a/editor/src/components/canvas/controls/bounding-box-hooks.ts b/editor/src/components/canvas/controls/bounding-box-hooks.ts index 58a1061fa0e9..b08407fcf92f 100644 --- a/editor/src/components/canvas/controls/bounding-box-hooks.ts +++ b/editor/src/components/canvas/controls/bounding-box-hooks.ts @@ -25,14 +25,29 @@ interface NotNullRefObject { export function useBoundingBox( selectedElements: ReadonlyArray, - onChangeCallback: (ref: NotNullRefObject, boundingBox: CanvasRectangle, scale: number) => void, + onChangeCallback: ( + ref: NotNullRefObject, + safeGappedBoundingBox: CanvasRectangle, + realBoundingBox: CanvasRectangle, + scale: number, + ) => void, ): React.RefObject { const controlRef = React.useRef(null) const boundingBoxCallback = React.useCallback( - (boundingBox: CanvasRectangle | null, scale: number) => { - const maybeZeroBoundingBox = zeroRectIfNullOrInfinity(boundingBox) + ( + safeGappedBoundingBox: CanvasRectangle | null, + realBoundingBox: CanvasRectangle | null, + scale: number, + ) => { + const maybeZeroSafeGappedBoundingBox = zeroRectIfNullOrInfinity(safeGappedBoundingBox) + const maybeZeroRealBoundingBox = zeroRectIfNullOrInfinity(realBoundingBox) if (controlRef.current != null) { - onChangeCallback(controlRef as NotNullRefObject, maybeZeroBoundingBox, scale) + onChangeCallback( + controlRef as NotNullRefObject, + maybeZeroSafeGappedBoundingBox, + maybeZeroRealBoundingBox, + scale, + ) } }, [onChangeCallback], @@ -48,7 +63,11 @@ export const RESIZE_CONTROL_SAFE_GAP = 4 // safe gap applied when the dimension function useBoundingBoxFromMetadataRef( selectedElements: ReadonlyArray, - boundingBoxCallback: (boundingRectangle: CanvasRectangle | null, scale: number) => void, + boundingBoxCallback: ( + safeGappedBoundingRectangle: CanvasRectangle | null, + realBoundingRectangle: CanvasRectangle | null, + scale: number, + ) => void, ): void { const metadataRef = useRefEditorState((store) => getMetadata(store.editor)) const scaleRef = useRefEditorState((store) => store.editor.canvas.scale) @@ -120,9 +139,10 @@ function useBoundingBoxFromMetadataRef( } return canvasRectangle(adjustedBoundingBox) } - const boundingBox = getAdjustedBoundingBox(boundingRectangleArray(frames)) + const realBoundingBox = boundingRectangleArray(frames) + const safeGappedBoundingBox = getAdjustedBoundingBox(realBoundingBox) - boundingBoxCallbackRef.current(boundingBox, scaleRef.current) + boundingBoxCallbackRef.current(safeGappedBoundingBox, realBoundingBox, scaleRef.current) }, [selectedElements, metadataRef, scaleRef, shouldApplySafeGap]) useSelectorWithCallback( diff --git a/editor/src/components/canvas/controls/highlight-control.tsx b/editor/src/components/canvas/controls/highlight-control.tsx index df861920ee64..07ff11977569 100644 --- a/editor/src/components/canvas/controls/highlight-control.tsx +++ b/editor/src/components/canvas/controls/highlight-control.tsx @@ -9,6 +9,7 @@ interface HighlightControlProps { canvasOffset: CanvasPoint scale: number color?: string + displayZeroSized: 'display-zero-sized' | 'do-not-display-zero-sized' } export const HighlightControl = React.memo((props: HighlightControlProps) => { @@ -18,14 +19,19 @@ export const HighlightControl = React.memo((props: HighlightControlProps) => { props.color === null ? colorTheme.canvasSelectionPrimaryOutline.value : props.color if (isZeroSizedElement(props.frame)) { - return ( - - ) + // Only display the zero size higlight if it should be displayed. + if (props.displayZeroSized === 'display-zero-sized') { + return ( + + ) + } else { + return null + } } else { return (
{ frame={frame} scale={scale} canvasOffset={canvasOffset} + displayZeroSized={'display-zero-sized'} /> ) }) 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 0afe912106ee..b7dce2fe3403 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 @@ -63,15 +63,15 @@ export const AbsoluteResizeControl = controlForStrategyMemoized( 'AbsoluteResizeControl scale', ) - const controlRef = useBoundingBox(targets, (ref, boundingBox) => { - if (isZeroSizedElement(boundingBox)) { + const controlRef = useBoundingBox(targets, (ref, safeGappedBoundingBox, realBoundingBox) => { + if (isZeroSizedElement(realBoundingBox)) { ref.current.style.display = 'none' } else { ref.current.style.display = 'block' - ref.current.style.left = boundingBox.x + 'px' - ref.current.style.top = boundingBox.y + 'px' - ref.current.style.width = boundingBox.width + 'px' - ref.current.style.height = boundingBox.height + 'px' + ref.current.style.left = safeGappedBoundingBox.x + 'px' + ref.current.style.top = safeGappedBoundingBox.y + 'px' + ref.current.style.width = safeGappedBoundingBox.width + 'px' + ref.current.style.height = safeGappedBoundingBox.height + 'px' } }) diff --git a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx index 893fc2c2df77..74244c4963cd 100644 --- a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx @@ -101,17 +101,20 @@ export const BorderRadiusControl = controlForStrategyMemoized EP.pathsEqual(p, selectedElement)) - const controlRef = useBoundingBox([selectedElement], (ref, boundingBox) => { - if (isZeroSizedElement(boundingBox)) { - ref.current.style.display = 'none' - } else { - ref.current.style.display = 'block' - ref.current.style.left = boundingBox.x + 'px' - ref.current.style.top = boundingBox.y + 'px' - ref.current.style.width = boundingBox.width + 'px' - ref.current.style.height = boundingBox.height + 'px' - } - }) + const controlRef = useBoundingBox( + [selectedElement], + (ref, safeGappedBoundingBox, realBoundingBox) => { + if (isZeroSizedElement(realBoundingBox)) { + ref.current.style.display = 'none' + } else { + ref.current.style.display = 'block' + ref.current.style.left = safeGappedBoundingBox.x + 'px' + ref.current.style.top = safeGappedBoundingBox.y + 'px' + ref.current.style.width = safeGappedBoundingBox.width + 'px' + ref.current.style.height = safeGappedBoundingBox.height + 'px' + } + }, + ) const borderRadius = useEditorState( Substores.metadata, 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 9773d2f96723..aa421d9fda13 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 @@ -284,17 +284,20 @@ export const PaddingResizeControl = controlForStrategyMemoized((props: PaddingCo const numberToPxValue = (n: number) => n + 'px' - const controlRef = useBoundingBox(selectedElements, (ref, boundingBox) => { - if (isZeroSizedElement(boundingBox)) { - ref.current.style.display = 'none' - } else { - ref.current.style.display = 'block' - ref.current.style.left = boundingBox.x + 'px' - ref.current.style.top = boundingBox.y + 'px' - ref.current.style.width = boundingBox.width + 'px' - ref.current.style.height = boundingBox.height + 'px' - } - }) + const controlRef = useBoundingBox( + selectedElements, + (ref, safeGappedBoundingBox, realBoundingBox) => { + if (isZeroSizedElement(realBoundingBox)) { + ref.current.style.display = 'none' + } else { + ref.current.style.display = 'block' + ref.current.style.left = safeGappedBoundingBox.x + 'px' + ref.current.style.top = safeGappedBoundingBox.y + 'px' + ref.current.style.width = safeGappedBoundingBox.width + 'px' + ref.current.style.height = safeGappedBoundingBox.height + 'px' + } + }, + ) const timeoutRef = React.useRef(null) diff --git a/editor/src/components/canvas/controls/select-mode/simple-outline-control.tsx b/editor/src/components/canvas/controls/select-mode/simple-outline-control.tsx index 4e0f6ba43962..570ed7b28e80 100644 --- a/editor/src/components/canvas/controls/select-mode/simple-outline-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/simple-outline-control.tsx @@ -83,17 +83,20 @@ export const OutlineControl = React.memo((props) => { 'OutlineControl scale', ) - const outlineRef = useBoundingBox(targets, (ref, boundingBox, canvasScale) => { - if (isZeroSizedElement(boundingBox)) { - ref.current.style.display = 'none' - } else { - ref.current.style.display = 'block' - ref.current.style.left = `${boundingBox.x - 0.5 / canvasScale}px` - ref.current.style.top = `${boundingBox.y - 0.5 / canvasScale}px` - ref.current.style.width = `${boundingBox.width + 1 / canvasScale}px` - ref.current.style.height = `${boundingBox.height + 1 / canvasScale}px` - } - }) + const outlineRef = useBoundingBox( + targets, + (ref, safeGappedBoundingBox, realBoundingBox, canvasScale) => { + if (isZeroSizedElement(realBoundingBox)) { + ref.current.style.display = 'none' + } else { + ref.current.style.display = 'block' + ref.current.style.left = `${safeGappedBoundingBox.x - 0.5 / canvasScale}px` + ref.current.style.top = `${safeGappedBoundingBox.y - 0.5 / canvasScale}px` + ref.current.style.width = `${safeGappedBoundingBox.width + 1 / canvasScale}px` + ref.current.style.height = `${safeGappedBoundingBox.height + 1 / canvasScale}px` + } + }, + ) if (targets.length > 0) { return ( diff --git a/editor/src/components/canvas/controls/select-mode/static-reparent-target-outline.tsx b/editor/src/components/canvas/controls/select-mode/static-reparent-target-outline.tsx index 3389502ae0f8..8e2e5f2c845d 100644 --- a/editor/src/components/canvas/controls/select-mode/static-reparent-target-outline.tsx +++ b/editor/src/components/canvas/controls/select-mode/static-reparent-target-outline.tsx @@ -38,6 +38,7 @@ export const StaticReparentTargetOutlineIndicator = controlForStrategyMemoized(( scale={scale} color={colorTheme.canvasSelectionPrimaryOutline.value} frame={parentFrame} + displayZeroSized={'do-not-display-zero-sized'} /> ) } else { diff --git a/editor/src/components/canvas/controls/zero-sized-element-controls.spec.browser2.tsx b/editor/src/components/canvas/controls/zero-sized-element-controls.spec.browser2.tsx index e7b32cb32b4a..d1fdb6877831 100644 --- a/editor/src/components/canvas/controls/zero-sized-element-controls.spec.browser2.tsx +++ b/editor/src/components/canvas/controls/zero-sized-element-controls.spec.browser2.tsx @@ -10,25 +10,27 @@ import { mouseDoubleClickAtPoint } from '../event-helpers.test-utils' import * as EP from '../../../core/shared/element-path' import { BakedInStoryboardUID } from '../../../core/model/scene-utils' import { selectComponents } from '../../editor/actions/meta-actions' -import { ZeroSizedControlTestID } from './zero-sized-element-controls' import type { ElementPath } from '../../../core/shared/project-file-types' +import { ZeroSizedEventsControlTestID } from './zero-sized-element-controls' async function selectTargetAndDoubleClickOnZeroSizeControl( renderResult: EditorRenderResult, target: ElementPath, testID: string, + shiftX: number = 0, + shiftY: number = 0, ) { await renderResult.dispatch(selectComponents([target], false), true) - const zeroSizeControl = renderResult.renderedDOM.getByTestId(ZeroSizedControlTestID) + const zeroSizeControl = renderResult.renderedDOM.getByTestId(ZeroSizedEventsControlTestID) const element = renderResult.renderedDOM.getByTestId(testID) const elementBounds = element.getBoundingClientRect() - const topLeftCorner = { - x: elementBounds.x, - y: elementBounds.y, + const targetPoint = { + x: elementBounds.x + shiftX, + y: elementBounds.y + shiftY, } - await mouseDoubleClickAtPoint(zeroSizeControl, topLeftCorner) + await mouseDoubleClickAtPoint(zeroSizeControl, targetPoint) await renderResult.getDispatchFollowUpActionsFinished() } @@ -70,6 +72,27 @@ describe('Zero sized element controls', () => { const target = EP.fromString(`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:aaa/bbb`) await selectTargetAndDoubleClickOnZeroSizeControl(renderResult, target, 'bbb') + expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual( + makeTestProjectCodeWithSnippet( + `
+
+
`, + ), + ) + }) + it('double click slightly outside a div element (where the zero sized border is visually) adds size', async () => { + const renderResult = await renderTestEditorWithCode( + makeTestProjectCodeWithSnippet( + `
+
+
`, + ), + 'await-first-dom-report', + ) + + const target = EP.fromString(`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:aaa/bbb`) + await selectTargetAndDoubleClickOnZeroSizeControl(renderResult, target, 'bbb', -2, -2) + expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual( makeTestProjectCodeWithSnippet( `
diff --git a/editor/src/components/canvas/controls/zero-sized-element-controls.tsx b/editor/src/components/canvas/controls/zero-sized-element-controls.tsx index db79c0d4ee60..13f0564bc26b 100644 --- a/editor/src/components/canvas/controls/zero-sized-element-controls.tsx +++ b/editor/src/components/canvas/controls/zero-sized-element-controls.tsx @@ -1,5 +1,6 @@ /** @jsxRuntime classic */ /** @jsx jsx */ +/** @jsxFrag React.Fragment */ import React from 'react' import { jsx } from '@emotion/react' import { MetadataUtils } from '../../../core/model/element-metadata-utils' @@ -40,6 +41,7 @@ import { useWindowToCanvasCoordinates } from '../dom-lookup-hooks' import { boundingArea, createInteractionViaMouse } from '../canvas-strategies/interaction-state' export const ZeroSizedControlTestID = 'zero-sized-control' +export const ZeroSizedEventsControlTestID = `${ZeroSizedControlTestID}-events` interface ZeroSizedElementControlProps { showAllPossibleElements: boolean } @@ -178,21 +180,29 @@ const ZeroSizeSelectControl = React.memo((props: ZeroSizeSelectControlProps) => } else { const frame = element.globalFrame return ( -
+ <> +
+
+ ) } }) @@ -400,16 +410,23 @@ export const ZeroSizeResizeControl = React.memo((props: ZeroSizeResizeControlPro return ( +
@@ -472,14 +489,37 @@ function zeroSizedControlDimensions( } { const ratio = getScaleRatio(scale) return { - left: rect.x - ZeroControlSize / 2, - top: rect.y - ZeroControlSize / 2, + left: rect.x - (ZeroControlSize / 2) * ratio, + top: rect.y - (ZeroControlSize / 2) * ratio, width: rect.width + ZeroControlSize * ratio, height: rect.height + ZeroControlSize * ratio, borderRadius: borderRadius ? (ZeroControlSize / 2) * ratio : undefined, } } +// So that we can capture events on what looks like the border +// we need some bounds that overlap around the dimensions of the +// control. +function zeroSizedEventControlDimensions( + rect: CanvasRectangle, + scale: number, +): { + left: number + top: number + width: number + height: number +} { + const ratio = getScaleRatio(scale) + const borderAdjustment = ZeroControlSize / 2 + const result = { + left: rect.x - (ZeroControlSize / 2) * ratio - borderAdjustment * ratio, + top: rect.y - (ZeroControlSize / 2) * ratio - borderAdjustment * ratio, + width: rect.width + (ZeroControlSize + borderAdjustment * 2) * ratio, + height: rect.height + (ZeroControlSize + borderAdjustment * 2) * ratio, + } + return result +} + function zeroSizedControlBoxShadow( scale: number, color: string | null | undefined,