Skip to content

Commit

Permalink
refactor(grid) Grid Element Direct Lookup Ordered Correctly. (#6667)
Browse files Browse the repository at this point in the history
- Moved a lot of common types and functions used when obtaining values
from elements for grids into `grid-measurements.tsx`.
- Added `getGridElementMeasurementHelperData` to retrieve the necessary
details for a grid element, along with some supporting functions.
- Extracted out a `getGlobalFrame` function for the common lookup used.
- Changed some test ID values in two components as they created the same
ones.
- `PaddingResizeControl` set to the `bottom` priority so that it doesn't
end up on top of the track resizing controls.
- `GridControl` now gets the cell dimensions directly from the grid
element.
- `GridElementContainingBlock` gets the grid child dimensions directly.
- Added `HelperControlsStateContext` and made the connected change to
`StoreName`.
- Added `helperControlsStore` to `Editor`.
- Added contexts for the helper store to `EditorRoot` and `HotRoot`.
- Wrapped the contents of `GridElementContainingBlocks` and
`GridMeasurementHelpers` with the new helper store context.
- Added calls to update the helper store in the regular and test
dispatch flows.
  • Loading branch information
seanparsons authored Nov 22, 2024
1 parent 1b27ff8 commit 0e30131
Show file tree
Hide file tree
Showing 17 changed files with 544 additions and 389 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe('resize a grid', () => {
await renderResult.dispatch(selectComponents([target], false), true)
await renderResult.getDispatchFollowUpActionsFinished()
const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-column-handle-1`)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-track-size-column-handle-1`)
const resizeControlRect = resizeControl.getBoundingClientRect()
const startPoint = canvasPoint({
x: resizeControlRect.x + resizeControlRect.width / 2,
Expand Down Expand Up @@ -259,7 +259,7 @@ export var storyboard = (
await renderResult.dispatch(selectComponents([target], false), true)
await renderResult.getDispatchFollowUpActionsFinished()
const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-column-handle-1`)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-track-size-column-handle-1`)
const resizeControlRect = resizeControl.getBoundingClientRect()
const startPoint = canvasPoint({
x: resizeControlRect.x + resizeControlRect.width / 2,
Expand Down Expand Up @@ -320,7 +320,7 @@ export var storyboard = (
await renderResult.dispatch(selectComponents([target], false), true)
await renderResult.getDispatchFollowUpActionsFinished()
const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-row-handle-1`)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-track-size-row-handle-1`)
const resizeControlRect = resizeControl.getBoundingClientRect()
const startPoint = canvasPoint({
x: resizeControlRect.x + resizeControlRect.width / 2,
Expand Down Expand Up @@ -435,7 +435,7 @@ export var storyboard = (
await renderResult.dispatch(selectComponents([target], false), true)
await renderResult.getDispatchFollowUpActionsFinished()
const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-column-handle-1`)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-track-size-column-handle-1`)
const resizeControlRect = resizeControl.getBoundingClientRect()
const startPoint = canvasPoint({
x: resizeControlRect.x + resizeControlRect.width / 2,
Expand Down Expand Up @@ -555,7 +555,7 @@ export var storyboard = (
await renderResult.dispatch(selectComponents([target], false), true)
await renderResult.getDispatchFollowUpActionsFinished()
const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-column-handle-1`)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-track-size-column-handle-1`)
const resizeControlRect = resizeControl.getBoundingClientRect()
const startPoint = canvasPoint({
x: resizeControlRect.x + resizeControlRect.width / 2,
Expand Down Expand Up @@ -674,7 +674,7 @@ export var storyboard = (
await renderResult.dispatch(selectComponents([target], false), true)
await renderResult.getDispatchFollowUpActionsFinished()
const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-column-handle-1`)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-track-size-column-handle-1`)
const resizeControlRect = resizeControl.getBoundingClientRect()
const startPoint = canvasPoint({
x: resizeControlRect.x + resizeControlRect.width / 2,
Expand Down Expand Up @@ -792,7 +792,7 @@ export var storyboard = (
await renderResult.dispatch(selectComponents([target], false), true)
await renderResult.getDispatchFollowUpActionsFinished()
const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-column-handle-1`)
const resizeControl = renderResult.renderedDOM.getByTestId(`grid-track-size-column-handle-1`)
const resizeControlRect = resizeControl.getBoundingClientRect()
const startPoint = canvasPoint({
x: resizeControlRect.x + resizeControlRect.width / 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti
props: { targets: selectedElements },
key: 'padding-resize-control',
show: 'visible-except-when-other-strategy-is-active',
priority: 'bottom',
})

const controlsToRender = optionalMap(
Expand Down
238 changes: 3 additions & 235 deletions editor/src/components/canvas/controls/grid-controls-for-strategies.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import { sides, type Sides } from 'utopia-api/core'
import type { ElementPath } from 'utopia-shared/src/types'
import {
isStaticGridRepeat,
parseCSSLength,
printGridAutoOrTemplateBase,
} from '../../inspector/common/css-utils'
import { printGridAutoOrTemplateBase } from '../../inspector/common/css-utils'
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import { mapDropNulls } from '../../../core/shared/array-utils'
import * as EP from '../../../core/shared/element-path'
import type { BorderWidths } from '../../../core/shared/element-template'
import { type GridAutoOrTemplateBase } from '../../../core/shared/element-template'
import type { CanvasRectangle } from '../../../core/shared/math-utils'
import { assertNever } from '../../../core/shared/utils'
import { Substores, useEditorState } from '../../editor/store/store-hook'
import type {
Expand All @@ -23,7 +16,6 @@ import { controlForStrategyMemoized } from '../canvas-strategies/canvas-strategy
import type { GridResizeEdge } from '../canvas-strategies/interaction-state'
import type { EdgePosition } from '../canvas-types'
import {
CanvasContainerID,
EdgePositionBottom,
EdgePositionLeft,
EdgePositionRight,
Expand All @@ -35,14 +27,8 @@ import {
GridRowColumnResizingControlsComponent,
} from './grid-controls'
import { isEdgePositionOnSide } from '../canvas-utils'
import { getFromElement } from '../direct-dom-lookups'
import { applicativeSidesPxTransform, getGridContainerProperties } from '../dom-walker'
import { applicative4Either, defaultEither, isRight, mapEither } from '../../../core/shared/either'
import { domRectToScaledCanvasRectangle, getRoundingFn } from '../../../core/shared/dom-utils'
import Utils from '../../../utils/utils'
import { useMonitorChangesToElements } from '../../../components/editor/store/store-monitor'
import { useKeepReferenceEqualityIfPossible } from '../../../utils/react-performance'
import type { CSSProperties } from 'react'
import {
gridContainerIdentifier,
gridItemIdentifier,
Expand All @@ -51,29 +37,13 @@ import {
} from '../../editor/store/editor-state'
import { findOriginalGrid } from '../canvas-strategies/strategies/grid-helpers'
import * as React from 'react'
import { addChangeCallback, removeChangeCallback } from '../observers'
import type { GridData } from './grid-measurements'
import { getGridMeasurementHelperData, useObserversToWatch } from './grid-measurements'

export const GridMeasurementHelperMap = { current: new WeakMap<HTMLElement, string>() }

export const GridCellTestId = (elementPath: ElementPath) => `grid-cell-${EP.toString(elementPath)}`

function getCellsCount(template: GridAutoOrTemplateBase | null): number {
if (template == null) {
return 0
}

switch (template.type) {
case 'DIMENSIONS':
return template.dimensions.reduce((acc, cur) => {
return acc + (isStaticGridRepeat(cur) ? cur.times : 1)
}, 0)
case 'FALLBACK':
return 0
default:
assertNever(template)
}
}

export function getNullableAutoOrTemplateBaseString(
template: GridAutoOrTemplateBase | null,
): string | undefined {
Expand All @@ -84,208 +54,6 @@ export function getNullableAutoOrTemplateBaseString(
}
}

export type GridMeasurementHelperData = {
frame: CanvasRectangle
rows: number
columns: number
cells: number
computedStyling: CSSProperties
gridTemplateColumns: GridAutoOrTemplateBase | null
gridTemplateRows: GridAutoOrTemplateBase | null
gridTemplateColumnsFromProps: GridAutoOrTemplateBase | null
gridTemplateRowsFromProps: GridAutoOrTemplateBase | null
border: BorderWidths
gap: number | null
rowGap: number | null
columnGap: number | null
padding: Sides
element: HTMLElement
}

export type ElementOrParent = 'parent' | 'element'

export function getGridMeasurementHelperData(
elementPath: ElementPath,
scale: number,
source: ElementOrParent,
): GridMeasurementHelperData | undefined {
return getFromElement(elementPath, gridMeasurementHelperDataFromElement(scale), source)
}

function getStylingSubset(styling: CSSStyleDeclaration): CSSProperties {
// Fields chosen to not overlap with any others, so as to not make React complain.
return {
gridAutoFlow: styling.gridAutoFlow,
gridAutoColumns: styling.gridAutoColumns,
gridAutoRows: styling.gridAutoRows,
gridTemplateColumns: styling.gridTemplateColumns,
gridTemplateRows: styling.gridTemplateRows,
gridColumn: styling.gridColumn,
gridRow: styling.gridRow,
gap: styling.gap,
rowGap: styling.rowGap,
columnGap: styling.columnGap,
justifyContent: styling.justifyContent,
alignContent: styling.alignContent,
padding: styling.padding,
paddingTop: styling.paddingTop,
paddingLeft: styling.paddingLeft,
paddingBottom: styling.paddingBottom,
paddingRight: styling.paddingRight,
borderTop: styling.borderTopWidth,
borderLeft: styling.borderLeftWidth,
borderBottom: styling.borderBottomWidth,
borderRight: styling.borderRightWidth,
}
}

export function gridMeasurementHelperDataFromElement(
scale: number,
): (element: HTMLElement) => GridMeasurementHelperData | undefined {
return (element) => {
const canvasRootContainer = document.getElementById(CanvasContainerID)
if (canvasRootContainer == null) {
return undefined
}

const computedStyle = getComputedStyle(element)
const boundingRectangle = element.getBoundingClientRect()

const computedStyling: CSSProperties = getStylingSubset(computedStyle)

const containerGridProperties = getGridContainerProperties(computedStyle)
const containerGridPropertiesFromProps = getGridContainerProperties(element.style)

const columns = getCellsCount(containerGridProperties.gridTemplateColumns)
const rows = getCellsCount(containerGridProperties.gridTemplateRows)
const borderTopWidth = parseCSSLength(computedStyle.borderTopWidth)
const borderRightWidth = parseCSSLength(computedStyle.borderRightWidth)
const borderBottomWidth = parseCSSLength(computedStyle.borderBottomWidth)
const borderLeftWidth = parseCSSLength(computedStyle.borderLeftWidth)
const border: BorderWidths = {
top: isRight(borderTopWidth) ? borderTopWidth.value.value : 0,
right: isRight(borderRightWidth) ? borderRightWidth.value.value : 0,
bottom: isRight(borderBottomWidth) ? borderBottomWidth.value.value : 0,
left: isRight(borderLeftWidth) ? borderLeftWidth.value.value : 0,
}
const padding = defaultEither(
sides(undefined, undefined, undefined, undefined),
applicative4Either(
applicativeSidesPxTransform,
parseCSSLength(computedStyle.paddingTop),
parseCSSLength(computedStyle.paddingRight),
parseCSSLength(computedStyle.paddingBottom),
parseCSSLength(computedStyle.paddingLeft),
),
)
const gap = defaultEither(
null,
mapEither((n) => n.value, parseCSSLength(computedStyle.gap)),
)

const rowGap = defaultEither(
null,
mapEither((n) => n.value, parseCSSLength(computedStyle.rowGap)),
)

const columnGap = defaultEither(
null,
mapEither((n) => n.value, parseCSSLength(computedStyle.columnGap)),
)

const elementRect = domRectToScaledCanvasRectangle(
boundingRectangle,
1 / scale,
getRoundingFn('nearest-half'),
)
const parentRect = domRectToScaledCanvasRectangle(
canvasRootContainer.getBoundingClientRect(),
1 / scale,
getRoundingFn('nearest-half'),
)
const frame = Utils.offsetRect(elementRect, Utils.negate(parentRect))

return {
frame: frame,
gridTemplateColumns: containerGridProperties.gridTemplateColumns,
gridTemplateRows: containerGridProperties.gridTemplateRows,
gridTemplateColumnsFromProps: containerGridPropertiesFromProps.gridTemplateColumns,
gridTemplateRowsFromProps: containerGridPropertiesFromProps.gridTemplateRows,
border: border,
padding: padding,
gap: gap,
rowGap: rowGap,
columnGap: columnGap,
rows: rows,
columns: columns,
cells: columns * rows,
computedStyling: computedStyling,
element: element,
}
}
}

function useObserversToWatch(elementPathOrPaths: Array<ElementPath> | ElementPath): number {
// Used to trigger extra renders.
const [counter, setCounter] = React.useState(0)
const bumpCounter = React.useCallback(() => {
setCounter((value) => value + 1)
}, [])

// Need to use the mount count for the callback trigger.
const mountCount = useEditorState(
Substores.canvas,
(store) => store.editor.canvas.mountCount,
'useObserversToWatch mountCount',
)

React.useEffect(() => {
// Add the change callback(s) for the element path or paths.
if (Array.isArray(elementPathOrPaths)) {
for (const elementPath of elementPathOrPaths) {
addChangeCallback(mountCount, elementPath, bumpCounter)
}
} else {
addChangeCallback(mountCount, elementPathOrPaths, bumpCounter)
}

return function cleanup() {
if (Array.isArray(elementPathOrPaths)) {
for (const elementPath of elementPathOrPaths) {
removeChangeCallback(elementPath, bumpCounter)
}
} else {
removeChangeCallback(elementPathOrPaths, bumpCounter)
}
}
}, [mountCount, elementPathOrPaths, bumpCounter])

return counter
}

export function useGridMeasurementHelperData(
elementPath: ElementPath,
source: ElementOrParent,
): GridMeasurementHelperData | undefined {
const scale = useEditorState(
Substores.canvas,
(store) => store.editor.canvas.scale,
'useGridMeasurementHelperData scale',
)

useMonitorChangesToElements([elementPath])

useObserversToWatch(elementPath)

return useKeepReferenceEqualityIfPossible(
getGridMeasurementHelperData(elementPath, scale, source),
)
}

export type GridData = GridMeasurementHelperData & {
identifier: GridIdentifier
}

export function useGridData(gridIdentifiers: GridIdentifier[]): GridData[] {
const scale = useEditorState(
Substores.canvas,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CSSProperties } from 'react'
import type { GridMeasurementHelperData } from './grid-controls-for-strategies'
import type { GridMeasurementHelperData } from './grid-measurements'

export function getGridHelperStyleMatchingTargetGrid(
grid: GridMeasurementHelperData,
Expand Down
Loading

0 comments on commit 0e30131

Please sign in to comment.