From d78264169896c30232bbab447ca40d1798e36eab Mon Sep 17 00:00:00 2001 From: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> Date: Mon, 9 Oct 2023 10:24:26 +0200 Subject: [PATCH] Feature/simple layout section (#4316) * a SimplifiedLayoutSubsection is born * inlining the DimensionRow * this should not be a ref * FixedHugDropdown v1 * simplifying useOnSubmitFixedFillHugType, step 1 * useOnSubmitFixedFillHugType simplification, step 2 * GroupConstraintSelect only uses single prop * leftover delete * GroupChildPinControl separated * WidthHeightNumberControl * ClipContentControl row! * a ConstraintsSection appeared! * steps towards a working FrameChildConstraintsSection * working constraint dropdown * pindetect multiselect now! * framePoints are displaying something * Update constraints-section.tsx * mouse based pinning works now! * updating performance tests * feature(inspector) Basic cut of simplified layout controls. * feature(inspector) Updates now re-using some strategy code. * reset the number input on empty input * moving FrameUpdatingLayoutSection to its own component * moving ClipContentControl and FrameUpdatingLayoutSection into their own modules * moving pinnning helpers to dedicated file * reset the number control if the user tries to input a unit * Update frame-updating-layout-section.tsx * updating the perf tests * feature(inspector) Simplified layout section now better disables itself for multiselect. * pad the constraints dropdowns so they match the height of the pin control * oops missing useCallback * fixing a variable name, adding comment --------- Co-authored-by: Sean Parsons --- .../keyboard-absolute-move-strategy.ts | 4 +- .../keyboard-absolute-resize-strategy.tsx | 149 +++++--- .../shared-move-strategies-helpers.ts | 76 ++++- editor/src/components/canvas/canvas-utils.ts | 2 +- .../components/inspector/common/css-utils.ts | 34 +- .../common/layout-property-path-hooks.ts | 14 +- .../inspector/constraints-section.tsx | 264 ++++++++++++++ .../inspector/controls/pin-control.tsx | 21 +- .../inspector/fill-hug-fixed-control.tsx | 189 +++++----- .../components/inspector/inspector-common.ts | 32 ++ .../fixed-edge-basic-strategy.ts | 56 +++ .../inspector-strategies.ts | 12 +- editor/src/components/inspector/inspector.tsx | 23 +- .../inspector/resize-to-fit-control.tsx | 1 + .../flex-element-subsection.tsx | 7 +- .../clip-content-control.tsx | 35 ++ .../frame-updating-layout-section.tsx | 272 +++++++++++++++ .../gigantic-size-pins-subsection.tsx | 6 +- .../simplified-layout-subsection.tsx | 40 +++ .../text-auto-sizing-control.tsx | 6 +- .../inspector/simplified-pinning-helpers.tsx | 322 ++++++++++++++++++ .../components/inspector/sizing-section.tsx | 4 +- .../utility-controls/pin-control.tsx | 63 +++- editor/src/core/layout/layout-helpers-new.ts | 10 +- ...performance-regression-tests.spec.tsx.snap | 8 + .../performance-regression-tests.spec.tsx | 4 +- editor/src/utils/feature-switches.ts | 3 + 27 files changed, 1465 insertions(+), 192 deletions(-) create mode 100644 editor/src/components/inspector/constraints-section.tsx create mode 100644 editor/src/components/inspector/inspector-strategies/fixed-edge-basic-strategy.ts create mode 100644 editor/src/components/inspector/sections/layout-section/self-layout-subsection/clip-content-control.tsx create mode 100644 editor/src/components/inspector/sections/layout-section/self-layout-subsection/frame-updating-layout-section.tsx create mode 100644 editor/src/components/inspector/sections/layout-section/self-layout-subsection/simplified-layout-subsection.tsx create mode 100644 editor/src/components/inspector/simplified-pinning-helpers.tsx diff --git a/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-move-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-move-strategy.ts index ea73ee7bbe1d..4bd7050ad1df 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-move-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-move-strategy.ts @@ -17,7 +17,7 @@ import { zeroRectangle, } from '../../../../core/shared/math-utils' import { - getMoveCommandsForSelectedElement, + getInteractionMoveCommandsForSelectedElement, getMultiselectBounds, } from './shared-move-strategies-helpers' import { setElementsToRerenderCommand } from '../../commands/set-elements-to-rerender-command' @@ -75,7 +75,7 @@ export function keyboardAbsoluteMoveStrategy( }) }) selectedElements.forEach((selectedElement) => { - const elementResult = getMoveCommandsForSelectedElement( + const elementResult = getInteractionMoveCommandsForSelectedElement( selectedElement, keyboardMovement, canvasState, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx index 5dee9ee00d8e..571ce3c8db78 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy.tsx @@ -1,6 +1,7 @@ +import type { ElementInstanceMetadataMap } from '../../../../core/shared/element-template' import { isJSXElement } from '../../../../core/shared/element-template' import { MetadataUtils } from '../../../../core/model/element-metadata-utils' -import type { CanvasVector } from '../../../../core/shared/math-utils' +import type { CanvasRectangle, CanvasVector } from '../../../../core/shared/math-utils' import { canvasRectangle, canvasVector, @@ -11,6 +12,7 @@ import { } from '../../../../core/shared/math-utils' import type { KeyCharacter } from '../../../../utils/keyboard' import Keyboard from '../../../../utils/keyboard' +import type { AllElementProps } from '../../../editor/store/editor-state' import { withUnderlyingTarget } from '../../../editor/store/editor-state' import type { CanvasFrameAndTarget, EdgePosition } from '../../canvas-types' import { EdgePositionBottom, EdgePositionRight } from '../../canvas-types' @@ -38,6 +40,10 @@ import { } from './shared-keyboard-strategy-helpers' import { getMultiselectBounds } from './shared-move-strategies-helpers' import { retargetStrategyToChildrenOfFragmentLikeElements } from './fragment-like-helpers' +import type { ElementPath } from '../../../../core/shared/project-file-types' +import type { ProjectContentTreeRoot } from '../../../../components/assets' +import type { InspectorStrategy } from '../../../../components/inspector/inspector-strategies/inspector-strategy' +import type { ElementPathTrees } from '../../../../core/shared/element-path-tree' interface VectorAndEdge { movement: CanvasVector @@ -111,6 +117,97 @@ function getFitness(interactionSession: InteractionSession | null): number { export const ResizeMinimumValue = 1 +export interface ChangeBoundsResult { + commands: Array + intendedBounds: Array +} + +export function changeBounds( + projectContents: ProjectContentTreeRoot, + startingMetadata: ElementInstanceMetadataMap, + selectedElements: Array, + originalFrame: CanvasRectangle, + originalIntendedBounds: Array, + edgePosition: EdgePosition, + movement: CanvasVector, +): ChangeBoundsResult { + let commands: Array = [] + let intendedBounds: Array = originalIntendedBounds + + selectedElements.forEach((selectedElement) => { + const element = withUnderlyingTarget(selectedElement, projectContents, null, (_, e) => e) + + const elementMetadata = MetadataUtils.findElementByElementPath( + startingMetadata, + selectedElement, + ) + const elementParentBounds = + elementMetadata?.specialSizeMeasurements.immediateParentBounds ?? null + const elementParentFlexDirection = + elementMetadata?.specialSizeMeasurements.parentFlexDirection ?? null + const elementGlobalFrame = nullIfInfinity(elementMetadata?.globalFrame ?? null) + + if (element != null && isJSXElement(element)) { + const elementResult = createResizeCommands( + element, + selectedElement, + edgePosition, + movement, + elementGlobalFrame, + elementParentBounds, + elementParentFlexDirection, + ) + commands.push(...elementResult.commands) + if (elementResult.intendedBounds != null) { + intendedBounds = addOrMergeIntendedBounds( + intendedBounds, + originalFrame, + elementResult.intendedBounds, + ) + } + } + }) + + return { + commands: commands, + intendedBounds: intendedBounds, + } +} + +export function resizeInspectorStrategy( + projectContents: ProjectContentTreeRoot, + originalFrame: CanvasRectangle, + edgePosition: EdgePosition, + movement: CanvasVector, +): InspectorStrategy { + return { + name: 'Resize by pixels', + strategy: ( + metadata: ElementInstanceMetadataMap, + selectedElements: Array, + _elementPathTree: ElementPathTrees, + _allElementProps: AllElementProps, + ): Array | null => { + let commands: Array = [] + const changeBoundsResult = changeBounds( + projectContents, + metadata, + selectedElements, + originalFrame, + [], + edgePosition, + movement, + ) + commands.push(...changeBoundsResult.commands) + commands.push( + pushIntendedBoundsAndUpdateGroups(changeBoundsResult.intendedBounds, 'starting-metadata'), + ) + commands.push(setElementsToRerenderCommand(selectedElements)) + return commands + }, + } +} + export function keyboardAbsoluteResizeStrategy( canvasState: InteractionCanvasState, interactionSession: InteractionSession | null, @@ -168,44 +265,18 @@ export function keyboardAbsoluteResizeStrategy( null, 'non-center-based', ) - selectedElements.forEach((selectedElement) => { - const element = withUnderlyingTarget( - selectedElement, - canvasState.projectContents, - null, - (_, e) => e, - ) - - const elementMetadata = MetadataUtils.findElementByElementPath( - canvasState.startingMetadata, - selectedElement, - ) - const elementParentBounds = - elementMetadata?.specialSizeMeasurements.immediateParentBounds ?? null - const elementParentFlexDirection = - elementMetadata?.specialSizeMeasurements.parentFlexDirection ?? null - const elementGlobalFrame = nullIfInfinity(elementMetadata?.globalFrame ?? null) - - if (element != null && isJSXElement(element)) { - const elementResult = createResizeCommands( - element, - selectedElement, - movementWithEdge.edge, - movementWithEdge.movement, - elementGlobalFrame, - elementParentBounds, - elementParentFlexDirection, - ) - commands.push(...elementResult.commands) - if (elementResult.intendedBounds != null) { - intendedBounds = addOrMergeIntendedBounds( - intendedBounds, - originalFrame, - elementResult.intendedBounds, - ) - } - } - }) + + const changeBoundsResult = changeBounds( + canvasState.projectContents, + canvasState.startingMetadata, + selectedElements, + originalFrame, + intendedBounds, + movementWithEdge.edge, + movementWithEdge.movement, + ) + intendedBounds = changeBoundsResult.intendedBounds + commands.push(...changeBoundsResult.commands) }) const guidelines = getKeyboardStrategyGuidelines(canvasState, interactionSession, newFrame) commands.push(setSnappingGuidelines('mid-interaction', guidelines)) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/shared-move-strategies-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/shared-move-strategies-helpers.ts index 92b19d6e321b..a3080b6ef7a3 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/shared-move-strategies-helpers.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/shared-move-strategies-helpers.ts @@ -57,6 +57,8 @@ import type { AbsolutePin } from './resize-helpers' import type { FlexDirection } from '../../../inspector/common/css-utils' import { memoize } from '../../../../core/shared/memoize' import { is } from '../../../../core/shared/equality-utils' +import type { ProjectContentTreeRoot } from '../../../../components/assets' +import type { InspectorStrategy } from '../../../../components/inspector/inspector-strategies/inspector-strategy' export interface MoveCommandsOptions { ignoreLocalFrame?: boolean @@ -79,7 +81,7 @@ export const getAdjustMoveCommands = let commands: Array = [] let intendedBounds: Array = [] filteredSelectedElements.forEach((selectedElement) => { - const elementResult = getMoveCommandsForSelectedElement( + const elementResult = getInteractionMoveCommandsForSelectedElement( selectedElement, snappedDragVector, canvasState, @@ -164,22 +166,20 @@ export function applyMoveCommon( } export function getMoveCommandsForSelectedElement( + projectContents: ProjectContentTreeRoot, + startingMetadata: ElementInstanceMetadataMap, selectedElement: ElementPath, + mappedPath: ElementPath, drag: CanvasVector, - canvasState: InteractionCanvasState, - interactionSession: InteractionSession, options?: MoveCommandsOptions, ): { commands: Array intendedBounds: Array } { - const element: JSXElement | null = getElementFromProjectContents( - selectedElement, - canvasState.projectContents, - ) + const element: JSXElement | null = getElementFromProjectContents(selectedElement, projectContents) const elementMetadata = MetadataUtils.findElementByElementPath( - canvasState.startingMetadata, // TODO should this be using the current metadata? + startingMetadata, // TODO should this be using the current metadata? selectedElement, ) @@ -188,22 +188,16 @@ export function getMoveCommandsForSelectedElement( const localFrame = options?.ignoreLocalFrame ? null - : MetadataUtils.getLocalFrameFromSpecialSizeMeasurements( - selectedElement, - canvasState.startingMetadata, - ) + : MetadataUtils.getLocalFrameFromSpecialSizeMeasurements(selectedElement, startingMetadata) const globalFrame = nullIfInfinity( - MetadataUtils.getFrameInCanvasCoords(selectedElement, canvasState.startingMetadata), + MetadataUtils.getFrameInCanvasCoords(selectedElement, startingMetadata), ) if (element == null) { return { commands: [], intendedBounds: [] } } - const mappedPath = - interactionSession.updatedTargetPaths[EP.toString(selectedElement)] ?? selectedElement - return createMoveCommandsForElement( element, selectedElement, @@ -216,6 +210,56 @@ export function getMoveCommandsForSelectedElement( ) } +export function getInteractionMoveCommandsForSelectedElement( + selectedElement: ElementPath, + drag: CanvasVector, + canvasState: InteractionCanvasState, + interactionSession: InteractionSession, + options?: MoveCommandsOptions, +): { + commands: Array + intendedBounds: Array +} { + const mappedPath = + interactionSession.updatedTargetPaths[EP.toString(selectedElement)] ?? selectedElement + + return getMoveCommandsForSelectedElement( + canvasState.projectContents, + canvasState.startingMetadata, + selectedElement, + mappedPath, + drag, + options, + ) +} + +export function moveInspectorStrategy( + projectContents: ProjectContentTreeRoot, + movement: CanvasVector, +): InspectorStrategy { + return { + name: 'Move by pixels', + strategy: (metadata, selectedElementPaths, _elementPathTree, _allElementProps) => { + let commands: Array = [] + let intendedBounds: Array = [] + for (const selectedPath of selectedElementPaths) { + const moveCommandsResult = getMoveCommandsForSelectedElement( + projectContents, + metadata, + selectedPath, + selectedPath, + movement, + ) + commands.push(...moveCommandsResult.commands) + intendedBounds.push(...moveCommandsResult.intendedBounds) + } + commands.push(pushIntendedBoundsAndUpdateGroups(intendedBounds, 'starting-metadata')) + commands.push(setElementsToRerenderCommand(selectedElementPaths)) + return commands + }, + } +} + export function createMoveCommandsForElement( element: JSXElement, selectedElement: ElementPath, diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index ade658b54c8c..d644ab75b9a5 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -221,7 +221,7 @@ function referenceParentValueForProp(prop: LayoutPinnedProp, parentSize: Size): } } -function valueToUseForPin( +export function valueToUseForPin( prop: LayoutPinnedProp, absoluteValue: number, pinIsPercentPin: boolean, diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index 75b22d4e84bb..d348a446f8bb 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -608,6 +608,20 @@ export function isFixedSize(value: CSSNumber): boolean { } } +export function isCssNumberAndFixedSize(value: unknown): boolean { + if (!isCSSNumber(value)) { + return false + } + return isFixedSize(value) +} + +export function isCssNumberAndPercentage(value: unknown): boolean { + if (!isCSSNumber(value)) { + return false + } + return value.unit === '%' +} + function parseCSSNumberUnit( input: string, units: Array, @@ -5170,22 +5184,10 @@ export const trivialDefaultValues: ParsedPropertiesWithNonTrivial = { }, alignSelf: FlexAlignment.Auto, position: 'relative', - left: { - value: 0, - unit: 'px', - }, - top: { - value: 0, - unit: 'px', - }, - right: { - value: 0, - unit: 'px', - }, - bottom: { - value: 0, - unit: 'px', - }, + left: nontrivial, // nontrivial means we will never treat these props as "do not show if it equals default value" + top: nontrivial, + right: nontrivial, + bottom: nontrivial, minWidth: { value: 0, unit: 'px', diff --git a/editor/src/components/inspector/common/layout-property-path-hooks.ts b/editor/src/components/inspector/common/layout-property-path-hooks.ts index b50ff9c84d72..4e80a3c6bedf 100644 --- a/editor/src/components/inspector/common/layout-property-path-hooks.ts +++ b/editor/src/components/inspector/common/layout-property-path-hooks.ts @@ -7,7 +7,10 @@ import { valueToUseForPin, VerticalFramePoints, } from 'utopia-api/core' -import type { LayoutPinnedProp } from '../../../core/layout/layout-helpers-new' +import type { + LayoutPinnedProp, + LayoutPinnedPropIncludingCenter, +} from '../../../core/layout/layout-helpers-new' import { framePointForPinnedProp, HorizontalLayoutPinnedProps, @@ -314,7 +317,7 @@ export type FramePinsInfo = { [key in FramePoint]: FramePinInfo } export interface UsePinTogglingResult { framePins: FramePinsInfo - togglePin: (newFrameProp: LayoutPinnedProp) => void + togglePin: (newFrameProp: LayoutPinnedPropIncludingCenter) => void resetAllPins: () => void } @@ -378,7 +381,12 @@ export function usePinToggling(): UsePinTogglingResult { const bottom = useInspectorLayoutInfo('bottom') const togglePin = React.useCallback( - (newFrameProp: LayoutPinnedProp) => { + (newFramePropIncludingCenter: LayoutPinnedPropIncludingCenter) => { + if (newFramePropIncludingCenter == 'centerX' || newFramePropIncludingCenter == 'centerY') { + // early return, we ignore centerX centerY + return + } + const newFrameProp: LayoutPinnedProp = newFramePropIncludingCenter const frameInfo: ReadonlyArray = elementFrames.map((frame, index) => { const path = selectedViewsRef.current[index] const parentPath = EP.parentPath(path) diff --git a/editor/src/components/inspector/constraints-section.tsx b/editor/src/components/inspector/constraints-section.tsx new file mode 100644 index 000000000000..089c70c80cca --- /dev/null +++ b/editor/src/components/inspector/constraints-section.tsx @@ -0,0 +1,264 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from '@emotion/react' + +import React from 'react' +import { useContextSelector } from 'use-context-selector' +import type { LayoutPinnedPropIncludingCenter } from '../../core/layout/layout-helpers-new' +import { when } from '../../utils/react-conditionals' +import { FlexColumn, FlexRow, InspectorSubsectionHeader, PopupList, UtopiaTheme } from '../../uuiui' +import type { SelectOption } from '../../uuiui-deps' +import { getControlStyles } from '../../uuiui-deps' +import { InspectorRowHoverCSS } from '../context-menu-wrapper' +import { useDispatch } from '../editor/store/dispatch-context' +import { Substores, useEditorState, useRefEditorState } from '../editor/store/store-hook' +import type { FramePinsInfo } from './common/layout-property-path-hooks' +import { InspectorPropsContext } from './common/property-path-hooks' +import { PinControl } from './controls/pin-control' +import { + GroupChildPinControl, + GroupConstraintSelect, + allElementsAreGroupChildren, + anySelectedElementGroupOrChildOfGroup, +} from './fill-hug-fixed-control' +import { selectedViewsSelector } from './inpector-selectors' +import type { RequestedPins } from './simplified-pinning-helpers' +import { + HorizontalPinChangeOptions, + HorizontalPinChangeOptionsIncludingMixed, + VerticalPinChangeOptions, + VerticalPinChangeOptionsIncludingMixed, + getFixedPointsForPinning, + getFrameChangeActions, + useDetectedPinning, +} from './simplified-pinning-helpers' +import { PinHeightSVG, PinWidthSVG } from './utility-controls/pin-control' +import { UIGridRow } from './widgets/ui-grid-row' + +export const ConstraintsSection = React.memo(() => { + const noGroupOrGroupChildrenSelected = !useEditorState( + Substores.metadata, + anySelectedElementGroupOrChildOfGroup, + 'ConstraintsSection someGroupOrGroupChildrenSelected', + ) + const onlyGroupChildrenSelected = useEditorState( + Substores.metadata, + allElementsAreGroupChildren, + 'ConstraintsSection onlyGroupChildrenSelected', + ) + + return ( + + + + Constraints + + + {when(noGroupOrGroupChildrenSelected, )} + {when(onlyGroupChildrenSelected, )} + + ) +}) +ConstraintsSection.displayName = 'ConstraintsSection' + +const GroupChildConstraintsSection = React.memo(() => { + return ( + + + + + + + + + + + + + + + + ) +}) +GroupChildConstraintsSection.displayName = 'GroupChildConstraintsSection' + +const FrameChildConstraintsSection = React.memo(() => { + return ( + + + {' '} + + + + + + + + + + + + + ) +}) +FrameChildConstraintsSection.displayName = 'FrameChildConstraintsSection' + +const FrameChildPinControl = React.memo(() => { + const dispatch = useDispatch() + + const propertyTarget = useContextSelector(InspectorPropsContext, (contextData) => { + return contextData.targetPath + }) + + const selectedViewsRef = useRefEditorState(selectedViewsSelector) + const metadataRef = useRefEditorState((store) => store.editor.jsxMetadata) + + const pins = useDetectedPinning() + + const onPinControlMouseDown = React.useCallback( + (frameProp: LayoutPinnedPropIncludingCenter, event: React.MouseEvent) => { + const cmdPressed = event.metaKey + const requestedPinChange: RequestedPins | 'no-op' = (() => { + switch (frameProp) { + case 'left': { + if (cmdPressed && pins.horizontal === 'right-and-width') { + return 'left-and-right' + } else { + return 'left-and-width' + } + } + case 'right': { + if (cmdPressed && pins.horizontal === 'left-and-width') { + return 'left-and-right' + } else { + return 'right-and-width' + } + } + case 'width': { + return 'left-and-width' + } + case 'top': { + if (cmdPressed && pins.vertical === 'bottom-and-height') { + return 'top-and-bottom' + } else { + return 'top-and-height' + } + } + case 'bottom': { + if (cmdPressed && pins.vertical === 'top-and-height') { + return 'top-and-bottom' + } else { + return 'bottom-and-height' + } + } + case 'height': { + return 'top-and-height' + } + case 'centerX': { + if (cmdPressed) { + return 'scale-horizontal' + } else { + return 'no-op' + } + } + case 'centerY': { + if (cmdPressed) { + return 'scale-vertical' + } else { + return 'no-op' + } + } + default: + const _exhaustiveCheck: never = frameProp + throw new Error(`Unhandled frameProp: ${_exhaustiveCheck}`) + } + })() + + if (requestedPinChange === 'no-op') { + // no-op, early return :) + return + } + dispatch( + getFrameChangeActions( + metadataRef.current, + propertyTarget, + selectedViewsRef.current, + requestedPinChange, + ), + ) + }, + [dispatch, metadataRef, selectedViewsRef, propertyTarget, pins], + ) + + const framePoints: FramePinsInfo = React.useMemo(() => getFixedPointsForPinning(pins), [pins]) + + return ( + + ) +}) +FrameChildPinControl.displayName = 'FrameChildPinControl' + +const FrameChildConstraintSelect = React.memo((props: { dimension: 'width' | 'height' }) => { + const { dimension } = props + + const dispatch = useDispatch() + + const propertyTarget = useContextSelector(InspectorPropsContext, (contextData) => { + return contextData.targetPath + }) + + const editorRef = useRefEditorState((store) => ({ + selectedViews: store.editor.selectedViews, + metadata: store.editor.jsxMetadata, + })) + + const pins = useDetectedPinning() + + const optionsToUse = + dimension === 'width' + ? Object.values(HorizontalPinChangeOptions) + : Object.values(VerticalPinChangeOptions) + + const activeOption = + dimension === 'width' + ? HorizontalPinChangeOptionsIncludingMixed[pins.horizontal] + : VerticalPinChangeOptionsIncludingMixed[pins.vertical] + + const onSubmit = React.useCallback( + (option: SelectOption) => { + const requestedPins: RequestedPins = option.value + dispatch( + getFrameChangeActions( + editorRef.current.metadata, + propertyTarget, + editorRef.current.selectedViews, + requestedPins, + ), + ) + }, + [dispatch, propertyTarget, editorRef], + ) + + return ( + + ) +}) diff --git a/editor/src/components/inspector/controls/pin-control.tsx b/editor/src/components/inspector/controls/pin-control.tsx index b3b209df586e..b765002ad207 100644 --- a/editor/src/components/inspector/controls/pin-control.tsx +++ b/editor/src/components/inspector/controls/pin-control.tsx @@ -4,13 +4,19 @@ import type { ControlStyles } from '../common/control-styles' import type { ControlStatus } from '../common/control-status' import { getControlStyles } from '../common/control-styles' import { FramePoint } from 'utopia-api/core' -import type { LayoutPinnedProp } from '../../../core/layout/layout-helpers-new' +import type { + LayoutPinnedProp, + LayoutPinnedPropIncludingCenter, +} from '../../../core/layout/layout-helpers-new' import type { FramePinsInfo } from '../common/layout-property-path-hooks' import { UtopiaTheme, SquareButton, colorTheme, color } from '../../../uuiui' import { unless } from '../../../utils/react-conditionals' interface PinControlProps { - handlePinMouseDown: (frameProp: LayoutPinnedProp) => void + handlePinMouseDown: ( + frameProp: LayoutPinnedPropIncludingCenter, + event: React.MouseEvent, + ) => void name: string controlStatus: ControlStatus framePoints: FramePinsInfo @@ -82,9 +88,10 @@ function getTestId(prefix: string, id: string): string { export const PinControl = (props: PinControlProps) => { const controlStyles: ControlStyles = getControlStyles(props.controlStatus) - const handlePinMouseDown = (frameProp: LayoutPinnedProp) => () => { - props.handlePinMouseDown(frameProp) - } + const handlePinMouseDown = + (frameProp: LayoutPinnedPropIncludingCenter) => (e: React.MouseEvent) => { + props.handlePinMouseDown(frameProp, e) + } const exclude: ExcludePinControls = React.useMemo( () => @@ -290,7 +297,7 @@ export const PinControl = (props: PinControlProps) => { data-testid={getTestId(props.name, 'pin-centerx-transparent')} stroke='transparent' fill='transparent' - onMouseDown={Utils.NO_OP} + onMouseDown={handlePinMouseDown('centerX')} /> { data-testid={getTestId(props.name, 'pin-centery-transparent')} stroke='transparent' fill='transparent' - onMouseDown={Utils.NO_OP} + onMouseDown={handlePinMouseDown('centerY')} /> , diff --git a/editor/src/components/inspector/fill-hug-fixed-control.tsx b/editor/src/components/inspector/fill-hug-fixed-control.tsx index 52b96bcb937b..3897163367f3 100644 --- a/editor/src/components/inspector/fill-hug-fixed-control.tsx +++ b/editor/src/components/inspector/fill-hug-fixed-control.tsx @@ -6,7 +6,14 @@ import { createSelector } from 'reselect' import { optionalMap } from '../../core/shared/optional-utils' import { intersection } from '../../core/shared/set-utils' import { assertNever } from '../../core/shared/utils' -import { NumberInput, PopupList, useColorTheme, UtopiaTheme } from '../../uuiui' +import { + FlexRow, + InspectorSubsectionHeader, + NumberInput, + PopupList, + useColorTheme, + UtopiaTheme, +} from '../../uuiui' import type { ControlStatus, ControlStyles, @@ -32,7 +39,7 @@ import { } from './inspector-common' import { setPropFillStrategies, - setPropFixedStrategies, + setPropFixedSizeStrategies, setPropHugStrategies, } from './inspector-strategies/inspector-strategies' import type { InspectorStrategy } from './inspector-strategies/inspector-strategy' @@ -47,6 +54,7 @@ import { UIGridRow } from './widgets/ui-grid-row' import { mapDropNulls, safeIndex, uniqBy } from '../../core/shared/array-utils' import { fixedSizeDimensionHandlingText } from '../text-editor/text-handling' import { when } from '../../utils/react-conditionals' +import type { LayoutPinnedPropIncludingCenter } from '../../core/layout/layout-helpers-new' import { isLayoutPinnedProp, type LayoutPinnedProp } from '../../core/layout/layout-helpers-new' import type { AllElementProps } from '../editor/store/editor-state' import { jsExpressionValue, emptyComments } from '../../core/shared/element-template' @@ -61,6 +69,7 @@ import { FlexCol } from 'utopia-api' import { PinControl } from './controls/pin-control' import type { FramePinsInfo } from './common/layout-property-path-hooks' import { MixedPlaceholder } from '../../uuiui/inputs/base-input' +import { PinHeightSVG, PinWidthSVG } from './utility-controls/pin-control' import { convertGroupToFrameCommands, getInstanceForGroupToFrameConversion, @@ -104,8 +113,6 @@ function selectOption(mode: FixedHugFillMode): SelectOption { } } -interface FillHugFixedControlProps {} - const fixedHugFillOptionsSelector = createSelector( metadataSelector, pathTreesSelector, @@ -123,7 +130,7 @@ const fixedHugFillOptionsSelector = createSelector( }, ) -export const FillHugFixedControl = React.memo((props) => { +export const FillHugFixedControlOld = React.memo(() => { const onlyGroupChildrenSelected = useEditorState( Substores.metadata, allElementsAreGroupChildren, @@ -161,7 +168,7 @@ export const FillHugFixedControl = React.memo((props) ) }) -const GroupChildPinControl = React.memo(() => { +export const GroupChildPinControl = React.memo(() => { const dispatch = useDispatch() const selectedViewsRef = useRefEditorState(selectedViewsSelector) const allElementPropsRef = useRefEditorState((store) => store.editor.allElementProps) @@ -173,7 +180,11 @@ const GroupChildPinControl = React.memo(() => { ) const handleGroupConstraintPinMouseDown = React.useCallback( - (dimension: LayoutPinnedProp) => { + (dimension: LayoutPinnedPropIncludingCenter) => { + if (dimension === 'centerX' || dimension === 'centerY') { + // early return, we ignore centerX centerY + return + } return setGroupChildConstraint( dispatch, 'toggle', @@ -313,7 +324,7 @@ const WidthHeightNumberControl = React.memo((props: { dimension: 'width' | 'heig selectedViewsRef.current, elementPathTreeRef.current, allElementPropsRef.current, - setPropFixedStrategies('always', axis, value), + setPropFixedSizeStrategies('always', axis, value), ) return } @@ -341,7 +352,7 @@ const WidthHeightNumberControl = React.memo((props: { dimension: 'width' | 'heig selectedViewsRef.current, elementPathTreeRef.current, allElementPropsRef.current, - setPropFixedStrategies('always', axis, value), + setPropFixedSizeStrategies('always', axis, value), ) } }, @@ -509,87 +520,89 @@ export const FixedHugDropdown = React.memo((props: { dimension: 'width' | 'heigh }) FixedHugDropdown.displayName = 'FixedHugDropdown' -const GroupConstraintSelect = React.memo(({ dimension }: { dimension: LayoutPinnedProp }) => { - const dispatch = useDispatch() - const colorTheme = useColorTheme() - - const constraintType = useEditorState( - Substores.metadata, - (store) => - checkGroupChildConstraint( - dimension, - store.editor.selectedViews, - store.editor.allElementProps, - ), - 'GroupConstraintSelect constraintType', - ) +export const GroupConstraintSelect = React.memo( + ({ dimension }: { dimension: LayoutPinnedProp }) => { + const dispatch = useDispatch() + const colorTheme = useColorTheme() + + const constraintType = useEditorState( + Substores.metadata, + (store) => + checkGroupChildConstraint( + dimension, + store.editor.selectedViews, + store.editor.allElementProps, + ), + 'GroupConstraintSelect constraintType', + ) - const editorRef = useRefEditorState((store) => ({ - selectedViews: store.editor.selectedViews, - allElementProps: store.editor.allElementProps, - })) - - function optionFromType(value: GroupChildConstraintOptionType | 'mixed') { - switch (value) { - case 'constrained': - return groupChildConstraintOptionValues.constrained - case 'not-constrained': - return groupChildConstraintOptionValues.notConstrained - case 'mixed': - return groupChildConstraintOptionValues.mixed - default: - assertNever(value) + const editorRef = useRefEditorState((store) => ({ + selectedViews: store.editor.selectedViews, + allElementProps: store.editor.allElementProps, + })) + + function optionFromType(value: GroupChildConstraintOptionType | 'mixed') { + switch (value) { + case 'constrained': + return groupChildConstraintOptionValues.constrained + case 'not-constrained': + return groupChildConstraintOptionValues.notConstrained + case 'mixed': + return groupChildConstraintOptionValues.mixed + default: + assertNever(value) + } } - } - const listValue = React.useMemo(() => { - return optionFromType(constraintType) - }, [constraintType]) - - const mainColor = React.useMemo(() => { - switch (constraintType) { - case 'constrained': - return colorTheme.fg0.value - case 'mixed': - return colorTheme.fg6Opacity50.value - case 'not-constrained': - return colorTheme.subduedForeground.value - default: - assertNever(constraintType) - } - }, [constraintType, colorTheme]) + const listValue = React.useMemo(() => { + return optionFromType(constraintType) + }, [constraintType]) + + const mainColor = React.useMemo(() => { + switch (constraintType) { + case 'constrained': + return colorTheme.fg0.value + case 'mixed': + return colorTheme.fg6Opacity50.value + case 'not-constrained': + return colorTheme.subduedForeground.value + default: + assertNever(constraintType) + } + }, [constraintType, colorTheme]) - const onSubmitValue = React.useCallback( - (option: SelectOption) => { - return setGroupChildConstraint( - dispatch, - option.value, - editorRef.current.selectedViews, - editorRef.current.allElementProps, - dimension, - ) - }, - [dispatch, editorRef, dimension], - ) + const onSubmitValue = React.useCallback( + (option: SelectOption) => { + return setGroupChildConstraint( + dispatch, + option.value, + editorRef.current.selectedViews, + editorRef.current.allElementProps, + dimension, + ) + }, + [dispatch, editorRef, dimension], + ) - return ( - - ) -}) + return ( + + ) + }, +) GroupConstraintSelect.displayName = 'GroupConstraintSelect' @@ -608,7 +621,7 @@ function strategyForChangingFillFixedHugType( case 'detected': case 'computed': case 'hug-group': - return setPropFixedStrategies('always', axis, cssNumber(fixedValue, null)) + return setPropFixedSizeStrategies('always', axis, cssNumber(fixedValue, null)) default: assertNever(mode) } @@ -643,7 +656,7 @@ function isNumberInputEnabled(value: FixedHugFill | null): boolean { return value?.type === 'fixed' || value?.type === 'fill' || value?.type === 'hug-group' } -const anySelectedElementGroupOrChildOfGroup = createSelector( +export const anySelectedElementGroupOrChildOfGroup = createSelector( metadataSelector, (store: MetadataSubstate) => store.editor.elementPathTree, selectedViewsSelector, @@ -660,7 +673,7 @@ const anySelectedElementGroupOrChildOfGroup = createSelector( }, ) -const allElementsAreGroupChildren = createSelector( +export const allElementsAreGroupChildren = createSelector( metadataSelector, (store: MetadataSubstate) => store.editor.elementPathTree, selectedViewsSelector, diff --git a/editor/src/components/inspector/inspector-common.ts b/editor/src/components/inspector/inspector-common.ts index f40c76674eda..239d5b35ad1a 100644 --- a/editor/src/components/inspector/inspector-common.ts +++ b/editor/src/components/inspector/inspector-common.ts @@ -996,6 +996,38 @@ export function getFramePointsFromMetadata(elementMetadata: ElementInstanceMetad } } +type GetFramePointsFromMetadataResult = { + left?: CSSNumber | 'max-content' | undefined + right?: CSSNumber | 'max-content' | undefined + centerX?: CSSNumber | 'max-content' | undefined + width?: CSSNumber | 'max-content' | undefined + top?: CSSNumber | 'max-content' | undefined + bottom?: CSSNumber | 'max-content' | undefined + centerY?: CSSNumber | 'max-content' | undefined + height?: CSSNumber | 'max-content' | undefined +} + +export function getFramePointsFromMetadataTypeFixed( + elementMetadata: ElementInstanceMetadata, +): GetFramePointsFromMetadataResult { + if (isRight(elementMetadata.element) && isJSXElement(elementMetadata.element.value)) { + const jsxElement = elementMetadata.element.value + return LayoutPinnedProps.reduce((working, point) => { + const value = getLayoutLengthValueOrKeyword(point, right(jsxElement.props), ['style']) + if (isLeft(value)) { + return working + } else { + return { + ...working, + [point]: value.value, + } + } + }, {}) + } else { + return {} + } +} + export function removeExtraPinsWhenSettingSize( axis: Axis, elementMetadata: ElementInstanceMetadata | null, diff --git a/editor/src/components/inspector/inspector-strategies/fixed-edge-basic-strategy.ts b/editor/src/components/inspector/inspector-strategies/fixed-edge-basic-strategy.ts new file mode 100644 index 000000000000..ffb0a45aeb2a --- /dev/null +++ b/editor/src/components/inspector/inspector-strategies/fixed-edge-basic-strategy.ts @@ -0,0 +1,56 @@ +import * as PP from '../../../core/shared/property-path' +import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import type { WhenToRun } from '../../canvas/commands/commands' +import { + setCssLengthProperty, + setExplicitCssValue, +} from '../../canvas/commands/set-css-length-command' +import type { CSSNumber } from '../common/css-utils' +import type { InspectorStrategy } from './inspector-strategy' +import { queueGroupTrueUp } from '../../canvas/commands/queue-group-true-up-command' +import { + groupErrorToastCommand, + maybeInvalidGroupState, +} from '../../canvas/canvas-strategies/strategies/group-helpers' +import { trueUpElementChanged } from '../../../components/editor/store/editor-state' +import type { LayoutEdgeProp } from '../../../core/layout/layout-helpers-new' + +export const fixedEdgeBasicStrategy = ( + whenToRun: WhenToRun, + edge: LayoutEdgeProp, + value: CSSNumber, +): InspectorStrategy => ({ + name: 'Set edge to Fixed', + strategy: (metadata, elementPaths) => { + if (elementPaths.length === 0) { + return null + } + + const invalidGroupState = maybeInvalidGroupState(elementPaths, metadata, { + onGroup: () => (value.unit === '%' ? 'group-has-percentage-pins' : null), + onGroupChild: (path) => { + return value.unit === '%' ? 'child-has-percentage-pins' : null + }, + }) + if (invalidGroupState != null) { + return [groupErrorToastCommand(invalidGroupState)] + } + + return elementPaths.flatMap((path) => { + const elementMetadata = MetadataUtils.findElementByElementPath(metadata, path) + const parentFlexDirection = + elementMetadata?.specialSizeMeasurements.parentFlexDirection ?? null + + return [ + setCssLengthProperty( + whenToRun, + path, + PP.create('style', edge), + setExplicitCssValue(value), + parentFlexDirection, + ), + queueGroupTrueUp([trueUpElementChanged(path)]), + ] + }) + }, +}) diff --git a/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts b/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts index ea009f1a099f..c0017c5255a4 100644 --- a/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts +++ b/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts @@ -22,6 +22,8 @@ import { setSpacingModePacked, setSpacingModeSpaceBetween } from './spacing-mode import { convertLayoutToFlexCommands } from '../../common/shared-strategies/convert-to-flex-strategy' import { fixedSizeBasicStrategy } from './fixed-size-basic-strategy' import { setFlexDirectionSwapAxes } from './change-flex-direction-swap-axes' +import { fixedEdgeBasicStrategy } from './fixed-edge-basic-strategy' +import type { LayoutEdgeProp } from '../../../core/layout/layout-helpers-new' export const setFlexAlignStrategies = (flexAlignment: FlexAlignment): Array => [ { @@ -157,12 +159,20 @@ export const setPropFillStrategies = ( fillContainerStrategyFlow(axis, value, otherAxisSetToFill), ] -export const setPropFixedStrategies = ( +export const setPropFixedSizeStrategies = ( whenToRun: WhenToRun, axis: Axis, value: CSSNumber, ): Array => [fixedSizeBasicStrategy(whenToRun, axis, value)] +export function setPropFixedEdgeStrategies( + whenToRun: WhenToRun, + edge: LayoutEdgeProp, + value: CSSNumber, +): Array { + return [fixedEdgeBasicStrategy(whenToRun, edge, value)] +} + export const setPropHugStrategies = (axis: Axis): Array => [ hugContentsBasicStrategy(axis), ] diff --git a/editor/src/components/inspector/inspector.tsx b/editor/src/components/inspector/inspector.tsx index 602b71f0f6ec..fd3db9364472 100644 --- a/editor/src/components/inspector/inspector.tsx +++ b/editor/src/components/inspector/inspector.tsx @@ -80,6 +80,9 @@ import { import { FlexCol } from 'utopia-api' import { SettingsPanel } from './sections/settings-panel/inspector-settingspanel' import { strictEvery } from '../../core/shared/array-utils' +import { SimplifiedLayoutSubsection } from './sections/layout-section/self-layout-subsection/simplified-layout-subsection' +import { isFeatureEnabled } from '../../utils/feature-switches' +import { ConstraintsSection } from './constraints-section' export interface ElementPathElement { name?: string @@ -394,10 +397,22 @@ export const Inspector = React.memo((props: InspectorProps) => { multiselectedContract === 'fragment', // Position and Sizing sections are shown if Frame or Group is selected <> - - + {when( + isFeatureEnabled('Simplified Layout Section'), + <> + + + , + )} + {unless( + isFeatureEnabled('Simplified Layout Section'), + <> + + + , + )} , )} {unless( diff --git a/editor/src/components/inspector/resize-to-fit-control.tsx b/editor/src/components/inspector/resize-to-fit-control.tsx index 364e5908194b..63e9363fd082 100644 --- a/editor/src/components/inspector/resize-to-fit-control.tsx +++ b/editor/src/components/inspector/resize-to-fit-control.tsx @@ -201,3 +201,4 @@ export const ResizeToFitControl = React.memo(() => { ) }) +ResizeToFitControl.displayName = 'ResizeToFitControl' diff --git a/editor/src/components/inspector/sections/layout-section/flex-element-subsection/flex-element-subsection.tsx b/editor/src/components/inspector/sections/layout-section/flex-element-subsection/flex-element-subsection.tsx index aacf5ece3615..5e4368ea8ae2 100644 --- a/editor/src/components/inspector/sections/layout-section/flex-element-subsection/flex-element-subsection.tsx +++ b/editor/src/components/inspector/sections/layout-section/flex-element-subsection/flex-element-subsection.tsx @@ -33,8 +33,7 @@ import { useContextSelector } from 'use-context-selector' import { useDispatch } from '../../../../../components/editor/store/dispatch-context' import { executeFirstApplicableStrategy } from '../../../../../components/inspector/inspector-strategies/inspector-strategy' import type { CSSNumber } from '../../../../../components/inspector/common/css-utils' -import { setPropFixedStrategies } from '../../../../../components/inspector/inspector-strategies/inspector-strategies' -import { allElemsEqual } from '../../../../../core/shared/array-utils' +import { setPropFixedSizeStrategies } from '../../../../../components/inspector/inspector-strategies/inspector-strategies' function buildMarginProps(propertyTarget: ReadonlyArray): Array { return [ @@ -325,7 +324,7 @@ const FlexWidthControls = React.memo(() => { editorStateRef.current.selectedViews, editorStateRef.current.elementPathTree, editorStateRef.current.allElementProps, - setPropFixedStrategies(transient ? 'mid-interaction' : 'always', 'horizontal', value), + setPropFixedSizeStrategies(transient ? 'mid-interaction' : 'always', 'horizontal', value), ) }, [dispatch, editorStateRef], @@ -360,7 +359,7 @@ const FlexHeightControls = React.memo(() => { editorStateRef.current.selectedViews, editorStateRef.current.elementPathTree, editorStateRef.current.allElementProps, - setPropFixedStrategies(transient ? 'mid-interaction' : 'always', 'vertical', value), + setPropFixedSizeStrategies(transient ? 'mid-interaction' : 'always', 'vertical', value), ) }, [dispatch, editorStateRef], diff --git a/editor/src/components/inspector/sections/layout-section/self-layout-subsection/clip-content-control.tsx b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/clip-content-control.tsx new file mode 100644 index 000000000000..8f0576c863ff --- /dev/null +++ b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/clip-content-control.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { useInspectorStyleInfo, useIsSubSectionVisible } from '../../../common/property-path-hooks' +import { BooleanControl } from '../../../controls/boolean-control' + +export const ClipContentControl = React.memo(() => { + const isVisible = useIsSubSectionVisible('overflow') + + const { value, controlStatus, controlStyles, onSubmitValue, onUnsetValues } = + useInspectorStyleInfo( + 'overflow', + (parsed) => !parsed.overflow, + (clipContent: boolean) => ({ + overflow: !clipContent, + }), + ) + + if (!isVisible) { + return null + } + + return ( + <> + + Clip content + + ) +}) diff --git a/editor/src/components/inspector/sections/layout-section/self-layout-subsection/frame-updating-layout-section.tsx b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/frame-updating-layout-section.tsx new file mode 100644 index 000000000000..c5742ba0ea00 --- /dev/null +++ b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/frame-updating-layout-section.tsx @@ -0,0 +1,272 @@ +import React from 'react' +import { moveInspectorStrategy } from '../../../../../components/canvas/canvas-strategies/strategies/shared-move-strategies-helpers' +import { MetadataUtils } from '../../../../../core/model/element-metadata-utils' +import type { + CanvasRectangle, + CanvasVector, + LocalRectangle, +} from '../../../../../core/shared/math-utils' +import { + canvasRectangle, + canvasVector, + isInfinityRectangle, + localRectangle, + zeroRectangle, +} from '../../../../../core/shared/math-utils' +import { assertNever } from '../../../../../core/shared/utils' +import { NumberInput } from '../../../../../uuiui' +import { resizeInspectorStrategy } from '../../../../canvas/canvas-strategies/strategies/keyboard-absolute-resize-strategy' +import { + EdgePositionBottom, + EdgePositionLeft, + EdgePositionRight, + EdgePositionTop, + type EdgePosition, +} from '../../../../canvas/canvas-types' +import { InspectorContextMenuWrapper } from '../../../../context-menu-wrapper' +import { useDispatch } from '../../../../editor/store/dispatch-context' +import { Substores, useEditorState, useRefEditorState } from '../../../../editor/store/store-hook' +import { metadataSelector, selectedViewsSelector } from '../../../../inspector/inpector-selectors' +import { unsetPropertyMenuItem } from '../../../common/context-menu-items' +import { + cssNumber, + isEmptyInputValue, + isUnknownInputValue, + type CSSNumber, + type UnknownOrEmptyInput, +} from '../../../common/css-utils' +import { useInspectorLayoutInfo } from '../../../common/property-path-hooks' +import { executeFirstApplicableStrategy } from '../../../inspector-strategies/inspector-strategy' +import { UIGridRow } from '../../../widgets/ui-grid-row' + +type TLWH = 'top' | 'left' | 'width' | 'height' + +function getTLWHEdgePosition(property: TLWH): EdgePosition { + switch (property) { + case 'top': + return EdgePositionTop + case 'height': + return EdgePositionBottom + case 'left': + return EdgePositionLeft + case 'width': + return EdgePositionRight + default: + assertNever(property) + } +} + +function getMovementFromValues(property: TLWH, oldValue: number, newValue: number): CanvasVector { + switch (property) { + case 'left': + case 'width': + return canvasVector({ x: newValue - oldValue, y: 0 }) + case 'top': + case 'height': + return canvasVector({ x: 0, y: newValue - oldValue }) + default: + assertNever(property) + } +} + +export const FrameUpdatingLayoutSection = React.memo(() => { + const dispatch = useDispatch() + const metadataRef = useRefEditorState(metadataSelector) + const selectedViewsRef = useRefEditorState(selectedViewsSelector) + const elementPathTreeRef = useRefEditorState((store) => store.editor.elementPathTree) + const allElementPropsRef = useRefEditorState((store) => store.editor.allElementProps) + const projectContentsRef = useRefEditorState((store) => store.editor.projectContents) + const singleItemSelected = useEditorState( + Substores.selectedViews, + (store) => store.editor.selectedViews.length === 1, + 'SimplifiedLayoutSubsection singleItemSelected', + ) + const originalGlobalFrame: CanvasRectangle = useEditorState( + Substores.metadata, + (store) => { + if (singleItemSelected) { + const metadata = MetadataUtils.findElementByElementPath( + store.editor.jsxMetadata, + store.editor.selectedViews[0], + ) + const maybeInfinityGlobalFrame = metadata?.globalFrame ?? canvasRectangle(zeroRectangle) + return isInfinityRectangle(maybeInfinityGlobalFrame) + ? canvasRectangle(zeroRectangle) + : maybeInfinityGlobalFrame + } else { + return canvasRectangle(zeroRectangle) + } + }, + 'SimplifiedLayoutSubsection originalGlobalFrame', + ) + const originalLocalFrame: LocalRectangle = useEditorState( + Substores.metadata, + (store) => { + if (singleItemSelected) { + const metadata = MetadataUtils.findElementByElementPath( + store.editor.jsxMetadata, + store.editor.selectedViews[0], + ) + const maybeInfinityLocalFrame = metadata?.localFrame ?? localRectangle(zeroRectangle) + return isInfinityRectangle(maybeInfinityLocalFrame) + ? localRectangle(zeroRectangle) + : maybeInfinityLocalFrame + } else { + return localRectangle(zeroRectangle) + } + }, + 'SimplifiedLayoutSubsection originalGlobalFrame', + ) + + const updateFrame = React.useCallback( + (edgePosition: EdgePosition, movement: CanvasVector) => { + if (edgePosition === EdgePositionTop || edgePosition === EdgePositionLeft) { + executeFirstApplicableStrategy( + dispatch, + metadataRef.current, + selectedViewsRef.current, + elementPathTreeRef.current, + allElementPropsRef.current, + [moveInspectorStrategy(projectContentsRef.current, movement)], + ) + } else { + executeFirstApplicableStrategy( + dispatch, + metadataRef.current, + selectedViewsRef.current, + elementPathTreeRef.current, + allElementPropsRef.current, + [ + resizeInspectorStrategy( + projectContentsRef.current, + originalGlobalFrame, + edgePosition, + movement, + ), + ], + ) + } + }, + [ + allElementPropsRef, + dispatch, + elementPathTreeRef, + metadataRef, + originalGlobalFrame, + projectContentsRef, + selectedViewsRef, + ], + ) + + return ( + <> + + + + + + + + + + ) +}) +FrameUpdatingLayoutSection.displayName = 'FrameUpdatingLayoutSection' + +interface LayoutPinPropertyControlProps { + label: string + property: TLWH + currentValue: number + updateFrame: (edgePosition: EdgePosition, movement: CanvasVector) => void + enabled: boolean +} + +const FrameUpdatingLayoutControl = React.memo((props: LayoutPinPropertyControlProps) => { + const { property, currentValue, updateFrame } = props + const pointInfo = useInspectorLayoutInfo(props.property) + + // a way to reset the NumberInput to the real measured value it was displaying before the user started typing into it + const [mountCount, resetNumberInputToOriginalValue] = React.useReducer((c) => c + 1, 0) + + const onSubmitValue = React.useCallback( + (newValue: UnknownOrEmptyInput) => { + if (props.enabled) { + if (isUnknownInputValue(newValue)) { + // Ignore right now. + resetNumberInputToOriginalValue() + } else if (isEmptyInputValue(newValue)) { + // Reset the NumberInput + resetNumberInputToOriginalValue() + } else { + if (newValue.unit == null || newValue.unit === 'px') { + const edgePosition = getTLWHEdgePosition(property) + const movement = getMovementFromValues(property, currentValue, newValue.value) + updateFrame(edgePosition, movement) + } else { + console.error('Attempting to use a value with a unit, which is invalid.') + resetNumberInputToOriginalValue() + } + } + } else { + resetNumberInputToOriginalValue() + } + }, + [props.enabled, property, currentValue, updateFrame], + ) + + return ( + + + + ) +}) +FrameUpdatingLayoutControl.displayName = 'FrameUpdatingLayoutControl' diff --git a/editor/src/components/inspector/sections/layout-section/self-layout-subsection/gigantic-size-pins-subsection.tsx b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/gigantic-size-pins-subsection.tsx index 21490c4894b2..76d571d84978 100644 --- a/editor/src/components/inspector/sections/layout-section/self-layout-subsection/gigantic-size-pins-subsection.tsx +++ b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/gigantic-size-pins-subsection.tsx @@ -2,6 +2,7 @@ import React from 'react' import type { LayoutFlexElementNumericProp, LayoutPinnedProp, + LayoutPinnedPropIncludingCenter, } from '../../../../../core/layout/layout-helpers-new' import { InspectorContextMenuWrapper } from '../../../../context-menu-wrapper' import { unsetPropertyMenuItem } from '../../../common/context-menu-items' @@ -94,6 +95,7 @@ export const PinsLayoutNumberControl = React.memo((props: PinsLayoutNumberContro data={{}} > void framePins: FramePinsInfo - togglePin: (prop: LayoutPinnedProp) => void + togglePin: (prop: LayoutPinnedPropIncludingCenter) => void } const PinControls = React.memo((props: PinControlsProps) => { @@ -325,7 +327,7 @@ const PinControls = React.memo((props: PinControlsProps) => { ) }) -function pinsLayoutNumberControl(prop: LayoutPinnedProp) { +export function pinsLayoutNumberControl(prop: LayoutPinnedProp) { return } diff --git a/editor/src/components/inspector/sections/layout-section/self-layout-subsection/simplified-layout-subsection.tsx b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/simplified-layout-subsection.tsx new file mode 100644 index 000000000000..2eb149b36f6a --- /dev/null +++ b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/simplified-layout-subsection.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { FlexColumn, FlexRow, InspectorSubsectionHeader } from '../../../../../uuiui' +import { EditorContractDropdown } from '../../../editor-contract-section' +import { FixedHugDropdown } from '../../../fill-hug-fixed-control' +import { UIGridRow } from '../../../widgets/ui-grid-row' +import { ClipContentControl } from './clip-content-control' +import { FrameUpdatingLayoutSection } from './frame-updating-layout-section' + +export const SimplifiedLayoutSubsection = React.memo(() => { + return ( + + + + + + + + + + + + + + + + + + ) +}) +SimplifiedLayoutSubsection.displayName = 'SimplifiedLayoutSubsection' diff --git a/editor/src/components/inspector/sections/style-section/text-subsection/text-auto-sizing-control.tsx b/editor/src/components/inspector/sections/style-section/text-subsection/text-auto-sizing-control.tsx index 69c093c0f5d0..5542f0f40b70 100644 --- a/editor/src/components/inspector/sections/style-section/text-subsection/text-auto-sizing-control.tsx +++ b/editor/src/components/inspector/sections/style-section/text-subsection/text-auto-sizing-control.tsx @@ -18,7 +18,7 @@ import { import type { FixedHugFill } from '../../../inspector-common' import { detectFillHugFixedStateMultiselect, isFixedHugFillEqual } from '../../../inspector-common' import { - setPropFixedStrategies, + setPropFixedSizeStrategies, setPropHugStrategies, } from '../../../inspector-strategies/inspector-strategies' import { commandsForFirstApplicableStrategy } from '../../../inspector-strategies/inspector-strategy' @@ -120,7 +120,7 @@ export const TextAutoSizingControl = React.memo(() => { selectedViewsRef.current, elementPathTreeRef.current, allElementPropsRef.current, - setPropFixedStrategies( + setPropFixedSizeStrategies( 'always', 'horizontal', cssNumber(widthComputedValue.current ?? 0, null), @@ -132,7 +132,7 @@ export const TextAutoSizingControl = React.memo(() => { selectedViewsRef.current, elementPathTreeRef.current, allElementPropsRef.current, - setPropFixedStrategies( + setPropFixedSizeStrategies( 'always', 'vertical', cssNumber(heightComputedValue.current ?? 0, null), diff --git a/editor/src/components/inspector/simplified-pinning-helpers.tsx b/editor/src/components/inspector/simplified-pinning-helpers.tsx new file mode 100644 index 000000000000..26221f8addb4 --- /dev/null +++ b/editor/src/components/inspector/simplified-pinning-helpers.tsx @@ -0,0 +1,322 @@ +import { createSelector } from 'reselect' +import { + HorizontalLayoutPinnedProps, + VerticalLayoutPinnedProps, + type LayoutPinnedProp, +} from '../../core/layout/layout-helpers-new' +import { MetadataUtils } from '../../core/model/element-metadata-utils' +import type { ElementInstanceMetadataMap } from '../../core/shared/element-template' +import { emptyComments, jsExpressionValue } from '../../core/shared/element-template' +import { nullIfInfinity } from '../../core/shared/math-utils' +import type { ElementPath } from '../../core/shared/project-file-types' +import * as PP from '../../core/shared/property-path' +import { assertNever } from '../../core/shared/utils' +import invariant from '../../third-party/remix/invariant' +import type { SelectOption } from '../../uuiui-deps' +import { valueToUseForPin } from '../canvas/canvas-utils' +import type { SetProp, UnsetProperty } from '../editor/action-types' +import { setProp_UNSAFE, unsetProperty } from '../editor/actions/action-creators' +import { Substores, useEditorState } from '../editor/store/store-hook' +import { getFullFrame } from '../frame' +import { isCssNumberAndFixedSize, isCssNumberAndPercentage } from './common/css-utils' +import type { FramePinsInfo } from './common/layout-property-path-hooks' +import { metadataSelector, selectedViewsSelector } from './inpector-selectors' +import { getFramePointsFromMetadataTypeFixed } from './inspector-common' + +type HorizontalPinRequests = + | 'left-and-width' + | 'right-and-width' + | 'left-and-right' + | 'scale-horizontal' + +type VerticalPinRequests = + | 'top-and-height' + | 'bottom-and-height' + | 'top-and-bottom' + | 'scale-vertical' + +export type RequestedPins = HorizontalPinRequests | VerticalPinRequests + +type DetectedPins = { + horizontal: HorizontalPinRequests | 'mixed' + vertical: VerticalPinRequests | 'mixed' +} + +export const HorizontalPinChangeOptions: { + [key in HorizontalPinRequests]: SelectOption & { value: HorizontalPinRequests } +} = { + 'left-and-width': { + value: 'left-and-width', + label: 'Left', + }, + 'right-and-width': { + value: 'right-and-width', + label: 'Right', + }, + 'left-and-right': { + value: 'left-and-right', + label: 'Left and Right', + }, + 'scale-horizontal': { + value: 'scale-horizontal', + label: 'Scale', + }, +} as const + +export const HorizontalPinChangeOptionsIncludingMixed = { + ...HorizontalPinChangeOptions, + mixed: { + value: 'mixed', + label: 'Mixed', + }, +} as const + +export const VerticalPinChangeOptions: { + [key in VerticalPinRequests]: SelectOption & { value: VerticalPinRequests } +} = { + 'top-and-height': { + value: 'top-and-height', + label: 'Top', + }, + 'bottom-and-height': { + value: 'bottom-and-height', + label: 'Bottom', + }, + 'top-and-bottom': { + value: 'top-and-bottom', + label: 'Top and Bottom', + }, + 'scale-vertical': { + value: 'scale-vertical', + label: 'Scale', + }, +} as const + +export const VerticalPinChangeOptionsIncludingMixed = { + ...VerticalPinChangeOptions, + mixed: { + value: 'mixed', + label: 'Mixed', + }, +} as const + +const multiselectDetectPinsSetSelector = createSelector( + metadataSelector, + selectedViewsSelector, + (metadata, selectedViews) => multiselectDetectPinsSet(metadata, selectedViews), +) + +export function useDetectedPinning() { + return useEditorState( + Substores.metadata, + multiselectDetectPinsSetSelector, + 'FrameChildConstraintSelect pins', + ) +} + +function multiselectDetectPinsSet( + metadata: ElementInstanceMetadataMap, + targets: Array, +): DetectedPins { + if (targets.length == 0) { + return { horizontal: 'left-and-width', vertical: 'top-and-height' } + } + const results = targets.map((t) => detectPinsSet(metadata, t)) + const isMixedHorizontal = results.some((r) => r.horizontal !== results[0].horizontal) + const isMixedVertical = results.some((r) => r.vertical !== results[0].vertical) + + return { + horizontal: isMixedHorizontal ? 'mixed' : results[0].horizontal, + vertical: isMixedVertical ? 'mixed' : results[0].vertical, + } +} + +function detectPinsSet( + metadata: ElementInstanceMetadataMap, + target: ElementPath, +): { horizontal: HorizontalPinRequests | 'mixed'; vertical: VerticalPinRequests | 'mixed' } { + const element = MetadataUtils.findElementByElementPath(metadata, target) + if (element == null) { + return { horizontal: 'mixed', vertical: 'mixed' } + } + + const framePoints = getFramePointsFromMetadataTypeFixed(element) + + const horizontalPins: HorizontalPinRequests | 'mixed' = (() => { + if ( + isCssNumberAndFixedSize(framePoints.left) && + framePoints.right == null && + isCssNumberAndFixedSize(framePoints.width) + ) { + return 'left-and-width' + } + if ( + framePoints.left == null && + isCssNumberAndFixedSize(framePoints.right) && + isCssNumberAndFixedSize(framePoints.width) + ) { + return 'right-and-width' + } + if ( + isCssNumberAndFixedSize(framePoints.left) && + isCssNumberAndFixedSize(framePoints.right) && + framePoints.width == null + ) { + return 'left-and-right' + } + if ( + isCssNumberAndPercentage(framePoints.left) && + framePoints.right == null && + isCssNumberAndPercentage(framePoints.width) + ) { + return 'scale-horizontal' + } + return 'mixed' + })() + + const verticalPins: VerticalPinRequests | 'mixed' = (() => { + if ( + isCssNumberAndFixedSize(framePoints.top) && + framePoints.bottom == null && + isCssNumberAndFixedSize(framePoints.height) + ) { + return 'top-and-height' + } + if ( + framePoints.top == null && + isCssNumberAndFixedSize(framePoints.bottom) && + isCssNumberAndFixedSize(framePoints.height) + ) { + return 'bottom-and-height' + } + if ( + isCssNumberAndFixedSize(framePoints.top) && + isCssNumberAndFixedSize(framePoints.bottom) && + framePoints.height == null + ) { + return 'top-and-bottom' + } + if ( + isCssNumberAndPercentage(framePoints.top) && + framePoints.bottom == null && + isCssNumberAndPercentage(framePoints.height) + ) { + return 'scale-vertical' + } + return 'mixed' + })() + + return { horizontal: horizontalPins, vertical: verticalPins } +} + +export function getFixedPointsForPinning(pins: DetectedPins): FramePinsInfo { + const ignore = { isPrimaryPosition: false, isRelativePosition: false } + + return { + left: { + isPrimaryPosition: + pins.horizontal === 'left-and-right' || pins.horizontal === 'left-and-width', + isRelativePosition: false, + }, + top: { + isPrimaryPosition: pins.vertical === 'top-and-bottom' || pins.vertical === 'top-and-height', + isRelativePosition: false, + }, + bottom: { + isPrimaryPosition: + pins.vertical === 'top-and-bottom' || pins.vertical === 'bottom-and-height', + isRelativePosition: false, + }, + right: { + isPrimaryPosition: + pins.horizontal === 'left-and-right' || pins.horizontal === 'right-and-width', + isRelativePosition: false, + }, + width: ignore, + height: ignore, + centerX: + pins.horizontal === 'scale-horizontal' + ? { isPrimaryPosition: true, isRelativePosition: true } + : ignore, + centerY: + pins.vertical === 'scale-vertical' + ? { isPrimaryPosition: true, isRelativePosition: true } + : ignore, + } +} + +export function getFrameChangeActions( + metadata: ElementInstanceMetadataMap, + propertyTarget: ReadonlyArray, + targets: Array, + requestedPins: RequestedPins, +): Array { + const pinChange = getPinChanges(metadata, propertyTarget, targets) + switch (requestedPins) { + case 'left-and-width': + return pinChange(['left', 'width'], 'horizontal', 'px') + case 'right-and-width': + return pinChange(['right', 'width'], 'horizontal', 'px') + case 'left-and-right': + return pinChange(['left', 'right'], 'horizontal', 'px') + case 'scale-horizontal': + return pinChange(['left', 'width'], 'horizontal', '%') + + case 'top-and-height': + return pinChange(['top', 'height'], 'vertical', 'px') + case 'bottom-and-height': + return pinChange(['bottom', 'height'], 'vertical', 'px') + case 'top-and-bottom': + return pinChange(['top', 'bottom'], 'vertical', 'px') + case 'scale-vertical': + return pinChange(['top', 'height'], 'vertical', '%') + + default: + assertNever(requestedPins) + } +} + +const getPinChanges = + ( + metadata: ElementInstanceMetadataMap, + propertyTarget: ReadonlyArray, + targets: Array, + ) => + ( + pinsToSet: Array, + horizontal: 'horizontal' | 'vertical', + setAsPercentage: 'px' | '%', + ): Array => { + const pinsToUnset = + horizontal === 'horizontal' ? HorizontalLayoutPinnedProps : VerticalLayoutPinnedProps + + const unsetActions: Array = targets.flatMap((target) => + pinsToUnset.map((pin) => unsetProperty(target, PP.create(...propertyTarget, pin))), + ) + + const setActions: Array = targets.flatMap((target) => { + const localFrame = nullIfInfinity( + MetadataUtils.findElementByElementPath(metadata, target)?.localFrame, + ) + const coordinateSystemBounds = MetadataUtils.findElementByElementPath(metadata, target) + ?.specialSizeMeasurements.coordinateSystemBounds + + invariant(localFrame != null, 'LocalFrame should never be null') + invariant(coordinateSystemBounds != null, 'coordinateSystemBounds should never be null') + + const fullFrame = getFullFrame(localFrame) + + return pinsToSet.map((pin) => + setProp_UNSAFE( + target, + PP.create(...propertyTarget, pin), + jsExpressionValue( + valueToUseForPin(pin, fullFrame[pin], setAsPercentage === '%', coordinateSystemBounds), + emptyComments, + ), + ), + ) + }) + + return [...unsetActions, ...setActions] + } diff --git a/editor/src/components/inspector/sizing-section.tsx b/editor/src/components/inspector/sizing-section.tsx index e98aedbfe89c..4ee531903fc0 100644 --- a/editor/src/components/inspector/sizing-section.tsx +++ b/editor/src/components/inspector/sizing-section.tsx @@ -1,6 +1,6 @@ import React from 'react' import { FlexRow, InspectorSubsectionHeader } from '../../uuiui' -import { FillHugFixedControl } from './fill-hug-fixed-control' +import { FillHugFixedControlOld } from './fill-hug-fixed-control' import { ResizeToFitControl } from './resize-to-fit-control' interface SizingSectionProps {} @@ -19,7 +19,7 @@ export const SizingSection = React.memo(() => { - + ) }) diff --git a/editor/src/components/inspector/utility-controls/pin-control.tsx b/editor/src/components/inspector/utility-controls/pin-control.tsx index 23385927f225..cce42dd5e01e 100644 --- a/editor/src/components/inspector/utility-controls/pin-control.tsx +++ b/editor/src/components/inspector/utility-controls/pin-control.tsx @@ -6,7 +6,7 @@ import { getControlStyles } from '../common/control-styles' import { FramePoint } from 'utopia-api/core' import type { LayoutPinnedProp } from '../../../core/layout/layout-helpers-new' import type { FramePinsInfo } from '../common/layout-property-path-hooks' -import { UtopiaTheme, SquareButton } from '../../../uuiui' +import { UtopiaTheme, SquareButton, colorTheme } from '../../../uuiui' interface PinControlProps { handlePinMouseDown: (frameProp: LayoutPinnedProp) => void @@ -241,6 +241,67 @@ interface PinWidthControlProps { toggleWidth: () => void } +export const PinWidthSVG = React.memo(() => { + return ( + + + + + + + + + ) +}) + +export const PinHeightSVG = React.memo(() => { + return ( + + + + + + + + + ) +}) + export const PinWidthControl = React.memo((props: PinWidthControlProps) => { const controlStyles: ControlStyles = getControlStyles(props.controlStatus) return ( diff --git a/editor/src/core/layout/layout-helpers-new.ts b/editor/src/core/layout/layout-helpers-new.ts index 853686b50549..19b4df4c28e1 100644 --- a/editor/src/core/layout/layout-helpers-new.ts +++ b/editor/src/core/layout/layout-helpers-new.ts @@ -32,7 +32,15 @@ export type LayoutTargetableProp = | 'flexGrow' | 'flexShrink' -export type LayoutPinnedProp = LayoutDimension | 'left' | 'top' | 'right' | 'bottom' +export type LayoutEdgeProp = 'left' | 'top' | 'right' | 'bottom' + +export type LayoutPinnedProp = LayoutDimension | LayoutEdgeProp + +export function layoutPinnedPropIsEdgeProp(prop: LayoutPinnedProp): prop is LayoutEdgeProp { + return prop === 'left' || prop === 'right' || prop === 'top' || prop === 'bottom' +} + +export type LayoutPinnedPropIncludingCenter = LayoutPinnedProp | 'centerX' | 'centerY' export const VerticalLayoutPinnedProps: Array = ['top', 'bottom', 'height'] 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 1dbc31b21ca8..7589d19f3e44 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 @@ -125,9 +125,13 @@ Array [ "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)(FragmentSection)", + "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)(SimplifiedLayoutSubsection)", + "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)(ConstraintsSection)", + "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/UtopiaSpiedExoticType(Symbol(react.fragment))", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/UtopiaSpiedExoticType(Symbol(react.fragment))", + "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/UtopiaSpiedExoticType(Symbol(react.fragment))", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)(StyleSection)", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", @@ -811,9 +815,13 @@ Array [ "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)(FragmentSection)", + "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)(SimplifiedLayoutSubsection)", + "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)(ConstraintsSection)", + "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/UtopiaSpiedExoticType(Symbol(react.fragment))", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/UtopiaSpiedExoticType(Symbol(react.fragment))", + "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/UtopiaSpiedExoticType(Symbol(react.fragment))", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)(StyleSection)", "/UtopiaSpiedExoticType(Symbol(react.provider))/Symbol(react.provider)/Inspector/Symbol(react.memo)()", diff --git a/editor/src/core/performance/performance-regression-tests.spec.tsx b/editor/src/core/performance/performance-regression-tests.spec.tsx index 2e8fcb54f5cb..984aeeed107d 100644 --- a/editor/src/core/performance/performance-regression-tests.spec.tsx +++ b/editor/src/core/performance/performance-regression-tests.spec.tsx @@ -183,7 +183,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`609`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`613`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -249,7 +249,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`681`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`685`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) }) diff --git a/editor/src/utils/feature-switches.ts b/editor/src/utils/feature-switches.ts index 7cf663d4c06f..5af7ec5e1ae6 100644 --- a/editor/src/utils/feature-switches.ts +++ b/editor/src/utils/feature-switches.ts @@ -13,6 +13,7 @@ export type FeatureName = | 'Canvas Strategies Debug Panel' | 'Project Thumbnail Generation' | 'Draggable Floating Panels' + | 'Simplified Layout Section' | 'Debug - Print UIDs' export const AllFeatureNames: FeatureName[] = [ @@ -27,6 +28,7 @@ export const AllFeatureNames: FeatureName[] = [ 'Canvas Strategies Debug Panel', 'Project Thumbnail Generation', 'Draggable Floating Panels', + 'Simplified Layout Section', 'Debug - Print UIDs', ] @@ -40,6 +42,7 @@ let FeatureSwitches: { [feature in FeatureName]: boolean } = { 'Performance Test Triggers': !(PRODUCTION_CONFIG as boolean), 'Canvas Strategies Debug Panel': false, 'Project Thumbnail Generation': false, + 'Simplified Layout Section': false, 'Draggable Floating Panels': !IS_TEST_ENVIRONMENT, 'Debug - Print UIDs': false, }