From 09db5c4bbd079349412cebc6f8a897947d538359 Mon Sep 17 00:00:00 2001
From: Balint Gabor <127662+gbalint@users.noreply.github.com>
Date: Mon, 29 Jul 2024 12:18:03 +0200
Subject: [PATCH] Fix selection issues (#6128)

**Problem:**

1. In a selected scene, dragging an element should drag the scene not
the element
2. In a selected scene, clicking on an element should select the element
3. In a selected scene, dragging an element under the threshold not move
anything, but select the element

4. In a selected scene, cmd-dragging an element should drag the element
not the scene
5. In a selected scene, cmd-clicking an element should select the
element
6. In a selected scene, cmd-dragging an element under the drag threshold
should not move anything, but select the element

**Fix:**
To fix 1-2-3 we need to make sure that dragging below the threshold is
handled as a "click", so the selection happens on mouse up.

To fix 4-5-6 we need to make sure the cmd-mousedown already selects the
element, which guarantees that the following dragging operates on the
element (and not the scene)

Meanwhile, the failing tests uncovered that we have an extra bug: you
can deselect a selected item if you click on a non-visible (e.g.
cropped) part of it. To fix it I modified the behavior of
`findValidTarget` in `dont-prefer-selected` mode: it means we should
change our selection to something different if possible, but we should
still not deselect it. To make this clearer I renamed
`don't-prefer-selected` to `prefer-more-specific-selection`

As I fixed the tests I had to realize that our dragging helper functions
are not good enough: we can specifiy modifier keys, but we can not
specify separate modifier keys for the mouse down and for the mouse move
events. Which is necessary because e.g. cmd changes the behavior
differently in the two cases: cmd on mouse down changes how selection
work, so it changes what will be dragged, while cmd on mouse move
changes which strategy will be applied (cmd allows reparenting).

**Manual Tests:**
I hereby swear that:

- [x] I opened a hydrogen project and it loaded
- [x] I could navigate to various routes in Preview mode

Fixes https://github.com/concrete-utopia/utopia/issues/6012
---
 ...eparent-strategy-helpers.spec.browser2.tsx |   4 +-
 .../absolute-move-strategy.spec.browser2.tsx  |  18 +-
 ...solute-reparent-strategy.spec.browser2.tsx |  12 +-
 ...solute-and-move-strategy.spec.browser2.tsx |  15 +-
 .../select-mode/select-mode-hooks.tsx         | 106 +++++++----
 .../select-mode/select-mode.spec.browser2.tsx | 168 +++++++++++++++++-
 .../controls/selection-area.spec.browser2.tsx |   9 +-
 .../text-edit-mode/text-edit-mode-hooks.tsx   |   2 +-
 .../canvas/event-helpers.test-utils.tsx       |   9 +-
 9 files changed, 285 insertions(+), 58 deletions(-)

diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-flex-reparent-strategy-helpers.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-flex-reparent-strategy-helpers.spec.browser2.tsx
index 2878285b03f9..ec806d991cf4 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-flex-reparent-strategy-helpers.spec.browser2.tsx
+++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-flex-reparent-strategy-helpers.spec.browser2.tsx
@@ -37,6 +37,7 @@ async function dragElement(
   modifiers: Modifiers,
   includeMouseUp: boolean,
   midDragCallback?: () => Promise<void>,
+  mouseDownModifiers?: Modifiers,
 ): Promise<void> {
   const targetElement = renderResult.renderedDOM.getByTestId(targetTestId)
   const targetElementBounds = targetElement.getBoundingClientRect()
@@ -51,10 +52,11 @@ async function dragElement(
   if (includeMouseUp) {
     await mouseDragFromPointToPoint(canvasControlsLayer, startPoint, endPoint, {
       modifiers: modifiers,
+      mouseDownModifiers: mouseDownModifiers,
       midDragCallback: midDragCallback,
     })
   } else {
-    await mouseDownAtPoint(canvasControlsLayer, startPoint, { modifiers: modifiers })
+    await mouseDownAtPoint(canvasControlsLayer, startPoint)
     await mouseMoveToPoint(canvasControlsLayer, endPoint, {
       modifiers: modifiers,
       eventOptions: { buttons: 1 },
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-move-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-move-strategy.spec.browser2.tsx
index 6c18d89c5a5f..537d4d8ca82c 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-move-strategy.spec.browser2.tsx
+++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-move-strategy.spec.browser2.tsx
@@ -52,6 +52,7 @@ async function dragByPixels(
   delta: WindowPoint,
   testid: string,
   modifiers: Modifiers = emptyModifiers,
+  mouseDownModifiers?: Modifiers,
 ) {
   const targetElement = editor.renderedDOM.getByTestId(testid)
   const targetElementBounds = targetElement.getBoundingClientRect()
@@ -60,6 +61,7 @@ async function dragByPixels(
 
   await mouseDragFromPointWithDelta(canvasControlsLayer, targetElementCenter, delta, {
     modifiers,
+    mouseDownModifiers,
     midDragCallback: async () => {
       NO_OP()
     },
@@ -72,8 +74,9 @@ async function dragElement(
   dragDelta: WindowPoint,
   modifiers: Modifiers,
   midDragCallback?: () => Promise<void>,
+  mouseDownModifiers?: Modifiers,
 ): Promise<void> {
-  await mouseDownAtPoint(canvasControlsLayer, startPoint, { modifiers: cmdModifier })
+  await mouseDownAtPoint(canvasControlsLayer, startPoint, { modifiers: mouseDownModifiers })
   await mouseDragFromPointWithDelta(canvasControlsLayer, startPoint, dragDelta, {
     modifiers,
     midDragCallback,
@@ -316,7 +319,7 @@ describe('Absolute Move Strategy', () => {
 
       await selectComponentsForTest(editor, [targetElement])
 
-      await dragByPixels(editor, windowPoint({ x: 15, y: 15 }), 'bbb', cmdModifier)
+      await dragByPixels(editor, windowPoint({ x: 15, y: 15 }), 'bbb')
 
       expect(getPrintedUiJsCode(editor.getEditorState())).toEqual(
         makeTestProjectCodeWithSnippet(
@@ -947,8 +950,6 @@ describe('Absolute Move Strategy', () => {
       EP.fromString(`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:aaa/bbb/ccc`),
     ])
 
-    // await wait(10000)
-
     await dragByPixels(editor, windowPoint({ x: 15, y: 15 }), 'bbb')
 
     expect(getPrintedUiJsCode(editor.getEditorState())).toEqual(
@@ -1215,7 +1216,14 @@ describe('Absolute Move Strategy', () => {
     const startPoint = windowPoint({ x: targetElementBounds.x + 5, y: targetElementBounds.y + 5 })
     const dragDelta = windowPoint({ x: 40, y: 25 })
 
-    await dragElement(canvasControlsLayer, startPoint, dragDelta, emptyModifiers)
+    await dragElement(
+      canvasControlsLayer,
+      startPoint,
+      dragDelta,
+      cmdModifier,
+      undefined,
+      cmdModifier,
+    )
 
     await renderResult.getDispatchFollowUpActionsFinished()
     expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.spec.browser2.tsx
index 49083754b234..20884f475651 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.spec.browser2.tsx
+++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.spec.browser2.tsx
@@ -72,6 +72,7 @@ async function dragElement(
   modifiers: Modifiers,
   checkCursor: CheckCursor | null,
   midDragCallback: (() => Promise<void>) | null,
+  mouseDownModifiers?: Modifiers,
 ) {
   const targetElement = renderResult.renderedDOM.getByTestId(targetTestId)
   const targetElementBounds = targetElement.getBoundingClientRect()
@@ -90,6 +91,7 @@ async function dragElement(
   await mouseClickAtPoint(canvasControlsLayer, startPoint, { modifiers: cmdModifier })
   await mouseDragFromPointWithDelta(canvasControlsLayer, startPoint, dragDelta, {
     modifiers: modifiers,
+    mouseDownModifiers: mouseDownModifiers,
     midDragCallback: combinedMidDragCallback,
   })
 }
@@ -1325,7 +1327,15 @@ export var ${BakedInStoryboardVariableName} = (props) => {
           )
 
           const dragDelta = windowPoint({ x: 50, y: 0 })
-          await dragElement(renderResult, 'child-1', dragDelta, cmdModifier, null, null)
+          await dragElement(
+            renderResult,
+            'child-1',
+            dragDelta,
+            cmdModifier,
+            null,
+            null,
+            cmdModifier,
+          )
 
           await renderResult.getDispatchFollowUpActionsFinished()
 
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/convert-to-absolute-and-move-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/convert-to-absolute-and-move-strategy.spec.browser2.tsx
index e711710d7e34..014c148bdef7 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/convert-to-absolute-and-move-strategy.spec.browser2.tsx
+++ b/editor/src/components/canvas/canvas-strategies/strategies/convert-to-absolute-and-move-strategy.spec.browser2.tsx
@@ -55,6 +55,7 @@ import {
 import { selectComponentsForTest } from '../../../../utils/utils.test-utils'
 import { ConvertToAbsoluteAndMoveStrategyID } from './convert-to-absolute-and-move-strategy'
 import CanvasActions from '../../canvas-actions'
+import { ctrlModifier } from '../../../../utils/modifiers'
 
 const complexProject = () => {
   const code = `
@@ -1140,12 +1141,7 @@ describe('Convert to absolute/escape hatch', () => {
             y: elementBounds.y + 10,
           },
           {
-            modifiers: {
-              alt: false,
-              cmd: true,
-              ctrl: true,
-              shift: false,
-            },
+            modifiers: ctrlModifier,
           },
         )
 
@@ -1805,12 +1801,7 @@ describe('Escape hatch strategy on awkward project', () => {
         y: 15,
       },
       {
-        modifiers: {
-          alt: false,
-          cmd: true,
-          ctrl: true,
-          shift: false,
-        },
+        modifiers: ctrlModifier,
       },
     )
 
diff --git a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx
index 33dabf21c3f4..2703490e0f4b 100644
--- a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx
+++ b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx
@@ -303,7 +303,7 @@ function getCandidateSelectableViews(
 export function useFindValidTarget(): (
   selectableViews: Array<ElementPath>,
   mousePoint: WindowPoint | null,
-  preferAlreadySelected: 'prefer-selected' | 'dont-prefer-selected',
+  preferAlreadySelected: 'prefer-selected' | 'prefer-more-specific-selection',
 ) => {
   elementPath: ElementPath
   isSelected: boolean
@@ -325,7 +325,7 @@ export function useFindValidTarget(): (
     (
       selectableViews: Array<ElementPath>,
       mousePoint: WindowPoint | null,
-      preferAlreadySelected: 'prefer-selected' | 'dont-prefer-selected',
+      preferAlreadySelected: 'prefer-selected' | 'prefer-more-specific-selection',
     ) => {
       const {
         selectedViews,
@@ -336,26 +336,43 @@ export function useFindValidTarget(): (
         elementPathTree,
         allElementProps,
       } = storeRef.current
-      const validElementMouseOver: ElementPath | null =
-        preferAlreadySelected === 'prefer-selected'
-          ? getSelectionOrValidTargetAtPoint(
-              componentMetadata,
-              selectedViews,
-              hiddenInstances,
-              selectableViews,
-              mousePoint,
-              canvasScale,
-              canvasOffset,
-              elementPathTree,
-              allElementProps,
-            )
-          : getValidTargetAtPoint(
-              selectableViews,
-              mousePoint,
-              canvasScale,
-              canvasOffset,
-              componentMetadata,
-            )
+      const validElementMouseOver: ElementPath | null = (() => {
+        if (preferAlreadySelected === 'prefer-selected') {
+          return getSelectionOrValidTargetAtPoint(
+            componentMetadata,
+            selectedViews,
+            hiddenInstances,
+            selectableViews,
+            mousePoint,
+            canvasScale,
+            canvasOffset,
+            elementPathTree,
+            allElementProps,
+          )
+        }
+        const newSelection = getValidTargetAtPoint(
+          selectableViews,
+          mousePoint,
+          canvasScale,
+          canvasOffset,
+          componentMetadata,
+        )
+        if (newSelection != null) {
+          return newSelection
+        }
+        return getSelectionOrValidTargetAtPoint(
+          componentMetadata,
+          selectedViews,
+          hiddenInstances,
+          selectableViews,
+          mousePoint,
+          canvasScale,
+          canvasOffset,
+          elementPathTree,
+          allElementProps,
+        )
+      })()
+
       const validElementPath: ElementPath | null =
         validElementMouseOver != null ? validElementMouseOver : null
       if (validElementPath != null) {
@@ -452,7 +469,11 @@ export function useCalculateHighlightedViews(
   return React.useCallback(
     (targetPoint: WindowPoint, eventCmdPressed: boolean) => {
       const selectableViews: Array<ElementPath> = getHighlightableViews(eventCmdPressed, false)
-      const validElementPath = findValidTarget(selectableViews, targetPoint, 'dont-prefer-selected')
+      const validElementPath = findValidTarget(
+        selectableViews,
+        targetPoint,
+        'prefer-more-specific-selection',
+      )
       const validElementPathForHover = findValidTarget(
         selectableViews,
         targetPoint,
@@ -528,14 +549,15 @@ export function useHighlightCallbacks(
 function getPreferredSelectionForEvent(
   eventType: 'mousedown' | 'mouseup' | string,
   isDoubleClick: boolean,
-): 'prefer-selected' | 'dont-prefer-selected' {
+  cmdModifier: boolean,
+): 'prefer-selected' | 'prefer-more-specific-selection' {
   // mousedown keeps selection on a single click to allow dragging overlapping elements and selection happens on mouseup
   // with continuous clicking mousedown should select
   switch (eventType) {
     case 'mousedown':
-      return isDoubleClick ? 'dont-prefer-selected' : 'prefer-selected'
+      return isDoubleClick || cmdModifier ? 'prefer-more-specific-selection' : 'prefer-selected'
     case 'mouseup':
-      return isDoubleClick ? 'prefer-selected' : 'dont-prefer-selected'
+      return isDoubleClick ? 'prefer-selected' : 'prefer-more-specific-selection'
     default:
       return 'prefer-selected'
   }
@@ -561,6 +583,7 @@ function useSelectOrLiveModeSelectAndHover(
   )
   const windowToCanvasCoordinates = useWindowToCanvasCoordinates()
   const interactionSessionHappened = React.useRef(false)
+  const draggedOverThreshold = React.useRef(false)
   const didWeHandleMouseDown = React.useRef(false) //  this is here to avoid selecting when closing text editing
 
   const { onMouseMove: innerOnMouseMove } = useHighlightCallbacks(
@@ -582,6 +605,14 @@ function useSelectOrLiveModeSelectAndHover(
           editorStoreRef.current.editor.canvas.interactionSession,
           editorStoreRef.current.editor.keysPressed['space'],
         ) || event.buttons === 4
+
+      const draggingOverThreshold =
+        editorStoreRef.current.editor.canvas.interactionSession?.interactionData?.type === 'DRAG'
+          ? editorStoreRef.current.editor.canvas.interactionSession?.interactionData?.drag != null
+          : false
+
+      draggedOverThreshold.current = draggedOverThreshold.current || draggingOverThreshold
+
       if (isDragIntention) {
         return
       }
@@ -601,20 +632,23 @@ function useSelectOrLiveModeSelectAndHover(
     (event: React.MouseEvent<HTMLDivElement>) => {
       const isLeftClick = event.button === 0
       const isRightClick = event.type === 'contextmenu' && event.detail === 0
-      const isDragIntention =
+      const isCanvasPanIntention =
         editorStoreRef.current.editor.keysPressed['space'] || event.button === 1
-      const hasInteractionSessionWithMouseMoved =
+
+      const draggingOverThreshold =
         editorStoreRef.current.editor.canvas.interactionSession?.interactionData?.type === 'DRAG'
           ? editorStoreRef.current.editor.canvas.interactionSession?.interactionData?.drag != null
           : false
+
       const hasInteractionSession = editorStoreRef.current.editor.canvas.interactionSession != null
       const hadInteractionSessionThatWasCancelled =
         interactionSessionHappened.current && !hasInteractionSession
 
       const activeControl = editorStoreRef.current.editor.canvas.interactionSession?.activeControl
+
       const mouseUpSelectionAllowed =
         didWeHandleMouseDown.current &&
-        !hadInteractionSessionThatWasCancelled &&
+        (!hadInteractionSessionThatWasCancelled || !draggedOverThreshold.current) &&
         (activeControl == null || activeControl.type === 'BOUNDING_AREA')
 
       if (event.type === 'mousedown') {
@@ -625,6 +659,7 @@ function useSelectOrLiveModeSelectAndHover(
         interactionSessionHappened.current = false
         // didWeHandleMouseDown is used to avoid selecting when closing text editing
         didWeHandleMouseDown.current = false
+        draggedOverThreshold.current = false
 
         if (!mouseUpSelectionAllowed) {
           // We should skip this mouseup
@@ -633,8 +668,8 @@ function useSelectOrLiveModeSelectAndHover(
       }
 
       if (
-        isDragIntention ||
-        hasInteractionSessionWithMouseMoved ||
+        isCanvasPanIntention ||
+        draggingOverThreshold ||
         !active ||
         !(isLeftClick || isRightClick)
       ) {
@@ -643,8 +678,13 @@ function useSelectOrLiveModeSelectAndHover(
       }
 
       const doubleClick = event.type === 'mousedown' && event.detail > 0 && event.detail % 2 === 0
+      const cmdMouseDown = event.type === 'mousedown' && event.metaKey
       const selectableViews = getSelectableViewsForSelectMode(event.metaKey, doubleClick)
-      const preferAlreadySelected = getPreferredSelectionForEvent(event.type, doubleClick)
+      const preferAlreadySelected = getPreferredSelectionForEvent(
+        event.type,
+        doubleClick,
+        event.metaKey,
+      )
       const foundTarget = findValidTarget(
         selectableViews,
         windowPoint(point(event.clientX, event.clientY)),
@@ -658,7 +698,7 @@ function useSelectOrLiveModeSelectAndHover(
       if (foundTarget != null || isDeselect) {
         if (
           event.button !== 2 &&
-          event.type !== 'mouseup' &&
+          (event.type !== 'mouseup' || cmdMouseDown) &&
           foundTarget != null &&
           draggingAllowed &&
           // grid has its own drag handling
diff --git a/editor/src/components/canvas/controls/select-mode/select-mode.spec.browser2.tsx b/editor/src/components/canvas/controls/select-mode/select-mode.spec.browser2.tsx
index d13d6108bffa..42b2fd55f15c 100644
--- a/editor/src/components/canvas/controls/select-mode/select-mode.spec.browser2.tsx
+++ b/editor/src/components/canvas/controls/select-mode/select-mode.spec.browser2.tsx
@@ -33,7 +33,6 @@ import type { Modifiers } from '../../../../utils/modifiers'
 import { cmdModifier, emptyModifiers, shiftCmdModifier } from '../../../../utils/modifiers'
 import { FOR_TESTS_setNextGeneratedUids } from '../../../../core/model/element-template-utils.test-utils'
 import type { ElementPath } from '../../../../core/shared/project-file-types'
-import { setFeatureForBrowserTestsUseInDescribeBlockOnly } from '../../../../utils/utils.test-utils'
 
 async function fireSingleClickEvents(
   target: HTMLElement,
@@ -1532,6 +1531,7 @@ describe('mouseup selection', () => {
       </div>
     `)
 
+  const ScenePath = EP.elementPath([[BakedInStoryboardUID, TestSceneUID]])
   const RedPath = EP.elementPath([
     [BakedInStoryboardUID, TestSceneUID, TestAppUID],
     ['app-root', 'red'],
@@ -1562,7 +1562,171 @@ describe('mouseup selection', () => {
     return getDOMRectCentre(elementRect)
   }
 
-  it('mouseup in the gap between a multi-selection will select the element behind if no drag happens', async () => {
+  describe('Interactions in selected scene', () => {
+    it('Can select element in selected scene by clicking on it', async () => {
+      const renderResult = await renderTestEditorWithCode(
+        MouseupTestProject,
+        'await-first-dom-report',
+      )
+
+      await renderResult.dispatch([selectComponents([ScenePath], false)], true)
+
+      const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
+
+      const redCentre = await getElementCentre('red', renderResult)
+      await mouseDownAtPoint(canvasControlsLayer, redCentre)
+
+      // selection should not change on mouse down
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([ScenePath])
+      await mouseUpAtPoint(canvasControlsLayer, redCentre)
+
+      // selection should change on mouse up
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath])
+
+      // Check nothing has changed in the project
+      expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(MouseupTestProject)
+    })
+
+    it('Can select element in selected scene by dragging it under the drag threshold', async () => {
+      const renderResult = await renderTestEditorWithCode(
+        MouseupTestProject,
+        'await-first-dom-report',
+      )
+
+      await renderResult.dispatch([selectComponents([ScenePath], false)], true)
+
+      const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
+
+      const redCentre = await getElementCentre('red', renderResult)
+      await mouseMoveToPoint(canvasControlsLayer, redCentre)
+
+      await mouseDownAtPoint(canvasControlsLayer, redCentre)
+      // selection should not change on mouse down
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([ScenePath])
+
+      await mouseMoveToPoint(canvasControlsLayer, { x: redCentre.x + 1, y: redCentre.y + 1 })
+
+      await mouseUpAtPoint(canvasControlsLayer, { x: redCentre.x + 1, y: redCentre.y + 1 })
+
+      // selection should change on mouse up because dragging was below threshold, so this interaction was something like a click
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath])
+
+      // Check nothing has changed in the project
+      expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(MouseupTestProject)
+    })
+
+    it('When the scene is selected, dragging an element inside it drags the scene and the selection doesnt change', async () => {
+      const renderResult = await renderTestEditorWithCode(
+        MouseupTestProject,
+        'await-first-dom-report',
+      )
+
+      await renderResult.dispatch([selectComponents([ScenePath], false)], true)
+
+      const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
+
+      const redCentre = await getElementCentre('red', renderResult)
+      await mouseMoveToPoint(canvasControlsLayer, redCentre)
+      await mouseDownAtPoint(canvasControlsLayer, redCentre)
+
+      // selection should not change on mouse down
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([ScenePath])
+
+      await mouseMoveToPoint(canvasControlsLayer, { x: redCentre.x + 100, y: redCentre.y + 100 })
+      await mouseUpAtPoint(canvasControlsLayer, { x: redCentre.x + 100, y: redCentre.y + 100 })
+
+      // selection should not change on mouse up because dragging of the scene was successful
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([ScenePath])
+
+      // Dragging was successful so the project has changed
+      expect(getPrintedUiJsCode(renderResult.getEditorState())).not.toEqual(MouseupTestProject)
+    })
+
+    it('When the scene is selected, cmd-mousedown selects the element inside it', async () => {
+      const renderResult = await renderTestEditorWithCode(
+        MouseupTestProject,
+        'await-first-dom-report',
+      )
+
+      await renderResult.dispatch([selectComponents([ScenePath], false)], true)
+
+      const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
+
+      const redCentre = await getElementCentre('red', renderResult)
+      await mouseMoveToPoint(canvasControlsLayer, redCentre)
+
+      await mouseDownAtPoint(canvasControlsLayer, redCentre, { modifiers: cmdModifier })
+
+      // selection should change on mouse down because cmd was down
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath])
+    })
+
+    it('When the scene is selected, cmd-drag drags the element inside it', async () => {
+      const renderResult = await renderTestEditorWithCode(
+        MouseupTestProject,
+        'await-first-dom-report',
+      )
+
+      await renderResult.dispatch([selectComponents([ScenePath], false)], true)
+
+      const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
+
+      const redCentre = await getElementCentre('red', renderResult)
+      await mouseMoveToPoint(canvasControlsLayer, redCentre)
+
+      await mouseDownAtPoint(canvasControlsLayer, redCentre, { modifiers: cmdModifier })
+
+      // selection should change on mouse down because cmd was down. This is necessary, the following drag will move the element inside
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath])
+
+      await mouseMoveToPoint(
+        canvasControlsLayer,
+        { x: redCentre.x + 100, y: redCentre.y + 100 },
+        { modifiers: cmdModifier },
+      )
+      await mouseUpAtPoint(canvasControlsLayer, { x: redCentre.x + 100, y: redCentre.y + 100 })
+
+      // selection is still on dragged the element
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath])
+
+      // Dragging was successful so the project has changed
+      expect(getPrintedUiJsCode(renderResult.getEditorState())).not.toEqual(MouseupTestProject)
+    })
+
+    it('When the scene is selected, cmd-dragging it below the drag threshold selects the element inside it', async () => {
+      const renderResult = await renderTestEditorWithCode(
+        MouseupTestProject,
+        'await-first-dom-report',
+      )
+
+      await renderResult.dispatch([selectComponents([ScenePath], false)], true)
+
+      const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID)
+
+      const redCentre = await getElementCentre('red', renderResult)
+      await mouseMoveToPoint(canvasControlsLayer, redCentre)
+
+      await mouseDownAtPoint(canvasControlsLayer, redCentre, { modifiers: cmdModifier })
+
+      // selection should change on mouse down because cmd was down
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath])
+
+      await mouseMoveToPoint(
+        canvasControlsLayer,
+        { x: redCentre.x + 100, y: redCentre.y + 100 },
+        { modifiers: cmdModifier },
+      )
+      await mouseUpAtPoint(canvasControlsLayer, { x: redCentre.x + 1, y: redCentre.y + 1 })
+
+      // selection is still on the element which was already selected on mousedown
+      expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath])
+
+      // Dragging was not successful so the project hasn't changed
+      expect(getPrintedUiJsCode(renderResult.getEditorState())).not.toEqual(MouseupTestProject)
+    })
+  })
+
+  it('mouseup in the gap between a multi-selection will not select the element behind if no drag happens', async () => {
     const renderResult = await renderTestEditorWithCode(
       MouseupTestProject,
       'await-first-dom-report',
diff --git a/editor/src/components/canvas/controls/selection-area.spec.browser2.tsx b/editor/src/components/canvas/controls/selection-area.spec.browser2.tsx
index ba2520178615..c7133afb532a 100644
--- a/editor/src/components/canvas/controls/selection-area.spec.browser2.tsx
+++ b/editor/src/components/canvas/controls/selection-area.spec.browser2.tsx
@@ -739,6 +739,7 @@ export var ${BakedInStoryboardVariableName} = (props) => {
           moveBeforeMouseDown: true,
           staggerMoveEvents: true,
           modifiers: shiftModifier,
+          mouseDownModifiers: shiftModifier,
         },
       )
 
@@ -759,6 +760,7 @@ export var ${BakedInStoryboardVariableName} = (props) => {
           moveBeforeMouseDown: true,
           staggerMoveEvents: true,
           modifiers: shiftModifier,
+          mouseDownModifiers: shiftModifier,
         },
       )
 
@@ -813,7 +815,12 @@ export var ${BakedInStoryboardVariableName} = (props) => {
       container,
       { x: rect.x + 620, y: rect.y + 100 },
       { x: rect.x + 670, y: rect.y + 310 },
-      { moveBeforeMouseDown: true, staggerMoveEvents: true, modifiers: shiftModifier },
+      {
+        moveBeforeMouseDown: true,
+        staggerMoveEvents: true,
+        modifiers: shiftModifier,
+        mouseDownModifiers: shiftModifier,
+      },
     )
 
     expect(renderResult.getEditorState().editor.selectedViews.map(EP.toString)).toEqual([
diff --git a/editor/src/components/canvas/controls/text-edit-mode/text-edit-mode-hooks.tsx b/editor/src/components/canvas/controls/text-edit-mode/text-edit-mode-hooks.tsx
index bb735835fff2..9cf14a4857c9 100644
--- a/editor/src/components/canvas/controls/text-edit-mode/text-edit-mode-hooks.tsx
+++ b/editor/src/components/canvas/controls/text-edit-mode/text-edit-mode-hooks.tsx
@@ -50,7 +50,7 @@ export function useTextEditModeSelectAndHover(active: boolean): MouseCallbacks {
       const foundTarget = findValidTarget(
         textEditableViews,
         windowPoint(point(event.clientX, event.clientY)),
-        'dont-prefer-selected',
+        'prefer-more-specific-selection',
       )
       if (foundTarget == null) {
         return
diff --git a/editor/src/components/canvas/event-helpers.test-utils.tsx b/editor/src/components/canvas/event-helpers.test-utils.tsx
index d05cdd786d87..66073a73ad42 100644
--- a/editor/src/components/canvas/event-helpers.test-utils.tsx
+++ b/editor/src/components/canvas/event-helpers.test-utils.tsx
@@ -178,6 +178,7 @@ export async function mouseDragFromPointWithDelta(
   dragDelta: Point,
   options: {
     modifiers?: Modifiers
+    mouseDownModifiers?: Modifiers
     eventOptions?: MouseEventInit
     staggerMoveEvents?: boolean
     midDragCallback?: () => Promise<void>
@@ -197,6 +198,7 @@ export async function mouseDragFromPointToPoint(
   endPoint: Point,
   options: {
     modifiers?: Modifiers
+    mouseDownModifiers?: Modifiers
     eventOptions?: MouseEventInit
     staggerMoveEvents?: boolean
     midDragCallback?: () => Promise<void>
@@ -216,10 +218,13 @@ export async function mouseDragFromPointToPoint(
   if (options.moveBeforeMouseDown) {
     await mouseMoveToPoint(eventSourceElement, startPoint, options)
   }
+
+  const mouseDownOptions = { ...options, modifiers: options.mouseDownModifiers }
+
   if (options.realMouseDown) {
-    dispatchMouseDownEventAtPoint(startPoint, options)
+    dispatchMouseDownEventAtPoint(startPoint, mouseDownOptions)
   } else {
-    await mouseDownAtPoint(eventSourceElement, startPoint, options)
+    await mouseDownAtPoint(eventSourceElement, startPoint, mouseDownOptions)
   }
 
   if (staggerMoveEvents) {