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 bf9c276238a9..aeec1504654d 100644 --- a/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx +++ b/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx @@ -217,6 +217,5 @@ export function controlsForGridPlaceholders( }, key: `GridControls${suffix == null ? '' : suffix}`, show: whenToShow, - priority: 'bottom', } } diff --git a/editor/src/components/canvas/controls/grid-controls-ruler-markers.tsx b/editor/src/components/canvas/controls/grid-controls-ruler-markers.tsx index be44ba0c6698..d3c58737b402 100644 --- a/editor/src/components/canvas/controls/grid-controls-ruler-markers.tsx +++ b/editor/src/components/canvas/controls/grid-controls-ruler-markers.tsx @@ -1,82 +1,110 @@ import React from 'react' -import { colorTheme } from '../../../uuiui' +import type { UtopiColor } from '../../../uuiui' +import { RulerMarkerIconSize } from './grid-controls' -export type RulerMarkerType = 'span-start' | 'span-end' | 'auto' | 'pinned' +export type RulerMarkerType = 'span-start' | 'span-end' | 'auto' | 'auto-short' | 'pinned' -const upFacingTriangle = ( - - - -) +interface MarkerSVGProps { + scale: number +} + +function MarkerSVG({ scale, children }: React.PropsWithChildren) { + return ( + + {children} + + ) +} + +function upFacingTriangle(fillColor: UtopiColor, scale: number): React.ReactNode { + return ( + + + + ) +} + +function rightFacingTriangle(fillColor: UtopiColor, scale: number): React.ReactNode { + return ( + + + + ) +} + +function downFacingTriangle(fillColor: UtopiColor, scale: number): React.ReactNode { + return ( + + + + ) +} + +function leftFacingTriangle(fillColor: UtopiColor, scale: number): React.ReactNode { + return ( + + + + ) +} -const rightFacingTriangle = ( - - - -) +function regularVerticalPipe(fillColor: UtopiColor, scale: number): React.ReactNode { + return ( + + + + ) +} -const downFacingTriangle = ( - - - -) +function regularHorizontalPipe(fillColor: UtopiColor, scale: number): React.ReactNode { + return ( + + + + ) +} -const leftFacingTriangle = ( - - - -) +function shortVerticalPipe(fillColor: UtopiColor, scale: number): React.ReactNode { + return ( + + + + ) +} -const verticalPipe = ( - - - -) +function shortHorizontalPipe(fillColor: UtopiColor, scale: number): React.ReactNode { + return ( + + + + ) +} -const horizontalPipe = ( - - - -) +type ColorToReactNode = (fillColor: UtopiColor, scale: number) => React.ReactNode export const rulerMarkerIcons: { - [key in RulerMarkerType]: { column: React.ReactNode; row: React.ReactNode } + [key in RulerMarkerType]: { column: ColorToReactNode; row: ColorToReactNode } } = { 'span-start': { column: rightFacingTriangle, @@ -87,8 +115,12 @@ export const rulerMarkerIcons: { row: upFacingTriangle, }, auto: { - column: verticalPipe, - row: horizontalPipe, + column: regularVerticalPipe, + row: regularHorizontalPipe, + }, + 'auto-short': { + column: shortVerticalPipe, + row: shortHorizontalPipe, }, pinned: { column: downFacingTriangle, diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index a6f18b1d6e64..10f860cac8eb 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -27,6 +27,7 @@ import { } from '../../../core/shared/element-template' import type { CanvasPoint, CanvasRectangle, LocalRectangle } from '../../../core/shared/math-utils' import { + boundingRectangleArray, canvasPoint, canvasRectangle, isFiniteRectangle, @@ -49,6 +50,7 @@ import { optionalMap } from '../../../core/shared/optional-utils' import { assertNever, NO_OP } from '../../../core/shared/utils' import { Modifier } from '../../../utils/modifiers' import { when } from '../../../utils/react-conditionals' +import type { UtopiColor } from '../../../uuiui' import { useColorTheme, UtopiaStyles } from '../../../uuiui' import { useDispatch } from '../../editor/store/dispatch-context' import { @@ -60,7 +62,6 @@ import { } from '../../editor/store/store-hook' import type { GridDimension, GridDiscreteDimension } from '../../inspector/common/css-utils' import { - isCSSKeyword, isDynamicGridRepeat, isGridCSSRepeat, printCSSNumberWithDefaultUnit, @@ -79,6 +80,7 @@ import { getGridChildCellCoordBoundsFromCanvas, gridCellTargetId, } from '../canvas-strategies/strategies/grid-cell-bounds' +import type { GridCellGlobalFrames } from '../canvas-strategies/strategies/grid-helpers' import { getGlobalFrameOfGridCellFromMetadata, getGridRelatedIndexes, @@ -128,8 +130,8 @@ import { useGridMeasurementHelperData, } from './grid-measurements' import type { Property } from 'csstype' -import { isInsertMode, isSelectMode } from '../../editor/editor-modes' import { isFeatureEnabled } from '../../../utils/feature-switches' +import type { ThemeObject } from '../../../uuiui/styles/theme/theme-helpers' const CELL_ANIMATION_DURATION = 0.15 // seconds @@ -1188,6 +1190,8 @@ export const GridControlsComponent = ({ targets }: GridControlsProps) => { }), ) + const [showGridCellOutlines, setShowGridCellOutlines] = React.useState(false) + const isGridItemSelectedWithoutInteraction = selectedGridItems.length > 0 && !isGridItemInteractionActive @@ -1215,7 +1219,9 @@ export const GridControlsComponent = ({ targets }: GridControlsProps) => { ) })} @@ -1223,7 +1229,13 @@ export const GridControlsComponent = ({ targets }: GridControlsProps) => { {when( isFeatureEnabled('Grid Ruler Markers'), selectedGridItems.map((path) => { - return + return ( + + ) }), )} @@ -2157,12 +2169,14 @@ function useSelectedGridItems(): ElementPath[] { ) } -const rulerMarkerIconSize = 12 // px +export const RulerMarkerIconSize = 11 // px type RulerMarkerData = { parentGrid: GridContainerProperties cellRect: CanvasRectangle gridRect: CanvasRectangle + otherColumnMarkers: Array + otherRowMarkers: Array columnStart: RulerMarkerPositionData columnEnd: RulerMarkerPositionData rowStart: RulerMarkerPositionData @@ -2170,14 +2184,34 @@ type RulerMarkerData = { } type RulerMarkerPositionData = { + markerType: 'selected' | 'target' | 'other' + rowOrColumn: 'row' | 'column' top: number left: number position: GridPositionOrSpan | null bound: 'start' | 'end' } -const RulerMarkers = React.memo((props: { path: ElementPath }) => { - const markers: RulerMarkerData | null = useEditorState( +interface RulerMarkersProps { + setShowGridCellOutlines: (show: boolean) => void + path: ElementPath +} + +const RulerMarkers = React.memo((props: RulerMarkersProps) => { + const gridRect: CanvasRectangle | null = useEditorState( + Substores.metadata, + (store) => { + const originalGrid = findOriginalGrid(store.editor.jsxMetadata, EP.parentPath(props.path)) + if (originalGrid == null) { + return null + } + + return MetadataUtils.getFrameOrZeroRectInCanvasCoords(originalGrid, store.editor.jsxMetadata) + }, + 'RulerMarkers gridRect', + ) + + const parentGridCellGlobalFrames = useEditorState( Substores.metadata, (store) => { const elementMetadata = MetadataUtils.findElementByElementPath( @@ -2188,6 +2222,30 @@ const RulerMarkers = React.memo((props: { path: ElementPath }) => { return null } + return elementMetadata.specialSizeMeasurements.parentGridCellGlobalFrames + }, + 'RulerMarkers parentGridCellGlobalFrames', + ) + + const rulerMarkerData: RulerMarkerData | null = useEditorState( + Substores.metadata, + (store) => { + if (gridRect == null) { + return null + } + + if (parentGridCellGlobalFrames == null) { + return null + } + + const elementMetadata = MetadataUtils.findElementByElementPath( + store.editor.jsxMetadata, + props.path, + ) + if (elementMetadata == null) { + return null + } + const elementGridProperties = elementMetadata.specialSizeMeasurements.elementGridProperties if (elementGridProperties == null) { return null @@ -2200,12 +2258,6 @@ const RulerMarkers = React.memo((props: { path: ElementPath }) => { return null } - const parentGridCellGlobalFrames = - elementMetadata.specialSizeMeasurements.parentGridCellGlobalFrames - if (parentGridCellGlobalFrames == null) { - return null - } - const cellBounds = getGridChildCellCoordBoundsFromCanvas( elementMetadata, parentGridCellGlobalFrames, @@ -2241,11 +2293,6 @@ const RulerMarkers = React.memo((props: { path: ElementPath }) => { cellBounds.height, ) - const gridRect = MetadataUtils.getFrameOrZeroRectInCanvasCoords( - originalGrid, - store.editor.jsxMetadata, - ) - const cellRect = parentGridCellGlobalFrames[cellBounds.row - 1][cellBounds.column - 1] const cellRectResized = canvasRectangle({ x: cellRect.x, @@ -2254,128 +2301,342 @@ const RulerMarkers = React.memo((props: { path: ElementPath }) => { height: height, }) + let otherColumnMarkers: Array = [] + let otherRowMarkers: Array = [] + + function addOtherMarker( + rowOrColumn: RulerMarkerPositionData['rowOrColumn'], + bound: RulerMarkerPositionData['bound'], + rulerLeft: number, + rulerTop: number, + ): void { + const otherMarker: RulerMarkerPositionData = { + markerType: 'other', + rowOrColumn: rowOrColumn, + top: rulerTop, + left: rulerLeft, + position: null, + bound: bound, + } + const addTo = rowOrColumn === 'row' ? otherRowMarkers : otherColumnMarkers + addTo.push(otherMarker) + } + + // Add the additional markers for columns. + const lastColumnIndex = parentGridCellGlobalFrames[0].length - 1 + for (let columnIndex = 0; columnIndex <= lastColumnIndex; columnIndex++) { + const cell = parentGridCellGlobalFrames[0][columnIndex] + if (left !== cell.x) { + addOtherMarker('column', 'start', cell.x, gridRect.y) + } + if (left + width !== cell.x + cell.width) { + addOtherMarker('column', 'end', cell.x + cell.width, gridRect.y) + } + } + + // Add the additional markers for rows. + const lastRowIndex = parentGridCellGlobalFrames.length - 1 + for (let rowIndex = 0; rowIndex <= lastRowIndex; rowIndex++) { + const cell = parentGridCellGlobalFrames[rowIndex][0] + if (top !== cell.y) { + addOtherMarker('row', 'start', gridRect.x, cell.y) + } + if (top + height !== cell.y + cell.height) { + addOtherMarker('row', 'end', gridRect.x, cell.y + cell.height) + } + } + + const columnStart: RulerMarkerPositionData = { + markerType: 'selected', + rowOrColumn: 'column', + top: gridRect.y, + left: left, + position: elementGridProperties.gridColumnStart, + bound: 'start', + } + const columnEnd: RulerMarkerPositionData = { + markerType: 'selected', + rowOrColumn: 'column', + top: gridRect.y, + left: left + width, + position: elementGridProperties.gridColumnEnd, + bound: 'end', + } + const rowStart: RulerMarkerPositionData = { + markerType: 'selected', + rowOrColumn: 'row', + top: top, + left: gridRect.x, + position: elementGridProperties.gridRowStart, + bound: 'start', + } + const rowEnd: RulerMarkerPositionData = { + markerType: 'selected', + rowOrColumn: 'row', + top: top + height, + left: gridRect.x, + position: elementGridProperties.gridRowEnd, + bound: 'end', + } + return { parentGrid: parentGrid, cellRect: cellRectResized, gridRect: gridRect, - columnStart: { - top: gridRect.y, - left: left, - position: elementGridProperties.gridColumnStart, - bound: 'start', - }, - columnEnd: { - top: gridRect.y, - left: left + width, - position: elementGridProperties.gridColumnEnd, - bound: 'end', - }, - rowStart: { - top: top, - left: gridRect.x, - position: elementGridProperties.gridRowStart, - bound: 'start', - }, - rowEnd: { - top: top + height, - left: gridRect.x, - position: elementGridProperties.gridRowEnd, - bound: 'end', - }, + otherColumnMarkers: otherColumnMarkers, + otherRowMarkers: otherRowMarkers, + columnStart: columnStart, + columnEnd: columnEnd, + rowStart: rowStart, + rowEnd: rowEnd, } }, 'RulerMarkers markers', ) - if (markers == null) { + const [showExtraMarkers, setShowExtraMarkers] = React.useState<'row' | 'column' | null>(null) + const markerMouseUp = React.useCallback( + (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + setShowExtraMarkers(null) + props.setShowGridCellOutlines(false) + window.removeEventListener('mouseup', markerMouseUp) + }, + [props], + ) + const rowMarkerMouseDown = React.useCallback( + (event: React.MouseEvent) => { + event.preventDefault() + event.stopPropagation() + setShowExtraMarkers('row') + props.setShowGridCellOutlines(true) + window.addEventListener('mouseup', markerMouseUp) + }, + [markerMouseUp, props], + ) + const columnMarkerMouseDown = React.useCallback( + (event: React.MouseEvent) => { + event.preventDefault() + event.stopPropagation() + setShowExtraMarkers('column') + props.setShowGridCellOutlines(true) + window.addEventListener('mouseup', markerMouseUp) + }, + [markerMouseUp, props], + ) + + if (rulerMarkerData == null || gridRect == null) { return null } return ( - {/* Indicators */} - - - - + + {/* Other markers for unselected tracks */} + {rulerMarkerData.otherColumnMarkers.map((marker, index) => { + return ( + + ) + })} + {/* Selected item markers */} + + + + + + {/* Other markers for unselected tracks */} + {rulerMarkerData.otherRowMarkers.map((marker, index) => { + return ( + + ) + })} + {/* Selected item markers */} + + + + {/* Offset lines */} + {/* Cell outline */} ) }) RulerMarkers.displayName = 'RulerMarkers' +interface GridRulerProps { + axis: 'row' | 'column' + gridRect: CanvasRectangle + cellFrames: GridCellGlobalFrames | null + rulerVisible: 'visible' | 'not-visible' +} + +const GridRuler = React.memo>( + ({ axis, gridRect, rulerVisible, cellFrames, children }) => { + const colorTheme = useColorTheme() + const scale = useEditorState( + Substores.canvas, + (store) => store.editor.canvas.scale, + 'GridRuler scale', + ) + + // Make sure the ruler extends to cover all the cells and not just the grid dimensions. + const cellMaxRect = React.useMemo(() => { + return boundingRectangleArray([gridRect, ...(cellFrames?.flat() ?? [])]) ?? gridRect + }, [cellFrames, gridRect]) + + const columnLeft = gridRect.x + const rowLeft = gridRect.x - RulerMarkerIconSize / scale + const left = axis === 'row' ? rowLeft : columnLeft + + const columnTop = gridRect.y - RulerMarkerIconSize / scale + const rowTop = gridRect.y + const top = axis === 'row' ? rowTop : columnTop + + const width = axis === 'row' ? RulerMarkerIconSize / scale : cellMaxRect.width + const height = axis === 'row' ? cellMaxRect.height : RulerMarkerIconSize / scale + + return ( +
+ {children} +
+ ) + }, +) +GridRuler.displayName = 'GridRuler' + const RulerMarkerIndicator = React.memo( (props: { + gridRect: CanvasRectangle parentGrid: GridContainerProperties marker: RulerMarkerPositionData axis: 'row' | 'column' + testID: string + visible: 'visible' | 'not-visible' + onMouseDown?: (event: React.MouseEvent) => void }) => { const colorTheme = useColorTheme() - const markerType = getRulerMarkerType({ - position: props.marker.position, - bound: props.marker.bound, - }) - const markerIcon = rulerMarkerIcons[markerType][props.axis] - const canvasScale = useEditorState( Substores.canvasOffset, (store) => store.editor.canvas.scale, 'RulerMarkerIndicator canvasScale', ) - const scaledTop = props.marker.top * canvasScale - const top = - scaledTop - - skewMarkerPosition(props.axis === 'column', props.axis, props.marker.bound, markerType) + const markerType = getRulerMarkerType(props.marker) + const markerColor = getRulerMarkerColor(colorTheme, props.marker) + const markerIcon = rulerMarkerIcons[markerType][props.axis](markerColor, canvasScale) + + const scaledTop = + props.axis === 'row' + ? props.marker.top - props.gridRect.y - RulerMarkerIconSize / canvasScale / 2 + 0.5 + : 0 - const scaledLeft = props.marker.left * canvasScale - const left = - scaledLeft - - skewMarkerPosition(props.axis === 'row', props.axis, props.marker.bound, markerType) + const scaledLeft = + props.axis === 'column' + ? props.marker.left - props.gridRect.x - RulerMarkerIconSize / canvasScale / 2 + 0.5 + : 0 const labelText = React.useMemo(() => { if (props.marker.position == null) { @@ -2388,16 +2649,17 @@ const RulerMarkerIndicator = React.memo( return (
.${labelClass}`]: { visibility: 'hidden', @@ -2417,16 +2679,17 @@ const RulerMarkerIndicator = React.memo( style={{ position: 'absolute', background: colorTheme.primary.value, - borderRadius: 2, - padding: '3px 6px', + borderRadius: 2 / canvasScale, + padding: `${3 / canvasScale}px ${6 / canvasScale}px`, color: colorTheme.white.value, - height: 20, + height: 20 / canvasScale, display: 'flex', alignItems: 'center', justifyContent: 'center', - top: props.axis === 'column' ? -23 : 0, + top: props.axis === 'column' ? -23 / canvasScale : 0, left: props.axis === 'column' ? 0 : undefined, - right: props.axis === 'row' ? rulerMarkerIconSize + 1 : undefined, + right: props.axis === 'row' ? (RulerMarkerIconSize + 1) / canvasScale : undefined, + fontSize: 11 / canvasScale, }} > {labelText} @@ -2438,15 +2701,14 @@ const RulerMarkerIndicator = React.memo( ) RulerMarkerIndicator.displayName = 'RulerMarkerIndicator' -function getRulerMarkerType(props: { - position: GridPositionOrSpan | null - bound: 'start' | 'end' -}): RulerMarkerType { - const isAuto = isAutoGridPin(props.position) - const isSpanStart = props.bound === 'start' && isGridSpan(props.position) - const isSpanEnd = props.bound === 'end' && isGridSpan(props.position) +function getRulerMarkerType(marker: RulerMarkerPositionData): RulerMarkerType { + const isAuto = isAutoGridPin(marker.position) + const isSpanStart = marker.bound === 'start' && isGridSpan(marker.position) + const isSpanEnd = marker.bound === 'end' && isGridSpan(marker.position) - if (isSpanStart) { + if (marker.markerType === 'other' || marker.markerType === 'target') { + return 'auto-short' + } else if (isSpanStart) { return 'span-start' } else if (isSpanEnd) { return 'span-end' @@ -2457,6 +2719,19 @@ function getRulerMarkerType(props: { } } +function getRulerMarkerColor(colorTheme: ThemeObject, marker: RulerMarkerPositionData): UtopiColor { + switch (marker.markerType) { + case 'selected': + return colorTheme.primary + case 'target': + return colorTheme.brandPurple + case 'other': + return colorTheme.grey65 + default: + assertNever(marker.markerType) + } +} + function getCellCanvasWidthFromBounds( grid: CanvasRectangle[][], index: number, @@ -2527,74 +2802,6 @@ export const GridHelperControls = () => { } GridHelperControls.displayName = 'GridHelperControls' -// This function returns the amount of pixels used to adjust the position of -// individual ruler markers, which need specific skews based on their shape. -function skewMarkerPosition( - isOnTheSameAxis: boolean, - axis: 'column' | 'row', - bound: 'start' | 'end', - markerType: RulerMarkerType, -): number { - if (isOnTheSameAxis) { - return rulerMarkerIconSize - } - - // span-end triangle, on the column - const spanEndColumn = axis === 'column' && markerType === 'span-end' - if (spanEndColumn) { - return 9 - } - // pinned triangle, on the column - const pinnedEndColumn = axis === 'column' && markerType === 'pinned' && bound === 'end' - if (pinnedEndColumn) { - return 5 - } - // any other ending marker, on the column - const endColumn = bound === 'end' && axis === 'column' - if (endColumn) { - return 1 - } - - // span-end triangle, on the row - const spanEndRow = axis === 'row' && markerType === 'span-end' - if (spanEndRow) { - return 9 - } - // any other ending marker, on the row - const endRow = bound === 'end' && axis === 'row' - if (endRow) { - return 4 - } - - // span-start triangle, on the column - const spanStartColumn = axis === 'column' && markerType === 'span-start' - if (spanStartColumn) { - return 0 - } - const pinnedStartColumn = axis === 'column' && markerType === 'pinned' && bound === 'start' - if (pinnedStartColumn) { - return 5 - } - // any starting marker, on the column - const startColumn = bound === 'start' && axis === 'column' - if (startColumn) { - return 1 - } - - // span-start starting triangle, on the row - const spanStartRow = axis === 'row' && markerType === 'span-start' - if (spanStartRow) { - return 0 - } - // any other starting marker, on the row - const startRow = bound === 'start' && axis === 'row' - if (startRow) { - return 4 - } - - return 0 -} - const GridCellOffsetLine = React.memo( (props: { top: number; left: number; size: number; orientation: 'vertical' | 'horizontal' }) => { const colorTheme = useColorTheme()