From 544461aa4b836821b933a9186287cfee6eb4f20e Mon Sep 17 00:00:00 2001
From: Federico Ruggi <1081051+ruggi@users.noreply.github.com>
Date: Tue, 10 Dec 2024 11:39:07 +0100
Subject: [PATCH 1/3] Fix getGridRelativeContainingBlock for flow elements
(#6713)
**Problem:**
`getGridRelativeContainingBlock` doesn't correctly return the item
position for flow elements. It was temporarily patched with the
positioning coming from `getGridChildCellCoordBoundsFromCanvas`, but
this PR instead fixes it for good, which cascade-solves a bunch of other
small bugs.
**Fix:**
The flow positioning requires _all_ preceding grid items to be available
in the grid, so this PR does just that: inject every (ordered!) child
into the temporary grid, retrieve the wanted one, and return its
bounding box.
**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
Fixes #6712
---
.../grid-change-element-location-strategy.ts | 8 --
.../strategies/grid-helpers.ts | 102 +++++++++++-------
.../strategies/grid-move-absolute.ts | 39 +++----
.../canvas/controls/grid-controls.tsx | 6 +-
.../canvas/controls/grid-measurements.tsx | 96 +++--------------
5 files changed, 100 insertions(+), 151 deletions(-)
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-strategy.ts
index 2557a35c9e47..7ef0e98a20e6 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-strategy.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-strategy.ts
@@ -34,16 +34,8 @@ import {
isAutoGridPin,
getCommandsForGridItemPlacement,
sortElementsByGridPosition,
- getGridRelativeContainingBlock,
} from './grid-helpers'
import type { GridCellCoordinates } from './grid-cell-bounds'
-import {
- canvasRectangle,
- offsetPoint,
- offsetRect,
- rectContainsPoint,
- zeroCanvasRect,
-} from '../../../../core/shared/math-utils'
export const gridChangeElementLocationStrategy: CanvasStrategyFactory = (
canvasState: InteractionCanvasState,
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts
index 4250ba0d1680..b722d1e84c82 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts
@@ -19,6 +19,7 @@ import {
} from '../../../../core/shared/element-template'
import {
localRectangle,
+ zeroRectangle,
zeroRectIfNullOrInfinity,
type CanvasRectangle,
type LocalRectangle,
@@ -750,8 +751,8 @@ const TemporaryGridID = 'temporary-grid'
export function getGridRelativeContainingBlock(
gridMetadata: ElementInstanceMetadata,
- cellMetadata: ElementInstanceMetadata,
- cellProperties: GridElementProperties,
+ gridElements: ElementInstanceMetadata[],
+ targetElement: ElementPath,
options?: {
forcePositionRelative?: boolean
},
@@ -828,47 +829,68 @@ export function getGridRelativeContainingBlock(
}
offsetContainer.appendChild(gridElement)
- // Create a child of the grid element with the appropriate properties.
- const gridChildElement = document.createElement('div')
+ function addItemToGrid(measurements: SpecialSizeMeasurements) {
+ // Create a child of the grid element with the appropriate properties.
+ const gridChildElement = document.createElement('div')
- if (cellProperties.gridColumnStart != null) {
- gridChildElement.style.gridColumnStart = printPinAsString(
- gridProperties,
- cellProperties.gridColumnStart,
- 'column',
- )
- }
- if (cellProperties.gridColumnEnd != null) {
- gridChildElement.style.gridColumnEnd = printPinAsString(
- gridProperties,
- cellProperties.gridColumnEnd,
- 'column',
- )
- }
- if (cellProperties.gridRowStart != null) {
- gridChildElement.style.gridRowStart = printPinAsString(
- gridProperties,
- cellProperties.gridRowStart,
- 'row',
- )
- }
- if (cellProperties.gridRowEnd != null) {
- gridChildElement.style.gridRowEnd = printPinAsString(
- gridProperties,
- cellProperties.gridRowEnd,
- 'row',
- )
+ const { elementGridProperties, position } = measurements
+
+ if (elementGridProperties.gridColumnStart != null) {
+ gridChildElement.style.gridColumnStart = printPinAsString(
+ gridProperties,
+ elementGridProperties.gridColumnStart,
+ 'column',
+ )
+ }
+ if (elementGridProperties.gridColumnEnd != null) {
+ gridChildElement.style.gridColumnEnd = printPinAsString(
+ gridProperties,
+ elementGridProperties.gridColumnEnd,
+ 'column',
+ )
+ }
+ if (elementGridProperties.gridRowStart != null) {
+ gridChildElement.style.gridRowStart = printPinAsString(
+ gridProperties,
+ elementGridProperties.gridRowStart,
+ 'row',
+ )
+ }
+ if (elementGridProperties.gridRowEnd != null) {
+ gridChildElement.style.gridRowEnd = printPinAsString(
+ gridProperties,
+ elementGridProperties.gridRowEnd,
+ 'row',
+ )
+ }
+ gridChildElement.style.position = options?.forcePositionRelative
+ ? 'relative'
+ : position ?? 'initial'
+ // Fill out the entire space available.
+ gridChildElement.style.top = '0'
+ gridChildElement.style.left = '0'
+ gridChildElement.style.bottom = '0'
+ gridChildElement.style.right = '0'
+
+ return gridChildElement
}
- gridChildElement.style.position = options?.forcePositionRelative
- ? 'relative'
- : cellMetadata.specialSizeMeasurements.position ?? 'initial'
- // Fill out the entire space available.
- gridChildElement.style.top = '0'
- gridChildElement.style.left = '0'
- gridChildElement.style.bottom = '0'
- gridChildElement.style.right = '0'
- gridElement.appendChild(gridChildElement)
+ // add every element into the grid, so flow elements will be correctly positioned
+ const createdElements = gridElements.map((element) =>
+ addItemToGrid(element.specialSizeMeasurements),
+ )
+ createdElements.forEach((elem) => gridElement.appendChild(elem))
+
+ // get the index of the generated target element
+ const targetIndex = gridElements.findIndex((element) =>
+ EP.pathsEqual(element.elementPath, targetElement),
+ )
+ if (targetIndex < 0 || targetIndex >= createdElements.length) {
+ // this should never happen
+ return localRectangle(zeroRectangle)
+ }
+ // grab the generated element via its index
+ const gridChildElement = createdElements[targetIndex]
// Get the result and cleanup the temporary elements.
try {
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts
index fe46c424ee44..2ecadc5fa9b0 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts
@@ -1,34 +1,29 @@
import type { ElementPath } from 'utopia-shared/src/types'
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import * as EP from '../../../../core/shared/element-path'
-import type { JSXElementChild } from '../../../../core/shared/element-template'
+import type {
+ JSXElementChild,
+ SpecialSizeMeasurements,
+} from '../../../../core/shared/element-template'
import {
isJSXElement,
type ElementInstanceMetadata,
type ElementInstanceMetadataMap,
type GridContainerProperties,
} from '../../../../core/shared/element-template'
-import type { CanvasRectangle, CanvasVector } from '../../../../core/shared/math-utils'
+import type { CanvasRectangle } from '../../../../core/shared/math-utils'
import {
canvasRectangle,
canvasRectangleToLocalRectangle,
isNotNullFiniteRectangle,
nullIfInfinity,
- offsetPoint,
offsetRect,
rectangleContainsRectangleInclusive,
- rectContainsPoint,
- windowPoint,
- zeroCanvasRect,
zeroRectIfNullOrInfinity,
- zeroSize,
} from '../../../../core/shared/math-utils'
import type { CanvasCommand } from '../../commands/commands'
import { showGridControls } from '../../commands/show-grid-controls-command'
-import {
- controlsForGridPlaceholders,
- GridElementChildContainingBlockKey,
-} from '../../controls/grid-controls-for-strategies'
+import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies'
import type { CanvasStrategyFactory } from '../canvas-strategies'
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies'
import type { InteractionCanvasState } from '../canvas-strategy-types'
@@ -57,8 +52,6 @@ import { getMoveCommandsForDrag } from './shared-move-strategies-helpers'
import { toFirst } from '../../../../core/shared/optics/optic-utilities'
import { eitherRight, fromTypeGuard } from '../../../../core/shared/optics/optic-creators'
import { defaultEither } from '../../../../core/shared/either'
-import { forceNotNull } from '../../../..//core/shared/optional-utils'
-import { windowToCanvasCoordinates } from '../../dom-lookup'
export const gridMoveAbsoluteStrategy: CanvasStrategyFactory = (
canvasState: InteractionCanvasState,
@@ -197,8 +190,8 @@ export function getNewGridElementPropsCheckingOriginalGrid(
// Identify the containing block position and size.
const originalContainingBlockRectangle = getGridRelativeContainingBlock(
originalGridMetadata,
- selectedElementMetadata,
- selectedElementMetadata.specialSizeMeasurements.elementGridProperties,
+ [selectedElementMetadata],
+ selectedElementMetadata.elementPath,
)
// Capture the original position of the grid child.
@@ -347,11 +340,21 @@ function runGridMoveAbsolute(
)
// Get the containing block of the grid child.
+ const patchedSpecialSizeMeasurements: SpecialSizeMeasurements = {
+ ...selectedElementMetadata.specialSizeMeasurements,
+ elementGridProperties:
+ newGridElementProps?.gridElementProperties ??
+ selectedElementMetadata.specialSizeMeasurements.elementGridProperties,
+ }
const containingBlockRectangle = getGridRelativeContainingBlock(
originalGrid,
- selectedElementMetadata,
- newGridElementProps?.gridElementProperties ??
- selectedElementMetadata.specialSizeMeasurements.elementGridProperties,
+ [
+ {
+ ...selectedElementMetadata,
+ specialSizeMeasurements: patchedSpecialSizeMeasurements,
+ },
+ ],
+ selectedElementMetadata.elementPath,
)
// Get the appropriately shifted and typed local frame value to use.
diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx
index dc0866da7d46..011f3ed07876 100644
--- a/editor/src/components/canvas/controls/grid-controls.tsx
+++ b/editor/src/components/canvas/controls/grid-controls.tsx
@@ -2261,7 +2261,11 @@ const RulerMarkers = React.memo((props: RulerMarkersProps) => {
}
const parentGrid = elementMetadata.specialSizeMeasurements.parentContainerGridProperties
- const cellRect = calculateGridCellRectangle(store.editor.jsxMetadata, props.path)
+ const cellRect = calculateGridCellRectangle(
+ store.editor.jsxMetadata,
+ store.editor.elementPathTree,
+ props.path,
+ )
if (cellRect == null) {
return null
}
diff --git a/editor/src/components/canvas/controls/grid-measurements.tsx b/editor/src/components/canvas/controls/grid-measurements.tsx
index 8600da3ef9cc..ce2418a87189 100644
--- a/editor/src/components/canvas/controls/grid-measurements.tsx
+++ b/editor/src/components/canvas/controls/grid-measurements.tsx
@@ -10,7 +10,6 @@ import { applicative4Either, defaultEither, isRight, mapEither } from '../../../
import * as EP from '../../../core/shared/element-path'
import type {
BorderWidths,
- ElementInstanceMetadata,
ElementInstanceMetadataMap,
GridAutoOrTemplateBase,
GridElementProperties,
@@ -28,8 +27,6 @@ import type { GridIdentifier } from '../../editor/store/editor-state'
import { Substores, useEditorState } from '../../editor/store/store-hook'
import { useMonitorChangesToElements } from '../../editor/store/store-monitor'
import { isStaticGridRepeat, parseCSSLength } from '../../inspector/common/css-utils'
-import { getGridChildCellCoordBoundsFromCanvas } from '../canvas-strategies/strategies/grid-cell-bounds'
-import type { GridCellGlobalFrames } from '../canvas-strategies/strategies/grid-helpers'
import {
findOriginalGrid,
getGridRelativeContainingBlock,
@@ -42,6 +39,7 @@ import {
getGridElementProperties,
} from '../dom-walker'
import { addChangeCallback, removeChangeCallback } from '../observers'
+import type { ElementPathTrees } from '../../../core/shared/element-path-tree'
export type GridElementMeasurementHelperData = {
frame: CanvasRectangle
@@ -357,42 +355,16 @@ export function useGridElementMeasurementHelperData(
return useKeepReferenceEqualityIfPossible(getGridElementMeasurementHelperData(elementPath, scale))
}
-function getCellPositionFromCanvas(
- elementMetadata: ElementInstanceMetadata,
- parentGridCellGlobalFrames: GridCellGlobalFrames,
-): { left: number; top: number } | null {
- const cellCoordBoundsFromCanvas = getGridChildCellCoordBoundsFromCanvas(
- elementMetadata,
- parentGridCellGlobalFrames,
- )
- if (cellCoordBoundsFromCanvas == null) {
- return null
- }
-
- if (parentGridCellGlobalFrames.length === 0) {
- return null
- }
-
- const firstRow = parentGridCellGlobalFrames[0]
- const left = firstRow[cellCoordBoundsFromCanvas.column - 1].x
-
- const cellBoundsRowIndex = cellCoordBoundsFromCanvas.row - 1
- if (
- parentGridCellGlobalFrames.length <= cellBoundsRowIndex ||
- parentGridCellGlobalFrames[cellBoundsRowIndex].length === 0
- ) {
+export function calculateGridCellRectangle(
+ jsxMetadata: ElementInstanceMetadataMap,
+ pathTree: ElementPathTrees,
+ path: ElementPath,
+): CanvasRectangle | null {
+ const cellItemMetadata = MetadataUtils.findElementByElementPath(jsxMetadata, path)
+ if (cellItemMetadata == null) {
return null
}
- const firstColumn = parentGridCellGlobalFrames[cellBoundsRowIndex][0]
- const top = firstColumn.y
- return { left, top }
-}
-
-function getCellDimensions(
- jsxMetadata: ElementInstanceMetadataMap,
- cellItemMetadata: ElementInstanceMetadata,
-): { width: number; height: number } | null {
const originalGrid = findOriginalGrid(jsxMetadata, EP.parentPath(cellItemMetadata.elementPath))
if (originalGrid == null) {
return null
@@ -406,54 +378,10 @@ function getCellDimensions(
const coordinateSystemBounds =
cellItemMetadata.specialSizeMeasurements.immediateParentBounds ?? zeroCanvasRect
- const calculatedCellBounds = getGridRelativeContainingBlock(
- gridMetadata,
- cellItemMetadata,
- cellItemMetadata.specialSizeMeasurements.elementGridProperties,
- {
- forcePositionRelative: true,
- },
- )
- const cellRect = offsetRect(canvasRectangle(calculatedCellBounds), coordinateSystemBounds)
- const { width, height } = cellRect
-
- return { width, height }
-}
-
-/**
- * Returns the _actual_ canvas rectangle for a grid _cell_ (not its contained item) that's rendered on the canvas.
- * This is done by combining grabbing the position from the parent grid frames (which is required for flow elements!),
- * with the HTML calculation of dimensions in getGridRelativeContainingBlock.
- */
-export function calculateGridCellRectangle(
- jsxMetadata: ElementInstanceMetadataMap,
- path: ElementPath,
-): CanvasRectangle | null {
- const cellItemMetadata = MetadataUtils.findElementByElementPath(jsxMetadata, path)
- if (cellItemMetadata == null) {
- return null
- }
-
- const parentGridCellGlobalFrames =
- cellItemMetadata.specialSizeMeasurements.parentGridCellGlobalFrames
- if (parentGridCellGlobalFrames == null) {
- return null
- }
-
- const position = getCellPositionFromCanvas(cellItemMetadata, parentGridCellGlobalFrames)
- if (position == null) {
- return null
- }
-
- const dimensions = getCellDimensions(jsxMetadata, cellItemMetadata)
- if (dimensions == null) {
- return null
- }
+ const siblings = MetadataUtils.getSiblingsOrdered(jsxMetadata, pathTree, path)
- return canvasRectangle({
- x: position.left,
- y: position.top,
- width: dimensions.width,
- height: dimensions.height,
+ const calculatedCellBounds = getGridRelativeContainingBlock(gridMetadata, siblings, path, {
+ forcePositionRelative: true,
})
+ return offsetRect(canvasRectangle(calculatedCellBounds), coordinateSystemBounds)
}
From e34fdc378dbf7d230bc0a44223564ed42605b6f7 Mon Sep 17 00:00:00 2001
From: Federico Ruggi <1081051+ruggi@users.noreply.github.com>
Date: Tue, 10 Dec 2024 15:31:35 +0100
Subject: [PATCH 2/3] Tweak design when resizing grid stretching items (#6718)
**Problem:**
When resizing a stretching grid item there should be no blue translucent
overlay and the cursor should stay consistent during the interaction.
**Fix:**
Do that.
| Before | After |
|---------|--------------|
| ![Kapture 2024-12-10 at 13 18
45](https://github.com/user-attachments/assets/091ae790-ebab-4289-8345-45dd803c390d)
| ![Kapture 2024-12-10 at 13 17
39](https://github.com/user-attachments/assets/2a859957-8b80-4bf9-a062-5cf524930f44)
|
**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
Fixes #6717
---
.../strategies/grid-resize-element-strategy.ts | 15 +++++++++++++--
.../components/canvas/controls/grid-controls.tsx | 3 +--
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts
index 09c8a5ea3003..2141f6add524 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts
@@ -17,6 +17,8 @@ import {
import { gridContainerIdentifier, gridItemIdentifier } from '../../../editor/store/editor-state'
import { cssKeyword } from '../../../inspector/common/css-utils'
import { isFillOrStretchModeAppliedOnAnySide } from '../../../inspector/inspector-common'
+import { CSSCursor } from '../../canvas-types'
+import { setCursorCommand } from '../../commands/set-cursor-command'
import {
controlsForGridPlaceholders,
gridEdgeToEdgePosition,
@@ -172,9 +174,18 @@ export const gridResizeElementStrategy: CanvasStrategyFactory = (
),
}
+ const cursor =
+ interactionSession.activeControl.edge === 'column-start' ||
+ interactionSession.activeControl.edge === 'column-end'
+ ? CSSCursor.ColResize
+ : CSSCursor.RowResize
+
return strategyApplicationResult(
- getCommandsForGridItemPlacement(selectedElement, gridTemplate, gridProps),
- [EP.parentPath(selectedElement), selectedElement],
+ [
+ ...getCommandsForGridItemPlacement(selectedElement, gridTemplate, gridProps),
+ setCursorCommand(cursor),
+ ],
+ [(EP.parentPath(selectedElement), selectedElement)],
)
},
}
diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx
index 011f3ed07876..b0ccf8058658 100644
--- a/editor/src/components/canvas/controls/grid-controls.tsx
+++ b/editor/src/components/canvas/controls/grid-controls.tsx
@@ -1682,7 +1682,6 @@ interface GridResizeControlProps {
export const GridResizeControlsComponent = ({ target }: GridResizeControlProps) => {
const gridTarget = getGridIdentifierContainerOrComponentPath(target)
- const colorTheme = useColorTheme()
const element = useEditorState(
Substores.metadata,
@@ -1854,7 +1853,7 @@ export const GridResizeControlsComponent = ({ target }: GridResizeControlProps)
left: bounds?.x ?? element.globalFrame.x,
width: bounds?.width ?? element.globalFrame.width,
height: bounds?.height ?? element.globalFrame.height,
- backgroundColor: isResizing ? colorTheme.primary25.value : 'transparent',
+ backgroundColor: 'transparent',
}}
>
Date: Wed, 11 Dec 2024 12:24:34 +0100
Subject: [PATCH 3/3] Split controls for grid rulers and placeholders, don't
show placeholders during item resize (#6720)
**Problem:**
Grid control placeholders should not show while resizing a grid item.
**Fix:**
1. Split the grid controls into two components - one for the
placeholders and one for the ruler/markers.
2. Do not show the placeholders while resizing a grid item (via its
edges)
3. Do some prep work to only show row/col placeholder lines during
marker resize (an incremental PR will take care of that)
| Before | After |
|-----------|--------------|
| ![Kapture 2024-12-11 at 12 22
44](https://github.com/user-attachments/assets/bca7def9-91fc-4095-9cff-9058b77ee609)
| ![Kapture 2024-12-11 at 12 22
02](https://github.com/user-attachments/assets/255ceb5f-6a5e-4085-82b3-5e057ffb4c3e)
|
Fixes #[6719](https://github.com/concrete-utopia/utopia/issues/6719)
---
.../strategies/basic-resize-strategy.tsx | 12 ++-
...nge-element-location-duplicate-strategy.ts | 10 +-
...ange-element-location-keyboard-strategy.ts | 10 +-
.../grid-change-element-location-strategy.ts | 6 +-
.../strategies/grid-move-absolute.ts | 6 +-
.../strategies/grid-reorder-strategy.ts | 6 +-
.../grid-resize-element-ruler-strategy.ts | 2 +
.../grid-resize-element-strategy.ts | 4 +-
.../controls/grid-controls-for-strategies.tsx | 26 +++++
.../canvas/controls/grid-controls.tsx | 100 +++++++++++-------
10 files changed, 134 insertions(+), 48 deletions(-)
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx
index 8c3a65d3baf0..976888db8e84 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx
+++ b/editor/src/components/canvas/canvas-strategies/strategies/basic-resize-strategy.tsx
@@ -36,7 +36,10 @@ import { queueTrueUpElement } from '../../commands/queue-true-up-command'
import { setCursorCommand } from '../../commands/set-cursor-command'
import { updateHighlightedViews } from '../../commands/update-highlighted-views-command'
-import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies'
+import {
+ controlsForGridPlaceholders,
+ controlsForGridRulers,
+} from '../../controls/grid-controls-for-strategies'
import { ImmediateParentBounds } from '../../controls/parent-bounds'
import { ImmediateParentOutlines } from '../../controls/parent-outlines'
import { AbsoluteResizeControl } from '../../controls/select-mode/absolute-resize-control'
@@ -130,7 +133,12 @@ export function basicResizeStrategy(
key: 'parent-bounds-control',
show: 'visible-only-while-active',
}),
- ...(isGridCell ? [controlsForGridPlaceholders(gridItemIdentifier(selectedElement))] : []),
+ ...(isGridCell
+ ? [
+ controlsForGridPlaceholders(gridItemIdentifier(selectedElement)),
+ controlsForGridRulers(gridItemIdentifier(selectedElement)),
+ ]
+ : []),
],
fitness:
interactionSession != null &&
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-duplicate-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-duplicate-strategy.ts
index ffa2066191ae..b10120351cef 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-duplicate-strategy.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-duplicate-strategy.ts
@@ -7,7 +7,10 @@ import { setCursorCommand } from '../../commands/set-cursor-command'
import { updateHighlightedViews } from '../../commands/update-highlighted-views-command'
import { updateSelectedViews } from '../../commands/update-selected-views-command'
-import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies'
+import {
+ controlsForGridPlaceholders,
+ controlsForGridRulers,
+} from '../../controls/grid-controls-for-strategies'
import type { CanvasStrategyFactory } from '../canvas-strategies'
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies'
import type { CustomStrategyState, InteractionCanvasState } from '../canvas-strategy-types'
@@ -79,7 +82,10 @@ export const gridChangeElementLocationDuplicateStrategy: CanvasStrategyFactory =
category: 'tools',
type: 'pointer',
},
- controlsToRender: [controlsForGridPlaceholders(gridItemIdentifier(selectedElement))],
+ controlsToRender: [
+ controlsForGridPlaceholders(gridItemIdentifier(selectedElement)),
+ controlsForGridRulers(gridItemIdentifier(selectedElement)),
+ ],
fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 3),
apply: () => {
if (
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-keyboard-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-keyboard-strategy.ts
index 0bee8900873a..be83591b9b74 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-keyboard-strategy.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-keyboard-strategy.ts
@@ -2,7 +2,10 @@ import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import * as EP from '../../../../core/shared/element-path'
import type { GridPositionValue } from '../../../../core/shared/element-template'
import { gridPositionValue } from '../../../../core/shared/element-template'
-import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies'
+import {
+ controlsForGridPlaceholders,
+ controlsForGridRulers,
+} from '../../controls/grid-controls-for-strategies'
import type { CanvasStrategy, InteractionCanvasState } from '../canvas-strategy-types'
import {
emptyStrategyApplicationResult,
@@ -68,7 +71,10 @@ export function gridChangeElementLocationResizeKeyboardStrategy(
category: 'modalities',
type: 'reorder-large',
},
- controlsToRender: [controlsForGridPlaceholders(gridItemIdentifier(target))],
+ controlsToRender: [
+ controlsForGridPlaceholders(gridItemIdentifier(target)),
+ controlsForGridRulers(gridItemIdentifier(target)),
+ ],
fitness: fitness(interactionSession),
apply: () => {
if (interactionSession == null || interactionSession.interactionData.type !== 'KEYBOARD') {
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-strategy.ts
index 7ef0e98a20e6..03d6a8fcb7a7 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-strategy.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-change-element-location-strategy.ts
@@ -16,7 +16,10 @@ import { getTargetGridCellData } from '../../../inspector/grid-helpers'
import type { CanvasCommand } from '../../commands/commands'
import { reorderElement } from '../../commands/reorder-element-command'
import { showGridControls } from '../../commands/show-grid-controls-command'
-import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies'
+import {
+ controlsForGridPlaceholders,
+ controlsForGridRulers,
+} from '../../controls/grid-controls-for-strategies'
import type { CanvasStrategyFactory } from '../canvas-strategies'
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies'
import type { InteractionCanvasState } from '../canvas-strategy-types'
@@ -97,6 +100,7 @@ export const gridChangeElementLocationStrategy: CanvasStrategyFactory = (
},
controlsToRender: [
controlsForGridPlaceholders(gridItemIdentifier(selectedElement), 'visible-only-while-active'),
+ controlsForGridRulers(gridItemIdentifier(selectedElement), 'visible-only-while-active'),
],
fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 2),
apply: () => {
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts
index 2ecadc5fa9b0..aa206cd67988 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-move-absolute.ts
@@ -23,7 +23,10 @@ import {
} from '../../../../core/shared/math-utils'
import type { CanvasCommand } from '../../commands/commands'
import { showGridControls } from '../../commands/show-grid-controls-command'
-import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies'
+import {
+ controlsForGridPlaceholders,
+ controlsForGridRulers,
+} from '../../controls/grid-controls-for-strategies'
import type { CanvasStrategyFactory } from '../canvas-strategies'
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies'
import type { InteractionCanvasState } from '../canvas-strategy-types'
@@ -102,6 +105,7 @@ export const gridMoveAbsoluteStrategy: CanvasStrategyFactory = (
},
controlsToRender: [
controlsForGridPlaceholders(gridItemIdentifier(selectedElement), 'visible-only-while-active'),
+ controlsForGridRulers(gridItemIdentifier(selectedElement), 'visible-only-while-active'),
],
fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_CELL_HANDLE', 2),
apply: () => {
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-reorder-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-reorder-strategy.ts
index abec7c1437cd..52e38352c41f 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-reorder-strategy.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-reorder-strategy.ts
@@ -11,7 +11,10 @@ import { absolute } from '../../../../utils/utils'
import type { CanvasCommand } from '../../commands/commands'
import { reorderElement } from '../../commands/reorder-element-command'
import { showGridControls } from '../../commands/show-grid-controls-command'
-import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies'
+import {
+ controlsForGridPlaceholders,
+ controlsForGridRulers,
+} from '../../controls/grid-controls-for-strategies'
import type { CanvasStrategyFactory } from '../canvas-strategies'
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies'
import type { InteractionCanvasState } from '../canvas-strategy-types'
@@ -95,6 +98,7 @@ export const gridReorderStrategy: CanvasStrategyFactory = (
},
controlsToRender: [
controlsForGridPlaceholders(gridItemIdentifier(selectedElement), 'visible-only-while-active'),
+ controlsForGridRulers(gridItemIdentifier(selectedElement), 'visible-only-while-active'),
],
fitness: onlyFitWhenDraggingThisControl(
interactionSession,
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-ruler-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-ruler-strategy.ts
index 30c1fb75487a..d8eeee81929c 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-ruler-strategy.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-ruler-strategy.ts
@@ -17,6 +17,7 @@ import { gridContainerIdentifier, gridItemIdentifier } from '../../../editor/sto
import { isCSSKeyword } from '../../../inspector/common/css-utils'
import {
controlsForGridPlaceholders,
+ controlsForGridRulers,
GridResizeControls,
} from '../../controls/grid-controls-for-strategies'
import type { CanvasStrategyFactory } from '../canvas-strategies'
@@ -82,6 +83,7 @@ export const gridResizeElementRulerStrategy: CanvasStrategyFactory = (
show: 'always-visible',
},
controlsForGridPlaceholders(gridItemIdentifier(selectedElement)),
+ controlsForGridRulers(gridItemIdentifier(selectedElement)),
],
fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_RESIZE_RULER_HANDLE', 1),
apply: () => {
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts
index 2141f6add524..1c9129be903a 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts
@@ -20,7 +20,7 @@ import { isFillOrStretchModeAppliedOnAnySide } from '../../../inspector/inspecto
import { CSSCursor } from '../../canvas-types'
import { setCursorCommand } from '../../commands/set-cursor-command'
import {
- controlsForGridPlaceholders,
+ controlsForGridRulers,
gridEdgeToEdgePosition,
GridResizeControls,
} from '../../controls/grid-controls-for-strategies'
@@ -88,7 +88,7 @@ export const gridResizeElementStrategy: CanvasStrategyFactory = (
key: `grid-resize-controls-${EP.toString(selectedElement)}`,
show: 'always-visible',
},
- controlsForGridPlaceholders(gridItemIdentifier(selectedElement)),
+ controlsForGridRulers(gridItemIdentifier(selectedElement)),
],
fitness: onlyFitWhenDraggingThisControl(interactionSession, 'GRID_RESIZE_HANDLE', 1),
apply: () => {
diff --git a/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx b/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx
index 9a828d007812..b91113d0677d 100644
--- a/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx
+++ b/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx
@@ -25,6 +25,7 @@ import {
GridControlsComponent,
GridResizeControlsComponent,
GridRowColumnResizingControlsComponent,
+ GridRulersControlsComponent,
} from './grid-controls'
import { isEdgePositionOnSide } from '../canvas-utils'
import { useMonitorChangesToElements } from '../../../components/editor/store/store-monitor'
@@ -167,6 +168,15 @@ export function isGridControlsProps(props: unknown): props is GridControlsProps
export const GridControls = controlForStrategyMemoized
(GridControlsComponent)
+export interface GridRulersControlsProps {
+ type: 'GRID_RULERS_CONTROLS_PROPS'
+ targets: GridIdentifier[]
+}
+
+export const GridRulersControls = controlForStrategyMemoized(
+ GridRulersControlsComponent,
+)
+
interface GridResizeControlProps {
target: GridIdentifier
}
@@ -221,3 +231,19 @@ export function controlsForGridPlaceholders(
show: whenToShow,
}
}
+
+export function controlsForGridRulers(
+ gridPath: GridIdentifier | Array,
+ whenToShow: WhenToShowControl = 'always-visible',
+ suffix: string | null = null,
+): ControlWithProps {
+ return {
+ control: GridRulersControls,
+ props: {
+ type: 'GRID_RULERS_CONTROLS_PROPS',
+ targets: Array.isArray(gridPath) ? gridPath : [gridPath],
+ },
+ key: `GridRulersControls${suffix == null ? '' : suffix}`,
+ show: whenToShow,
+ }
+}
diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx
index b0ccf8058658..4a2475f88264 100644
--- a/editor/src/components/canvas/controls/grid-controls.tsx
+++ b/editor/src/components/canvas/controls/grid-controls.tsx
@@ -135,6 +135,7 @@ import {
import type { Property } from 'csstype'
import { isFeatureEnabled } from '../../../utils/feature-switches'
import type { ThemeObject } from '../../../uuiui/styles/theme/theme-helpers'
+import { atom, useAtom } from 'jotai'
const CELL_ANIMATION_DURATION = 0.15 // seconds
@@ -160,6 +161,8 @@ function getLabelForAxis(
const GRID_RESIZE_HANDLE_SIZE = 15 // px
+const forceShowGridPlaceholdersAtom = atom('not-visible')
+
interface GridResizingControlProps {
dimension: GridDimension
dimensionIndex: number
@@ -646,9 +649,11 @@ export const GridRowColumnResizingControlsComponent = ({
)
}
+type GridControlVisibility = 'all' | 'not-visible'
+
interface GridControlProps {
grid: GridData
- controlsVisible: 'visible' | 'not-visible'
+ controlsVisible: GridControlVisibility
}
const GridControl = React.memo(({ grid, controlsVisible }) => {
@@ -917,7 +922,7 @@ const GridControl = React.memo(({ grid, controlsVisible }) =>
gridPath: gridPath,
})
- const placeholders = controlsVisible === 'visible' ? range(0, grid.cells) : []
+ const placeholders = controlsVisible !== 'not-visible' ? range(0, grid.cells) : []
const baseStyle = getGridHelperStyleMatchingTargetGrid(grid)
const style = {
...baseStyle,
@@ -1029,7 +1034,7 @@ const GridControl = React.memo(({ grid, controlsVisible }) =>
backgroundColor:
activelyDraggingOrResizingCell != null &&
EP.toUid(cell.elementPath) !== activelyDraggingOrResizingCell &&
- controlsVisible === 'visible'
+ controlsVisible === 'all'
? '#ffffff66'
: 'transparent',
borderRadius: cell.borderRadius ?? 0,
@@ -1044,7 +1049,7 @@ const GridControl = React.memo(({ grid, controlsVisible }) =>
interactionData?.dragStart != null &&
interactionData?.drag != null &&
hoveringStart != null &&
- controlsVisible === 'visible' ? (
+ controlsVisible === 'all' ? (
{
}),
)
- const [showGridCellOutlines, setShowGridCellOutlines] = React.useState(false)
-
const isGridItemSelectedWithoutInteraction =
selectedGridItems.length > 0 && !isGridItemInteractionActive
+ const [forceShowGridPlaceholders] = useAtom(forceShowGridPlaceholdersAtom)
+
if (grids.length === 0) {
return null
}
@@ -1222,31 +1227,34 @@ export const GridControlsComponent = ({ targets }: GridControlsProps) => {
)
})}
- {/* Ruler markers */}
- {when(
- isFeatureEnabled('Grid Ruler Markers'),
- selectedGridItems.map((path) => {
- return (
-
- )
- }),
- )}
)
}
+export const GridRulersControlsComponent = React.memo(() => {
+ const selectedGridItems = useSelectedGridItems()
+
+ if (!isFeatureEnabled('Grid Ruler Markers')) {
+ return null
+ }
+ return (
+
+
+ {selectedGridItems.map((path) => {
+ return
+ })}
+
+
+ )
+})
+GridRulersControlsComponent.displayName = 'GridRulersControlsComponent'
+
const MIN_INDICATORS_DISTANCE = 32 // px
const AbsoluteDistanceIndicators = React.memo(
@@ -2195,7 +2203,6 @@ type RulerMarkerPositionData = {
}
interface RulerMarkersProps {
- setShowGridCellOutlines: (show: boolean) => void
path: ElementPath
}
@@ -2411,17 +2418,19 @@ const RulerMarkers = React.memo((props: RulerMarkersProps) => {
[canvasOffsetRef, dispatch, canvasScale],
)
+ const [, setForceShowGridPlaceholders] = useAtom(forceShowGridPlaceholdersAtom)
+
const markerMouseUp = React.useCallback(
(event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
setShowExtraMarkers(null)
setFrozenMarkers(null)
- props.setShowGridCellOutlines(false)
+ setForceShowGridPlaceholders('not-visible')
window.removeEventListener('mouseup', markerMouseUp)
},
- [props],
+ [setForceShowGridPlaceholders],
)
const rowMarkerMouseDown = React.useCallback(
@@ -2430,13 +2439,13 @@ const RulerMarkers = React.memo((props: RulerMarkersProps) => {
event.stopPropagation()
setShowExtraMarkers('row')
- props.setShowGridCellOutlines(true)
+ setForceShowGridPlaceholders('all')
setFrozenMarkers(rulerMarkerData)
startResizeInteraction(EP.toUid(props.path), edge)(event)
window.addEventListener('mouseup', markerMouseUp)
},
- [markerMouseUp, props, startResizeInteraction, rulerMarkerData],
+ [markerMouseUp, props, startResizeInteraction, rulerMarkerData, setForceShowGridPlaceholders],
)
const columnMarkerMouseDown = React.useCallback(
@@ -2445,13 +2454,13 @@ const RulerMarkers = React.memo((props: RulerMarkersProps) => {
event.stopPropagation()
setShowExtraMarkers('column')
- props.setShowGridCellOutlines(true)
+ setForceShowGridPlaceholders('all')
setFrozenMarkers(rulerMarkerData)
startResizeInteraction(EP.toUid(props.path), edge)(event)
window.addEventListener('mouseup', markerMouseUp)
},
- [markerMouseUp, props, startResizeInteraction, rulerMarkerData],
+ [markerMouseUp, props, startResizeInteraction, rulerMarkerData, setForceShowGridPlaceholders],
)
if (rulerMarkerData == null || gridRect == null) {
@@ -2647,23 +2656,41 @@ const SnapLine = React.memo(
}) => {
const colorTheme = useColorTheme()
- const [targetMarker, targetFrozenMarker] = React.useMemo(() => {
+ const targetMarker = React.useMemo(() => {
+ if (props.edge == null) {
+ return null
+ }
+ switch (props.edge) {
+ case 'column-end':
+ return props.markers.columnEnd
+ case 'column-start':
+ return props.markers.columnStart
+ case 'row-end':
+ return props.markers.rowEnd
+ case 'row-start':
+ return props.markers.rowStart
+ default:
+ assertNever(props.edge)
+ }
+ }, [props.edge, props.markers])
+
+ const targetFrozenMarker = React.useMemo(() => {
if (props.edge == null || props.frozenMarkers == null) {
- return []
+ return null
}
switch (props.edge) {
case 'column-end':
- return [props.markers.columnEnd, props.frozenMarkers.columnEnd]
+ return props.frozenMarkers.columnEnd
case 'column-start':
- return [props.markers.columnStart, props.frozenMarkers.columnStart]
+ return props.frozenMarkers.columnStart
case 'row-end':
- return [props.markers.rowEnd, props.frozenMarkers.rowEnd]
+ return props.frozenMarkers.rowEnd
case 'row-start':
- return [props.markers.rowStart, props.frozenMarkers.rowStart]
+ return props.frozenMarkers.rowStart
default:
assertNever(props.edge)
}
- }, [props.edge, props.markers, props.frozenMarkers])
+ }, [props.edge, props.frozenMarkers])
const axis = props.edge === 'column-end' || props.edge === 'column-start' ? 'column' : 'row'
@@ -2672,7 +2699,6 @@ const SnapLine = React.memo(
(store) => store.editor.canvas.scale,
'SnapLine canvasScale',
)
-
if (
props.edge == null ||
targetMarker == null ||