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 all 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
1 change: 1 addition & 0 deletions editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
"@shopify/hydrogen": "2024.4.1",
"@stitches/react": "1.2.8",
"@svgr/plugin-jsx": "5.5.0",
"@testing-library/user-event": "14.5.2",
"@tippyjs/react": "4.1.0",
"@twind/core": "1.1.3",
"@twind/preset-autoprefix": "1.0.7",
Expand Down
9 changes: 9 additions & 0 deletions editor/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
} from '../canvas-strategies/canvas-strategies'
import CanvasActions from '../canvas-actions'
import { getNavigatorTargetsFromEditorState } from '../../navigator/navigator-utils'
import { addFlexLayout } from '../../inspector/layout-systems.test-utils'

const DefaultRouteTextContent = 'Hello Remix!'
const RootTextContent = 'This is root!'
Expand Down Expand Up @@ -1756,8 +1757,7 @@ export default function Index() {

const absoluteDiv = await clickElementOnCanvasControlsLayer(renderResult, AbsoluteDivTestId)

const targetElement = renderResult.renderedDOM.getByTestId(AddRemoveLayoutSystemControlTestId())
await mouseClickAtPoint(targetElement, { x: 1, y: 1 }, { modifiers: cmdModifier })
await addFlexLayout(renderResult)

expect(absoluteDiv.style.display).toEqual('flex')
})
Expand Down
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 never guess row-reverse or column-reverse
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,118 @@
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,
nukeAllAbsolutePositioningPropsCommands,
prunePropsCommands,
sizeToVisualDimensions,
} 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),
...childrenPaths.flatMap((child) => [
...nukeAllAbsolutePositioningPropsCommands(child),
...sizeToVisualDimensions(metadata, elementPathTree, child),
]),
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(' '),
),
]
})
}
45 changes: 37 additions & 8 deletions editor/src/components/editor/global-shortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ import {
PASTE_TO_REPLACE,
WRAP_IN_DIV,
COMMENT_SHORTCUT,
CONVERT_TO_GRID_CONTAINER,
} from './shortcut-definitions'
import type { EditorState, LockedElements, NavigatorEntry, UserState } from './store/editor-state'
import type { EditorState, LockedElements, NavigatorEntry } from './store/editor-state'
import { getOpenFile, RightMenuTab } from './store/editor-state'
import { CanvasMousePositionRaw, WindowMousePositionRaw } from '../../utils/global-positions'
import { pickColorWithEyeDropper } from '../canvas/canvas-utils'
Expand All @@ -114,11 +115,7 @@ import {
createHoverInteractionViaMouse,
} from '../canvas/canvas-strategies/interaction-state'
import type { ElementInstanceMetadataMap } from '../../core/shared/element-template'
import {
emptyComments,
jsExpressionValue,
isJSXElementLike,
} from '../../core/shared/element-template'
import { emptyComments, jsExpressionValue } from '../../core/shared/element-template'
import {
toggleTextBold,
toggleTextItalic,
Expand All @@ -128,25 +125,26 @@ import {
import { commandsForFirstApplicableStrategy } from '../inspector/inspector-strategies/inspector-strategy'
import {
addFlexLayoutStrategies,
addGridLayoutStrategies,
removeFlexLayoutStrategies,
removeGridLayoutStrategies,
} from '../inspector/inspector-strategies/inspector-strategies'
import {
detectAreElementsFlexContainers,
toggleResizeToFitSetToFixed,
toggleAbsolutePositioningCommands,
detectAreElementsGridContainers,
} from '../inspector/inspector-common'
import { zeroCanvasPoint } from '../../core/shared/math-utils'
import * as EP from '../../core/shared/element-path'
import { createWrapInGroupActions } from '../canvas/canvas-strategies/strategies/group-conversion-helpers'
import { isRight } from '../../core/shared/either'
import type { ElementPathTrees } from '../../core/shared/element-path-tree'
import { createPasteToReplacePostActionActions } from '../canvas/canvas-strategies/post-action-options/post-action-options'
import { wrapInDivStrategy } from './wrap-in-callbacks'
import { type ProjectServerState } from './store/project-server-state'
import { allowedToEditProject } from './store/collaborative-editing'
import { hasCommentPermission } from './store/permissions'
import { type ShowComponentPickerContextMenuCallback } from '../navigator/navigator-item/component-picker-context-menu'
import { showReplaceComponentPicker } from '../context-menu-items'

function updateKeysPressed(
keysPressed: KeysPressed,
Expand Down Expand Up @@ -916,6 +914,37 @@ export function handleKeyDown(
}
return [EditorActions.applyCommandsAction(commands)]
},
[CONVERT_TO_GRID_CONTAINER]: () => {
if (!isSelectMode(editor.mode)) {
return []
}
const elementsConsideredForGridConversion = editor.selectedViews.filter(
(elementPath) =>
MetadataUtils.getJSXElementFromMetadata(editor.jsxMetadata, elementPath) != null,
)
const selectedElementsGridContainers = detectAreElementsGridContainers(
editor.jsxMetadata,
elementsConsideredForGridConversion,
)
const commands = commandsForFirstApplicableStrategy(
selectedElementsGridContainers
? removeGridLayoutStrategies(
editor.jsxMetadata,
elementsConsideredForGridConversion,
editor.elementPathTree,
)
: addGridLayoutStrategies(
editor.jsxMetadata,
elementsConsideredForGridConversion,
editor.elementPathTree,
editor.allElementProps,
),
)
if (commands == null) {
return []
}
return [EditorActions.applyCommandsAction(commands)]
},
[REMOVE_ABSOLUTE_POSITIONING]: () => {
if (!isSelectMode(editor.mode)) {
return []
Expand Down
5 changes: 5 additions & 0 deletions editor/src/components/editor/shortcut-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const REMOVE_ABSOLUTE_POSITIONING = 'remove-absolute-positioning'
export const RESIZE_TO_FIT = 'resize-to-fit'
export const OPEN_INSERT_MENU = 'open-insert-menu'
export const WRAP_IN_DIV = 'wrap-in-div'
export const CONVERT_TO_GRID_CONTAINER = 'convert-to-grid-container'

export type ShortcutDetails = { [key: string]: Shortcut }

Expand Down Expand Up @@ -232,6 +233,10 @@ export const shortcutDetailsWithDefaults: ShortcutDetails = {
'Convert selected elements to flex containers',
key('a', ['shift']),
),
[CONVERT_TO_GRID_CONTAINER]: shortcut(
'Convert selected elements to grid containers',
key('a', ['shift', 'alt']),
),
[REMOVE_ABSOLUTE_POSITIONING]: shortcut(`Strip absolute sizing props props`, key('x', [])),
[COPY_STYLE_PROPERTIES]: shortcut('Copy style properties', key('c', ['alt', 'cmd'])),
[PASTE_STYLE_PROPERTIES]: shortcut('Paste style properties', key('v', ['alt', 'cmd'])),
Expand Down
Loading
Loading