Skip to content

Commit

Permalink
Add support for touch-based graph navigation and selection (#11056)
Browse files Browse the repository at this point in the history
Fixes #9493

Tested on windows desktop (standard mouse), macbook touchpad and iphone, which should behave similarily to windows touchscreen devices.
  • Loading branch information
Frizi authored Sep 20, 2024
1 parent b53d7b0 commit c996707
Show file tree
Hide file tree
Showing 27 changed files with 539 additions and 702 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- [Fixed "rename project" button being broken after not changing project
name][11103]
- [Numbers starting with dot (`.5`) are accepted in Numeric Widget][11108]
- [Add support for interacting with graph editor using touch devices.][11056]

[10774]: https://github.com/enso-org/enso/pull/10774
[10814]: https://github.com/enso-org/enso/pull/10814
Expand All @@ -35,6 +36,7 @@
[11030]: https://github.com/enso-org/enso/pull/11030
[11103]: https://github.com/enso-org/enso/pull/11103
[11108]: https://github.com/enso-org/enso/pull/11108
[11056]: https://github.com/enso-org/enso/pull/11056

#### Enso Standard Library

Expand Down
1 change: 0 additions & 1 deletion app/dashboard/src/components/aria.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { Mutable } from 'enso-common/src/utilities/data/object'
import * as aria from 'react-aria'

export type * from '@react-types/shared'
// @ts-expect-error The conflicting exports are props types ONLY.
export * from 'react-aria'
// @ts-expect-error The conflicting exports are props types ONLY.
export * from 'react-aria-components'
Expand Down
1 change: 1 addition & 0 deletions app/gui2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@noble/hashes": "^1.4.0",
"@tanstack/vue-query": ">= 5.54.0 < 5.56.0",
"@vueuse/core": "^10.4.1",
"@vueuse/gesture": "^2.0.0",
"ag-grid-community": "^30.2.1",
"ag-grid-enterprise": "^30.2.1",
"ag-grid-vue3": "^30.2.1",
Expand Down
3 changes: 2 additions & 1 deletion app/gui2/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function checkAvailablePort(port: number) {

const portFromEnv = parseInt(process.env.PLAYWRIGHT_PORT ?? '', 10)
const PORT = Number.isFinite(portFromEnv) ? portFromEnv : await findFreePortInRange(4300, 4999)
console.log(`Selected playwright server port: ${PORT}`)
// Make sure to set the env to actual port that is being used. This is necessary for workers to
// pick up the same configuration.
process.env.PLAYWRIGHT_PORT = `${PORT}`
Expand Down Expand Up @@ -117,7 +118,7 @@ export default defineConfig({
`corepack pnpm build && corepack pnpm exec vite preview --port ${PORT} --strictPort`
: `corepack pnpm exec vite dev --port ${PORT}`,
// Build from scratch apparently can take a while on CI machines.
timeout: 120 * 1000,
timeout: 240 * 1000,
port: PORT,
// We use our special, mocked version of server, thus do not want to re-use user's one.
reuseExistingServer: false,
Expand Down
12 changes: 3 additions & 9 deletions app/gui2/src/components/GraphEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -724,15 +724,7 @@ const documentationEditorFullscreen = ref(false)
@drop.prevent="handleFileDrop($event)"
>
<div class="vertical">
<div
ref="viewportNode"
class="viewport"
v-on.="graphNavigator.pointerEvents"
v-on..="nodeSelection.events"
@click="handleClick"
@pointermove.capture="graphNavigator.pointerEventsCapture.pointermove"
@wheel.capture="graphNavigator.pointerEventsCapture.wheel"
>
<div ref="viewportNode" class="viewport" @click="handleClick">
<GraphNodes
@nodeOutputPortDoubleClick="handleNodeOutputPortDoubleClick"
@nodeDoubleClick="(id) => stackNavigator.enterNode(id)"
Expand Down Expand Up @@ -840,8 +832,10 @@ const documentationEditorFullscreen = ref(false)
}
.viewport {
position: relative; /* Needed for safari when using contain: layout */
contain: layout;
overflow: clip;
touch-action: none;
--group-color-fallback: #006b8a;
--node-color-no-type: #596b81;
--output-node-color: #006b8a;
Expand Down
9 changes: 7 additions & 2 deletions app/gui2/src/components/GraphEditor/GraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const props = defineProps<{
const emit = defineEmits<{
dragging: [offset: Vec2]
draggingCommited: []
draggingCancelled: []
delete: []
replaceSelection: []
outputPortClick: [event: PointerEvent, portId: AstId]
Expand Down Expand Up @@ -262,10 +263,14 @@ const dragPointer = usePointer(
startEpochMs.value = Number(new Date())
significantMove.value = false
break
case 'stop': {
case 'stop':
startEpochMs.value = 0
emit('draggingCommited')
}
break
case 'cancel':
startEpochMs.value = 0
emit('draggingCancelled')
break
}
},
// Pointer is captured by `target`, to make it receive the `up` and `click` event in case this
Expand Down
2 changes: 2 additions & 0 deletions app/gui2/src/components/GraphEditor/GraphNodes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const displacingWithArrows = useArrows(
if (!oneOfMoved) return false
dragging.startOrUpdate(oneOfMoved, pos.relative)
if (type === 'stop') dragging.finishDrag()
else if (type === 'cancel') dragging.cancelDrag()
},
{ predicate: (_) => selection.selected.size > 0 },
)
Expand Down Expand Up @@ -70,6 +71,7 @@ const graphNodeSelections = shallowRef<HTMLElement>()
@delete="graphStore.deleteNodes([id])"
@dragging="nodeIsDragged(id, $event)"
@draggingCommited="dragging.finishDrag()"
@draggingCancelled="dragging.cancelDrag()"
@outputPortClick="(event, port) => graphStore.createEdgeFromOutput(port, event)"
@outputPortDoubleClick="(_event, port) => emit('nodeOutputPortDoubleClick', port)"
@doubleClick="emit('nodeDoubleClick', id)"
Expand Down
14 changes: 14 additions & 0 deletions app/gui2/src/components/GraphEditor/dragging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ export function useDragging() {
this.stopPositionUpdate()
this.updateNodesPosition()
}
cancelDragging(): void {
console.log('cancelDragging')
this.stopPositionUpdate()
offset.value = Vec2.Zero
snapXTarget.value = 0
snapYTarget.value = 0
snapX.skip()
snapY.skip()
this.updateNodesPosition()
}

createSnapGrid() {
const nonDraggedRects = computed(() => {
Expand Down Expand Up @@ -198,5 +208,9 @@ export function useDragging() {
currentDrag?.finishDragging()
currentDrag = undefined
},
cancelDrag() {
currentDrag?.cancelDragging()
currentDrag = undefined
},
}
}
3 changes: 2 additions & 1 deletion app/gui2/src/components/GraphMouse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const scaledMousePos = computed(
() => navigator?.sceneMousePos?.scale(navigator?.scale ?? 1) ?? Vec2.Zero,
)
const scaledSelectionAnchor = computed(() => nodeSelection?.anchor?.scale(navigator?.scale ?? 1))
const scaledSelectionFocus = computed(() => nodeSelection?.focus?.scale(navigator?.scale ?? 1))
const isNativeDragging = ref(0)
useEvent(
Expand Down Expand Up @@ -44,7 +45,7 @@ useEvent(
<SelectionBrush
v-if="!isNativeDragging"
:transform="navigator?.prescaledTransform"
:position="scaledMousePos"
:position="scaledSelectionFocus"
:anchor="scaledSelectionAnchor"
/>
</template>
4 changes: 4 additions & 0 deletions app/gui2/src/components/ResizeHandles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ function resizeHandler(resizeX: 'left' | 'right' | false, resizeY: 'top' | 'bott
case 'stop':
emit('update:resizing', {})
break
case 'cancel':
if (initialBounds) bounds.value = initialBounds
emit('update:resizing', {})
break
}
})
}
Expand Down
5 changes: 5 additions & 0 deletions app/gui2/src/components/ScrollBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ function dragEventsHandler(axis: 'x' | 'y') {
emit('scroll', { type: 'stop' })
break
}
case 'cancel': {
emit('scroll', { type: 'move', startOffset: Vec2.Zero })
emit('scroll', { type: 'stop' })
break
}
}
return true
})
Expand Down
6 changes: 3 additions & 3 deletions app/gui2/src/components/SelectionBrush.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<script setup lang="ts">
import { useApproach } from '@/composables/animation'
import type { Vec2 } from '@/util/data/vec2'
import { Vec2 } from '@/util/data/vec2'
import { computed, shallowRef, watch } from 'vue'
const props = defineProps<{
position: Vec2
position: Vec2 | undefined
anchor: Vec2 | undefined
transform: string | undefined
}>()
const hidden = computed(() => props.anchor == null)
const lastSetAnchor = shallowRef<Vec2>()
const lastAnchoredPosition = shallowRef<Vec2>(props.position)
const lastAnchoredPosition = shallowRef<Vec2>(Vec2.Zero)
watch(
() => props.anchor,
(anchor) => {
Expand Down
16 changes: 2 additions & 14 deletions app/gui2/src/components/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import ExtendedMenu from '@/components/ExtendedMenu.vue'
import NavBreadcrumbs from '@/components/NavBreadcrumbs.vue'
import RecordControl from '@/components/RecordControl.vue'
import SelectionMenu from '@/components/SelectionMenu.vue'
import { injectGuiConfig } from '@/providers/guiConfig'
import { computed } from 'vue'
const showColorPicker = defineModel<boolean>('showColorPicker', { required: true })
const showCodeEditor = defineModel<boolean>('showCodeEditor', { required: true })
Expand All @@ -20,21 +18,10 @@ const emit = defineEmits<{
collapseNodes: []
removeNodes: []
}>()
const LEFT_PADDING_PX = 11
const config = injectGuiConfig()
const barStyle = computed(() => {
const offset = Number(config.value.window?.topBarOffset ?? '0')
return {
marginLeft: `${offset + LEFT_PADDING_PX}px`,
}
})
</script>

<template>
<div class="TopBar" :style="barStyle">
<div class="TopBar">
<NavBreadcrumbs />
<RecordControl />
<Transition name="selection-menu">
Expand Down Expand Up @@ -65,6 +52,7 @@ const barStyle = computed(() => {
top: 8px;
left: 0;
right: 0;
margin-left: 11px;
pointer-events: none;
> * {
pointer-events: auto;
Expand Down
42 changes: 29 additions & 13 deletions app/gui2/src/composables/__tests__/events.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Vec2 } from '@/util/data/vec2'
import { withSetup } from '@/util/testing'
import { afterEach, beforeEach, expect, test, vi, type Mock, type MockInstance } from 'vitest'
import { effectScope, nextTick } from 'vue'
import { nextTick } from 'vue'
import { useArrows } from '../events'

let rafSpy: MockInstance
Expand Down Expand Up @@ -57,6 +58,7 @@ function checkCbSequence(cb: Mock, steps: CbSequenceStep[]) {
event,
)
}
expect(cb).toHaveBeenCalledTimes(steps.length)
}

test.each`
Expand All @@ -69,8 +71,22 @@ test.each`
${['ArrowUp', 'ArrowLeft']} | ${10} | ${1000} | ${[2000, 3000]} | ${[[-10, -10], [-20, -20]]} | ${[[-10, -10], [-10, -10]]}
`(
'useArrows with $pressedKeys keys and $velocity velocity',
async ({ pressedKeys, velocity, t0, t, offset, delta }) => {
await effectScope().run(async () => {
async ({
pressedKeys,
velocity,
t0,
t,
offset,
delta,
}: {
pressedKeys: string[]
velocity: number
t0: number
t: number[]
offset: [number, number][]
delta: [number, number][]
}) => {
await withSetup(async () => {
const cb = vi.fn()
const expectedSequence: CbSequenceStep[] = []
const arrows = useArrows(cb, { velocity })
Expand All @@ -86,32 +102,32 @@ test.each`
expect(arrows.moving.value).toBeTruthy

for (let i = 0; i < t.length - 1; ++i) {
runFrame(t[i])
runFrame(t[i]!)
await nextTick()
expectedSequence.push(['move', offset[i], delta[i], undefined])
expectedSequence.push(['move', offset[i]!, delta[i]!, undefined])
}

const keyupEvents = Array.from(pressedKeys, (key) =>
keyEvent('keyup', { key, timeStamp: t[t.length - 1] }),
keyEvent('keyup', { key, timeStamp: t[t.length - 1]! }),
)
for (const event of keyupEvents) {
window.dispatchEvent(event)
}
await nextTick()
expectedSequence.push([
'stop',
offset[offset.length - 1],
delta[delta.length - 1],
offset[offset.length - 1]!,
delta[delta.length - 1]!,
keyupEvents[keyupEvents.length - 1],
])
expect(arrows.moving.value).toBeFalsy()
checkCbSequence(cb, expectedSequence)
})
})[0]
},
)

test('useArrow with non-overlaping keystrokes', async () => {
await effectScope().run(async () => {
await withSetup(async () => {
const cb = vi.fn()
const arrows = useArrows(cb, { velocity: 10 })
const rightDown = keyEvent('keydown', { key: 'ArrowRight', timeStamp: 0 })
Expand Down Expand Up @@ -143,11 +159,11 @@ test('useArrow with non-overlaping keystrokes', async () => {
['move', [0, 5], [0, 5], undefined],
['stop', [0, 10], [0, 5], downUp],
])
})
})[0]
})

test('useArrow with overlaping keystrokes', async () => {
await effectScope().run(async () => {
await withSetup(async () => {
const cb = vi.fn()
const arrows = useArrows(cb, { velocity: 10 })
const rightDown = keyEvent('keydown', { key: 'ArrowRight', timeStamp: 0 })
Expand Down Expand Up @@ -178,5 +194,5 @@ test('useArrow with overlaping keystrokes', async () => {
['move', [20, 15], [5, 10], undefined],
['stop', [20, 20], [0, 5], downUp],
])
})
})[0]
})
Loading

0 comments on commit c996707

Please sign in to comment.