Skip to content

Commit

Permalink
x for grids (#6332)
Browse files Browse the repository at this point in the history
https://github.com/user-attachments/assets/7627f89e-c87b-4246-9ac5-1f70ca4e254d

## Problem
The x shortcut isn't aware of grids

## Fix
Special case grid elements in the x shortcut.

### How it works
If an element is a grid child and it's absolute positioned (for example
after a reparent or drawing), pressing x removes the `position:
absolute`, absolute positioning prop, and any explicit width/height
props on the element, and sets
`gridRow`/`gridColumn`/`gridRowStart`/gridRowEnd`/`gridColumnStart`/gridColumnEnd`
in way that snaps the element into the grid cells it occupies.

When an element is a non-position-absolute grid child, and x is pressed,
`position: absolute` is set, the element is sized to its visual
dimensions with `width`/`height`, and `top: 0` / `left: 0` is added.
Making it possible to absolute resize the element after this is left to
a follow-up PR.

### Details
- Due to a circular dependency, a lot of grid cell related code was
factored out into `grid-cell-utils.ts`
  • Loading branch information
bkrmendy authored Sep 10, 2024
1 parent ac76e57 commit 74db368
Show file tree
Hide file tree
Showing 12 changed files with 541 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import type { InsertionSubject } from '../../editor/editor-modes'
import type { AllElementProps } from '../../editor/store/editor-state'
import type { CanvasCommand } from '../commands/commands'
import type { ActiveFrameAction } from '../commands/set-active-frames-command'
import type { GridCellCoordinates } from '../controls/grid-controls'
import type { StrategyApplicationStatus } from './interaction-state'
import type { TargetGridCellData } from './strategies/grid-helpers'
import type { GridCellCoordinates } from './strategies/grid-cell-bounds'

// TODO: fill this in, maybe make it an ADT for different strategies
export interface CustomStrategyState {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import type { ElementPath } from 'utopia-shared/src/types'
import type { ElementInstanceMetadata } from '../../../../core/shared/element-template'
import type { CanvasVector, WindowPoint, WindowRectangle } from '../../../../core/shared/math-utils'
import {
isInfinityRectangle,
offsetPoint,
rectContainsPoint,
windowPoint,
windowRectangle,
} from '../../../../core/shared/math-utils'
import { canvasPointToWindowPoint } from '../../dom-lookup'
import * as EP from '../../../../core/shared/element-path'

export type GridCellCoordinates = { row: number; column: number }

export function gridCellCoordinates(row: number, column: number): GridCellCoordinates {
return { row: row, column: column }
}

const gridCellTargetIdPrefix = 'grid-cell-target-'

export function gridCellTargetId(
gridElementPath: ElementPath,
row: number,
column: number,
): string {
return gridCellTargetIdPrefix + `${EP.toString(gridElementPath)}-${row}-${column}`
}

export function getGridCellUnderMouse(mousePoint: WindowPoint) {
return getGridCellAtPoint(mousePoint, false)
}

export function getGridCellUnderMouseRecursive(mousePoint: WindowPoint) {
return getGridCellAtPoint(mousePoint, true)
}

function isGridCellTargetId(id: string): boolean {
return id.startsWith(gridCellTargetIdPrefix)
}

export function getGridCellAtPoint(
point: WindowPoint,
duplicating: boolean,
): { id: string; coordinates: GridCellCoordinates; cellWindowRectangle: WindowRectangle } | null {
function maybeRecursivelyFindCellAtPoint(
elements: Element[],
): { element: Element; cellWindowRectangle: WindowRectangle } | null {
// If this used during duplication, the canvas controls will be in the way and we need to traverse the children too.
for (const element of elements) {
if (isGridCellTargetId(element.id)) {
const domRect = element.getBoundingClientRect()
const windowRect = windowRectangle(domRect)
if (rectContainsPoint(windowRect, point)) {
return { element: element, cellWindowRectangle: windowRect }
}
}

if (duplicating) {
const child = maybeRecursivelyFindCellAtPoint(Array.from(element.children))
if (child != null) {
return child
}
}
}

return null
}

const cellUnderMouse = maybeRecursivelyFindCellAtPoint(
document.elementsFromPoint(point.x, point.y),
)
if (cellUnderMouse == null) {
return null
}

const { element, cellWindowRectangle } = cellUnderMouse
const row = element.getAttribute('data-grid-row')
const column = element.getAttribute('data-grid-column')

return {
id: element.id,
cellWindowRectangle: cellWindowRectangle,
coordinates: gridCellCoordinates(
row == null ? 0 : parseInt(row),
column == null ? 0 : parseInt(column),
),
}
}

const GRID_BOUNDS_TOLERANCE = 5 // px

export function getGridCellBoundsFromCanvas(
cell: ElementInstanceMetadata,
canvasScale: number,
canvasOffset: CanvasVector,
) {
const cellFrame = cell.globalFrame
if (cellFrame == null || isInfinityRectangle(cellFrame)) {
return null
}

const canvasFrameWidth = cellFrame.width * canvasScale
const canvasFrameHeight = cellFrame.height * canvasScale

const cellOriginPoint = offsetPoint(
canvasPointToWindowPoint(cellFrame, canvasScale, canvasOffset),
windowPoint({ x: GRID_BOUNDS_TOLERANCE, y: GRID_BOUNDS_TOLERANCE }),
)
const cellOrigin = getGridCellAtPoint(cellOriginPoint, true)
if (cellOrigin == null) {
return null
}

const cellEndPoint = offsetPoint(
cellOriginPoint,
windowPoint({
x: canvasFrameWidth - GRID_BOUNDS_TOLERANCE,
y: canvasFrameHeight - GRID_BOUNDS_TOLERANCE,
}),
)
const cellEnd = getGridCellAtPoint(cellEndPoint, true)
if (cellEnd == null) {
return null
}

const cellOriginCoords = cellOrigin.coordinates
const cellEndCoords = cellEnd.coordinates

const cellWidth = cellEndCoords.column - cellOriginCoords.column + 1
const cellHeight = cellEndCoords.row - cellOriginCoords.row + 1

return {
column: cellOriginCoords.column,
row: cellOriginCoords.row,
width: cellWidth,
height: cellHeight,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ import {
canvasPoint,
isInfinityRectangle,
offsetPoint,
rectContainsPoint,
scaleVector,
windowPoint,
windowRectangle,
windowVector,
type WindowPoint,
} from '../../../../core/shared/math-utils'
Expand All @@ -26,83 +24,16 @@ import { setProperty } from '../../commands/set-property-command'
import { canvasPointToWindowPoint } from '../../dom-lookup'
import type { DragInteractionData } from '../interaction-state'
import type { GridCustomStrategyState, InteractionCanvasState } from '../canvas-strategy-types'
import type { GridCellCoordinates } from '../../controls/grid-controls'
import { gridCellCoordinates } from '../../controls/grid-controls'
import * as EP from '../../../../core/shared/element-path'
import { deleteProperties } from '../../commands/delete-properties-command'
import { cssNumber, isCSSKeyword } from '../../../inspector/common/css-utils'
import { setCssLengthProperty } from '../../commands/set-css-length-command'

export function getGridCellUnderMouse(mousePoint: WindowPoint) {
return getGridCellAtPoint(mousePoint, false)
}

function getGridCellUnderMouseRecursive(mousePoint: WindowPoint) {
return getGridCellAtPoint(mousePoint, true)
}

const gridCellTargetIdPrefix = 'grid-cell-target-'

export function gridCellTargetId(
gridElementPath: ElementPath,
row: number,
column: number,
): string {
return gridCellTargetIdPrefix + `${EP.toString(gridElementPath)}-${row}-${column}`
}

function isGridCellTargetId(id: string): boolean {
return id.startsWith(gridCellTargetIdPrefix)
}

export function getGridCellAtPoint(
point: WindowPoint,
duplicating: boolean,
): { id: string; coordinates: GridCellCoordinates; cellWindowRectangle: WindowRectangle } | null {
function maybeRecursivelyFindCellAtPoint(
elements: Element[],
): { element: Element; cellWindowRectangle: WindowRectangle } | null {
// If this used during duplication, the canvas controls will be in the way and we need to traverse the children too.
for (const element of elements) {
if (isGridCellTargetId(element.id)) {
const domRect = element.getBoundingClientRect()
const windowRect = windowRectangle(domRect)
if (rectContainsPoint(windowRect, point)) {
return { element: element, cellWindowRectangle: windowRect }
}
}

if (duplicating) {
const child = maybeRecursivelyFindCellAtPoint(Array.from(element.children))
if (child != null) {
return child
}
}
}

return null
}

const cellUnderMouse = maybeRecursivelyFindCellAtPoint(
document.elementsFromPoint(point.x, point.y),
)
if (cellUnderMouse == null) {
return null
}

const { element, cellWindowRectangle } = cellUnderMouse
const row = element.getAttribute('data-grid-row')
const column = element.getAttribute('data-grid-column')

return {
id: element.id,
cellWindowRectangle: cellWindowRectangle,
coordinates: gridCellCoordinates(
row == null ? 0 : parseInt(row),
column == null ? 0 : parseInt(column),
),
}
}
import type { GridCellCoordinates } from './grid-cell-bounds'
import {
getGridCellUnderMouse,
getGridCellUnderMouseRecursive,
gridCellCoordinates,
} from './grid-cell-bounds'

export function runGridRearrangeMove(
targetElement: ElementPath,
Expand Down Expand Up @@ -501,53 +432,3 @@ function gridChildAbsoluteMoveCommands(
),
]
}

const GRID_BOUNDS_TOLERANCE = 5 // px

export function getGridCellBoundsFromCanvas(
cell: ElementInstanceMetadata,
canvasScale: number,
canvasOffset: CanvasVector,
) {
const cellFrame = cell.globalFrame
if (cellFrame == null || isInfinityRectangle(cellFrame)) {
return null
}

const canvasFrameWidth = cellFrame.width * canvasScale
const canvasFrameHeight = cellFrame.height * canvasScale

const cellOriginPoint = offsetPoint(
canvasPointToWindowPoint(cellFrame, canvasScale, canvasOffset),
windowPoint({ x: GRID_BOUNDS_TOLERANCE, y: GRID_BOUNDS_TOLERANCE }),
)
const cellOrigin = getGridCellAtPoint(cellOriginPoint, true)
if (cellOrigin == null) {
return null
}

const cellEndPoint = offsetPoint(
cellOriginPoint,
windowPoint({
x: canvasFrameWidth - GRID_BOUNDS_TOLERANCE,
y: canvasFrameHeight - GRID_BOUNDS_TOLERANCE,
}),
)
const cellEnd = getGridCellAtPoint(cellEndPoint, true)
if (cellEnd == null) {
return null
}

const cellOriginCoords = cellOrigin.coordinates
const cellEndCoords = cellEnd.coordinates

const cellWidth = cellEndCoords.column - cellOriginCoords.column + 1
const cellHeight = cellEndCoords.row - cellOriginCoords.row + 1

return {
column: cellOriginCoords.column,
row: cellOriginCoords.row,
width: cellWidth,
height: cellHeight,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
strategyApplicationResult,
} from '../canvas-strategy-types'
import type { InteractionSession } from '../interaction-state'
import { getGridCellBoundsFromCanvas, setGridPropsCommands } from './grid-helpers'
import { setGridPropsCommands } from './grid-helpers'
import { getGridCellBoundsFromCanvas } from './grid-cell-bounds'
import { accumulatePresses } from './shared-keyboard-strategy-helpers'

export function gridRearrangeResizeKeyboardStrategy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { GridCellTestId } from '../../controls/grid-controls'
import { mouseDragFromPointToPoint } from '../../event-helpers.test-utils'
import type { EditorRenderResult } from '../../ui-jsx.test-utils'
import { renderTestEditorWithCode } from '../../ui-jsx.test-utils'
import { gridCellTargetId } from './grid-helpers'
import { gridCellTargetId } from './grid-cell-bounds'

describe('grid rearrange move strategy', () => {
it('can rearrange elements on a grid', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import { flattenSelection } from './shared-move-strategies-helpers'
import type { CanvasRectangle, CanvasVector } from '../../../../core/shared/math-utils'
import { canvasVector, isInfinityRectangle, offsetPoint } from '../../../../core/shared/math-utils'
import { showGridControls } from '../../commands/show-grid-controls-command'
import type { GridCellCoordinates } from '../../controls/grid-controls'
import { GridControls } from '../../controls/grid-controls'
import {
gridPositionValue,
Expand All @@ -49,6 +48,7 @@ import { removeAbsolutePositioningProps } from './reparent-helpers/reparent-prop
import { canvasPointToWindowPoint } from '../../dom-lookup'
import type { TargetGridCellData } from './grid-helpers'
import { getTargetCell, setGridPropsCommands } from './grid-helpers'
import type { GridCellCoordinates } from './grid-cell-bounds'

export function gridReparentStrategy(
reparentTarget: ReparentTarget,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import { mouseDragFromPointToPoint } from '../../event-helpers.test-utils'
import type { EditorRenderResult } from '../../ui-jsx.test-utils'
import { renderTestEditorWithCode } from '../../ui-jsx.test-utils'
import type { GridResizeEdge } from '../interaction-state'
import { gridCellTargetId } from './grid-helpers'
import { wait } from '../../../../core/model/performance-scripts'
import { gridCellTargetId } from './grid-cell-bounds'

async function runCellResizeTest(
editor: EditorRenderResult,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
strategyApplicationResult,
} from '../canvas-strategy-types'
import type { InteractionSession } from '../interaction-state'
import { getGridCellUnderMouse } from './grid-cell-bounds'
import type { TargetGridCellData } from './grid-helpers'
import { getGridCellUnderMouse, setGridPropsCommands } from './grid-helpers'
import { setGridPropsCommands } from './grid-helpers'

export const gridResizeElementStrategy: CanvasStrategyFactory = (
canvasState: InteractionCanvasState,
Expand Down
9 changes: 2 additions & 7 deletions editor/src/components/canvas/controls/grid-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import {
GridResizeEdges,
gridResizeHandle,
} from '../canvas-strategies/interaction-state'
import { gridCellTargetId } from '../canvas-strategies/strategies/grid-helpers'
import { resizeBoundingBoxFromSide } from '../canvas-strategies/strategies/resize-helpers'
import type { EdgePosition } from '../canvas-types'
import {
Expand All @@ -80,17 +79,13 @@ import { useCanvasAnimation } from '../ui-jsx-canvas-renderer/animation-context'
import { CanvasOffsetWrapper } from './canvas-offset-wrapper'
import { CanvasLabel } from './select-mode/controls-common'
import { useMaybeHighlightElement } from './select-mode/select-mode-hooks'
import type { GridCellCoordinates } from '../canvas-strategies/strategies/grid-cell-bounds'
import { gridCellTargetId } from '../canvas-strategies/strategies/grid-cell-bounds'

const CELL_ANIMATION_DURATION = 0.15 // seconds

export const GridCellTestId = (elementPath: ElementPath) => `grid-cell-${EP.toString(elementPath)}`

export type GridCellCoordinates = { row: number; column: number }

export function gridCellCoordinates(row: number, column: number): GridCellCoordinates {
return { row: row, column: column }
}

function getCellsCount(template: GridAutoOrTemplateBase | null): number {
if (template == null) {
return 0
Expand Down
1 change: 1 addition & 0 deletions editor/src/components/editor/global-shortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,7 @@ export function handleKeyDown(
editor.allElementProps,
editor.elementPathTree,
editor.selectedViews,
{ scale: editor.canvas.scale, offset: editor.canvas.realCanvasOffset },
)

if (commands.length === 0) {
Expand Down
Loading

0 comments on commit 74db368

Please sign in to comment.