Skip to content

Commit

Permalink
Don't select invisible portions of truncated elements during draw-to-…
Browse files Browse the repository at this point in the history
…select (#4393)

* don't select invisible portions of truncated elements

* re-enable test

* skip selected

* no scene

* fix with stagger

* tweak for corner cases

* use getAllTargetsAtPoint

---------

Co-authored-by: Federico Ruggi <[email protected]>
  • Loading branch information
ruggi and ruggishop authored Oct 18, 2023
1 parent d1fd463 commit 62f5d58
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -1981,6 +1981,8 @@ export var ${BakedInStoryboardVariableName} = (props) => {
expect(guidelines).toHaveLength(2)
await wait(5000)
},
moveBeforeMouseDown: true,
staggerMoveEvents: true,
},
)
})
Expand Down
68 changes: 67 additions & 1 deletion editor/src/components/canvas/controls/selection-area-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import * as EP from '../../../core/shared/element-path'
import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template'
import type { CanvasRectangle, WindowRectangle } from '../../../core/shared/math-utils'
import type {
CanvasRectangle,
CanvasVector,
WindowRectangle,
} from '../../../core/shared/math-utils'
import {
canvasRectangle,
getRectCenter,
isFiniteRectangle,
rectangleContainsRectangle,
rectangleIntersection,
windowRectangle,
} from '../../../core/shared/math-utils'
import type { ElementPath } from '../../../core/shared/project-file-types'
import type { KeysPressed } from '../../../utils/keyboard'
import type { InteractionSession } from '../canvas-strategies/interaction-state'
import { isDragToPan } from '../canvas-strategies/interaction-state'
import { canvasPointToWindowPoint, getAllTargetsAtPoint } from '../dom-lookup'

type ElementUnderSelectionAreaType = 'scene' | 'regular'

type ElementUnderSelectionArea = {
path: ElementPath
frame: CanvasRectangle | null
type: ElementUnderSelectionAreaType
fullyContained: boolean
selected: boolean
Expand Down Expand Up @@ -46,6 +55,8 @@ function maybeElementFrame(
export const filterUnderSelectionArea = (
paths: ElementPath[],
metadata: ElementInstanceMetadataMap,
scale: number,
canvasOffset: CanvasVector,
area: CanvasRectangle | null,
selectedViews: ElementPath[],
): ElementPath[] => {
Expand All @@ -57,6 +68,7 @@ export const filterUnderSelectionArea = (
const frame = maybeElementFrame(path, metadata)
return {
path: path,
frame: frame,
type: getElementUnderSelectionAreaType(metadata, path),
fullyContained: frame != null && rectangleContainsRectangle(area, frame),
selected: EP.containsPath(path, selectedViews),
Expand Down Expand Up @@ -99,6 +111,21 @@ export const filterUnderSelectionArea = (
if (parentScene != null && parentScene.fullyContained) {
return false
}

// NOTE: this is a mitigation step for a measuring problem, where overflowing elements'
// dimensions are not calculated correctly. This should be fixed at the root in the measurements,
// but until then this should help a bit.
if (element.type === 'regular' && !element.zeroSized) {
return isElementIntersactionActuallyUnderAreaAndVisible(
metadata,
scale,
canvasOffset,
area,
element.path,
element.frame,
element.selected,
)
}
return true
})
.map((r) => r.path)
Expand Down Expand Up @@ -132,3 +159,42 @@ export function isValidMouseEventForSelectionArea(
!isDragToPan(interactionSession, keysPressed['space'])
)
}

function isElementIntersactionActuallyUnderAreaAndVisible(
jsxMetadata: ElementInstanceMetadataMap,
canvasScale: number,
canvasOffset: CanvasVector,
area: CanvasRectangle | null,
path: ElementPath,
frame: CanvasRectangle | null,
isSelected: boolean,
): boolean {
if (area != null && frame != null && isFiniteRectangle(frame)) {
const intersect = rectangleIntersection(area, frame)
if (intersect == null) {
return false
}
if (isSelected && rectangleContainsRectangle(frame, area)) {
return true
}

const intersectionCenter = canvasPointToWindowPoint(
// since this is going to be used in a DOM lookup, we need the center because
// the targets might not be squares
getRectCenter(canvasRectangle(intersect)),
canvasScale,
canvasOffset,
)
const pathActuallyUnderArea = getAllTargetsAtPoint(
[path],
intersectionCenter,
canvasScale,
canvasOffset,
jsxMetadata,
).some((other) => EP.pathsEqual(path, other))
if (!pathActuallyUnderArea) {
return false
}
}
return true
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export function useSelectionArea(
return filterUnderSelectionArea(
allTargetsMatchingSelectableViews,
storeRef.current.jsxMetadata,
storeRef.current.scale,
storeRef.current.canvasOffset,
area,
localSelectedViews,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { mouseDragFromPointToPoint } from '../event-helpers.test-utils'
import { renderTestEditorWithCode } from '../ui-jsx.test-utils'
import { toggleHidden } from '../../editor/actions/action-creators'
import { shiftModifier } from '../../../utils/modifiers'
import { selectComponents } from '../../editor/actions/meta-actions'

describe('Selection area', () => {
it('can select an element on the storyboard', async () => {
Expand Down Expand Up @@ -768,4 +769,55 @@ export var ${BakedInStoryboardVariableName} = (props) => {
])
}
})
it('ignores elements that are not visible because of overflow', async () => {
const renderResult = await renderTestEditorWithCode(
`
import * as React from 'react'
export var ${BakedInStoryboardVariableName} = (props) => {
return (
<div data-uid='root'>
<div data-uid='outer' style={{ backgroundColor: '#aaaaaa33', position: 'absolute', left: 3, top: 67, width: 232, height: 'max-content', display: 'flex', flexDirection: 'row', padding: '45px 50px' }}>
<div data-uid='inner' style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', padding: '0px 0px 0px 20px', width: '100%', height: 70, overflowX: 'scroll', justifyContent: 'space-between', flexWrap: 'nowrap', gap: 20 }}>
{
// @utopia/uid=list
[1, 2, 3, 4, 5, 6, 7, 8].map((k) => (
<div key={k} style={{ backgroundColor: '#aaaaaa33', width: 50, height: 50 }}>
{k}
</div>
))
}
</div>
</div>
</div>
)
}
`,
'await-first-dom-report',
)
const container = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
const rect = container.getBoundingClientRect()

await mouseDragFromPointToPoint(
container,
{ x: rect.x + 620, y: rect.y + 100 },
{ x: rect.x + 670, y: rect.y + 310 },
{ moveBeforeMouseDown: true, staggerMoveEvents: true },
)

expect(renderResult.getEditorState().editor.selectedViews).toHaveLength(0)

await renderResult.dispatch(selectComponents([EP.fromString('root/outer/inner')], true), true)

await mouseDragFromPointToPoint(
container,
{ x: rect.x + 620, y: rect.y + 100 },
{ x: rect.x + 670, y: rect.y + 310 },
{ moveBeforeMouseDown: true, staggerMoveEvents: true, modifiers: shiftModifier },
)

expect(renderResult.getEditorState().editor.selectedViews.map(EP.toString)).toEqual([
'root/outer/inner',
])
})
})

0 comments on commit 62f5d58

Please sign in to comment.