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 }