Skip to content

Commit

Permalink
Feat/grid container subsection (#6118)
Browse files Browse the repository at this point in the history
**Problem:**

It should be possible to interact with grid container rows/cols
templates from the inspector.

**Fix:**

1. if a grid element is selected, show its rows and columns in the
inspector
2. allow editing their values
3. allow removing them
4. allow adding new cols/rows with a default `1fr` size
5. rearrange the grid children so they go "back into the grid" upon
removing a col/row


https://github.com/user-attachments/assets/2f28b870-a0a4-4a65-9880-73031ce9d1ee

Fixes #6117
  • Loading branch information
ruggi authored Jul 25, 2024
1 parent de1cdd3 commit 261461a
Show file tree
Hide file tree
Showing 2 changed files with 317 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ function asMaybeNamedAreaOrValue(
value: number | string | null,
): string | number {
if (value == null) {
return 0
return 1
} else if (typeof value === 'number') {
const template = axis === 'row' ? grid.gridTemplateRows : grid.gridTemplateColumns
if (template?.type === 'DIMENSIONS') {
Expand All @@ -389,6 +389,7 @@ function asMaybeNamedAreaOrValue(
return maybeAreaStart.areaName
}
}
return value === 0 ? 1 : value
}
return value
}
321 changes: 315 additions & 6 deletions editor/src/components/inspector/flex-section.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import { jsx } from '@emotion/react'
import React from 'react'
import { createSelector } from 'reselect'
import { when } from '../../utils/react-conditionals'
Expand All @@ -23,7 +26,24 @@ import {
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'
import { Icons, NumberInput, SquareButton, Subdued } from '../../uuiui'
import type { CSSNumber, GridCSSNumberUnit, UnknownOrEmptyInput } from './common/css-utils'
import { gridCSSNumber, isCSSNumber, type GridCSSNumber } from './common/css-utils'
import { applyCommandsAction } from '../editor/actions/action-creators'
import { setProperty } from '../canvas/commands/set-property-command'
import * as PP from '../../core/shared/property-path'
import type {
GridAutoOrTemplateBase,
GridContainerProperties,
GridPosition,
} from '../../core/shared/element-template'
import {
gridPositionValue,
type ElementInstanceMetadata,
type GridElementProperties,
} from '../../core/shared/element-template'
import { setGridPropsCommands } from '../canvas/canvas-strategies/strategies/grid-helpers'
import { type CanvasCommand } from '../canvas/commands/commands'

export const layoutSystemSelector = createSelector(
metadataSelector,
Expand Down Expand Up @@ -106,10 +126,38 @@ export const FlexSection = React.memo(() => {
[addFlexLayoutSystem, addGridLayoutSystem],
)

const grid = useEditorState(
Substores.metadata,
(store) =>
layoutSystem === 'grid' && store.editor.selectedViews.length === 1
? MetadataUtils.findElementByElementPath(
store.editor.jsxMetadata,
store.editor.selectedViews[0],
)
: null,
'FlexSection grid',
)

const columns: GridCSSNumber[] = React.useMemo(() => {
return getGridTemplateAxisValues({
calculated: grid?.specialSizeMeasurements.containerGridProperties.gridTemplateColumns ?? null,
fromProps:
grid?.specialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateColumns ?? null,
})
}, [grid])

const rows: GridCSSNumber[] = React.useMemo(() => {
return getGridTemplateAxisValues({
calculated: grid?.specialSizeMeasurements.containerGridProperties.gridTemplateRows ?? null,
fromProps:
grid?.specialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateRows ?? null,
})
}, [grid])

return (
<div>
<AddRemoveLayoutSystemControl />
<FlexCol css={{ gap: 10 }}>
<FlexCol css={{ gap: 10, paddingBottom: 10 }}>
{when(
layoutSystem === 'grid' || layoutSystem === 'flex',
<UIGridRow padded={true} variant='<-------------1fr------------->'>
Expand All @@ -123,14 +171,24 @@ export const FlexSection = React.memo(() => {
{when(
layoutSystem === 'grid',
<UIGridRow padded tall={false} variant={'<-------------1fr------------->'}>
<div>
<Subdued>Grid inspector coming soon...</Subdued>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{grid != null ? (
<React.Fragment>
<TemplateDimensionControl
axis={'column'}
grid={grid}
values={columns}
title='Columns'
/>
<TemplateDimensionControl axis={'row'} grid={grid} values={rows} title='Rows' />
</React.Fragment>
) : null}
</div>
</UIGridRow>,
)}
{when(
layoutSystem === 'flex',
<>
<React.Fragment>
<UIGridRow padded variant='<--1fr--><--1fr-->'>
<UIGridRow padded={false} variant='<-------------1fr------------->'>
<NineBlockControl />
Expand All @@ -145,9 +203,260 @@ export const FlexSection = React.memo(() => {
<UIGridRow padded={false} variant='<-------------1fr------------->'>
<SpacedPackedControl />
</UIGridRow>
</>,
</React.Fragment>,
)}
</FlexCol>
</div>
)
})

const TemplateDimensionControl = React.memo(
({
grid,
values,
axis,
title,
}: {
grid: ElementInstanceMetadata
values: GridCSSNumber[]
axis: 'column' | 'row'
title: string
}) => {
const dispatch = useDispatch()

const metadataRef = useRefEditorState((store) => store.editor.jsxMetadata)

const onUpdate = React.useCallback(
(index: number) => (value: UnknownOrEmptyInput<CSSNumber>) => {
if (!isCSSNumber(value)) {
return
}

const newValues = [...values]
newValues[index] = gridCSSNumber(
value.value,
(value.unit as GridCSSNumberUnit) ?? values[index].unit,
values[index].areaName,
)

dispatch([
applyCommandsAction([
setProperty(
'always',
grid.elementPath,
PP.create('style', axis === 'column' ? 'gridTemplateColumns' : 'gridTemplateRows'),
gridNumbersToTemplateString(newValues),
),
]),
])
},
[grid, values, dispatch, axis],
)

const onRemove = React.useCallback(
(index: number) => () => {
const newValues = values.filter((_, idx) => idx !== index)

let commands: CanvasCommand[] = [
setProperty(
'always',
grid.elementPath,
PP.create('style', axis === 'column' ? 'gridTemplateColumns' : 'gridTemplateRows'),
gridNumbersToTemplateString(newValues),
),
]

// adjust the position of the elements if they need to be moved
const adjustedGridTemplate = removeTemplateValueAtIndex(
grid.specialSizeMeasurements.containerGridProperties,
axis,
index,
)

const gridIndex = index + 1 // grid boundaries are 1-based

const children = MetadataUtils.getChildrenUnordered(metadataRef.current, grid.elementPath)
for (const child of children) {
let updated: Partial<GridElementProperties> = {
...child.specialSizeMeasurements.elementGridProperties,
}

function needsAdjusting(pos: GridPosition | null, bound: number) {
return pos != null &&
pos !== 'auto' &&
pos.numericalPosition != null &&
pos.numericalPosition >= bound
? pos.numericalPosition
: null
}

const position = child.specialSizeMeasurements.elementGridProperties
if (axis === 'column') {
const adjustColumnStart = needsAdjusting(position.gridColumnStart, gridIndex)
const adjustColumnEnd = needsAdjusting(position.gridColumnEnd, gridIndex + 1)
if (adjustColumnStart != null) {
updated.gridColumnStart = gridPositionValue(adjustColumnStart - 1)
}
if (adjustColumnEnd != null) {
updated.gridColumnEnd = gridPositionValue(adjustColumnEnd - 1)
}
} else {
const adjustRowStart = needsAdjusting(position.gridRowStart, gridIndex)
const adjustRowEnd = needsAdjusting(position.gridRowEnd, gridIndex + 1)
if (adjustRowStart != null) {
updated.gridRowStart = gridPositionValue(adjustRowStart - 1)
}
if (adjustRowEnd != null) {
updated.gridRowEnd = gridPositionValue(adjustRowEnd - 1)
}
}

commands.push(...setGridPropsCommands(child.elementPath, adjustedGridTemplate, updated))
}

dispatch([applyCommandsAction(commands)])
},
[grid, values, dispatch, axis, metadataRef],
)

const onAdd = React.useCallback(() => {
const newValues = values.concat(gridCSSNumber(1, 'fr', null))
dispatch([
applyCommandsAction([
setProperty(
'always',
grid.elementPath,
PP.create('style', axis === 'column' ? 'gridTemplateColumns' : 'gridTemplateRows'),
gridNumbersToTemplateString(newValues),
),
]),
])
}, [dispatch, grid, axis, values])

return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 6,
}}
>
<div style={{ fontWeight: 600, display: 'flex', alignItems: 'center' }}>
<div style={{ flex: 1 }}>{title}</div>
<SquareButton>
<Icons.Plus width={12} height={12} onClick={onAdd} />
</SquareButton>
</div>
{values.map((col, index) => {
return (
<div
key={`col-${col}-${index}`}
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
css={{
'& > .removeButton': {
visibility: 'hidden',
},
':hover': {
'& > .removeButton': {
visibility: 'visible',
},
},
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flex: 1 }}>
<Subdued
style={{
width: 40,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={col.areaName ?? undefined}
>
{col.areaName ?? index + 1}
</Subdued>
<NumberInput
style={{ flex: 1 }}
value={col}
numberType={'Length'}
onSubmitValue={onUpdate(index)}
onTransientSubmitValue={onUpdate(index)}
onForcedSubmitValue={onUpdate(index)}
defaultUnitToHide={null}
testId={`col-${col}-${index}`}
/>
</div>
<SquareButton className='removeButton'>
{/* TODO replace this with a context menu! */}
<Icons.Cross onClick={onRemove(index)} />
</SquareButton>
</div>
)
})}
</div>
)
},
)
TemplateDimensionControl.displayName = 'TemplateDimensionControl'

function removeTemplateValueAtIndex(
original: GridContainerProperties,
axis: 'column' | 'row',
index: number,
): GridContainerProperties {
function removeDimension(dimensions: GridCSSNumber[]) {
return dimensions.filter((_, idx) => idx !== index)
}

const gridTemplateRows =
axis === 'row' && original.gridTemplateRows?.type === 'DIMENSIONS'
? {
...original.gridTemplateRows,
dimensions: removeDimension(original.gridTemplateRows.dimensions),
}
: original.gridTemplateRows

const gridTemplateColumns =
axis === 'column' && original.gridTemplateColumns?.type === 'DIMENSIONS'
? {
...original.gridTemplateColumns,
dimensions: removeDimension(original.gridTemplateColumns.dimensions),
}
: original.gridTemplateColumns

return {
...original,
gridTemplateRows: gridTemplateRows,
gridTemplateColumns: gridTemplateColumns,
}
}

function gridNumbersToTemplateString(values: GridCSSNumber[]) {
return values
.map((v) => {
const areaName = v.areaName != null ? `[${v.areaName}] ` : ''
const unit = v.unit != null ? `${v.unit}` : ''
return areaName + `${v.value}` + unit
})
.join(' ')
}

function getGridTemplateAxisValues(template: {
calculated: GridAutoOrTemplateBase | null
fromProps: GridAutoOrTemplateBase | null
}): GridCSSNumber[] {
const { calculated, fromProps } = template
if (fromProps?.type !== 'DIMENSIONS' && calculated?.type !== 'DIMENSIONS') {
return []
}

const calculatedDimensions = calculated?.type === 'DIMENSIONS' ? calculated.dimensions : []
const fromPropsDimensions = fromProps?.type === 'DIMENSIONS' ? fromProps.dimensions : []
if (calculatedDimensions.length === 0) {
return fromPropsDimensions
} else if (calculatedDimensions.length === fromPropsDimensions.length) {
return fromPropsDimensions
} else {
return calculatedDimensions
}
}

0 comments on commit 261461a

Please sign in to comment.