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 ||