Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert to grid #6097

Merged
merged 19 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ElementPath } from 'utopia-shared/src/types'
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import type { ElementPathTrees } from '../../../core/shared/element-path-tree'
import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template'
import {
isElementNonDOMElement,
replaceNonDOMElementPathsWithTheirChildrenRecursive,
} from '../../canvas/canvas-strategies/strategies/fragment-like-helpers'
import type { AllElementProps } from '../../editor/store/editor-state'

export type FlexDirectionRowColumn = 'row' | 'column' // a limited subset as we won't never guess row-reverse or column-reverse
bkrmendy marked this conversation as resolved.
Show resolved Hide resolved
export type FlexAlignItems = 'center' | 'flex-end'

export function getChildrenPathsForContainer(
metadata: ElementInstanceMetadataMap,
elementPathTree: ElementPathTrees,
path: ElementPath,
allElementProps: AllElementProps,
) {
return MetadataUtils.getChildrenPathsOrdered(metadata, elementPathTree, path).flatMap((child) =>
isElementNonDOMElement(metadata, allElementProps, elementPathTree, child)
? replaceNonDOMElementPathsWithTheirChildrenRecursive(
metadata,
allElementProps,
elementPathTree,
[child],
)
: child,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ import {
sizeToVisualDimensions,
} from '../../inspector/inspector-common'
import { setHugContentForAxis } from '../../inspector/inspector-strategies/hug-contents-strategy'

type FlexDirectionRowColumn = 'row' | 'column' // a limited subset as we won't never guess row-reverse or column-reverse
type FlexAlignItems = 'center' | 'flex-end'
import type { FlexAlignItems, FlexDirectionRowColumn } from './convert-strategies-common'
import { getChildrenPathsForContainer } from './convert-strategies-common'

function checkConstraintsForThreeElementRow(
allElementProps: AllElementProps,
Expand Down Expand Up @@ -268,19 +267,11 @@ export function convertLayoutToFlexCommands(
]
}

const childrenPaths = MetadataUtils.getChildrenPathsOrdered(
const childrenPaths = getChildrenPathsForContainer(
metadata,
elementPathTree,
path,
).flatMap((child) =>
isElementNonDOMElement(metadata, allElementProps, elementPathTree, child)
? replaceNonDOMElementPathsWithTheirChildrenRecursive(
metadata,
allElementProps,
elementPathTree,
[child],
)
: child,
allElementProps,
)

const parentFlexDirection =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { ElementPath } from 'utopia-shared/src/types'
import type { ElementPathTrees } from '../../../core/shared/element-path-tree'
import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template'
import type { AllElementProps } from '../../editor/store/editor-state'
import type { CanvasCommand } from '../../canvas/commands/commands'
import {
flexContainerProps,
gridContainerProps,
prunePropsCommands,
} from '../../inspector/inspector-common'
import { getChildrenPathsForContainer } from './convert-strategies-common'
import type { CanvasFrameAndTarget } from '../../canvas/canvas-types'
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import { setProperty } from '../../canvas/commands/set-property-command'
import * as PP from '../../../core/shared/property-path'

function guessLayoutInfoAlongAxis(
children: Array<CanvasFrameAndTarget>,
sortFn: (a: CanvasFrameAndTarget, b: CanvasFrameAndTarget) => number,
comesAfter: (a: CanvasFrameAndTarget, b: CanvasFrameAndTarget) => boolean,
gapBetween: (a: CanvasFrameAndTarget, b: CanvasFrameAndTarget) => number,
): { nChildren: number; averageGap: number } {
if (children.length === 0) {
return { nChildren: 0, averageGap: 0 }
}

const sortedChildren = children.sort(sortFn)
let childrenAlongAxis = 1
let gaps: number[] = []
let currentChild = sortedChildren[0]
for (const child of sortedChildren.slice(1)) {
if (comesAfter(currentChild, child)) {
childrenAlongAxis += 1
gaps.push(gapBetween(currentChild, child))
currentChild = child
}
}

const averageGap =
gaps.length === 0 ? 0 : Math.floor(gaps.reduce((a, b) => a + b, 0) / gaps.length)

return {
nChildren: childrenAlongAxis,
averageGap: averageGap,
}
}

function guessMatchingGridSetup(children: Array<CanvasFrameAndTarget>): {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should also have a logic branch so that it makes sure that the combined number of resulting cells is >= than the number of children. For example if you have N children resulting from duplication, all having the exact same coordinates, the resulting grid template would be incorrect (e.g. 1x1 instead of 1xN)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed on a side channel, I'll implement this on a separate PR because that way we'll have a holistic overview of the convert-to-grid strategy

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keeping this unresolved so we can find it quickly later

gap: number
numberOfColumns: number
numberOfRows: number
} {
const horizontalData = guessLayoutInfoAlongAxis(
children,
(a, b) => a.frame.x - b.frame.x,
bkrmendy marked this conversation as resolved.
Show resolved Hide resolved
(a, b) => a.frame.x + a.frame.width <= b.frame.x,
(a, b) => b.frame.x - (a.frame.x + a.frame.width),
)
const verticalData = guessLayoutInfoAlongAxis(
children,
(a, b) => a.frame.y - b.frame.y,
(a, b) => a.frame.y + a.frame.height <= b.frame.y,
(a, b) => b.frame.y - (a.frame.y + a.frame.height),
)

return {
gap: (horizontalData.averageGap + verticalData.averageGap) / 2,
numberOfColumns: horizontalData.nChildren,
numberOfRows: verticalData.nChildren,
}
}

export function convertLayoutToGridCommands(
metadata: ElementInstanceMetadataMap,
elementPathTree: ElementPathTrees,
elementPaths: Array<ElementPath>,
allElementProps: AllElementProps,
): Array<CanvasCommand> {
return elementPaths.flatMap((elementPath) => {
const childrenPaths = getChildrenPathsForContainer(
metadata,
elementPathTree,
elementPath,
allElementProps,
)
const childFrames: Array<CanvasFrameAndTarget> = childrenPaths.map((child) => ({
target: child,
frame: MetadataUtils.getFrameOrZeroRectInCanvasCoords(child, metadata),
}))

const { gap, numberOfColumns, numberOfRows } = guessMatchingGridSetup(childFrames)

return [
ruggi marked this conversation as resolved.
Show resolved Hide resolved
...prunePropsCommands(flexContainerProps, elementPath),
...prunePropsCommands(gridContainerProps, elementPath),
setProperty('always', elementPath, PP.create('style', 'display'), 'grid'),
setProperty('always', elementPath, PP.create('style', 'gap'), gap),
setProperty(
'always',
elementPath,
PP.create('style', 'gridTemplateColumns'),
Array(numberOfColumns).fill('1fr').join(' '),
),
setProperty(
'always',
elementPath,
PP.create('style', 'gridTemplateRows'),
Array(numberOfRows).fill('1fr').join(' '),
),
]
})
}
150 changes: 120 additions & 30 deletions editor/src/components/inspector/flex-section.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,154 @@
import React from 'react'
import { createSelector } from 'reselect'
import { when } from '../../utils/react-conditionals'
import { Substores, useEditorState } from '../editor/store/store-hook'
import { Substores, useEditorState, useRefEditorState } from '../editor/store/store-hook'
import { AddRemoveLayoutSystemControl } from './add-remove-layout-system-control'
import { FlexDirectionToggle } from './flex-direction-control'
import { selectedViewsSelector, metadataSelector } from './inpector-selectors'
import { detectAreElementsFlexContainers } from './inspector-common'
import { NineBlockControl } from './nine-block-controls'
import { UIGridRow } from './widgets/ui-grid-row'
import {
DisabledFlexGroupPicker,
PaddingRow,
} from '../../components/inspector/sections/layout-section/layout-system-subsection/layout-system-controls'
import { LayoutSystemControl } from '../../components/inspector/sections/layout-section/layout-system-subsection/layout-system-controls'
import { SpacedPackedControl } from './spaced-packed-control'
import { ThreeBarControl } from './three-bar-control'
import { FlexGapControl } from './sections/layout-section/flex-container-subsection/flex-container-controls'
import { FlexContainerControls } from './sections/layout-section/flex-container-subsection/flex-container-subsection'
import { FlexCol } from 'utopia-api'
import { MetadataUtils } from '../../core/model/element-metadata-utils'
import { strictEvery } from '../../core/shared/array-utils'
import { useDispatch } from '../editor/store/dispatch-context'
import {
addFlexLayoutStrategies,
addGridLayoutStrategies,
} from './inspector-strategies/inspector-strategies'
import { executeFirstApplicableStrategy } from './inspector-strategies/inspector-strategy'
import type { DetectedLayoutSystem } from 'utopia-shared/src/types'
import { assertNever } from '../../core/shared/utils'
import { Subdued } from '../../uuiui'

export const areElementsFlexContainersSelector = createSelector(
export const layoutSystemSelector = createSelector(
metadataSelector,
selectedViewsSelector,
detectAreElementsFlexContainers,
(metadata, selectedViews) => {
const detectedLayoutSystems = selectedViews.map(
(path) =>
MetadataUtils.findElementByElementPath(metadata, path)?.specialSizeMeasurements
.layoutSystemForChildren ?? null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this return 'none' rather than null? I'm genuinely unsure

Copy link
Contributor Author

@bkrmendy bkrmendy Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completely fair question, I added the ?? null to remove the | undefined from the type of specialSizeMeasurements.layoutSystemForChildren to make it a less noisy type. I didn't add 'none' to make the type of detectedLayoutSystems similar to what we'd get if we checked specialSizeMeasurements.layoutSystemForChildren for a single element. The default value might as well be 'none' (it wouldn't matter for the downstream checks), but I didn't want to get creative and wanted to stick to the "spirit" of specialSizeMeasurements.layoutSystemForChildren

)

const allLayoutSystemsTheSame = strictEvery(
detectedLayoutSystems,
(e) => e === detectedLayoutSystems[0],
)

if (allLayoutSystemsTheSame) {
return detectedLayoutSystems[0]
}

return null
},
)

export const FlexSection = React.memo(() => {
const allElementsInFlexLayout = useEditorState(
const layoutSystem = useEditorState(
Substores.metadata,
areElementsFlexContainersSelector,
layoutSystemSelector,
'FlexSection areAllElementsInFlexLayout',
)

const dispatch = useDispatch()
const elementMetadataRef = useRefEditorState(metadataSelector)
const selectedViewsRef = useRefEditorState(selectedViewsSelector)
const elementPathTreeRef = useRefEditorState((store) => store.editor.elementPathTree)
const allElementPropsRef = useRefEditorState((store) => store.editor.allElementProps)

const addFlexLayoutSystem = React.useCallback(
() =>
executeFirstApplicableStrategy(
dispatch,
addFlexLayoutStrategies(
elementMetadataRef.current,
selectedViewsRef.current,
elementPathTreeRef.current,
allElementPropsRef.current,
),
),
[allElementPropsRef, dispatch, elementMetadataRef, elementPathTreeRef, selectedViewsRef],
)

const addGridLayoutSystem = React.useCallback(
() =>
executeFirstApplicableStrategy(
dispatch,
addGridLayoutStrategies(
elementMetadataRef.current,
selectedViewsRef.current,
elementPathTreeRef.current,
allElementPropsRef.current,
),
),
[allElementPropsRef, dispatch, elementMetadataRef, elementPathTreeRef, selectedViewsRef],
)

const onLayoutSystemChange = React.useCallback(
(value: DetectedLayoutSystem) => {
switch (value) {
case 'flex':
return addFlexLayoutSystem()
case 'grid':
return addGridLayoutSystem()
case 'flow':
case 'none':
return
default:
assertNever(value)
}
},
[addFlexLayoutSystem, addGridLayoutSystem],
)

return (
<div>
<AddRemoveLayoutSystemControl />
{when(
allElementsInFlexLayout,
<FlexCol css={{ gap: 10 }}>
<FlexCol css={{ gap: 10 }}>
{when(
layoutSystem != null,
<UIGridRow padded={true} variant='<-------------1fr------------->'>
<DisabledFlexGroupPicker
layoutSystem={null}
<LayoutSystemControl
layoutSystem={layoutSystem}
providesCoordinateSystemForChildren={false}
onChange={onLayoutSystemChange}
/>
</UIGridRow>
<UIGridRow padded variant='<--1fr--><--1fr-->'>
</UIGridRow>,
)}
{when(
layoutSystem === 'grid',
<UIGridRow padded tall={false} variant={'<-------------1fr------------->'}>
<div>
<Subdued>Grid inspector coming soon...</Subdued>
</div>
</UIGridRow>,
)}
{when(
layoutSystem === 'flex',
<>
<UIGridRow padded variant='<--1fr--><--1fr-->'>
<UIGridRow padded={false} variant='<-------------1fr------------->'>
<NineBlockControl />
<ThreeBarControl />
</UIGridRow>
<FlexCol css={{ gap: 10 }}>
<FlexDirectionToggle />
<FlexContainerControls seeMoreVisible={true} />
<FlexGapControl />
</FlexCol>
</UIGridRow>
<UIGridRow padded={false} variant='<-------------1fr------------->'>
<NineBlockControl />
<ThreeBarControl />
<SpacedPackedControl />
</UIGridRow>
<FlexCol css={{ gap: 10 }}>
<FlexDirectionToggle />
<FlexContainerControls seeMoreVisible={true} />
<FlexGapControl />
</FlexCol>
</UIGridRow>
<UIGridRow padded={false} variant='<-------------1fr------------->'>
<SpacedPackedControl />
</UIGridRow>
</FlexCol>,
)}
</>,
)}
</FlexCol>
,
</div>
)
})
9 changes: 9 additions & 0 deletions editor/src/components/inspector/inspector-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,15 @@ export const flexContainerProps = [
styleP('justifyContent'),
]

export const gridContainerProps = [
styleP('gap'),
styleP('display'),
styleP('gridTemplateRows'),
styleP('gridTemplateColumns'),
styleP('gridAutoColumns'),
styleP('gridAutoRows'),
]

export const flexChildProps = [
styleP('flex'),
styleP('flexGrow'),
Expand Down
Loading
Loading