Skip to content

Commit 66fc4c8

Browse files
authored
GridMeasurementHelper component to continuously measure grid cells (#6517)
**Problem:** When you reparent from a grid to another grid, for a single frame the element blinks at the same cell position as it was in the original grid: https://screenshot.click/09-31-10wrd-ysw94.mp4 **Root cause:** As you hover over the target grid, the grid-reparent-strategy is selected. It triggers a showGridControl command, so the grid control is shown and the cells can be measured. However, it won't be able to position to the real target cell in this frame yet, because the control is not enabled yet, so we don't have the cell bounds in the metadata. **Fix:** Instead of using grid controls to measure let show permanent grid measurement helpers so grids can always be measured. **Commit Details:** (< vv pls delete this section if's not relevant) - Added memoized function `getAllGrids` which returns all the grids from the metadata - Added `GridMeasurementHelper` component which renders a grid with placeholder divs in its cells (similarly to `GridControl`, but much-much simpler) - Rendering `GridMeasurementHelper` for all grids - Use rendered grid measurement helpers in dom-walker to measure the size of grid cell bounds - Remove all the hacks (most notably `getMetadataWithGridCellBounds`) which made it possible to use latestMetadata in interaction when startingMetadata did not include grid cell bounds. This is not needed anymore, because the grid cells are always measured using the always-on `GridMeasurementHelper` components **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Play mode
1 parent a4487d5 commit 66fc4c8

11 files changed

+249
-146
lines changed

editor/src/components/canvas/canvas-strategies/strategies/grid-draw-to-insert-strategy.tsx

+5-9
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
getStyleAttributesForFrameInAbsolutePosition,
4444
updateInsertionSubjectWithAttributes,
4545
} from './draw-to-insert-metastrategy'
46-
import { getMetadataWithGridCellBounds, setGridPropsCommands } from './grid-helpers'
46+
import { setGridPropsCommands } from './grid-helpers'
4747
import { newReparentSubjects } from './reparent-helpers/reparent-strategy-helpers'
4848
import { getReparentTargetUnified } from './reparent-helpers/reparent-strategy-parent-lookup'
4949
import { getGridCellUnderMouseFromMetadata } from './grid-cell-bounds'
@@ -132,13 +132,10 @@ const gridDrawToInsertStrategyInner =
132132
canvasState.propertyControlsInfo,
133133
)?.newParent.intendedParentPath
134134

135-
const { metadata: parent, customStrategyState: updatedCustomState } =
136-
getMetadataWithGridCellBounds(
137-
targetParent,
138-
canvasState.startingMetadata,
139-
interactionSession.latestMetadata,
140-
customStrategyState,
141-
)
135+
const parent = MetadataUtils.findElementByElementPath(
136+
canvasState.startingMetadata,
137+
targetParent,
138+
)
142139

143140
if (targetParent == null || parent == null || !MetadataUtils.isGridLayoutedContainer(parent)) {
144141
return null
@@ -175,7 +172,6 @@ const gridDrawToInsertStrategyInner =
175172
updateHighlightedViews('mid-interaction', [targetParent]),
176173
],
177174
[targetParent],
178-
updatedCustomState ?? undefined,
179175
)
180176
}
181177

editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts

-56
Original file line numberDiff line numberDiff line change
@@ -710,62 +710,6 @@ export function getGridRelatedIndexes(params: {
710710
return expandedRelatedIndexes[params.index] ?? []
711711
}
712712

713-
export function getMetadataWithGridCellBounds(
714-
path: ElementPath | null | undefined,
715-
startingMetadata: ElementInstanceMetadataMap,
716-
latestMetadata: ElementInstanceMetadataMap,
717-
customStrategyState: CustomStrategyState,
718-
): {
719-
metadata: ElementInstanceMetadata | null
720-
customStrategyState: CustomStrategyState | null
721-
} {
722-
if (path == null) {
723-
return {
724-
metadata: null,
725-
customStrategyState: null,
726-
}
727-
}
728-
729-
const fromStartingMetadata = MetadataUtils.findElementByElementPath(startingMetadata, path)
730-
731-
if (fromStartingMetadata?.specialSizeMeasurements.gridCellGlobalFrames != null) {
732-
return {
733-
metadata: fromStartingMetadata,
734-
customStrategyState: null,
735-
}
736-
}
737-
738-
const fromStrategyState = customStrategyState.grid.metadataCacheForGrids[EP.toString(path)]
739-
if (fromStrategyState != null) {
740-
return {
741-
metadata: fromStrategyState,
742-
customStrategyState: null,
743-
}
744-
}
745-
746-
const fromLatestMetadata = MetadataUtils.findElementByElementPath(latestMetadata, path)
747-
if (fromLatestMetadata?.specialSizeMeasurements.gridCellGlobalFrames != null) {
748-
return {
749-
metadata: fromLatestMetadata,
750-
customStrategyState: {
751-
...customStrategyState,
752-
grid: {
753-
...customStrategyState.grid,
754-
metadataCacheForGrids: {
755-
...customStrategyState.grid.metadataCacheForGrids,
756-
[EP.toString(path)]: fromLatestMetadata,
757-
},
758-
},
759-
},
760-
}
761-
}
762-
763-
return {
764-
metadata: fromStartingMetadata,
765-
customStrategyState: null,
766-
}
767-
}
768-
769713
function getOriginalElementGridConfiguration(
770714
gridCellGlobalFrames: GridCellGlobalFrames,
771715
interactionData: DragInteractionData,

editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategy.tsx

+8-16
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import type { InsertionPath } from '../../../editor/store/insertion-path'
1717
import { CSSCursor } from '../../canvas-types'
1818
import { setCursorCommand } from '../../commands/set-cursor-command'
1919
import { propertyToSet, updateBulkProperties } from '../../commands/set-property-command'
20-
import { showGridControls } from '../../commands/show-grid-controls-command'
2120
import { updateSelectedViews } from '../../commands/update-selected-views-command'
2221
import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies'
2322
import { ParentBounds } from '../../controls/parent-bounds'
@@ -39,7 +38,7 @@ import {
3938
import type { DragInteractionData, InteractionSession, UpdatedPathMap } from '../interaction-state'
4039
import { honoursPropsPosition, shouldKeepMovingDraggedGroupChildren } from './absolute-utils'
4140
import { replaceFragmentLikePathsWithTheirChildrenRecursive } from './fragment-like-helpers'
42-
import { getMetadataWithGridCellBounds, runGridRearrangeMove } from './grid-helpers'
41+
import { runGridRearrangeMove } from './grid-helpers'
4342
import { ifAllowedToReparent, isAllowedToReparent } from './reparent-helpers/reparent-helpers'
4443
import { removeAbsolutePositioningProps } from './reparent-helpers/reparent-property-changes'
4544
import type { ReparentTarget } from './reparent-helpers/reparent-strategy-helpers'
@@ -164,13 +163,10 @@ export function applyGridReparent(
164163
return emptyStrategyApplicationResult
165164
}
166165

167-
const { metadata: grid, customStrategyState: updatedCustomState } =
168-
getMetadataWithGridCellBounds(
169-
newParent.intendedParentPath,
170-
canvasState.startingMetadata,
171-
interactionSession.latestMetadata,
172-
customStrategyState,
173-
)
166+
const grid = MetadataUtils.findElementByElementPath(
167+
canvasState.startingMetadata,
168+
newParent.intendedParentPath,
169+
)
174170

175171
if (grid == null) {
176172
return strategyApplicationResult([], [newParent.intendedParentPath])
@@ -231,12 +227,6 @@ export function applyGridReparent(
231227
newParent.intendedParentPath,
232228
])
233229

234-
const baseCustomState = updatedCustomState ?? customStrategyState
235-
const customStrategyStatePatch = {
236-
...baseCustomState,
237-
elementsToRerender: elementsToRerender,
238-
}
239-
240230
return strategyApplicationResult(
241231
[
242232
...outcomes.flatMap((c) => c.commands),
@@ -245,7 +235,9 @@ export function applyGridReparent(
245235
setCursorCommand(CSSCursor.Reparent),
246236
],
247237
elementsToRerender,
248-
customStrategyStatePatch,
238+
{
239+
elementsToRerender: elementsToRerender,
240+
},
249241
)
250242
},
251243
)

editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts

+5-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
22
import * as EP from '../../../../core/shared/element-path'
3-
import type { GridElementProperties, GridPosition } from '../../../../core/shared/element-template'
43
import {
54
type CanvasRectangle,
65
isInfinityRectangle,
76
rectangleIntersection,
87
} from '../../../../core/shared/math-utils'
9-
import { isCSSKeyword } from '../../../inspector/common/css-utils'
108
import { isFillOrStretchModeApplied } from '../../../inspector/inspector-common'
119
import {
1210
controlsForGridPlaceholders,
@@ -22,13 +20,12 @@ import {
2220
strategyApplicationResult,
2321
} from '../canvas-strategy-types'
2422
import type { InteractionSession } from '../interaction-state'
25-
import { getMetadataWithGridCellBounds, setGridPropsCommands } from './grid-helpers'
23+
import { setGridPropsCommands } from './grid-helpers'
2624
import { resizeBoundingBoxFromSide } from './resize-helpers'
2725

2826
export const gridResizeElementStrategy: CanvasStrategyFactory = (
2927
canvasState: InteractionCanvasState,
3028
interactionSession: InteractionSession | null,
31-
customState,
3229
) => {
3330
const selectedElements = getTargetPathsFromInteractionTarget(canvasState.interactionTarget)
3431
if (selectedElements.length !== 1) {
@@ -86,13 +83,10 @@ export const gridResizeElementStrategy: CanvasStrategyFactory = (
8683
return emptyStrategyApplicationResult
8784
}
8885

89-
const { metadata: container, customStrategyState: updatedCustomState } =
90-
getMetadataWithGridCellBounds(
91-
EP.parentPath(selectedElement),
92-
canvasState.startingMetadata,
93-
interactionSession.latestMetadata,
94-
customState,
95-
)
86+
const container = MetadataUtils.findElementByElementPath(
87+
canvasState.startingMetadata,
88+
EP.parentPath(selectedElement),
89+
)
9690

9791
if (container == null) {
9892
return emptyStrategyApplicationResult
@@ -123,7 +117,6 @@ export const gridResizeElementStrategy: CanvasStrategyFactory = (
123117
return strategyApplicationResult(
124118
setGridPropsCommands(selectedElement, gridTemplate, gridProps),
125119
[parentGridPath],
126-
updatedCustomState ?? undefined,
127120
)
128121
},
129122
}

editor/src/components/canvas/controls/grid-controls-for-strategies.tsx

+64-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** @jsxRuntime classic */
22
/** @jsx jsx */
3+
import fastDeepEqual from 'fast-deep-equal'
34
import type { Sides } from 'utopia-api/core'
45
import type { ElementPath } from 'utopia-shared/src/types'
56
import { isStaticGridRepeat, printGridAutoOrTemplateBase } from '../../inspector/common/css-utils'
@@ -61,22 +62,72 @@ export function getNullableAutoOrTemplateBaseString(
6162
}
6263
}
6364

64-
export type GridData = {
65-
elementPath: ElementPath
65+
export type GridMeasurementHelperData = {
6666
frame: CanvasRectangle
6767
gridTemplateColumns: GridAutoOrTemplateBase | null
6868
gridTemplateRows: GridAutoOrTemplateBase | null
69-
gridTemplateColumnsFromProps: GridAutoOrTemplateBase | null
70-
gridTemplateRowsFromProps: GridAutoOrTemplateBase | null
7169
gap: number | null
72-
justifyContent: string | null
73-
alignContent: string | null
7470
rowGap: number | null
7571
columnGap: number | null
72+
justifyContent: string | null
73+
alignContent: string | null
7674
padding: Sides
77-
rows: number
7875
columns: number
7976
cells: number
77+
}
78+
79+
export function useGridMeasurentHelperData(
80+
elementPath: ElementPath,
81+
): GridMeasurementHelperData | null {
82+
return useEditorState(
83+
Substores.metadata,
84+
(store) => {
85+
const element = MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, elementPath)
86+
87+
const targetGridContainer = MetadataUtils.isGridLayoutedContainer(element) ? element : null
88+
89+
if (
90+
targetGridContainer == null ||
91+
targetGridContainer.globalFrame == null ||
92+
!isFiniteRectangle(targetGridContainer.globalFrame)
93+
) {
94+
return null
95+
}
96+
97+
const columns = getCellsCount(
98+
targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns,
99+
)
100+
const rows = getCellsCount(
101+
targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows,
102+
)
103+
104+
return {
105+
frame: targetGridContainer.globalFrame,
106+
gridTemplateColumns:
107+
targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateColumns,
108+
gridTemplateRows:
109+
targetGridContainer.specialSizeMeasurements.containerGridProperties.gridTemplateRows,
110+
gap: targetGridContainer.specialSizeMeasurements.gap,
111+
rowGap: targetGridContainer.specialSizeMeasurements.rowGap,
112+
columnGap: targetGridContainer.specialSizeMeasurements.columnGap,
113+
justifyContent: targetGridContainer.specialSizeMeasurements.justifyContent,
114+
alignContent: targetGridContainer.specialSizeMeasurements.alignContent,
115+
padding: targetGridContainer.specialSizeMeasurements.padding,
116+
columns: columns,
117+
cells: rows * columns,
118+
}
119+
},
120+
'useGridMeasurentHelperData',
121+
fastDeepEqual, //TODO: this should not be needed, but it seems EditorStateKeepDeepEquality is not running, and metadata reference is always updated
122+
)
123+
}
124+
125+
export type GridData = GridMeasurementHelperData & {
126+
elementPath: ElementPath
127+
gridTemplateColumnsFromProps: GridAutoOrTemplateBase | null
128+
gridTemplateRowsFromProps: GridAutoOrTemplateBase | null
129+
rows: number
130+
cells: number
80131
metadata: ElementInstanceMetadata
81132
}
82133

@@ -156,6 +207,12 @@ export const GridRowColumnResizingControls =
156207
)
157208

158209
export const GridControlsKey = (gridPath: ElementPath) => `grid-controls-${EP.toString(gridPath)}`
210+
export const GridControlKey = (gridPath: ElementPath) => `grid-control-${EP.toString(gridPath)}`
211+
212+
export const GridMeasurementHelpersKey = (gridPath: ElementPath) =>
213+
`grid-measurement-helpers-${EP.toString(gridPath)}`
214+
export const GridMeasurementHelperKey = (gridPath: ElementPath) =>
215+
`grid-measurement-helper-${EP.toString(gridPath)}`
159216

160217
export interface GridControlProps {
161218
grid: GridData

0 commit comments

Comments
 (0)