Nested networks UI (#885)

* Initial UI for nested nodes

* Clean up deleting nodes

* Print address of nested network

* Add exiting network message

* Implement the breadcrumb trail

* Remove whitespace

* Fix double click not registering in Chromium

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2022-12-17 15:38:29 +00:00
committed by Keavon Chambers
parent 52117d642c
commit 9d40539dc7
12 changed files with 226 additions and 67 deletions

View File

@@ -1,6 +1,6 @@
<template>
<LayoutCol class="node-graph">
<LayoutRow class="options-bar"></LayoutRow>
<LayoutRow class="options-bar"><WidgetLayout :layout="nodeGraphBarLayout" /></LayoutRow>
<LayoutRow
class="graph"
ref="graph"
@@ -8,6 +8,7 @@
@pointerdown="(e: PointerEvent) => pointerDown(e)"
@pointermove="(e: PointerEvent) => pointerMove(e)"
@pointerup="(e: PointerEvent) => pointerUp(e)"
@dblclick="(e: MouseEvent) => doubleClick(e)"
:style="{
'--grid-spacing': `${gridSpacing}px`,
'--grid-offset-x': `${transform.x * transform.scale}px`,
@@ -15,7 +16,7 @@
'--dot-radius': `${dotRadius}px`,
}"
>
<LayoutCol class="node-list" v-if="nodeListLocation" :style="{ marginLeft: `${nodeListX}px`, marginTop: `${nodeListY}px` }">
<LayoutCol class="node-list" data-node-list v-if="nodeListLocation" :style="{ marginLeft: `${nodeListX}px`, marginTop: `${nodeListY}px` }">
<TextInput placeholder="Search Nodes..." :value="searchTerm" @update:value="(val) => (searchTerm = val)" v-focus />
<LayoutCol v-for="nodeCategory in nodeCategories" :key="nodeCategory[0]">
<TextLabel>{{ nodeCategory[0] }}</TextLabel>
@@ -305,6 +306,7 @@ import TextButton from "@/components/widgets/buttons/TextButton.vue";
import TextInput from "@/components/widgets/inputs/TextInput.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
const WHEEL_RATE = (1 / 600) * 3;
const GRID_COLLAPSE_SPACING = 10;
@@ -343,6 +345,9 @@ export default defineComponent({
nodes() {
return this.nodeGraph.state.nodes;
},
nodeGraphBarLayout() {
return this.nodeGraph.state.nodeGraphBarLayout;
},
nodeCategories() {
const categories = new Map();
this.nodeGraph.state.nodeTypes.forEach((node) => {
@@ -484,6 +489,7 @@ export default defineComponent({
this.transform.x -= scrollY / this.transform.scale;
}
},
// TODO: Move the event listener from the graph to the window so dragging outside the graph area (or even the browser window) works
pointerDown(e: PointerEvent) {
if (e.button === 2) {
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
@@ -497,40 +503,53 @@ export default defineComponent({
const port = (e.target as HTMLDivElement).closest("[data-port]") as HTMLDivElement;
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
const nodeList = (e.target as HTMLElement).closest(".node-list") as HTMLElement | undefined;
const nodeId = node?.getAttribute("data-node") || undefined;
const nodeList = (e.target as HTMLElement).closest("[data-node-list]") as HTMLElement | undefined;
// If the user is clicking on the add nodes list, exit here
if (nodeList) return;
// Clicked on a port dot
if (port) {
const isOutput = Boolean(port.getAttribute("data-port") === "output");
if (isOutput) this.linkInProgressFromConnector = port;
} else {
const nodeId = node?.getAttribute("data-node") || undefined;
if (nodeId) {
const id = BigInt(nodeId);
if (e.shiftKey || e.ctrlKey) {
if (this.selected.includes(id)) this.selected.splice(this.selected.lastIndexOf(id), 1);
else this.selected.push(id);
} else if (!this.selected.includes(id)) {
this.selected = [id];
} else {
this.selectIfNotDragged = id;
}
if (this.selected.includes(id)) {
this.draggingNodes = { startX: e.x, startY: e.y, roundX: 0, roundY: 0 };
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
graphDiv?.setPointerCapture(e.pointerId);
}
return;
}
this.editor.instance.selectNodes(new BigUint64Array(this.selected));
} else if (!nodeList) {
this.selected = [];
this.editor.instance.selectNodes(new BigUint64Array(this.selected));
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
graphDiv?.setPointerCapture(e.pointerId);
this.panning = true;
// Clicked on a node
if (nodeId) {
const id = BigInt(nodeId);
if (e.shiftKey || e.ctrlKey) {
if (this.selected.includes(id)) this.selected.splice(this.selected.lastIndexOf(id), 1);
else this.selected.push(id);
} else if (!this.selected.includes(id)) {
this.selected = [id];
} else {
this.selectIfNotDragged = id;
}
if (this.selected.includes(id)) {
this.draggingNodes = { startX: e.x, startY: e.y, roundX: 0, roundY: 0 };
}
this.editor.instance.selectNodes(new BigUint64Array(this.selected));
return;
}
// Clicked on the graph background
this.panning = true;
this.selected = [];
this.editor.instance.selectNodes(new BigUint64Array(this.selected));
},
doubleClick(e: MouseEvent) {
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
const nodeId = node?.getAttribute("data-node") || undefined;
if (nodeId) {
const id = BigInt(nodeId);
this.editor.instance.doubleClickNode(id);
}
},
pointerMove(e: PointerEvent) {
@@ -556,9 +575,6 @@ export default defineComponent({
}
},
pointerUp(e: PointerEvent) {
const graph: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
graph?.releasePointerCapture(e.pointerId);
this.panning = false;
if (this.linkInProgressToConnector instanceof HTMLDivElement && this.linkInProgressFromConnector) {
@@ -617,6 +633,7 @@ export default defineComponent({
TextLabel,
TextButton,
TextInput,
WidgetLayout,
},
});
</script>

View File

@@ -1,7 +1,7 @@
import { reactive, readonly } from "vue";
import { type Editor } from "@/wasm-communication/editor";
import { type FrontendNode, type FrontendNodeLink, type FrontendNodeType, UpdateNodeGraph, UpdateNodeTypes } from "@/wasm-communication/messages";
import { type FrontendNode, type FrontendNodeLink, type FrontendNodeType, UpdateNodeGraph, UpdateNodeTypes, UpdateNodeGraphBarLayout, defaultWidgetLayout } from "@/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createNodeGraphState(editor: Editor) {
@@ -9,6 +9,7 @@ export function createNodeGraphState(editor: Editor) {
nodes: [] as FrontendNode[],
links: [] as FrontendNodeLink[],
nodeTypes: [] as FrontendNodeType[],
nodeGraphBarLayout: defaultWidgetLayout(),
});
// Set up message subscriptions on creation
@@ -19,6 +20,9 @@ export function createNodeGraphState(editor: Editor) {
editor.subscriptions.subscribeJsMessage(UpdateNodeTypes, (updateNodeTypes) => {
state.nodeTypes = updateNodeTypes.nodeTypes;
});
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphBarLayout, (updateNodeGraphBarLayout) => {
state.nodeGraphBarLayout = updateNodeGraphBarLayout;
});
return {
state: readonly(state) as typeof state,

View File

@@ -1313,6 +1313,15 @@ export class UpdateMenuBarLayout extends JsMessage {
layout!: MenuBarEntry[];
}
export class UpdateNodeGraphBarLayout extends JsMessage {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] {
return menuBarEntry.map((entry) => ({
@@ -1382,6 +1391,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateMenuBarLayout,
UpdateMouseCursor,
UpdateNodeGraph,
UpdateNodeGraphBarLayout,
UpdateNodeTypes,
UpdateNodeGraphVisibility,
UpdateOpenDocumentsList,

View File

@@ -598,6 +598,13 @@ impl JsEditorHandle {
self.dispatch(message);
}
/// Notifies the backend that the user double clicked a node
#[wasm_bindgen(js_name = doubleClickNode)]
pub fn double_click_node(&self, node: u64) {
let message = NodeGraphMessage::DoubleClickNode { node };
self.dispatch(message);
}
/// Notifies the backend that the selected nodes have been moved
#[wasm_bindgen(js_name = moveSelectedNodes)]
pub fn move_selected_nodes(&self, displacement_x: i32, displacement_y: i32) {