Skip to content

Commit

Permalink
Grids: presetve repeat from inspector changes (#6388)
Browse files Browse the repository at this point in the history
**Problem:**

As a followup to #6380 , we should preserve `repeat` functions & co.
when altering the grid template from the inspector inputs too.

**Fix:**

1. Factored out the logic to alter a table from the resize strategy to
helper functions in `grid-helpers.ts`
2. Adjusted the logic to support removing elements too
3. Refactored the callbacks in the inspector section to use the new
helpers

Fixes #6387
  • Loading branch information
ruggi authored Sep 19, 2024
1 parent db198f6 commit 96795b0
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 198 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import {
} from '../../../../core/shared/math-utils'
import * as PP from '../../../../core/shared/property-path'
import { absolute } from '../../../../utils/utils'
import { cssNumber, isCSSKeyword } from '../../../inspector/common/css-utils'
import type { GridDimension } from '../../../inspector/common/css-utils'
import {
cssNumber,
gridCSSRepeat,
isCSSKeyword,
isGridCSSRepeat,
} from '../../../inspector/common/css-utils'
import type { CanvasCommand } from '../../commands/commands'
import { deleteProperties } from '../../commands/delete-properties-command'
import { reorderElement } from '../../commands/reorder-element-command'
Expand All @@ -35,6 +41,8 @@ import type { DragInteractionData } from '../interaction-state'
import type { GridCellCoordinates } from './grid-cell-bounds'
import { getGridCellUnderMouseFromMetadata, gridCellCoordinates } from './grid-cell-bounds'
import { memoize } from '../../../../core/shared/memoize'
import { mapDropNulls } from '../../../../core/shared/array-utils'
import { assertNever } from '../../../../core/shared/utils'

export function runGridRearrangeMove(
targetElement: ElementPath,
Expand Down Expand Up @@ -620,3 +628,173 @@ function gridTemplateToNumbers(gridTemplate: GridTemplate | null): Array<number>

return result
}

type DimensionIndexes = {
originalIndex: number // the index of this element in the original values
repeatedIndex: number // the index of this element, if it's generated via a repeat, inside the repeated values array definition
}

export type ExpandedGridDimension = GridDimension & {
indexes: DimensionIndexes
}

function expandedGridDimension(
dim: GridDimension,
originalIndex: number,
repeatedIndex: number = 0,
): ExpandedGridDimension {
return {
...dim,
indexes: {
originalIndex: originalIndex,
repeatedIndex: repeatedIndex,
},
}
}

export function expandGridDimensions(template: GridDimension[]): ExpandedGridDimension[] {
// Expanded representation of the original values, where repeated elements are serialized.
// Each element also contains the indexes information to be used later on to build the resized
// template string.
return template.reduce((acc, cur, index) => {
if (isGridCSSRepeat(cur)) {
const repeatGroup = cur.value.map((dim, repeatedIndex) =>
expandedGridDimension(dim, index, repeatedIndex),
)
let expanded: ExpandedGridDimension[] = []
for (let i = 0; i < cur.times; i++) {
expanded.push(...repeatGroup)
}
return [...acc, ...expanded]
} else {
return [...acc, expandedGridDimension(cur, index)]
}
}, [] as ExpandedGridDimension[])
}

function alterGridTemplateDimensions(params: {
originalValues: GridDimension[]
target: ExpandedGridDimension
patch: AlterGridTemplateDimensionPatch
}): GridDimension[] {
return mapDropNulls((dim, index) => {
if (index !== params.target.indexes.originalIndex) {
return dim
} else if (isGridCSSRepeat(dim)) {
const repeatedIndex = params.target.indexes.repeatedIndex ?? 0
const before = dim.value.slice(0, repeatedIndex)
const after = dim.value.slice(repeatedIndex + 1)
switch (params.patch.type) {
case 'REMOVE':
if (before.length + after.length === 0) {
return null
}
return gridCSSRepeat(dim.times, [...before, ...after])
case 'REPLACE':
return gridCSSRepeat(dim.times, [...before, params.patch.newValue, ...after])
default:
assertNever(params.patch)
}
} else {
switch (params.patch.type) {
case 'REPLACE':
return params.patch.newValue
case 'REMOVE':
return null
default:
assertNever(params.patch)
}
}
}, params.originalValues)
}

export type ReplaceGridDimensionPatch = {
type: 'REPLACE'
newValue: GridDimension
}

export type RemoveGridDimensionPatch = {
type: 'REMOVE'
}

export type AlterGridTemplateDimensionPatch = ReplaceGridDimensionPatch | RemoveGridDimensionPatch

export function replaceGridTemplateDimensionAtIndex(
template: GridDimension[],
expanded: ExpandedGridDimension[],
index: number,
newValue: GridDimension,
): GridDimension[] {
return alterGridTemplateDimensions({
originalValues: template,
target: expanded[index],
patch: {
type: 'REPLACE',
newValue: newValue,
},
})
}

export function removeGridTemplateDimensionAtIndex(
template: GridDimension[],
expanded: ExpandedGridDimension[],
index: number,
): GridDimension[] {
return alterGridTemplateDimensions({
originalValues: template,
target: expanded[index],
patch: {
type: 'REMOVE',
},
})
}

// Return an array of related indexes to a given index inside a grid's template dimensions.
export function getGridRelatedIndexes(params: {
template: GridDimension[]
index: number
}): number[] {
let relatedIndexes: number[][][] = [] // This looks scary but it's not! It's just a list of indexes, containing a list of the indexes *per group element*.
// For example, 1fr repeat(3, 10px 20px) 1fr, will be represented as:
/**
* [
* [ [0] ]
* [ [1, 3] [2, 4] ]
* [ [5] ]
* ]
*/
let elementCount = 0 // basically the expanded index
for (const dim of params.template) {
if (dim.type === 'REPEAT') {
let groupIndexes: number[][] = []
// for each value push the related indexes as many times as the repeats counter
for (let valueIndex = 0; valueIndex < dim.value.length; valueIndex++) {
let repeatedValueIndexes: number[] = []
for (let repeatIndex = 0; repeatIndex < dim.times; repeatIndex++) {
repeatedValueIndexes.push(elementCount + valueIndex + repeatIndex * dim.value.length)
}
groupIndexes.push(repeatedValueIndexes)
}
relatedIndexes.push(groupIndexes)
elementCount += dim.value.length * dim.times // advance the counter as many times as the repeated values *combined*
} else {
relatedIndexes.push([[elementCount]])
elementCount++
}
}

// Now, expand the indexes calculated above so they "flatten out" to match the generated values
let expandedRelatedIndexes: number[][] = []
params.template.forEach((dim, dimIndex) => {
if (dim.type === 'REPEAT') {
for (let repeatIndex = 0; repeatIndex < dim.times * dim.value.length; repeatIndex++) {
const indexes = relatedIndexes[dimIndex][repeatIndex % dim.value.length]
expandedRelatedIndexes.push(indexes)
}
} else {
expandedRelatedIndexes.push(relatedIndexes[dimIndex][0])
}
})

return expandedRelatedIndexes[params.index] ?? []
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ import type { GridDimension } from '../../../../components/inspector/common/css-
import {
cssNumber,
gridCSSNumber,
gridCSSRepeat,
isGridCSSNumber,
isGridCSSRepeat,
printArrayGridDimensions,
} from '../../../../components/inspector/common/css-utils'
import { toFirst } from '../../../../core/shared/optics/optic-utilities'
Expand All @@ -36,6 +34,7 @@ import type { Either } from '../../../../core/shared/either'
import { foldEither, isLeft, isRight } from '../../../../core/shared/either'
import { roundToNearestWhole } from '../../../../core/shared/math-utils'
import type { GridAutoOrTemplateBase } from '../../../../core/shared/element-template'
import { expandGridDimensions, replaceGridTemplateDimensionAtIndex } from './grid-helpers'

export const resizeGridStrategy: CanvasStrategyFactory = (
canvasState: InteractionCanvasState,
Expand Down Expand Up @@ -116,24 +115,7 @@ export const resizeGridStrategy: CanvasStrategyFactory = (
return emptyStrategyApplicationResult
}

// Expanded representation of the original values, where repeated elements are serialized.
// Each element also contains the indexes information to be used later on to build the resized
// template string.
const expandedOriginalValues = originalValues.dimensions.reduce((acc, cur, index) => {
if (isGridCSSRepeat(cur)) {
const repeatGroup = cur.value.map((dim, repeatedIndex) =>
expandedGridDimension(dim, index, repeatedIndex),
)
let expanded: ExpandedGridDimension[] = []
for (let i = 0; i < cur.times; i++) {
expanded.push(...repeatGroup)
}
return [...acc, ...expanded]
} else {
return [...acc, expandedGridDimension(cur, index)]
}
}, [] as ExpandedGridDimension[])

const expandedOriginalValues = expandGridDimensions(originalValues.dimensions)
const mergedValues: GridAutoOrTemplateBase = {
type: calculatedValues.type,
dimensions: calculatedValues.dimensions.map((dim, index) => {
Expand Down Expand Up @@ -181,11 +163,12 @@ export const resizeGridStrategy: CanvasStrategyFactory = (
areaName,
)

const newDimensions = buildResizedDimensions({
newValue: newValue,
originalValues: originalValues.dimensions,
target: expandedOriginalValues[control.columnOrRow],
})
const newDimensions = replaceGridTemplateDimensionAtIndex(
originalValues.dimensions,
expandedOriginalValues,
control.columnOrRow,
newValue,
)

const propertyValueAsString = printArrayGridDimensions(newDimensions)

Expand All @@ -207,51 +190,6 @@ export const resizeGridStrategy: CanvasStrategyFactory = (
}
}

type DimensionIndexes = {
originalIndex: number // the index of this element in the original values
repeatedIndex: number // the index of this element, if it's generated via a repeat, inside the repeated values array definition
}

function expandedGridDimension(
dim: GridDimension,
originalIndex: number,
repeatedIndex: number = 0,
): ExpandedGridDimension {
return {
...dim,
indexes: {
originalIndex: originalIndex,
repeatedIndex: repeatedIndex,
},
}
}

type ExpandedGridDimension = GridDimension & {
indexes: DimensionIndexes
}

function buildResizedDimensions(params: {
newValue: GridDimension
originalValues: GridDimension[]
target: ExpandedGridDimension
}) {
return params.originalValues.map((dim, index) => {
if (index !== params.target.indexes.originalIndex) {
return dim
} else if (isGridCSSRepeat(dim)) {
const repeatedIndex = params.target.indexes.repeatedIndex ?? 0
const repeatGroup = [
...dim.value.slice(0, repeatedIndex),
params.newValue,
...dim.value.slice(repeatedIndex + 1),
]
return gridCSSRepeat(dim.times, repeatGroup)
} else {
return params.newValue
}
})
}

function getNewDragValue(
dragAmount: number,
isFractional: boolean,
Expand Down
48 changes: 4 additions & 44 deletions editor/src/components/canvas/controls/grid-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
getGridPlaceholderDomElementFromCoordinates,
gridCellTargetId,
} from '../canvas-strategies/strategies/grid-cell-bounds'
import { getGridRelatedIndexes } from '../canvas-strategies/strategies/grid-helpers'

const CELL_ANIMATION_DURATION = 0.15 // seconds

Expand Down Expand Up @@ -360,51 +361,10 @@ export const GridResizing = React.memo((props: GridResizingProps) => {
if (props.fromPropsAxisValues?.type !== 'DIMENSIONS' || resizingIndex == null) {
return []
}

// Build an array of coresizing indexes per element.
let coresizeIndexes: number[][][] = [] // This looks scary but it's not! It's just a list of indexes, containing a list of the indexes *per group element*.
// For example, 1fr repeat(3, 10px 20px) 1fr, will be represented as:
/**
* [
* [ [0] ]
* [ [1, 3] [2, 4] ]
* [ [5] ]
* ]
*/
let elementCount = 0 // basically the expanded index
for (const dim of props.fromPropsAxisValues.dimensions) {
if (dim.type === 'REPEAT') {
let groupIndexes: number[][] = []
// for each value push the coresize indexes as many times as the repeats counter
for (let valueIndex = 0; valueIndex < dim.value.length; valueIndex++) {
let repeatedValueIndexes: number[] = []
for (let repeatIndex = 0; repeatIndex < dim.times; repeatIndex++) {
repeatedValueIndexes.push(elementCount + valueIndex + repeatIndex * dim.value.length)
}
groupIndexes.push(repeatedValueIndexes)
}
coresizeIndexes.push(groupIndexes)
elementCount += dim.value.length * dim.times // advance the counter as many times as the repeated values *combined*
} else {
coresizeIndexes.push([[elementCount]])
elementCount++
}
}

// Now, expand the indexes calculated above so they "flatten out" to match the generated values
let expandedCoresizeIndexes: number[][] = []
props.fromPropsAxisValues.dimensions.forEach((dim, dimIndex) => {
if (dim.type === 'REPEAT') {
for (let repeatIndex = 0; repeatIndex < dim.times * dim.value.length; repeatIndex++) {
const indexes = coresizeIndexes[dimIndex][repeatIndex % dim.value.length]
expandedCoresizeIndexes.push(indexes)
}
} else {
expandedCoresizeIndexes.push(coresizeIndexes[dimIndex][0])
}
return getGridRelatedIndexes({
template: props.fromPropsAxisValues.dimensions,
index: resizingIndex,
})

return expandedCoresizeIndexes[resizingIndex] ?? []
}, [props.fromPropsAxisValues, resizingIndex])

if (props.axisValues == null) {
Expand Down
12 changes: 8 additions & 4 deletions editor/src/components/inspector/common/css-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -823,14 +823,18 @@ export function printCSSNumber(

export function printGridDimension(dimension: GridDimension): string {
switch (dimension.type) {
case 'KEYWORD':
return dimension.value.value
case 'NUMBER':
case 'KEYWORD': {
const areaName = dimension.areaName != null ? `[${dimension.areaName}] ` : ''
return `${areaName}${dimension.value.value}`
}
case 'NUMBER': {
const printed = printCSSNumber(dimension.value, null)
const areaName = dimension.areaName != null ? `[${dimension.areaName}] ` : ''
return `${areaName}${printed}`
case 'REPEAT':
}
case 'REPEAT': {
return `repeat(${dimension.times}, ${printArrayGridDimensions(dimension.value)})`
}
default:
assertNever(dimension)
}
Expand Down
Loading

0 comments on commit 96795b0

Please sign in to comment.