Skip to content

Commit

Permalink
Grid Gap Control 2 (#6538)
Browse files Browse the repository at this point in the history
**Problem:**
The orange grid gap controls are sometimes 1px off, they are one frame
behind, they show weird delays when the canvas zooms.

**Fix:**
A lot of the problem came down to `gridGapControlBoundsFromMetadata`
trying to calculate the bounding boxes of where the gaps are in the
selected Grid. However this is error prone because a lot of these boxes
will fall on fractional pixel values, and we were not rounding them the
same way Chrome was rounding the real grid.

Instead of trying to fix the math, I had an idea to replicate our hack
for the grid cell outline controls: take the
ComputedStyle.gridTemplateRows / Columns, and create a helper grid which
has the correct position and sizing for our gap outlines. The trick is
that we want to take the original grid's gap value, and turn those into
grid tracks too, setting the helper grid's gap to zero.

So if my original grid's ComputedStyle has tracks [5px 10px 15px 5px]
and a gap of 13px, we would create a helper grid with the tracks [5px
13px 10px 13px 15px 13px 5px]!

Now the only job is to fill it with the elements and make sure to only
draw anything in the tracks that correspond to gaps in the original grid
(they are always the odd numbered tracks).

This means we can offload the entire positioning and rendering to
Chrome, and makes our life much simpler going forward.

**Commit Details:** 

- Fixed Hot Reload by creating a new file
`grid-gap-control-component.tsx`
- Brand new `GridGapControlComponent`, `GridPaddingOutlineForDimension`
and `GridRowHighlight` components.
- `GridPaddingOutlineForDimension` creates the helper grid with the
inlined gaps as tracks
- `GridRowHighlight` is responsible for the visual lines, and it
recreates the original grid in the given dimension so it can have a
handle for each original row
- `GridGapHandle` mostly unchanged

---------

Co-authored-by: Federico Ruggi <1081051+ruggi@users.noreply.github.com>
  • Loading branch information
balazsbajorics and ruggi authored Oct 15, 2024
1 parent 66fc4c8 commit 9d74ac4
Show file tree
Hide file tree
Showing 9 changed files with 597 additions and 558 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
import type { InteractionSession } from '../interaction-state'
import { colorTheme } from '../../../../uuiui'
import { activeFrameTargetPath, setActiveFrames } from '../../commands/set-active-frames-command'
import type { GridGapControlProps } from '../../controls/select-mode/grid-gap-control'
import type { GridGapControlProps } from '../../controls/select-mode/grid-gap-control-component'
import { GridGapControl } from '../../controls/select-mode/grid-gap-control'
import type { GridControlsProps } from '../../controls/grid-controls-for-strategies'
import { controlsForGridPlaceholders } from '../../controls/grid-controls-for-strategies'
Expand Down
44 changes: 44 additions & 0 deletions editor/src/components/canvas/controls/grid-controls-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { CSSProperties } from 'react'
import type { GridMeasurementHelperData } from './grid-controls-for-strategies'
import { getNullableAutoOrTemplateBaseString } from './grid-controls-for-strategies'

export function getGridHelperStyleMatchingTargetGrid(
grid: GridMeasurementHelperData,
): CSSProperties {
let style: CSSProperties = {
position: 'absolute',
top: grid.frame.y,
left: grid.frame.x,
width: grid.frame.width,
height: grid.frame.height,
display: 'grid',
gridTemplateColumns: getNullableAutoOrTemplateBaseString(grid.gridTemplateColumns),
gridTemplateRows: getNullableAutoOrTemplateBaseString(grid.gridTemplateRows),
justifyContent: grid.justifyContent ?? 'initial',
alignContent: grid.alignContent ?? 'initial',
pointerEvents: 'none',
padding:
grid.padding == null
? 0
: `${grid.padding.top}px ${grid.padding.right}px ${grid.padding.bottom}px ${grid.padding.left}px`,
}

// Gap needs to be set only if the other two are not present or we'll have rendering issues
// due to how measurements are calculated.
if (grid.rowGap != null && grid.columnGap != null) {
style.rowGap = grid.rowGap
style.columnGap = grid.columnGap
} else {
if (grid.gap != null) {
style.gap = grid.gap
}
if (grid.rowGap != null) {
style.rowGap = grid.rowGap
}
if (grid.columnGap != null) {
style.columnGap = grid.columnGap
}
}

return style
}
48 changes: 5 additions & 43 deletions editor/src/components/canvas/controls/grid-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import {
} from './grid-controls-for-strategies'
import { useMaybeHighlightElement } from './select-mode/select-mode-hooks'
import { useResizeEdges } from './select-mode/use-resize-edges'
import { getGridHelperStyleMatchingTargetGrid } from './grid-controls-helpers'

const CELL_ANIMATION_DURATION = 0.15 // seconds

Expand Down Expand Up @@ -774,9 +775,9 @@ const GridControl = React.memo<GridControlProps>(({ grid }) => {
})

const placeholders = range(0, grid.cells)

const style: CSSProperties = {
...getGridControlBaseStyle(grid),
const baseStyle = getGridHelperStyleMatchingTargetGrid(grid)
const style = {
...baseStyle,
backgroundColor:
activelyDraggingOrResizingCell != null ? colorTheme.primary10.value : 'transparent',
outline: `1px solid ${
Expand Down Expand Up @@ -940,7 +941,7 @@ const GridMeasurementHelper = React.memo<{ elementPath: ElementPath }>(({ elemen
const placeholders = range(0, gridData.cells)

const style: CSSProperties = {
...getGridControlBaseStyle(gridData),
...getGridHelperStyleMatchingTargetGrid(gridData),
opacity: 1,
}

Expand Down Expand Up @@ -1643,42 +1644,3 @@ function useAllGrids(metadata: ElementInstanceMetadataMap) {
return MetadataUtils.getAllGrids(metadata)
}, [metadata])
}

function getGridControlBaseStyle(gridData: GridMeasurementHelperData) {
let style: CSSProperties = {
position: 'absolute',
top: gridData.frame.y,
left: gridData.frame.x,
width: gridData.frame.width,
height: gridData.frame.height,
display: 'grid',
gridTemplateColumns: getNullableAutoOrTemplateBaseString(gridData.gridTemplateColumns),
gridTemplateRows: getNullableAutoOrTemplateBaseString(gridData.gridTemplateRows),
justifyContent: gridData.justifyContent ?? 'initial',
alignContent: gridData.alignContent ?? 'initial',
pointerEvents: 'none',
padding:
gridData.padding == null
? 0
: `${gridData.padding.top}px ${gridData.padding.right}px ${gridData.padding.bottom}px ${gridData.padding.left}px`,
}

// Gap needs to be set only if the other two are not present or we'll have rendering issues
// due to how measurements are calculated.
if (gridData.rowGap != null && gridData.columnGap != null) {
style.rowGap = gridData.rowGap
style.columnGap = gridData.columnGap
} else {
if (gridData.gap != null) {
style.gap = gridData.gap
}
if (gridData.rowGap != null) {
style.rowGap = gridData.rowGap
}
if (gridData.columnGap != null) {
style.columnGap = gridData.columnGap
}
}

return style
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,26 @@ export function useHoverWithDelay(
): [React.MouseEventHandler, React.MouseEventHandler] {
const fadeInTimeout = React.useRef<Timeout | null>(null)

const onHoverEnd = () => {
if (fadeInTimeout.current != null) {
clearTimeout(fadeInTimeout.current)
}
fadeInTimeout.current = null
update(false)
}
const onHoverEnd = React.useCallback(
(e: React.MouseEvent) => {
if (fadeInTimeout.current != null) {
clearTimeout(fadeInTimeout.current)
}
fadeInTimeout.current = null
update(false)
},
[update],
)

const onHoverStart = () => {
fadeInTimeout.current = setTimeout(() => update(true), delay)
}
const onHoverStart = React.useCallback(
(e: React.MouseEvent) => {
if (fadeInTimeout.current != null) {
clearTimeout(fadeInTimeout.current)
}
fadeInTimeout.current = setTimeout(() => update(true), delay)
},
[update, delay],
)

return [onHoverStart, onHoverEnd]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ const GapControlSegment = React.memo<GapControlSegmentProps>((props) => {

function handleDimensions(flexDirection: FlexDirection, scale: number): Size {
if (flexDirection === 'row' || flexDirection === 'row-reverse') {
return size(3 / scale, 12 / scale)
return size(4 / scale, 12 / scale)
}
if (flexDirection === 'column' || flexDirection === 'column-reverse') {
return size(12 / scale, 4 / scale)
Expand Down
Loading

0 comments on commit 9d74ac4

Please sign in to comment.