Skip to content

Commit

Permalink
Added editable node functionality integrated with the Component Brows…
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelMauderer authored Nov 6, 2023
1 parent 237aae3 commit faf8cb7
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 16 deletions.
2 changes: 1 addition & 1 deletion app/gui2/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const interactionBindings = defineKeybinds('current-interaction', {

export const componentBrowserBindings = defineKeybinds('component-browser', {
applySuggestion: ['Tab'],
acceptSuggestion: ['Enter'],
acceptInput: ['Enter'],
cancelEditing: ['Escape'],
moveUp: ['ArrowUp'],
moveDown: ['ArrowDown'],
Expand Down
35 changes: 25 additions & 10 deletions app/gui2/src/components/ComponentBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { Opt } from '@/util/opt'
import { allRanges } from '@/util/range'
import { Vec2 } from '@/util/vec2'
import type { SuggestionId } from 'shared/languageServerTypes/suggestions'
import type { ContentRange } from 'shared/yjsModel.ts'
import { computed, nextTick, onMounted, ref, watch, type Ref } from 'vue'
const ITEM_SIZE = 32
Expand All @@ -24,17 +25,24 @@ const TOP_BAR_HEIGHT = 32
const props = defineProps<{
position: Vec2
navigator: ReturnType<typeof useNavigator>
initialContent: string
initialCaretPosition: ContentRange
}>()
const emit = defineEmits<{
(e: 'finished'): void
finished: [selectedExpression: string]
}>()
onMounted(() => {
if (inputField.value != null) {
inputField.value.focus({ preventScroll: true })
selectLastAfterRefresh()
}
input.code.value = props.initialContent
nextTick(() => {
if (inputField.value != null) {
inputField.value.selectionStart = props.initialCaretPosition[0]
inputField.value.selectionEnd = props.initialCaretPosition[1]
inputField.value.focus({ preventScroll: true })
selectLastAfterRefresh()
}
})
})
const projectStore = useProjectStore()
Expand Down Expand Up @@ -82,7 +90,7 @@ function readInputFieldSelection() {
}
}
// HTMLInputElement's same event is not supported in chrome yet. We just react for any
// selectionchange in the document and check if the input selection chagned.
// selectionchange in the document and check if the input selection changed.
// BUT some operations like deleting does not emit 'selectionChange':
// https://bugs.chromium.org/p/chromium/issues/detail?id=725890
// Therefore we must also refresh selection after changing input.
Expand Down Expand Up @@ -114,7 +122,7 @@ function handleDefocus(e: FocusEvent) {
inputField.value.focus({ preventScroll: true })
}
} else {
emit('finished')
emit('finished', input.code.value)
}
}
Expand Down Expand Up @@ -272,7 +280,11 @@ function applySuggestion(component: Opt<Component> = null): SuggestionEntry | nu
function acceptSuggestion(index: Opt<Component> = null) {
const applied = applySuggestion(index)
const shouldFinish = applied != null && applied.kind !== SuggestionKind.Module
if (shouldFinish) emit('finished')
if (shouldFinish) emit('finished', input.code.value)
}
function acceptInput() {
emit('finished', input.code.value)
}
// === Key Events Handler ===
Expand All @@ -281,8 +293,8 @@ const handler = componentBrowserBindings.handler({
applySuggestion() {
applySuggestion()
},
acceptSuggestion() {
acceptSuggestion()
acceptInput() {
acceptInput()
},
moveUp() {
if (selected.value != null && selected.value < components.value.length - 1) {
Expand All @@ -298,6 +310,9 @@ const handler = componentBrowserBindings.handler({
}
scrollToSelected()
},
cancelEditing() {
emit('finished', props.initialContent)
},
})
</script>

Expand Down
39 changes: 38 additions & 1 deletion app/gui2/src/components/GraphEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { keyboardBusy, keyboardBusyExceptIn, useEvent } from '@/util/events'
import { Interaction } from '@/util/interaction'
import { Vec2 } from '@/util/vec2'
import * as set from 'lib0/set'
import type { ExprId } from 'shared/yjsModel.ts'
import { computed, onMounted, ref, watch } from 'vue'
import GraphEdges from './GraphEditor/GraphEdges.vue'
import GraphNodes from './GraphEditor/GraphNodes.vue'
Expand All @@ -26,6 +27,7 @@ const navigator = provideGraphNavigator(viewportNode)
const graphStore = useGraphStore()
const projectStore = useProjectStore()
const componentBrowserVisible = ref(false)
const componentBrowserInputContent = ref('')
const componentBrowserPosition = ref(Vec2.Zero)
const suggestionDb = useSuggestionDbStore()
Expand Down Expand Up @@ -59,6 +61,7 @@ const graphBindingsHandler = graphBindings.handler({
if (navigator.sceneMousePos != null && !componentBrowserVisible.value) {
componentBrowserPosition.value = navigator.sceneMousePos
componentBrowserVisible.value = true
componentBrowserInputContent.value = ''
}
},
newNode() {
Expand Down Expand Up @@ -207,6 +210,37 @@ async function handleFileDrop(event: DragEvent) {
console.error(`Uploading file failed. ${err}`)
}
}
function onComponentBrowserFinished(content: string) {
if (content != null && graphStore.editedNodeInfo != null) {
graphStore.setNodeContent(graphStore.editedNodeInfo.id, content)
}
componentBrowserVisible.value = false
graphStore.editedNodeInfo = undefined
}
function getNodeContent(id: ExprId): string {
const node = graphStore.nodes.get(id)
if (node == null) return ''
return node.rootSpan.repr()
}
// Watch the editedNode in the graph store
watch(
() => graphStore.editedNodeInfo,
(editedInfo) => {
if (editedInfo != null) {
const targetNode = graphStore.nodes.get(editedInfo.id)
const targetPos = targetNode?.position ?? Vec2.Zero
const offset = new Vec2(20, 35)
componentBrowserPosition.value = targetPos.add(offset)
componentBrowserInputContent.value = getNodeContent(editedInfo.id)
componentBrowserVisible.value = true
} else {
componentBrowserVisible.value = false
}
},
)
</script>

<template>
Expand All @@ -229,9 +263,12 @@ async function handleFileDrop(event: DragEvent) {
</div>
<ComponentBrowser
v-if="componentBrowserVisible"
ref="componentBrowser"
:navigator="navigator"
:position="componentBrowserPosition"
@finished="componentBrowserVisible = false"
@finished="onComponentBrowserFinished"
:initialContent="componentBrowserInputContent"
:initialCaretPosition="graphStore.editedNodeInfo?.range ?? [0, 0]"
/>
<TopBar
v-model:mode="projectStore.executionMode"
Expand Down
42 changes: 39 additions & 3 deletions app/gui2/src/components/GraphEditor/GraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const MAXIMUM_CLICK_DISTANCE_SQ = 50
const props = defineProps<{
node: Node
edited: boolean
}>()
const emit = defineEmits<{
Expand All @@ -38,6 +39,7 @@ const emit = defineEmits<{
replaceSelection: []
'update:selected': [selected: boolean]
outputPortAction: []
'update:edited': [cursorPosition: number]
}>()
const nodeSelection = injectGraphSelection(true)
Expand Down Expand Up @@ -308,6 +310,42 @@ const editableKeydownHandler = nodeEditBindings.handler({
},
})
function startEditingHandler(event: PointerEvent) {
let range, textNode, offset
offset = 0
if ((document as any).caretPositionFromPoint) {
range = (document as any).caretPositionFromPoint(event.clientX, event.clientY)
textNode = range.offsetNode
offset = range.offset
} else if (document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(event.clientX, event.clientY)
if (range == null) {
console.error('Could not find caret position when editing node.')
} else {
textNode = range.startContainer
offset = range.startOffset
}
} else {
console.error(
'Neither caretPositionFromPoint nor caretRangeFromPoint are supported by this browser',
)
}
let newRange = document.createRange()
newRange.setStart(textNode, offset)
let selection = window.getSelection()
if (selection != null) {
selection.removeAllRanges()
selection.addRange(newRange)
} else {
console.error('Could not set selection when editing node.')
}
emit('update:edited', offset)
}
const startEpochMs = ref(0)
let startEvent: PointerEvent | null = null
let startPos = Vec2.Zero
Expand Down Expand Up @@ -419,12 +457,10 @@ function hoverExpr(id: ExprId | undefined) {
<SvgIcon class="icon grab-handle" :name="icon"></SvgIcon
><span
ref="editableRootNode"
class="editable"
contenteditable
spellcheck="false"
@beforeinput="editContent"
@keydown="editableKeydownHandler"
@pointerdown.stop
@pointerdown.stop.prevent="startEditingHandler"
@blur="projectStore.stopCapturingUndo()"
><NodeTree
:ast="node.rootSpan"
Expand Down
3 changes: 3 additions & 0 deletions app/gui2/src/components/GraphEditor/GraphNodes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ function hoverNode(id: ExprId | undefined) {
<template>
<GraphNode
v-for="[id, node] in graphStore.nodes"
v-show="id != graphStore.editedNodeInfo?.id"
:key="id"
:node="node"
:edited="false"
@update:edited="graphStore.setEditedNode(id, $event)"
@updateRect="graphStore.updateNodeRect(id, $event)"
@delete="graphStore.deleteNode(id)"
@updateExprRect="graphStore.updateExprRect"
Expand Down
21 changes: 20 additions & 1 deletion app/gui2/src/stores/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import {
import { computed, reactive, ref, watch } from 'vue'
import * as Y from 'yjs'

export interface NodeEditInfo {
id: ExprId
range: ContentRange
}
export const useGraphStore = defineStore('graph', () => {
const proj = useProjectStore()

Expand All @@ -30,11 +34,11 @@ export const useGraphStore = defineStore('graph', () => {
const metadata = computed(() => proj.module?.doc.metadata)

const textContent = ref('')

const nodes = reactive(new Map<ExprId, Node>())
const exprNodes = reactive(new Map<ExprId, ExprId>())
const nodeRects = reactive(new Map<ExprId, Rect>())
const exprRects = reactive(new Map<ExprId, Rect>())
const editedNodeInfo = ref<NodeEditInfo>()

const unconnectedEdge = ref<UnconnectedEdge>()

Expand Down Expand Up @@ -327,10 +331,24 @@ export const useGraphStore = defineStore('graph', () => {
exprRects.set(id, rect)
}

function setEditedNode(id: ExprId | null, cursorPosition: number | null) {
if (id == null) {
editedNodeInfo.value = undefined
return
}
if (cursorPosition == null) {
console.warn('setEditedNode: cursorPosition is null')
return
}
const range = [cursorPosition, cursorPosition] as ContentRange
editedNodeInfo.value = { id, range }
}

return {
_ast,
transact,
nodes,
editedNodeInfo,
exprNodes,
unconnectedEdge,
edges,
Expand All @@ -353,6 +371,7 @@ export const useGraphStore = defineStore('graph', () => {
stopCapturingUndo,
updateNodeRect,
updateExprRect,
setEditedNode,
}
})

Expand Down

0 comments on commit faf8cb7

Please sign in to comment.