Skip to content

Commit

Permalink
Insert better default grids (#6512)
Browse files Browse the repository at this point in the history
**Problem:**

1. When creating a grid layout from the inspector the default grid
should be 2x2 with a 10px gap
2. There should be an insert menu entry in the canvas toolbar for a 3x3
grid with a 10px gap
3. There should be an entry for a grid in the floating component picker
too

**Fix:**

Implement all of the above:



https://github.com/user-attachments/assets/6214c47d-ae77-4735-b9ec-ab00198e4b90

Note: the icon for the grid is the one used for the navigator, there
should be a dedicated one with the right size (16x16) for the canvas
toolbar as well.

Fixes #6511
  • Loading branch information
ruggi authored and liady committed Dec 13, 2024
1 parent 6791ec7 commit 8741dfe
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ function guessLayoutInfoAlongAxis(
}
}

function guessMatchingGridSetup(children: Array<CanvasFrameAndTarget>): {
function guessMatchingGridSetup(
children: Array<CanvasFrameAndTarget>,
isFlexContainer: boolean,
): {
gap: number
numberOfColumns: number
numberOfRows: number
Expand All @@ -65,10 +68,12 @@ function guessMatchingGridSetup(children: Array<CanvasFrameAndTarget>): {
(a, b) => b.frame.y - (a.frame.y + a.frame.height),
)

const minRowsOrCols = isFlexContainer ? 1 : 2

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

Expand All @@ -90,9 +95,16 @@ export function convertLayoutToGridCommands(
frame: MetadataUtils.getFrameOrZeroRectInCanvasCoords(child, metadata),
}))

const { gap, numberOfColumns, numberOfRows } = guessMatchingGridSetup(childFrames)
const isFlexContainer = MetadataUtils.isFlexLayoutedContainer(
MetadataUtils.findElementByElementPath(metadata, elementPath),
)

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

return [
let commands = [
...prunePropsCommands(flexContainerProps, elementPath),
...prunePropsCommands(gridContainerProps, elementPath),
...childrenPaths.flatMap((child) => [
Expand All @@ -114,5 +126,11 @@ export function convertLayoutToGridCommands(
Array(numberOfRows).fill('1fr').join(' '),
),
]

if (!isFlexContainer) {
commands.push(setProperty('always', elementPath, PP.create('style', 'gap'), 10))
}

return commands
})
}
27 changes: 27 additions & 0 deletions editor/src/components/editor/canvas-toolbar-states.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import type { Optic } from '../../core/shared/optics/optics'
import { fromField, fromTypeGuard } from '../../core/shared/optics/optic-creators'
import { anyBy, set } from '../../core/shared/optics/optic-utilities'
import type { EditorAction } from './action-types'
import { MetadataUtils } from '../../core/model/element-metadata-utils'
import { getJSXAttributesAtPath } from '../../core/shared/jsx-attribute-utils'
import { create } from '../../core/shared/property-path'

// This is the data structure that governs the Canvas Toolbar's submenus and active buttons
type ToolbarMode =
Expand All @@ -21,6 +24,7 @@ type ToolbarMode =
imageInsertionActive: boolean
buttonInsertionActive: boolean
conditionalInsertionActive: boolean
gridInsertionActive: boolean
insertSidebarOpen: boolean
}
}
Expand Down Expand Up @@ -110,6 +114,28 @@ export function useToolbarMode(): ToolbarMode {
const insertionTargetConditional =
editorMode.type === 'insert' &&
editorMode.subjects.some((subject) => subject.insertionSubjectWrapper === 'conditional')
const insertionTargetGrid =
editorMode.type === 'insert' &&
editorMode.subjects.some((subject) => {
if (subject.element.name.baseVariable !== 'div') {
return false
}

const style = subject.element.props.find(
(p) => p.type === 'JSX_ATTRIBUTES_ENTRY' && p.key === 'style',
)
if (style == null) {
return false
}

const display = getJSXAttributesAtPath(subject.element.props, create('style', 'display'))
return (
style.type === 'JSX_ATTRIBUTES_ENTRY' &&
style.value.type === 'ATTRIBUTE_VALUE' &&
display.attribute.type === 'PART_OF_ATTRIBUTE_VALUE' &&
display.attribute.value === 'grid'
)
})

return {
primary: 'insert',
Expand All @@ -119,6 +145,7 @@ export function useToolbarMode(): ToolbarMode {
imageInsertionActive: insertionTargetImage,
buttonInsertionActive: insertionTargetButton,
conditionalInsertionActive: insertionTargetConditional,
gridInsertionActive: insertionTargetGrid,
insertSidebarOpen: rightMenuTab === RightMenuTab.Insert,
},
}
Expand Down
12 changes: 12 additions & 0 deletions editor/src/components/editor/canvas-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
useEnterDrawToInsertForButton,
useEnterDrawToInsertForConditional,
useEnterDrawToInsertForDiv,
useEnterDrawToInsertForGrid,
useEnterDrawToInsertForImage,
useEnterTextEditMode,
} from './insert-callbacks'
Expand Down Expand Up @@ -79,6 +80,7 @@ export const InsertOrEditTextButtonTestId = 'insert-or-edit-text-button'
export const PlayModeButtonTestId = 'canvas-toolbar-play-mode'
export const CommentModeButtonTestId = (status: string) => `canvas-toolbar-comment-mode-${status}`
export const InsertConditionalButtonTestId = 'insert-mode-conditional'
export const InsertGridButtonTestId = 'insert-mode-grid'
export const CanvasToolbarId = 'canvas-toolbar'

export const CanvasToolbarSearchPortalId = 'canvas-toolbar-search-portal'
Expand Down Expand Up @@ -220,6 +222,7 @@ export const CanvasToolbar = React.memo(() => {
const insertTextCallback = useEnterTextEditMode()
const insertButtonCallback = useEnterDrawToInsertForButton()
const insertConditionalCallback = useEnterDrawToInsertForConditional()
const insertGridCallback = useEnterDrawToInsertForGrid()

// Back to select mode, close the "floating" menu and turn off the forced insert mode.
const dispatchSwitchToSelectModeCloseMenus = React.useCallback(() => {
Expand Down Expand Up @@ -548,6 +551,15 @@ export const CanvasToolbar = React.memo(() => {
onClick={insertDivCallback}
/>
</Tooltip>
<Tooltip title='Insert grid' placement='bottom'>
<ToolbarButton
testid={InsertGridButtonTestId}
iconCategory='navigator-element'
iconType='grid'
onClick={insertGridCallback}
size={12}
/>
</Tooltip>
<Tooltip title='Insert image' placement='bottom'>
<ToolbarButton
iconType='image'
Expand Down
14 changes: 13 additions & 1 deletion editor/src/components/editor/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
simpleAttribute,
} from '../../core/shared/element-template'
import type { NormalisedFrame } from 'utopia-api/core'
import { defaultImageAttributes } from '../shared/project-components'
import { defaultImageAttributes, insertableGridStyle } from '../shared/project-components'

export function defaultSceneElement(
uid: string,
Expand Down Expand Up @@ -171,6 +171,18 @@ export function defaultButtonElement(uid: string): JSXElement {
)
}

export function defaultGridElement(uid: string): JSXElement {
return jsxElement(
jsxElementName('div', []),
uid,
jsxAttributesFromMap({
'data-uid': jsExpressionValue(uid, emptyComments),
style: jsExpressionValue(insertableGridStyle(), emptyComments),
}),
[],
)
}

export function defaultFlexRowOrColStyle(): JSExpression {
return jsExpressionValue(
{
Expand Down
5 changes: 5 additions & 0 deletions editor/src/components/editor/insert-callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { enableInsertModeForJSXElement, showToast } from './actions/action-creat
import {
defaultButtonElement,
defaultDivElement,
defaultGridElement,
defaultImgElement,
defaultSpanElement,
} from './defaults'
Expand Down Expand Up @@ -106,6 +107,10 @@ export function useEnterDrawToInsertForButton(): (event: React.MouseEvent<Elemen
return useEnterDrawToInsertForElement(defaultButtonElement)
}

export function useEnterDrawToInsertForGrid(): (event: React.MouseEvent<Element>) => void {
return useEnterDrawToInsertForElement(defaultGridElement)
}

export function useEnterDrawToInsertForConditional(): (event: React.MouseEvent<Element>) => void {
const conditionalInsertCallback = useEnterDrawToInsertForElement(defaultDivElement)

Expand Down
2 changes: 1 addition & 1 deletion editor/src/components/editor/insertmenu.spec.browser2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function getInsertItems() {
return screen.queryAllByTestId(/^component-picker-item-/gi)
}

const allInsertItemsCount = 23
const allInsertItemsCount = 24

function openInsertMenu(renderResult: EditorRenderResult) {
return renderResult.dispatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,29 @@ Array [
"type": "SAMPLES_GROUP",
},
},
Object {
"insertableComponents": Array [
Object {
"defaultSize": Object {
"height": 150,
"width": 150,
},
"element": [Function],
"icon": "component",
"importsToAdd": Object {},
"insertionCeiling": Object {
"type": "file-root",
},
"name": "Grid",
"stylePropOptions": Array [
"do-not-add",
],
},
],
"source": Object {
"type": "HTML_GRID",
},
},
Object {
"insertableComponents": Array [],
"source": Object {
Expand Down
60 changes: 60 additions & 0 deletions editor/src/components/shared/project-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { intrinsicHTMLElementNamesThatSupportChildren } from '../../core/shared/
import { getTopLevelElementByExportsDetail } from '../../core/model/project-file-utils'
import { type Icon } from 'utopia-api'
import type { FileRootPath } from '../canvas/ui-jsx-canvas'
import type { CSSProperties } from 'react'

export type StylePropOption = 'do-not-add' | 'add-size'

Expand Down Expand Up @@ -126,6 +127,14 @@ export function insertableComponentGroupDiv(): InsertableComponentGroupDiv {
return { type: 'HTML_DIV' }
}

export interface InsertableComponentGroupGrid {
type: 'HTML_GRID'
}

export function insertableComponentGroupGrid(): InsertableComponentGroupGrid {
return { type: 'HTML_GRID' }
}

export function insertableComponentGroupHTML(): InsertableComponentGroupHTML {
return {
type: 'HTML_GROUP',
Expand Down Expand Up @@ -207,6 +216,7 @@ export type InsertableComponentGroupType =
| InsertableComponentGroupSamples
| InsertableComponentGroupGroups
| InsertableComponentGroupMap
| InsertableComponentGroupGrid

export interface InsertableComponentGroup {
source: InsertableComponentGroupType
Expand Down Expand Up @@ -254,6 +264,8 @@ export function getInsertableGroupLabel(insertableType: InsertableComponentGroup
return 'Div'
case 'MAP_GROUP':
return 'List'
case 'HTML_GRID':
return 'Grid'
default:
assertNever(insertableType)
}
Expand All @@ -271,6 +283,7 @@ export function getInsertableGroupPackageStatus(
case 'GROUPS_GROUP':
case 'HTML_DIV':
case 'MAP_GROUP':
case 'HTML_GRID':
return 'loaded'
case 'PROJECT_DEPENDENCY_GROUP':
return insertableType.dependencyStatus
Expand Down Expand Up @@ -462,6 +475,47 @@ const divComponentGroup = {
),
}

export function insertableGridStyle(): CSSProperties {
return {
position: 'absolute',
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
gridTemplateRows: '1fr 1fr 1fr',
gap: 10,
}
}

const gridComponentGroup: ComponentDescriptorsForFile = {
grid: {
properties: {},
supportsChildren: true,
preferredChildComponents: [],
source: defaultComponentDescriptor(),
variants: [
{
insertMenuLabel: 'Grid',
elementToInsert: () =>
jsxElementWithoutUID(
'div',
jsxAttributesFromMap({
style: jsExpressionValue(
{
...insertableGridStyle(),
width: 150,
height: 150,
},
emptyComments,
),
}),
[],
),
importsToAdd: {},
},
],
...ComponentDescriptorDefaults,
},
}

const conditionalElementsDescriptors: ComponentDescriptorsForFile = {
conditional: {
properties: {},
Expand Down Expand Up @@ -832,6 +886,11 @@ export function getComponentGroups(
// Add groups group.
addDependencyDescriptor(insertableComponentGroupGroups(), groupElementsDescriptors) // TODO instead of this, use createWrapInGroupActions!

addDependencyDescriptor(insertableComponentGroupGrid(), gridComponentGroup, {
width: 150,
height: 150,
})

// Add entries for dependencies of the project.
for (const dependency of dependencies) {
if (isResolvedNpmDependency(dependency)) {
Expand Down Expand Up @@ -909,6 +968,7 @@ export function insertMenuModesForInsertableComponentGroupType(
case 'SAMPLES_GROUP':
case 'HTML_DIV':
case 'MAP_GROUP':
case 'HTML_GRID':
return insertMenuModes.all
case 'GROUPS_GROUP':
return insertMenuModes.onlyWrap
Expand Down

0 comments on commit 8741dfe

Please sign in to comment.