diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-cell-bounds.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-cell-bounds.ts
index a4378542506f..3d42495f0e96 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-cell-bounds.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-cell-bounds.ts
@@ -68,12 +68,13 @@ export function getClosestGridCellToPointFromMetadata(
return null
}
- return getClosestGridCellToPoint(gridCellGlobalFrames, point)
+ return getClosestGridCellToPoint(gridCellGlobalFrames, point, 'exclusive')
}
export function getClosestGridCellToPoint(
gridCellGlobalFrames: GridCellGlobalFrames,
point: CanvasPoint,
+ distanceMatch: 'inclusive' | 'exclusive',
): TargetGridCellData | null {
let closestCell: TargetGridCellData | null = null
let closestDistance = Infinity
@@ -81,7 +82,11 @@ export function getClosestGridCellToPoint(
for (let i = 0; i < gridCellGlobalFrames.length; i++) {
for (let j = 0; j < gridCellGlobalFrames[i].length; j++) {
const currentDistance = distanceFromPointToRectangle(point, gridCellGlobalFrames[i][j])
- if (currentDistance < closestDistance) {
+ const closeEnough =
+ distanceMatch === 'inclusive'
+ ? currentDistance <= closestDistance
+ : currentDistance < closestDistance
+ if (closeEnough) {
closestCell = {
gridCellCoordinates: gridCellCoordinates(i + 1, j + 1),
cellCanvasRectangle: gridCellGlobalFrames[i][j],
@@ -102,7 +107,7 @@ export function getGridChildCellCoordBoundsFromCanvas(
return null
}
- const cellOrigin = getClosestGridCellToPoint(gridCellGlobalFrames, cellFrame)
+ const cellOrigin = getClosestGridCellToPoint(gridCellGlobalFrames, cellFrame, 'inclusive')
if (cellOrigin == null) {
return null
}
@@ -114,7 +119,7 @@ export function getGridChildCellCoordBoundsFromCanvas(
y: cellFrame.height,
}),
)
- const cellEnd = getClosestGridCellToPoint(gridCellGlobalFrames, cellEndPoint)
+ const cellEnd = getClosestGridCellToPoint(gridCellGlobalFrames, cellEndPoint, 'exclusive')
if (cellEnd == null) {
return null
}
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 0710211b683c..f9df02f8816e 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts
+++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts
@@ -73,7 +73,7 @@ export function gridPositionToValue(
return p.numericalPosition
}
-export function isAutoGridPin(v: GridPositionOrSpan): boolean {
+export function isAutoGridPin(v: GridPositionOrSpan | null): boolean {
return isCSSKeyword(v) && v.value === 'auto'
}
@@ -498,6 +498,7 @@ export function getOriginalElementGridConfiguration(
const draggingFromCellCoords = getClosestGridCellToPoint(
gridCellGlobalFrames,
interactionData.dragStart,
+ 'exclusive',
)?.gridCellCoordinates
if (draggingFromCellCoords == null) {
return null
diff --git a/editor/src/components/canvas/controls/grid-controls-ruler-markers.tsx b/editor/src/components/canvas/controls/grid-controls-ruler-markers.tsx
new file mode 100644
index 000000000000..be44ba0c6698
--- /dev/null
+++ b/editor/src/components/canvas/controls/grid-controls-ruler-markers.tsx
@@ -0,0 +1,97 @@
+import React from 'react'
+import { colorTheme } from '../../../uuiui'
+
+export type RulerMarkerType = 'span-start' | 'span-end' | 'auto' | 'pinned'
+
+const upFacingTriangle = (
+
+)
+
+const rightFacingTriangle = (
+
+)
+
+const downFacingTriangle = (
+
+)
+
+const leftFacingTriangle = (
+
+)
+
+const verticalPipe = (
+
+)
+
+const horizontalPipe = (
+
+)
+
+export const rulerMarkerIcons: {
+ [key in RulerMarkerType]: { column: React.ReactNode; row: React.ReactNode }
+} = {
+ 'span-start': {
+ column: rightFacingTriangle,
+ row: downFacingTriangle,
+ },
+ 'span-end': {
+ column: leftFacingTriangle,
+ row: upFacingTriangle,
+ },
+ auto: {
+ column: verticalPipe,
+ row: horizontalPipe,
+ },
+ pinned: {
+ column: downFacingTriangle,
+ row: rightFacingTriangle,
+ },
+}
diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx
index a8f61ad94128..aa6503487aee 100644
--- a/editor/src/components/canvas/controls/grid-controls.tsx
+++ b/editor/src/components/canvas/controls/grid-controls.tsx
@@ -68,7 +68,10 @@ import {
gridResizeHandle,
} from '../canvas-strategies/interaction-state'
import type { GridCellCoordinates } from '../canvas-strategies/strategies/grid-cell-bounds'
-import { gridCellTargetId } from '../canvas-strategies/strategies/grid-cell-bounds'
+import {
+ getGridChildCellCoordBoundsFromCanvas,
+ gridCellTargetId,
+} from '../canvas-strategies/strategies/grid-cell-bounds'
import {
getGlobalFrameOfGridCellFromMetadata,
getGridRelatedIndexes,
@@ -78,6 +81,8 @@ import {
getGridIdentifierContainerOrComponentPath,
gridIdentifierToString,
gridIdentifiersSimilar,
+ findOriginalGrid,
+ isAutoGridPin,
} from '../canvas-strategies/strategies/grid-helpers'
import { canResizeGridTemplate } from '../canvas-strategies/strategies/resize-grid-strategy'
import { resizeBoundingBoxFromSide } from '../canvas-strategies/strategies/resize-helpers'
@@ -113,6 +118,8 @@ import { PinOutline, usePropsOrJSXAttributes } from './position-outline'
import { getLayoutProperty } from '../../../core/layout/getLayoutProperty'
import { styleStringInArray } from '../../../utils/common-constants'
import { gridContainerIdentifier, type GridIdentifier } from '../../editor/store/editor-state'
+import type { RulerMarkerType } from './grid-controls-ruler-markers'
+import { rulerMarkerIcons } from './grid-controls-ruler-markers'
const CELL_ANIMATION_DURATION = 0.15 // seconds
@@ -538,7 +545,7 @@ export const GridRowColumnResizingControlsComponent = ({
'GridRowColumnResizingControls scale',
)
- const isGridItemSelected = useIsGridItemSelected()
+ const selectedGridItems = useSelectedGridItems()
const gridsWithVisibleResizeControls = React.useMemo(() => {
return grids.filter((grid) => {
@@ -575,7 +582,7 @@ export const GridRowColumnResizingControlsComponent = ({
return (
{gridsWithVisibleResizeControls.flatMap((grid) => {
- if (isGridItemSelected) {
+ if (selectedGridItems.length > 0) {
return null
}
return (
@@ -598,7 +605,7 @@ export const GridRowColumnResizingControlsComponent = ({
)
})}
{gridsWithVisibleResizeControls.flatMap((grid) => {
- if (isGridItemSelected) {
+ if (selectedGridItems.length > 0) {
return null
}
return (
@@ -1158,7 +1165,7 @@ export const GridControlsComponent = ({ targets }: GridControlsProps) => {
const gridsWithVisibleControls: Array = [...targets, ...hoveredGrids]
- const isGridItemSelected = useIsGridItemSelected()
+ const selectedGridItems = useSelectedGridItems()
const isGridItemInteractionActive = useIsGridItemInteractionActive()
// Uniqify the grid paths, and then sort them by depth.
@@ -1174,7 +1181,8 @@ export const GridControlsComponent = ({ targets }: GridControlsProps) => {
}),
)
- const isGridItemSelectedWithoutInteraction = isGridItemSelected && !isGridItemInteractionActive
+ const isGridItemSelectedWithoutInteraction =
+ selectedGridItems.length > 0 && !isGridItemInteractionActive
if (grids.length === 0) {
return null
@@ -1204,6 +1212,10 @@ export const GridControlsComponent = ({ targets }: GridControlsProps) => {
/>
)
})}
+ {/* Ruler markers */}
+ {selectedGridItems.map((path) => {
+ return
+ })}
@@ -2163,10 +2175,282 @@ function useIsGridItemInteractionActive() {
)
}
-function useIsGridItemSelected() {
+function useSelectedGridItems(): ElementPath[] {
const selectedViewsRef = useRefEditorState((store) => store.editor.selectedViews)
const jsxMetadataRef = useRefEditorState((store) => store.editor.jsxMetadata)
- return selectedViewsRef.current.some((selected) =>
+ return selectedViewsRef.current.filter((selected) =>
MetadataUtils.isGridItem(jsxMetadataRef.current, selected),
)
}
+
+const rulerMarkerIconSize = 12 // px
+
+type RulerMarkerData = {
+ columnStart: RulerMarkerPositionData
+ columnEnd: RulerMarkerPositionData
+ rowStart: RulerMarkerPositionData
+ rowEnd: RulerMarkerPositionData
+}
+
+type RulerMarkerPositionData = {
+ top: number
+ left: number
+ position: GridPositionOrSpan | null
+ counterpart: GridPositionOrSpan | null
+ bound: 'start' | 'end'
+}
+
+const RulerMarkers = React.memo((props: { path: ElementPath }) => {
+ const markers: RulerMarkerData | null = useEditorState(
+ Substores.metadata,
+ (store) => {
+ const elementMetadata = MetadataUtils.findElementByElementPath(
+ store.editor.jsxMetadata,
+ props.path,
+ )
+ if (elementMetadata == null) {
+ return null
+ }
+
+ const elementGridProperties = elementMetadata.specialSizeMeasurements.elementGridProperties
+ if (elementGridProperties == null) {
+ return null
+ }
+
+ const originalGrid = findOriginalGrid(store.editor.jsxMetadata, EP.parentPath(props.path))
+ if (originalGrid == null) {
+ return null
+ }
+
+ const parentGridCellGlobalFrames =
+ elementMetadata.specialSizeMeasurements.parentGridCellGlobalFrames
+ if (parentGridCellGlobalFrames == null) {
+ return null
+ }
+
+ const cellBounds = getGridChildCellCoordBoundsFromCanvas(
+ elementMetadata,
+ parentGridCellGlobalFrames,
+ )
+ if (cellBounds == null) {
+ return null
+ }
+
+ if (parentGridCellGlobalFrames.length === 0) {
+ return null
+ }
+ const firstRow = parentGridCellGlobalFrames[0]
+ const cellBoundsColumnIndex = cellBounds.column - 1
+ const left = firstRow[cellBoundsColumnIndex].x
+ const width = getCellCanvasWidthFromBounds(
+ parentGridCellGlobalFrames,
+ cellBoundsColumnIndex,
+ cellBounds.width,
+ )
+
+ const cellBoundsRowIndex = cellBounds.row - 1
+ if (
+ parentGridCellGlobalFrames.length <= cellBoundsRowIndex ||
+ parentGridCellGlobalFrames[cellBoundsRowIndex].length === 0
+ ) {
+ return null
+ }
+ const firstColumn = parentGridCellGlobalFrames[cellBoundsRowIndex][0]
+ const top = firstColumn.y
+ const height = getCellCanvasHeightFromBounds(
+ parentGridCellGlobalFrames,
+ cellBoundsRowIndex,
+ cellBounds.height,
+ )
+
+ const gridRect = MetadataUtils.getFrameOrZeroRectInCanvasCoords(
+ originalGrid,
+ store.editor.jsxMetadata,
+ )
+
+ return {
+ columnStart: {
+ top: gridRect.y,
+ left: left,
+ position: elementGridProperties.gridColumnStart,
+ counterpart: elementGridProperties.gridColumnEnd,
+ bound: 'start',
+ },
+ columnEnd: {
+ top: gridRect.y,
+ left: left + width,
+ position: elementGridProperties.gridColumnEnd,
+ counterpart: elementGridProperties.gridColumnStart,
+ bound: 'end',
+ },
+ rowStart: {
+ top: top,
+ left: gridRect.x,
+ position: elementGridProperties.gridRowStart,
+ counterpart: elementGridProperties.gridRowEnd,
+ bound: 'start',
+ },
+ rowEnd: {
+ top: top + height,
+ left: gridRect.x,
+ position: elementGridProperties.gridRowEnd,
+ counterpart: elementGridProperties.gridRowStart,
+ bound: 'end',
+ },
+ }
+ },
+ 'RulerMarkers markers',
+ )
+
+ if (markers == null) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+
+ )
+})
+RulerMarkers.displayName = 'RulerMarkers'
+
+const RulerMarkerIndicator = React.memo(
+ (props: { marker: RulerMarkerPositionData; axis: 'row' | 'column' }) => {
+ const colorTheme = useColorTheme()
+
+ const markerType = getRulerMarkerType({
+ position: props.marker.position,
+ counterpart: props.marker.counterpart,
+ bound: props.marker.bound,
+ })
+ const markerIcon = rulerMarkerIcons[markerType][props.axis]
+
+ const canvasScale = useEditorState(
+ Substores.canvasOffset,
+ (store) => store.editor.canvas.scale,
+ 'RulerMarkerIndicator canvasScale',
+ )
+
+ function skewMarkerPosition(axis: 'column' | 'row') {
+ if (props.axis === axis) {
+ return rulerMarkerIconSize
+ } else if (markerType === 'span-end') {
+ return rulerMarkerIconSize - 1 // adjust span end position so it just touches the grid line
+ } else {
+ return rulerMarkerIconSize / 2
+ }
+ }
+
+ const scaledTop = props.marker.top * canvasScale
+ const top = scaledTop - skewMarkerPosition('column')
+
+ const scaledLeft = props.marker.left * canvasScale
+ const left = scaledLeft - skewMarkerPosition('row')
+
+ return (
+
+ {markerIcon}
+
+ )
+ },
+)
+RulerMarkerIndicator.displayName = 'RulerMarkerIndicator'
+
+function getRulerMarkerType(props: {
+ position: GridPositionOrSpan | null
+ counterpart: GridPositionOrSpan | null
+ bound: 'start' | 'end'
+}): RulerMarkerType {
+ const isAuto =
+ isAutoGridPin(props.position) ||
+ (props.bound === 'start' && isGridSpan(props.position) && isAutoGridPin(props.counterpart))
+ const isSpanStart =
+ props.bound === 'start' && isGridSpan(props.position) && isGridSpan(props.counterpart)
+ const isSpanEnd =
+ props.bound === 'end' && (isGridSpan(props.position) || isGridSpan(props.counterpart))
+
+ if (isSpanStart) {
+ return 'span-start'
+ } else if (isSpanEnd) {
+ return 'span-end'
+ } else if (isAuto) {
+ return 'auto'
+ } else {
+ return 'pinned'
+ }
+}
+
+function getCellCanvasWidthFromBounds(
+ grid: CanvasRectangle[][],
+ index: number,
+ cells: number,
+): number {
+ if (grid.length === 0) {
+ return 0
+ }
+
+ const currentRow = grid[0]
+ if (currentRow.length <= index) {
+ return 0
+ }
+ if (cells <= 1) {
+ return currentRow[index].width
+ }
+
+ function getPadding() {
+ if (currentRow.length <= 1) {
+ return 0
+ }
+ return currentRow[1].x - (currentRow[0].x + currentRow[0].width)
+ }
+ const padding = getPadding()
+
+ return currentRow.slice(index + 1, index + cells).reduce((acc, curr) => {
+ return acc + curr.width + padding
+ }, currentRow[index].width)
+}
+
+function getCellCanvasHeightFromBounds(
+ grid: CanvasRectangle[][],
+ index: number,
+ cells: number,
+): number {
+ const columns = grid.map((row) => row[0])
+ if (columns.length <= index) {
+ return 0
+ }
+
+ const currentColumn = columns[index]
+
+ if (cells <= 1) {
+ return currentColumn.height
+ }
+
+ function getPadding() {
+ if (grid.length <= 1) {
+ return 0
+ }
+ return grid[1][0].y - (grid[0][0].y + grid[0][0].height)
+ }
+ const padding = getPadding()
+
+ return columns.slice(index + 1, index + cells).reduce((acc, curr) => {
+ return acc + curr.height + padding
+ }, currentColumn.height)
+}
diff --git a/editor/src/components/inspector/grid-helpers.ts b/editor/src/components/inspector/grid-helpers.ts
index 3b91b1feb7a1..da8f9b4a67c0 100644
--- a/editor/src/components/inspector/grid-helpers.ts
+++ b/editor/src/components/inspector/grid-helpers.ts
@@ -79,7 +79,7 @@ export function getTargetGridCellData(
return null
}
const mousePos = offsetPoint(interactionData.dragStart, interactionData.drag)
- const targetCellData = getClosestGridCellToPoint(gridCellGlobalFrames, mousePos)
+ const targetCellData = getClosestGridCellToPoint(gridCellGlobalFrames, mousePos, 'exclusive')
if (targetCellData == null) {
return null
}