From ca09c6118f85bf553409006b5dd14030f60b7e8a Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Wed, 11 Dec 2024 17:12:00 +0200 Subject: [PATCH] feat(editor): pass scene size to style plugins (#6715) This prep PR adds the ability to pass the Scene size to the Tailwind plugin/ This was extracted from the spike since it added small changes in many files. **Details:** - We need the Scene size in [tailwind-style-plugin.ts](https://github.com/concrete-utopia/utopia/pull/6715/files#diff-ad30d4bf205861b9efac9bb4bdb2bb68707cace215f64915244c46dbfab0607dR23-R25) to be able (in the next PR) to calculate the matching breakpoint. - We get the Scene size in `getContainingSceneWidth` in [responsive-utils.ts](https://github.com/concrete-utopia/utopia/pull/6715/files#diff-4f8de694cb6cb8852ff046766f5015a01f9567d4c3b7472bbac36358b52932aaR7-R16) and we pass it to the Tailwind plugin. - The rest of the changes are just passing down this data. This PR doesn't change any functionality, but is merely a prep PR for the next, functional one. **Manual Tests:** I hereby swear that: - [X] I opened a hydrogen project and it loaded - [X] I could navigate to various routes in Play mode --- .../canvas-strategies/canvas-strategies.tsx | 2 ++ .../canvas-strategy-types.ts | 6 ++-- .../commands/adjust-css-length-command.ts | 1 + .../canvas/commands/set-css-length-command.ts | 1 + .../select-mode/border-radius-control.tsx | 1 + .../controls/select-mode/flex-gap-control.tsx | 1 + .../select-mode/padding-resize-control.tsx | 1 + .../select-mode/subdued-flex-gap-controls.tsx | 1 + .../select-mode/subdued-padding-control.tsx | 1 + .../plugins/inline-style-plugin.spec.ts | 1 + .../canvas/plugins/style-plugins.ts | 16 ++++++++++ .../canvas/plugins/tailwind-style-plugin.ts | 32 ++++++++++++------- .../src/components/canvas/responsive-utils.ts | 29 +++++++++++++++++ .../inspector/common/property-path-hooks.ts | 6 ++-- .../src/core/model/element-metadata-utils.ts | 7 ++++ 15 files changed, 90 insertions(+), 16 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index ce32f1cacebc..1f6367b241eb 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -228,6 +228,7 @@ export function pickCanvasStateFromEditorState( propertyControlsInfo: editorState.propertyControlsInfo, styleInfoReader: activePlugin.styleInfoFactory({ projectContents: editorState.projectContents, + jsxMetadata: editorState.jsxMetadata, }), } } @@ -255,6 +256,7 @@ export function pickCanvasStateFromEditorStateWithMetadata( propertyControlsInfo: editorState.propertyControlsInfo, styleInfoReader: activePlugin.styleInfoFactory({ projectContents: editorState.projectContents, + jsxMetadata: editorState.jsxMetadata, }), } } diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts b/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts index 80d553a97a9a..e27c7bdd404b 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts @@ -112,9 +112,11 @@ export function controlWithProps

(value: ControlWithProps

): ControlWithProp export type StyleInfoReader = (elementPath: ElementPath) => StyleInfo | null -export type StyleInfoFactory = (context: { +export type StyleInfoContext = { projectContents: ProjectContentTreeRoot -}) => StyleInfoReader + jsxMetadata: ElementInstanceMetadataMap +} +export type StyleInfoFactory = (context: StyleInfoContext) => StyleInfoReader export interface InteractionCanvasState { interactionTarget: InteractionTarget diff --git a/editor/src/components/canvas/commands/adjust-css-length-command.ts b/editor/src/components/canvas/commands/adjust-css-length-command.ts index 128aeac077d6..df4b81ecd292 100644 --- a/editor/src/components/canvas/commands/adjust-css-length-command.ts +++ b/editor/src/components/canvas/commands/adjust-css-length-command.ts @@ -78,6 +78,7 @@ export const runAdjustCssLengthProperties = ( const styleInfoReader = getActivePlugin(withConflictingPropertiesRemoved).styleInfoFactory({ projectContents: withConflictingPropertiesRemoved.projectContents, + jsxMetadata: withConflictingPropertiesRemoved.jsxMetadata, }) const styleInfo = styleInfoReader(command.target) diff --git a/editor/src/components/canvas/commands/set-css-length-command.ts b/editor/src/components/canvas/commands/set-css-length-command.ts index f69f66c328df..cc2a0998f8a2 100644 --- a/editor/src/components/canvas/commands/set-css-length-command.ts +++ b/editor/src/components/canvas/commands/set-css-length-command.ts @@ -87,6 +87,7 @@ export const runSetCssLengthProperty = ( const styleInfo = getActivePlugin(editorStateWithPropsDeleted).styleInfoFactory({ projectContents: editorStateWithPropsDeleted.projectContents, + jsxMetadata: editorStateWithPropsDeleted.jsxMetadata, })(command.target) if (styleInfo == null) { diff --git a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx index eb0d98c4d493..7f01a640b7e8 100644 --- a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx @@ -63,6 +63,7 @@ const borderRadiusSelector = createCachedSelector( (store: StyleInfoSubstate) => getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, }), (_: MetadataSubstate, x: ElementPath) => x, (metadata, styleInfoReader, selectedElement) => { diff --git a/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx b/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx index 672e28d2f801..6f0121923126 100644 --- a/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx @@ -139,6 +139,7 @@ export const FlexGapControl = controlForStrategyMemoized((p maybeFlexGapData( getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, })(selectedElement), MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, selectedElement), ), diff --git a/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx b/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx index c9d8906e69ba..81827667dec2 100644 --- a/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx @@ -362,6 +362,7 @@ export const PaddingResizeControl = controlForStrategyMemoized((props: PaddingCo const styleInfoReaderRef = useRefEditorState((store) => getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, }), ) diff --git a/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx b/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx index 539d082cc253..4e0091951593 100644 --- a/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx +++ b/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx @@ -43,6 +43,7 @@ export const SubduedFlexGapControl = React.memo((pro maybeFlexGapData( getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, })(selectedElement), MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, selectedElement), ), diff --git a/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx b/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx index 766a180c9386..3fb67c29a496 100644 --- a/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx @@ -29,6 +29,7 @@ export const SubduedPaddingControl = React.memo((pro const styleInfoReaderRef = useRefEditorState((store) => getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, }), ) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts b/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts index 8ba0e9733b8c..e0e8029bce6f 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts @@ -110,6 +110,7 @@ function getStyleInfoFromInlineStyle(editor: EditorRenderResult) { const styleInfoReader = InlineStylePlugin.styleInfoFactory({ projectContents: projectContents, + jsxMetadata: jsxMetadata, }) const styleInfo = styleInfoReader(EP.fromString('sb/scene/div')) return styleInfo diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index 42ecae83b215..79ef09308456 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -51,12 +51,27 @@ export function deleteCSSProp(property: string): DeleteCSSProp { export type StyleUpdate = UpdateCSSProp | DeleteCSSProp +export type SceneSize = { type: 'no-scene' } | { type: 'scene'; width: number } +export function noSceneSize(): SceneSize { + return { type: 'no-scene' } +} +export function sceneSize(width: number | undefined | null): SceneSize { + if (width == null) { + return noSceneSize() + } + return { type: 'scene', width: width } +} +export type StylePluginContext = { + sceneSize: SceneSize +} + export interface StylePlugin { name: string styleInfoFactory: StyleInfoFactory readStyleFromElementProps: ( attributes: JSXAttributes, prop: T, + context: StylePluginContext, ) => CSSStyleProperty> | null updateStyles: ( editorState: EditorState, @@ -248,6 +263,7 @@ export function patchRemovedProperties(editorState: EditorState): EditorState { const styleInfoReader = activePlugin.styleInfoFactory({ projectContents: editorState.projectContents, + jsxMetadata: editorState.jsxMetadata, }) const propertiesUpdatedDuringInteraction = getPropertiesUpdatedDuringInteraction(editorState) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 2941f0bc9a12..e9fadf9ef689 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -5,7 +5,7 @@ import { getElementFromProjectContents } from '../../editor/store/editor-state' import type { ParsedCSSProperties } from '../../inspector/common/css-utils' import { cssParsers } from '../../inspector/common/css-utils' import { mapDropNulls } from '../../../core/shared/array-utils' -import type { StylePlugin } from './style-plugins' +import type { StylePlugin, StylePluginContext } from './style-plugins' import type { Config } from 'tailwindcss/types/config' import type { StyleInfo } from '../canvas-types' import { cssStyleProperty, type CSSStyleProperty } from '../canvas-types' @@ -18,17 +18,20 @@ import { import { emptyComments, type JSXAttributes } from 'utopia-shared/src/types' import * as PP from '../../../core/shared/property-path' import { jsExpressionValue } from '../../../core/shared/element-template' - -function parseTailwindProperty( - value: string | number | undefined, - prop: T, -): CSSStyleProperty> | null { - const parsed = cssParsers[prop](value, null) - if (isLeft(parsed) || parsed.value == null) { - return null +import { getContainingSceneSize } from '../responsive-utils' + +const parseTailwindPropertyFactory = + (config: Config | null, context: StylePluginContext) => + ( + value: string | number | undefined, + prop: T, + ): CSSStyleProperty> | null => { + const parsed = cssParsers[prop](value, null) + if (isLeft(parsed) || parsed.value == null) { + return null + } + return cssStyleProperty(parsed.value, jsExpressionValue(value, emptyComments)) } - return cssStyleProperty(parsed.value, jsExpressionValue(value, emptyComments)) -} const TailwindPropertyMapping: Record = { left: 'positionLeft', @@ -100,6 +103,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ readStyleFromElementProps:

( attributes: JSXAttributes, prop: P, + context: StylePluginContext, ): CSSStyleProperty> | null => { const classNameAttribute = defaultEither( null, @@ -114,10 +118,11 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ } const mapping = getTailwindClassMapping(classNameAttribute.split(' '), config) + const parseTailwindProperty = parseTailwindPropertyFactory(config, context) return parseTailwindProperty(mapping[TailwindPropertyMapping[prop]], prop) }, styleInfoFactory: - ({ projectContents }) => + ({ projectContents, jsxMetadata }) => (elementPath) => { const classList = getClassNameAttribute( getElementFromProjectContents(elementPath, projectContents), @@ -128,6 +133,9 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ } const mapping = getTailwindClassMapping(classList.split(' '), config) + const parseTailwindProperty = parseTailwindPropertyFactory(config, { + sceneSize: getContainingSceneSize(elementPath, jsxMetadata), + }) return { gap: parseTailwindProperty(mapping[TailwindPropertyMapping.gap], 'gap'), diff --git a/editor/src/components/canvas/responsive-utils.ts b/editor/src/components/canvas/responsive-utils.ts index 52349915b1b6..9d828e6399d5 100644 --- a/editor/src/components/canvas/responsive-utils.ts +++ b/editor/src/components/canvas/responsive-utils.ts @@ -3,6 +3,35 @@ import * as csstree from 'css-tree' import type { StyleMediaSizeModifier, StyleModifier } from './canvas-types' import { type CSSNumber, type CSSNumberUnit, cssNumber } from '../inspector/common/css-utils' import { memoize } from '../../core/shared/memoize' +import type { ElementPath } from '../../core/shared/project-file-types' +import type { ElementInstanceMetadataMap } from '../../core/shared/element-template' +import { MetadataUtils } from '../../core/model/element-metadata-utils' +import { noSceneSize, sceneSize, type SceneSize } from './plugins/style-plugins' +import type { EditorState } from '../editor/store/editor-state' + +export function getContainingSceneSize( + selectedElement: ElementPath, + jsxMetadata: ElementInstanceMetadataMap, +): SceneSize { + if (selectedElement == null) { + return noSceneSize() + } + const containingScene = MetadataUtils.getParentSceneMetadata(jsxMetadata, selectedElement) + return sceneSize(containingScene?.specialSizeMeasurements?.clientWidth) +} + +export function getContainingSceneSizeFromEditorState(editor: EditorState): SceneSize { + if (editor == null) { + return noSceneSize() + } + // we're taking the first selected element because we're assuming elements are in the same scene + // TODO: support multiple selected elements that are in different scenes? + const selectedElement = editor.selectedViews.at(0) + if (selectedElement == null) { + return noSceneSize() + } + return getContainingSceneSize(selectedElement, editor.jsxMetadata) +} /** * Extracts the screen size from a CSS string, for example: diff --git a/editor/src/components/inspector/common/property-path-hooks.ts b/editor/src/components/inspector/common/property-path-hooks.ts index 1c8c719c6911..c0ef77d62597 100644 --- a/editor/src/components/inspector/common/property-path-hooks.ts +++ b/editor/src/components/inspector/common/property-path-hooks.ts @@ -1,5 +1,4 @@ import * as PP from '../../../core/shared/property-path' -import * as EP from '../../../core/shared/element-path' import deepEqual from 'fast-deep-equal' import * as ObjectPath from 'object-path' @@ -96,6 +95,7 @@ import { getActivePlugin } from '../../canvas/plugins/style-plugins' import { isStyleInfoKey, type StyleInfo } from '../../canvas/canvas-types' import { assertNever } from '../../../core/shared/utils' import { maybeCssPropertyFromInlineStyle } from '../../canvas/commands/utils/property-utils' +import { getContainingSceneSizeFromEditorState } from '../../canvas/responsive-utils' export interface InspectorPropsContextData { selectedViews: Array @@ -763,7 +763,9 @@ export function useGetMultiselectedProps

( const styleInfoReaderRef = useRefEditorState( (store) => (props: JSXAttributes, prop: keyof StyleInfo): GetModifiableAttributeResult => { - const elementStyle = getActivePlugin(store.editor).readStyleFromElementProps(props, prop) + const elementStyle = getActivePlugin(store.editor).readStyleFromElementProps(props, prop, { + sceneSize: getContainingSceneSizeFromEditorState(store.editor), + }) if (elementStyle == null) { return right({ type: 'ATTRIBUTE_NOT_FOUND' }) } diff --git a/editor/src/core/model/element-metadata-utils.ts b/editor/src/core/model/element-metadata-utils.ts index 7c3661bbcc77..e9892d1e1c96 100644 --- a/editor/src/core/model/element-metadata-utils.ts +++ b/editor/src/core/model/element-metadata-utils.ts @@ -1061,6 +1061,13 @@ export const MetadataUtils = { const elementMetadata = MetadataUtils.findElementByElementPath(metadata, path) return elementMetadata != null && isSceneFromMetadata(elementMetadata) }, + getParentSceneMetadata( + metadata: ElementInstanceMetadataMap, + path: ElementPath, + ): ElementInstanceMetadata | null { + const parentPath = MetadataUtils.findSceneOfTarget(path, metadata) + return parentPath == null ? null : MetadataUtils.findElementByElementPath(metadata, parentPath) + }, overflows(allElementProps: AllElementProps, path: ElementPath): boolean { const elementProps = allElementProps[EP.toString(path)] ?? {} const styleProps = elementProps.style ?? null