diff --git a/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx index 2da9612b7ae7..53f768b6e61d 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx @@ -11,6 +11,7 @@ import { isInfinityRectangle, offsetPoint, } from '../../../../core/shared/math-utils' +import * as PP from '../../../../core/shared/property-path' import { styleStringInArray } from '../../../../utils/common-constants' import { trueUpGroupElementChanged } from '../../../editor/store/editor-state' import { stylePropPathMappingFn } from '../../../inspector/common/property-path-hooks' @@ -20,6 +21,7 @@ import { oppositeEdgePosition } from '../../canvas-types' import { isEdgePositionACorner, isEdgePositionAHorizontalEdge, + isEdgePositionAVerticalEdge, pickPointOnRect, } from '../../canvas-utils' import type { LengthPropertyToAdjust } from '../../commands/adjust-css-length-command' @@ -27,6 +29,8 @@ import { adjustCssLengthProperties, lengthPropertyToAdjust, } from '../../commands/adjust-css-length-command' +import type { CanvasCommand } from '../../commands/commands' +import { deleteProperties } from '../../commands/delete-properties-command' import { pushIntendedBoundsAndUpdateGroups } from '../../commands/push-intended-bounds-and-update-groups-command' import { queueTrueUpElement } from '../../commands/queue-true-up-command' import { setCursorCommand } from '../../commands/set-cursor-command' @@ -219,21 +223,31 @@ export function basicResizeStrategy( const elementsToRerender = [...selectedElements, ...gridsToRerender] - return strategyApplicationResult( - [ - adjustCssLengthProperties('always', selectedElement, null, resizeProperties), - updateHighlightedViews('mid-interaction', []), - setCursorCommand(pickCursorFromEdgePosition(edgePosition)), - pushIntendedBoundsAndUpdateGroups( - [{ target: selectedElement, frame: resizedBounds }], - 'starting-metadata', - ), - ...groupChildren.map((c) => - queueTrueUpElement([trueUpGroupElementChanged(c.elementPath)]), - ), - ], - elementsToRerender, - ) + let commands: CanvasCommand[] = [ + adjustCssLengthProperties('always', selectedElement, null, resizeProperties), + updateHighlightedViews('mid-interaction', []), + setCursorCommand(pickCursorFromEdgePosition(edgePosition)), + pushIntendedBoundsAndUpdateGroups( + [{ target: selectedElement, frame: resizedBounds }], + 'starting-metadata', + ), + ...groupChildren.map((c) => + queueTrueUpElement([trueUpGroupElementChanged(c.elementPath)]), + ), + ] + + if (isEdgePositionAHorizontalEdge(edgePosition)) { + commands.push( + deleteProperties('always', selectedElement, [PP.create('style', 'justifySelf')]), + ) + } + if (isEdgePositionAVerticalEdge(edgePosition)) { + commands.push( + deleteProperties('always', selectedElement, [PP.create('style', 'alignSelf')]), + ) + } + + return strategyApplicationResult(commands, elementsToRerender) } else { return strategyApplicationResult( [ 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 38f456abf349..c8db573bd4af 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 @@ -398,6 +398,8 @@ const sizeLabel = (state: FixedHugFill['type'], actualSize: number): string => { case 'detected': case 'computed': return `${actualSize}` + case 'stretch': + return 'Stretch' default: assertNever(state) } diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 14cc63ab0cfa..4b02829f691a 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -4,8 +4,7 @@ import localforage from 'localforage' import { imagePathURL } from '../../../common/server' import { roundAttributeLayoutValues } from '../../../core/layout/layout-utils' import { - findElementAtPath, - findJSXElementAtPath, + getSimpleAttributeAtPath, getZIndexOrderedViewsWithoutDirectChildren, MetadataUtils, } from '../../../core/model/element-metadata-utils' @@ -102,6 +101,7 @@ import type { LocalRectangle, Size, CanvasVector, + MaybeInfinityCanvasRectangle, } from '../../../core/shared/math-utils' import { canvasRectangle, @@ -157,7 +157,7 @@ import { assertNever, fastForEach, getProjectLockedKey, identity } from '../../. import { emptyImports, mergeImports } from '../../../core/workers/common/project-file-utils' import type { UtopiaTsWorkers } from '../../../core/workers/common/worker-types' import type { IndexPosition } from '../../../utils/utils' -import Utils, { absolute } from '../../../utils/utils' +import Utils from '../../../utils/utils' import type { ProjectContentTreeRoot } from '../../assets' import { isProjectContentFile, @@ -430,7 +430,7 @@ import { import { loadStoredState } from '../stored-state' import { applyMigrations } from './migrations/migrations' -import { boundsInFile, defaultConfig } from 'utopia-vscode-common' +import { defaultConfig } from 'utopia-vscode-common' import { reorderElement } from '../../../components/canvas/commands/reorder-element-command' import type { BuiltInDependencies } from '../../../core/es-modules/package-manager/built-in-dependencies-list' import { fetchNodeModules } from '../../../core/es-modules/package-manager/fetch-packages' @@ -495,7 +495,6 @@ import { clearImageFileBlob, enableInsertModeForJSXElement, finishCheckpointTimer, - insertAsChildTarget, insertJSXElement, openCodeEditorFile, replaceMappedElement, @@ -524,7 +523,6 @@ import { styleStringInArray } from '../../../utils/common-constants' import { collapseTextElements } from '../../../components/text-editor/text-handling' import { LayoutPropertyList, StyleProperties } from '../../inspector/common/css-utils' import { - getFromPropOrFlagComment, isUtopiaPropOrCommentFlag, makeUtopiaFlagComment, removePropOrFlagComment, @@ -543,7 +541,10 @@ import { replaceWithElementsWrappedInFragmentBehaviour, } from '../store/insertion-path' import { getConditionalCaseCorrespondingToBranchPath } from '../../../core/model/conditionals' -import { deleteProperties } from '../../canvas/commands/delete-properties-command' +import { + deleteProperties, + deleteValuesAtPath, +} from '../../canvas/commands/delete-properties-command' import { treatElementAsFragmentLike } from '../../canvas/canvas-strategies/strategies/fragment-like-helpers' import { fixParentContainingBlockSettings, @@ -619,11 +620,12 @@ import { import type { FixUIDsState } from '../../../core/workers/parser-printer/uid-fix' import { fixTopLevelElementsUIDs } from '../../../core/workers/parser-printer/uid-fix' import { nextSelectedTab } from '../../navigator/left-pane/left-pane-utils' -import { getDefaultedRemixRootDir, getRemixRootDir } from '../store/remix-derived-data' +import { getDefaultedRemixRootDir } from '../store/remix-derived-data' import { isReplaceKeepChildrenAndStyleTarget } from '../../navigator/navigator-item/component-picker-context-menu' import { canCondenseJSXElementChild } from '../../../utils/can-condense' import { getNavigatorTargetsFromEditorState } from '../../navigator/navigator-utils' import { applyValuesAtPath } from '../../canvas/commands/adjust-number-command' +import { styleP } from '../../inspector/inspector-common' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 @@ -6589,10 +6591,54 @@ function removeErrorMessagesForFile(editor: EditorState, filename: string): Edit function alignFlexOrGridChildren(editor: EditorState, views: ElementPath[], alignment: Alignment) { let workingEditorState = { ...editor } for (const view of views) { - function apply(prop: 'alignSelf' | 'justifySelf', value: string) { - return applyValuesAtPath(workingEditorState, view, [ + // When updating alongside the given alignment, also update the opposite one so that it makes sense: + // For example, if alignment is 'alignSelf', delete the 'justifySelf' if currently set to stretch and, if so, + // set the explicit height of the element (and vice versa for 'justifySelf'). + function updateOpposite( + editorState: EditorState, + frame: MaybeInfinityCanvasRectangle | null, + target: 'alignSelf' | 'justifySelf', + dimension: 'width' | 'height', + ) { + let working = { ...editorState } + + working = deleteValuesAtPath(working, view, [styleP(target)]).editorStateWithChanges + + working = applyValuesAtPath(working, view, [ + { + path: styleP(dimension), + value: jsExpressionValue(zeroRectIfNullOrInfinity(frame)[dimension], emptyComments), + }, + ]).editorStateWithChanges + + return working + } + + function apply(editorState: EditorState, prop: 'alignSelf' | 'justifySelf', value: string) { + const element = MetadataUtils.findElementByElementPath(editor.jsxMetadata, view) + if (element == null || isLeft(element.element) || !isJSXElement(element.element.value)) { + return workingEditorState + } + + let working = editorState + + working = applyValuesAtPath(working, view, [ { path: PP.create('style', prop), value: jsExpressionValue(value, emptyComments) }, ]).editorStateWithChanges + + const alignSelfStretch = + getSimpleAttributeAtPath(right(element.element.value.props), styleP('alignSelf')).value === + 'stretch' + const justifySelfStretch = + getSimpleAttributeAtPath(right(element.element.value.props), styleP('justifySelf')) + .value === 'stretch' + + if (prop === 'alignSelf' && justifySelfStretch) { + working = updateOpposite(working, element.globalFrame, 'justifySelf', 'height') + } else if (prop === 'justifySelf' && alignSelfStretch) { + working = updateOpposite(working, element.globalFrame, 'alignSelf', 'width') + } + return working } const { align, justify } = MetadataUtils.getRelativeAlignJustify( @@ -6602,27 +6648,28 @@ function alignFlexOrGridChildren(editor: EditorState, views: ElementPath[], alig switch (alignment) { case 'bottom': - workingEditorState = apply(align, 'end') + workingEditorState = apply(workingEditorState, align, 'end') break case 'top': - workingEditorState = apply(align, 'start') + workingEditorState = apply(workingEditorState, align, 'start') break case 'vcenter': - workingEditorState = apply(align, 'center') + workingEditorState = apply(workingEditorState, align, 'center') break case 'hcenter': - workingEditorState = apply(justify, 'center') + workingEditorState = apply(workingEditorState, justify, 'center') break case 'left': - workingEditorState = apply(justify, 'start') + workingEditorState = apply(workingEditorState, justify, 'start') break case 'right': - workingEditorState = apply(justify, 'end') + workingEditorState = apply(workingEditorState, justify, 'end') break default: assertNever(alignment) } } + return workingEditorState } diff --git a/editor/src/components/inspector/fill-hug-fixed-control.tsx b/editor/src/components/inspector/fill-hug-fixed-control.tsx index e97e7e2e5e5b..08dd8f40050e 100644 --- a/editor/src/components/inspector/fill-hug-fixed-control.tsx +++ b/editor/src/components/inspector/fill-hug-fixed-control.tsx @@ -80,6 +80,7 @@ export const CollapsedLabel = 'Collapsed' as const export const HugGroupContentsLabel = 'Hug' as const export const ComputedLabel = 'Computed' as const export const DetectedLabel = 'Detected' as const +export const StretchLabel = 'Stretch' as const export function selectOptionLabel(mode: FixedHugFillMode): string { switch (mode) { @@ -101,6 +102,8 @@ export function selectOptionLabel(mode: FixedHugFillMode): string { return ComputedLabel case 'detected': return DetectedLabel + case 'stretch': + return StretchLabel default: assertNever(mode) } @@ -126,6 +129,8 @@ export function selectOptionIconType( return `fixed-${dimension}` case 'detected': return `fixed-${dimension}` + case 'stretch': + return `fill-${dimension}` default: assertNever(mode) } @@ -396,6 +401,7 @@ function strategyForChangingFillFixedHugType( ): Array { switch (mode) { case 'fill': + case 'stretch': return setPropFillStrategies(metadata, selectedElements, axis, 'default', otherAxisSetToFill) case 'hug': case 'squeeze': @@ -427,6 +433,7 @@ function pickFixedValue(value: FixedHugFill): CSSNumber | undefined { case 'fill': case 'hug-group': return value.value + case 'stretch': case 'hug': case 'squeeze': case 'collapsed': diff --git a/editor/src/components/inspector/inspector-common.ts b/editor/src/components/inspector/inspector-common.ts index e51e2fecd4fd..7f0b03ef28a1 100644 --- a/editor/src/components/inspector/inspector-common.ts +++ b/editor/src/components/inspector/inspector-common.ts @@ -647,6 +647,7 @@ export type FixedHugFill = | { type: 'computed'; value: CSSNumber } | { type: 'detected'; value: CSSNumber } | { type: 'scaled'; value: CSSNumber } + | { type: 'stretch' } export type FixedHugFillMode = FixedHugFill['type'] @@ -666,6 +667,30 @@ export function detectFillHugFixedState( return { fixedHugFill: null, controlStatus: 'off' } } + const width = foldEither( + () => null, + (value) => defaultEither(null, parseCSSNumber(value, 'Unitless')), + getSimpleAttributeAtPath(right(element.element.value.props), PP.create('style', 'width')), + ) + const height = foldEither( + () => null, + (value) => defaultEither(null, parseCSSNumber(value, 'Unitless')), + getSimpleAttributeAtPath(right(element.element.value.props), PP.create('style', 'height')), + ) + + if (MetadataUtils.isGridCell(metadata, elementPath)) { + if ( + (element.specialSizeMeasurements.alignSelf === 'stretch' && + axis === 'horizontal' && + width == null) || + (element.specialSizeMeasurements.justifySelf === 'stretch' && + axis === 'vertical' && + height == null) + ) { + return { fixedHugFill: { type: 'stretch' }, controlStatus: 'detected' } + } + } + const flexGrowLonghand = foldEither( () => null, (value) => defaultEither(null, parseCSSNumber(value, 'Unitless')), @@ -1071,7 +1096,11 @@ export function getFixedFillHugOptionsForElement( (!isGroup && basicHugContentsApplicableForContainer(metadata, pathTrees, selectedView)) ? 'hug' : null, - fillContainerApplicable(metadata, selectedView) ? 'fill' : null, + fillContainerApplicable(metadata, selectedView) + ? MetadataUtils.isGridCell(metadata, selectedView) + ? 'stretch' + : 'fill' + : null, ]), ) } @@ -1198,6 +1227,20 @@ export function removeExtraPinsWhenSettingSize( ) } +export function removeAlignJustifySelf( + axis: Axis, + elementMetadata: ElementInstanceMetadata | null, +): Array { + if (elementMetadata == null) { + return [] + } + return [ + deleteProperties('always', elementMetadata.elementPath, [ + styleP(axis === 'horizontal' ? 'alignSelf' : 'justifySelf'), + ]), + ] +} + export function isFixedHugFillEqual( a: { fixedHugFill: FixedHugFill | null; controlStatus: ControlStatus }, b: { fixedHugFill: FixedHugFill | null; controlStatus: ControlStatus }, @@ -1230,9 +1273,10 @@ export function isFixedHugFillEqual( a.fixedHugFill.value.value === b.fixedHugFill.value.value && a.fixedHugFill.value.unit === b.fixedHugFill.value.unit ) + case 'stretch': + return a.fixedHugFill.type === b.fixedHugFill.type default: - const _exhaustiveCheck: never = a.fixedHugFill - throw new Error(`Unknown type in FixedHugFill ${JSON.stringify(a.fixedHugFill)}`) + assertNever(a.fixedHugFill) } } diff --git a/editor/src/components/inspector/inspector-strategies/fill-container-basic-strategy.ts b/editor/src/components/inspector/inspector-strategies/fill-container-basic-strategy.ts index 230669cf7a09..20ca8f6a8b7b 100644 --- a/editor/src/components/inspector/inspector-strategies/fill-container-basic-strategy.ts +++ b/editor/src/components/inspector/inspector-strategies/fill-container-basic-strategy.ts @@ -1,7 +1,12 @@ import * as PP from '../../../core/shared/property-path' -import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import { getSimpleAttributeAtPath, MetadataUtils } from '../../../core/model/element-metadata-utils' import { clamp } from '../../../core/shared/math-utils' -import { setProperty } from '../../canvas/commands/set-property-command' +import { + propertyToDelete, + propertyToSet, + setProperty, + updateBulkProperties, +} from '../../canvas/commands/set-property-command' import type { FlexDirection } from '../common/css-utils' import { cssNumber, printCSSNumber } from '../common/css-utils' import type { Axis } from '../inspector-common' @@ -14,6 +19,8 @@ import { nukeSizingPropsForAxisCommand, nullOrNonEmpty, setParentToFixedIfHugCommands, + removeAlignJustifySelf, + styleP, } from '../inspector-common' import type { InspectorStrategy } from './inspector-strategy' import { @@ -24,8 +31,13 @@ import { groupErrorToastCommand, maybeInvalidGroupState, } from '../../canvas/canvas-strategies/strategies/group-helpers' -import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' +import { + isJSXElement, + type ElementInstanceMetadataMap, +} from '../../../core/shared/element-template' import type { ElementPath } from '../../../core/shared/project-file-types' +import { foldEither, defaultEither, right, isLeft } from '../../../core/shared/either' +import { parseString } from '../../../utils/value-parser-utils' export const fillContainerStrategyFlow = ( metadata: ElementInstanceMetadataMap, @@ -102,6 +114,8 @@ export const fillContainerStrategyFlexParent = ( } const commands = elements.flatMap((path) => { + const elementMetadata = MetadataUtils.findElementByElementPath(metadata, path) + const flexDirection = overrides.forceFlexDirectionForParent ?? detectParentFlexDirection(metadata, path) ?? 'row' @@ -113,6 +127,7 @@ export const fillContainerStrategyFlexParent = ( value === 'default' ? cssNumber(100, '%') : cssNumber(clamp(0, 100, value), '%') return [ ...setParentToFixedIfHugCommands(axis, metadata, path), + ...removeAlignJustifySelf(axis, elementMetadata), setCssLengthProperty( 'always', path, @@ -128,6 +143,7 @@ export const fillContainerStrategyFlexParent = ( return [ ...nukeAllAbsolutePositioningPropsCommands(path), + ...removeAlignJustifySelf(axis, elementMetadata), ...setParentToFixedIfHugCommands(axis, metadata, path), nukeSizingPropsForAxisCommand(axis, path), setProperty( @@ -142,3 +158,59 @@ export const fillContainerStrategyFlexParent = ( return nullOrNonEmpty(commands) }, }) + +export const fillContainerStrategyGridParent = ( + metadata: ElementInstanceMetadataMap, + elementPaths: ElementPath[], + axis: Axis, +): InspectorStrategy => ({ + name: 'Set to Fill Container, in grid layout', + strategy: () => { + const elements = elementPaths.filter( + (path) => fillContainerApplicable(metadata, path) && MetadataUtils.isGridCell(metadata, path), + ) + + if (elements.length === 0) { + return null + } + + const commands = elements.flatMap((path) => { + const element = MetadataUtils.findElementByElementPath(metadata, path) + if (element == null || isLeft(element.element) || !isJSXElement(element.element.value)) { + return [] + } + + const alignSelf = foldEither( + () => null, + (value) => defaultEither(null, parseString(value)), + getSimpleAttributeAtPath(right(element.element.value.props), styleP('alignSelf')), + ) + + const justifySelf = foldEither( + () => null, + (value) => defaultEither(null, parseString(value)), + getSimpleAttributeAtPath(right(element.element.value.props), styleP('justifySelf')), + ) + + let updates = [ + propertyToSet(styleP(axis === 'horizontal' ? 'alignSelf' : 'justifySelf'), 'stretch'), + ] + + // delete the opposite side value (justify <> align) if not set to stretch + if (axis === 'vertical' && alignSelf !== 'stretch') { + updates.push(propertyToDelete(styleP('alignSelf'))) + } else if (axis === 'horizontal' && justifySelf !== 'stretch') { + updates.push(propertyToDelete(styleP('justifySelf'))) + } + + return [ + ...nukeAllAbsolutePositioningPropsCommands(path), + ...setParentToFixedIfHugCommands(axis, metadata, path), + nukeSizingPropsForAxisCommand(axis, path), + updateBulkProperties('always', path, updates), + ] + }) + + return nullOrNonEmpty(commands) + }, +}) diff --git a/editor/src/components/inspector/inspector-strategies/fixed-size-basic-strategy.ts b/editor/src/components/inspector/inspector-strategies/fixed-size-basic-strategy.ts index 1c62f3d07fcf..504650a00db9 100644 --- a/editor/src/components/inspector/inspector-strategies/fixed-size-basic-strategy.ts +++ b/editor/src/components/inspector/inspector-strategies/fixed-size-basic-strategy.ts @@ -7,7 +7,11 @@ import { } from '../../canvas/commands/set-css-length-command' import type { CSSNumber } from '../common/css-utils' import type { Axis } from '../inspector-common' -import { removeExtraPinsWhenSettingSize, widthHeightFromAxis } from '../inspector-common' +import { + removeAlignJustifySelf, + removeExtraPinsWhenSettingSize, + widthHeightFromAxis, +} from '../inspector-common' import type { InspectorStrategy } from './inspector-strategy' import { queueTrueUpElement } from '../../canvas/commands/queue-true-up-command' import { @@ -48,6 +52,7 @@ export const fixedSizeBasicStrategy = ( return [ ...removeExtraPinsWhenSettingSize(axis, elementMetadata), + ...removeAlignJustifySelf(axis, elementMetadata), setCssLengthProperty( whenToRun, path, diff --git a/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts b/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts index 3e87820adcfb..a066a3494321 100644 --- a/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts +++ b/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts @@ -26,6 +26,7 @@ import { import { fillContainerStrategyFlexParent, fillContainerStrategyFlow, + fillContainerStrategyGridParent, } from './fill-container-basic-strategy' import { setSpacingModePacked, setSpacingModeSpaceBetween } from './spacing-mode-strategies' import { convertLayoutToFlexCommands } from '../../common/shared-strategies/convert-to-flex-strategy' @@ -226,6 +227,7 @@ export const setPropFillStrategies = ( value: 'default' | number, otherAxisSetToFill: boolean, ): Array => [ + fillContainerStrategyGridParent(metadata, elementPaths, axis), fillContainerStrategyFlexParent(metadata, elementPaths, axis, value), fillContainerStrategyFlow(metadata, elementPaths, axis, value, otherAxisSetToFill), ] diff --git a/editor/src/components/inspector/resize-to-fit-control.tsx b/editor/src/components/inspector/resize-to-fit-control.tsx index 7e5598f4aa9a..84f8d46a3e0c 100644 --- a/editor/src/components/inspector/resize-to-fit-control.tsx +++ b/editor/src/components/inspector/resize-to-fit-control.tsx @@ -40,6 +40,7 @@ function checkGroupSuitability( // Do not let a group be the target of a resize to fit operation. return !treatElementAsGroupLike(metadata, target) case 'fill': + case 'stretch': // Neither a group or the child of a group should be eligible for a resize to fill operation. return !( treatElementAsGroupLike(metadata, target) || treatElementAsGroupLike(metadata, parentPath)