mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 11:52:59 +03:00
Graph editor breadcrumbs integration (#8519)
Implements #7788 [Peek 2023-12-12 10-46.webm](https://github.com/enso-org/enso/assets/1428930/7d1f45f6-8323-4adc-9a8c-f6f4bcaeb143)
This commit is contained in:
parent
21d164ec3e
commit
23e0bafc75
@ -35,6 +35,7 @@ app/ide-desktop/lib/dashboard/playwright-report/
|
||||
app/ide-desktop/lib/dashboard/playwright/.cache/
|
||||
app/gui/view/documentation/assets/stylesheet.css
|
||||
app/gui2/rust-ffi/pkg
|
||||
app/gui2/src/assets/font-*.css
|
||||
Cargo.lock
|
||||
build.json
|
||||
app/gui2/playwright-report/
|
||||
|
@ -16,6 +16,7 @@ import PlusButton from '@/components/PlusButton.vue'
|
||||
import TopBar from '@/components/TopBar.vue'
|
||||
import { useDoubleClick } from '@/composables/doubleClick'
|
||||
import { keyboardBusy, keyboardBusyExceptIn, useEvent } from '@/composables/events'
|
||||
import { useStackNavigator } from '@/composables/stackNavigator'
|
||||
import { provideGraphNavigator } from '@/providers/graphNavigator'
|
||||
import { provideGraphSelection } from '@/providers/graphSelection'
|
||||
import { provideInteractionHandler, type Interaction } from '@/providers/interactionHandler'
|
||||
@ -27,7 +28,6 @@ import { groupColorVar, useSuggestionDbStore } from '@/stores/suggestionDatabase
|
||||
import { colorFromString } from '@/util/colors'
|
||||
import { Rect } from '@/util/data/rect'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
|
||||
import * as set from 'lib0/set'
|
||||
import type { ExprId, NodeMetadata } from 'shared/yjsModel'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
@ -206,12 +206,12 @@ const graphBindingsHandler = graphBindings.handler({
|
||||
if (keyboardBusy()) return false
|
||||
const selectedNode = set.first(nodeSelection.selected)
|
||||
if (selectedNode) {
|
||||
enterNode(selectedNode)
|
||||
stackNavigator.enterNode(selectedNode)
|
||||
}
|
||||
},
|
||||
exitNode() {
|
||||
if (keyboardBusy()) return false
|
||||
exitNode()
|
||||
stackNavigator.exitNode()
|
||||
},
|
||||
})
|
||||
|
||||
@ -220,7 +220,7 @@ const handleClick = useDoubleClick(
|
||||
graphBindingsHandler(e)
|
||||
},
|
||||
() => {
|
||||
exitNode()
|
||||
stackNavigator.exitNode()
|
||||
},
|
||||
).handleClick
|
||||
const codeEditorArea = ref<HTMLElement>()
|
||||
@ -232,31 +232,6 @@ const codeEditorHandler = codeEditorBindings.handler({
|
||||
},
|
||||
})
|
||||
|
||||
function enterNode(id: ExprId) {
|
||||
const expressionInfo = graphStore.db.getExpressionInfo(id)
|
||||
if (expressionInfo == undefined || expressionInfo.methodCall == undefined) {
|
||||
console.debug('Cannot enter node that has no method call.')
|
||||
return
|
||||
}
|
||||
const definedOnType = tryQualifiedName(expressionInfo.methodCall.methodPointer.definedOnType)
|
||||
if (!projectStore.modulePath?.ok) {
|
||||
console.warn('Cannot enter node while no module is open.')
|
||||
return
|
||||
}
|
||||
const openModuleName = qnLastSegment(projectStore.modulePath.value)
|
||||
if (definedOnType.ok && qnLastSegment(definedOnType.value) != openModuleName) {
|
||||
console.debug('Cannot enter node that is not defined on current module.')
|
||||
return
|
||||
}
|
||||
projectStore.executionContext.push(id)
|
||||
graphStore.updateState()
|
||||
}
|
||||
|
||||
function exitNode() {
|
||||
projectStore.executionContext.pop()
|
||||
graphStore.updateState()
|
||||
}
|
||||
|
||||
/** Track play button presses. */
|
||||
function onPlayButtonPress() {
|
||||
projectStore.lsRpcConnection.then(async () => {
|
||||
@ -410,17 +385,6 @@ async function handleFileDrop(event: DragEvent) {
|
||||
})
|
||||
}
|
||||
|
||||
const breadcrumbs = computed(() =>
|
||||
projectStore.executionContext.desiredStack.map((frame) => {
|
||||
switch (frame.type) {
|
||||
case 'ExplicitCall':
|
||||
return frame.methodPointer.name
|
||||
case 'LocalCall':
|
||||
return frame.expressionId
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// === Clipboard ===
|
||||
|
||||
const ENSO_MIME_TYPE = 'web application/enso'
|
||||
@ -507,6 +471,8 @@ function handleNodeOutputPortDoubleClick(id: ExprId) {
|
||||
interaction.setCurrent(creatingNodeFromPortDoubleClick)
|
||||
}
|
||||
|
||||
const stackNavigator = useStackNavigator()
|
||||
|
||||
function handleEdgeDrop(source: ExprId, position: Vec2) {
|
||||
componentBrowserUsage.value = { type: 'newNode', sourcePort: source }
|
||||
componentBrowserNodePosition.value = position
|
||||
@ -532,7 +498,7 @@ function handleEdgeDrop(source: ExprId, position: Vec2) {
|
||||
<div :style="{ transform: graphNavigator.transform }" class="htmlLayer">
|
||||
<GraphNodes
|
||||
@nodeOutputPortDoubleClick="handleNodeOutputPortDoubleClick"
|
||||
@nodeDoubleClick="enterNode"
|
||||
@nodeDoubleClick="(id) => stackNavigator.enterNode(id)"
|
||||
/>
|
||||
</div>
|
||||
<ComponentBrowser
|
||||
@ -549,10 +515,12 @@ function handleEdgeDrop(source: ExprId, position: Vec2) {
|
||||
v-model:mode="projectStore.executionMode"
|
||||
:title="projectStore.name"
|
||||
:modes="EXECUTION_MODES"
|
||||
:breadcrumbs="breadcrumbs"
|
||||
@breadcrumbClick="console.log(`breadcrumb #${$event + 1} clicked.`)"
|
||||
@back="exitNode"
|
||||
@forward="console.log('breadcrumbs \'forward\' button clicked.')"
|
||||
:breadcrumbs="stackNavigator.breadcrumbLabels.value"
|
||||
:allowNavigationLeft="stackNavigator.allowNavigationLeft.value"
|
||||
:allowNavigationRight="stackNavigator.allowNavigationRight.value"
|
||||
@breadcrumbClick="stackNavigator.handleBreadcrumbClick"
|
||||
@back="stackNavigator.exitNode"
|
||||
@forward="stackNavigator.enterNextNodeFromHistory"
|
||||
@execute="onPlayButtonPress()"
|
||||
/>
|
||||
<PlusButton @pointerdown="interaction.setCurrent(creatingNodeFromButton)" />
|
||||
|
@ -49,7 +49,6 @@ const emit = defineEmits<{
|
||||
draggingCommited: []
|
||||
delete: []
|
||||
replaceSelection: []
|
||||
nodeDoubleClick: []
|
||||
outputPortClick: [portId: ExprId]
|
||||
outputPortDoubleClick: [portId: ExprId]
|
||||
doubleClick: []
|
||||
|
@ -1,8 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import NavBreadcrumbs from '@/components/NavBreadcrumbs.vue'
|
||||
import NavBreadcrumbs, { type BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
|
||||
const props = defineProps<{ breadcrumbs: string[] }>()
|
||||
const props = defineProps<{
|
||||
breadcrumbs: BreadcrumbItem[]
|
||||
allowNavigationLeft: boolean
|
||||
allowNavigationRight: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ back: []; forward: []; breadcrumbClick: [index: number] }>()
|
||||
</script>
|
||||
|
||||
@ -13,20 +17,19 @@ const emit = defineEmits<{ back: []; forward: []; breadcrumbClick: [index: numbe
|
||||
<SvgIcon
|
||||
name="arrow_left"
|
||||
draggable="false"
|
||||
class="icon button inactive"
|
||||
class="icon button"
|
||||
:class="{ inactive: !props.allowNavigationLeft }"
|
||||
@pointerdown="emit('back')"
|
||||
/>
|
||||
<SvgIcon
|
||||
name="arrow_right"
|
||||
draggable="false"
|
||||
class="icon button"
|
||||
:class="{ inactive: !props.allowNavigationRight }"
|
||||
@pointerdown="emit('forward')"
|
||||
/>
|
||||
</div>
|
||||
<NavBreadcrumbs
|
||||
:breadcrumbs="props.breadcrumbs"
|
||||
@pointerdown="emit('breadcrumbClick', $event)"
|
||||
/>
|
||||
<NavBreadcrumbs :breadcrumbs="props.breadcrumbs" @selected="emit('breadcrumbClick', $event)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ text: string }>()
|
||||
const props = defineProps<{ text: string; active: boolean }>()
|
||||
const emit = defineEmits<{ click: [] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="NavBreadcrumb"><span @click="emit('click')" v-text="props.text"></span></div>
|
||||
<div :class="['NavBreadcrumb', { inactive: !props.active }]">
|
||||
<span @click="emit('click')" v-text="props.text"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@ -25,4 +27,8 @@ span {
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
}
|
||||
}
|
||||
|
||||
.inactive {
|
||||
opacity: 0.4;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,15 +3,28 @@ import SvgIcon from '@/components/SvgIcon.vue'
|
||||
|
||||
import NavBreadcrumb from '@/components/NavBreadcrumb.vue'
|
||||
|
||||
const props = defineProps<{ breadcrumbs: string[] }>()
|
||||
const emit = defineEmits<{ click: [index: number] }>()
|
||||
export interface BreadcrumbItem {
|
||||
label: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{ breadcrumbs: BreadcrumbItem[] }>()
|
||||
const emit = defineEmits<{ selected: [index: number] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="NavBreadcrumbs">
|
||||
<template v-for="(breadcrumb, index) in props.breadcrumbs" :key="index">
|
||||
<SvgIcon v-if="index > 0" name="arrow_right_head_only" class="arrow" />
|
||||
<NavBreadcrumb :text="breadcrumb" @click="emit('click', index)" />
|
||||
<SvgIcon
|
||||
v-if="index > 0"
|
||||
name="arrow_right_head_only"
|
||||
:class="['arrow', { inactive: !breadcrumb.active }]"
|
||||
/>
|
||||
<NavBreadcrumb
|
||||
:text="breadcrumb.label"
|
||||
:active="breadcrumb.active"
|
||||
@pointerdown="emit('selected', index)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -26,4 +39,8 @@ const emit = defineEmits<{ click: [index: number] }>()
|
||||
.arrow {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
opacity: 0.4;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,10 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import type { BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
|
||||
import ProjectTitle from '@/components/ProjectTitle.vue'
|
||||
import { injectGuiConfig } from '@/providers/guiConfig'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ title: string; breadcrumbs: string[]; modes: string[]; mode: string }>()
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
breadcrumbs: BreadcrumbItem[]
|
||||
modes: string[]
|
||||
mode: string
|
||||
allowNavigationLeft: boolean
|
||||
allowNavigationRight: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
execute: []
|
||||
back: []
|
||||
@ -36,6 +44,8 @@ const barStyle = computed(() => {
|
||||
/>
|
||||
<NavBar
|
||||
:breadcrumbs="props.breadcrumbs"
|
||||
:allowNavigationLeft="props.allowNavigationLeft"
|
||||
:allowNavigationRight="props.allowNavigationRight"
|
||||
@back="emit('back')"
|
||||
@forward="emit('forward')"
|
||||
@breadcrumbClick="emit('breadcrumbClick', $event)"
|
||||
|
119
app/gui2/src/composables/stackNavigator.ts
Normal file
119
app/gui2/src/composables/stackNavigator.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import type { BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
|
||||
import type { StackItem } from 'shared/languageServerTypes.ts'
|
||||
import type { ExprId } from 'shared/yjsModel.ts'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
export function useStackNavigator() {
|
||||
const projectStore = useProjectStore()
|
||||
const graphStore = useGraphStore()
|
||||
|
||||
const breadcrumbs = ref<StackItem[]>([])
|
||||
|
||||
const breadcrumbLabels = computed(() => {
|
||||
const activeStackLength = projectStore.executionContext.desiredStack.length
|
||||
return breadcrumbs.value.map((item, index) => {
|
||||
const label = stackItemToLabel(item)
|
||||
const isActive = index < activeStackLength
|
||||
return { label, active: isActive } satisfies BreadcrumbItem
|
||||
})
|
||||
})
|
||||
|
||||
const allowNavigationLeft = computed(() => {
|
||||
return projectStore.executionContext.desiredStack.length > 1
|
||||
})
|
||||
|
||||
const allowNavigationRight = computed(() => {
|
||||
return projectStore.executionContext.desiredStack.length < breadcrumbs.value.length
|
||||
})
|
||||
|
||||
function stackItemToLabel(item: StackItem): string {
|
||||
switch (item.type) {
|
||||
case 'ExplicitCall': {
|
||||
return item.methodPointer.name
|
||||
}
|
||||
case 'LocalCall': {
|
||||
const exprId = item.expressionId
|
||||
const info = graphStore.db.getExpressionInfo(exprId)
|
||||
return info?.methodCall?.methodPointer.name ?? 'unknown'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleBreadcrumbClick(index: number) {
|
||||
const activeStack = projectStore.executionContext.desiredStack
|
||||
if (index < activeStack.length) {
|
||||
const diff = activeStack.length - index - 1
|
||||
for (let i = 0; i < diff; i++) {
|
||||
projectStore.executionContext.pop()
|
||||
}
|
||||
} else if (index >= activeStack.length) {
|
||||
const diff = index - activeStack.length + 1
|
||||
for (let i = 0; i < diff; i++) {
|
||||
const stackItem = breadcrumbs.value[index - i]
|
||||
if (stackItem?.type === 'LocalCall') {
|
||||
const exprId = stackItem.expressionId
|
||||
projectStore.executionContext.push(exprId)
|
||||
} else {
|
||||
console.warn('Cannot enter non-local call.')
|
||||
}
|
||||
}
|
||||
}
|
||||
graphStore.updateState()
|
||||
}
|
||||
|
||||
function enterNode(id: ExprId) {
|
||||
const expressionInfo = graphStore.db.getExpressionInfo(id)
|
||||
if (expressionInfo == null || expressionInfo.methodCall == null) {
|
||||
console.debug('Cannot enter node that has no method call.')
|
||||
return
|
||||
}
|
||||
const definedOnType = tryQualifiedName(expressionInfo.methodCall.methodPointer.definedOnType)
|
||||
if (!projectStore.modulePath?.ok) {
|
||||
console.warn('Cannot enter node while no module is open.')
|
||||
return
|
||||
}
|
||||
const openModuleName = qnLastSegment(projectStore.modulePath.value)
|
||||
if (definedOnType.ok && qnLastSegment(definedOnType.value) !== openModuleName) {
|
||||
console.debug('Cannot enter node that is not defined on current module.')
|
||||
return
|
||||
}
|
||||
projectStore.executionContext.push(id)
|
||||
graphStore.updateState()
|
||||
breadcrumbs.value = projectStore.executionContext.desiredStack.slice()
|
||||
}
|
||||
|
||||
function exitNode() {
|
||||
projectStore.executionContext.pop()
|
||||
graphStore.updateState()
|
||||
}
|
||||
|
||||
/// Enter the next node from the history stack. This is the node that is the first greyed out item in the breadcrumbs.
|
||||
function enterNextNodeFromHistory() {
|
||||
const nextNodeIndex = projectStore.executionContext.desiredStack.length
|
||||
const nextNode = breadcrumbs.value[nextNodeIndex]
|
||||
if (nextNode?.type !== 'LocalCall') {
|
||||
console.warn('Cannot enter non-local call.')
|
||||
return
|
||||
}
|
||||
projectStore.executionContext.push(nextNode.expressionId)
|
||||
graphStore.updateState()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
breadcrumbs.value = projectStore.executionContext.desiredStack.slice()
|
||||
})
|
||||
|
||||
return {
|
||||
breadcrumbs,
|
||||
breadcrumbLabels,
|
||||
allowNavigationLeft,
|
||||
allowNavigationRight,
|
||||
handleBreadcrumbClick,
|
||||
enterNode,
|
||||
exitNode,
|
||||
enterNextNodeFromHistory,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user