diff --git a/app/gui2/src/components/GraphEditor.vue b/app/gui2/src/components/GraphEditor.vue index 7ac0f1a9d45..6a316a0172f 100644 --- a/app/gui2/src/components/GraphEditor.vue +++ b/app/gui2/src/components/GraphEditor.vue @@ -65,7 +65,7 @@ const interactionBindingsHandler = interactionBindings.handler({ // or the node that is being edited when creating from a port double click. function environmentForNodes(nodeIds: IterableIterator): Environment { const nodeRects = [...graphStore.nodeRects.values()] - const selectedNodeRects: Iterable = [...nodeIds] + const selectedNodeRects = [...nodeIds] .map((id) => graphStore.nodeRects.get(id)) .filter((item): item is Rect => item !== undefined) const screenBounds = graphNavigator.viewport @@ -73,15 +73,13 @@ function environmentForNodes(nodeIds: IterableIterator): Environment { return { nodeRects, selectedNodeRects, screenBounds, mousePosition } as Environment } -const placementEnvironment = computed(() => { - return environmentForNodes(nodeSelection.selected.values()) -}) +const placementEnvironment = computed(() => environmentForNodes(nodeSelection.selected.values())) -// Return the position for a new node, assuming there are currently nodes selected. If there are no nodes -// selected, return undefined. +/** Return the position for a new node, assuming there are currently nodes selected. If there are no nodes + * selected, return `undefined`. */ function placementPositionForSelection() { const hasNodeSelected = nodeSelection.selected.size > 0 - if (!hasNodeSelected) return undefined + if (!hasNodeSelected) return const gapBetweenNodes = 48.0 return previousNodeDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment.value, { horizontalGap: gapBetweenNodes, @@ -98,12 +96,10 @@ function targetComponentBrowserPosition() { const targetPos = targetNode?.position ?? Vec2.Zero return targetPos.add(COMPONENT_BROWSER_TO_NODE_OFFSET) } else { - const targetPos = placementPositionForSelection() - if (targetPos != undefined) { - return targetPos - } else { - return mouseDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment.value).position - } + return ( + placementPositionForSelection() ?? + mouseDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment.value).position + ) } } diff --git a/app/gui2/src/components/GraphEditor/GraphNode.vue b/app/gui2/src/components/GraphEditor/GraphNode.vue index d166ce3804e..9028ac860bc 100644 --- a/app/gui2/src/components/GraphEditor/GraphNode.vue +++ b/app/gui2/src/components/GraphEditor/GraphNode.vue @@ -27,6 +27,7 @@ const props = defineProps<{ const emit = defineEmits<{ updateRect: [rect: Rect] + 'update:vizRect': [rect: Rect | undefined] updateContent: [updates: [range: ContentRange, content: string][]] dragging: [offset: Vec2] draggingCommited: [] @@ -267,12 +268,14 @@ function portGroupStyle(port: PortData) {
(() => { :edited="id === graphStore.editedNodeInfo?.id" @update:edited="graphStore.setEditedNode(id, $event)" @updateRect="graphStore.updateNodeRect(id, $event)" + @update:vizRect="graphStore.updateVizRect(id, $event)" @delete="graphStore.deleteNode" @pointerenter="hoverNode(id)" @pointerleave="hoverNode(undefined)" diff --git a/app/gui2/src/components/GraphEditor/GraphVisualization.vue b/app/gui2/src/components/GraphEditor/GraphVisualization.vue index 1e35beeccd5..81462330235 100644 --- a/app/gui2/src/components/GraphEditor/GraphVisualization.vue +++ b/app/gui2/src/components/GraphEditor/GraphVisualization.vue @@ -13,25 +13,31 @@ import type { URLString } from '@/stores/visualization/compilerMessaging' import { toError } from '@/util/error' import type { Icon } from '@/util/iconName' import type { Opt } from '@/util/opt' -import type { Vec2 } from '@/util/vec2' +import { Rect } from '@/util/rect' +import { Vec2 } from '@/util/vec2' import type { ExprId, VisualizationIdentifier } from 'shared/yjsModel' -import { computed, onErrorCaptured, ref, shallowRef, watch, watchEffect } from 'vue' +import { computed, onErrorCaptured, onUnmounted, ref, shallowRef, watch, watchEffect } from 'vue' const visPreprocessor = ref(DEFAULT_VISUALIZATION_CONFIGURATION) const error = ref() +const TOP_WITHOUT_TOOLBAR_PX = 36 +const TOP_WITH_TOOLBAR_PX = 72 + const projectStore = useProjectStore() const visualizationStore = useVisualizationStore() const props = defineProps<{ currentType: Opt isCircularMenuVisible: boolean + nodePosition: Vec2 nodeSize: Vec2 typename?: string | undefined expressionId?: ExprId | undefined data?: any | undefined }>() const emit = defineEmits<{ + 'update:rect': [rect: Rect | undefined] setVisualizationId: [id: VisualizationIdentifier] setVisualizationVisible: [visible: boolean] }>() @@ -117,10 +123,45 @@ watchEffect(async () => { } }) +const isBelowToolbar = ref(false) +let width = ref(null) +let height = ref(150) + +watchEffect(() => + emit( + 'update:rect', + new Rect( + props.nodePosition, + new Vec2( + width.value ?? props.nodeSize.x, + height.value + (isBelowToolbar.value ? TOP_WITH_TOOLBAR_PX : TOP_WITHOUT_TOOLBAR_PX), + ), + ), + ), +) + +onUnmounted(() => emit('update:rect', undefined)) + provideVisualizationConfig({ fullscreen: false, - width: null, - height: 150, + get width() { + return width.value + }, + set width(value) { + width.value = value + }, + get height() { + return height.value + }, + set height(value) { + height.value = value + }, + get isBelowToolbar() { + return isBelowToolbar.value + }, + set isBelowToolbar(value) { + isBelowToolbar.value = value + }, get types() { return visualizationStore.types(props.typename) }, diff --git a/app/gui2/src/components/VisualizationContainer.vue b/app/gui2/src/components/VisualizationContainer.vue index 270f2695a2f..8641df3c30b 100644 --- a/app/gui2/src/components/VisualizationContainer.vue +++ b/app/gui2/src/components/VisualizationContainer.vue @@ -3,7 +3,7 @@ import SvgIcon from '@/components/SvgIcon.vue' import VisualizationSelector from '@/components/VisualizationSelector.vue' import { useVisualizationConfig } from '@/providers/visualizationConfig' import { PointerButtonMask, isClick, usePointer } from '@/util/events' -import { ref } from 'vue' +import { ref, watchEffect } from 'vue' const props = defineProps<{ /** If true, the visualization should be `overflow: visible` instead of `overflow: hidden`. */ @@ -16,6 +16,8 @@ const props = defineProps<{ const config = useVisualizationConfig() +watchEffect(() => (config.isBelowToolbar = props.belowToolbar)) + const isSelectorVisible = ref(false) function onWheel(event: WheelEvent) { diff --git a/app/gui2/src/providers/visualizationConfig.ts b/app/gui2/src/providers/visualizationConfig.ts index 62f51344844..d9d8324b3b2 100644 --- a/app/gui2/src/providers/visualizationConfig.ts +++ b/app/gui2/src/providers/visualizationConfig.ts @@ -13,8 +13,9 @@ export interface VisualizationConfig { readonly icon: Icon | URLString | undefined readonly isCircularMenuVisible: boolean readonly nodeSize: Vec2 + isBelowToolbar: boolean width: number | null - height: number | null + height: number fullscreen: boolean hide: () => void updateType: (type: VisualizationIdentifier) => void diff --git a/app/gui2/src/stores/graph/index.ts b/app/gui2/src/stores/graph/index.ts index c70ef68a1a0..a5eb74730cc 100644 --- a/app/gui2/src/stores/graph/index.ts +++ b/app/gui2/src/stores/graph/index.ts @@ -52,6 +52,7 @@ export const useGraphStore = defineStore('graph', () => { proj.computedValueRegistry, ) const nodeRects = reactive(new Map()) + const vizRects = reactive(new Map()) const exprRects = reactive(new Map()) const editedNodeInfo = ref() const imports = ref<{ import: Import; span: ContentRange }[]>([]) @@ -295,7 +296,7 @@ export const useGraphStore = defineStore('graph', () => { const { position } = nonDictatedPlacement(rect.size, { nodeRects: [...nodeRects.entries()] .filter(([id]) => db.nodeIdToNode.get(id)) - .map(([, rect]) => rect), + .map(([id, rect]) => vizRects.get(id) ?? rect), // The rest of the properties should not matter. selectedNodeRects: [], screenBounds: Rect.Zero, @@ -308,6 +309,11 @@ export const useGraphStore = defineStore('graph', () => { } } + function updateVizRect(id: ExprId, rect: Rect | undefined) { + if (rect) vizRects.set(id, rect) + else vizRects.delete(id) + } + function updateExprRect(id: ExprId, rect: Rect | undefined) { const current = exprRects.get(id) if (rect) { @@ -338,6 +344,7 @@ export const useGraphStore = defineStore('graph', () => { unconnectedEdge, edges, nodeRects, + vizRects, exprRects, createEdgeFromOutput, disconnectSource, @@ -353,6 +360,7 @@ export const useGraphStore = defineStore('graph', () => { setNodeVisualizationVisible, stopCapturingUndo, updateNodeRect, + updateVizRect, updateExprRect, setEditedNode, createNodeFromSource, diff --git a/app/gui2/src/util/selection.ts b/app/gui2/src/util/selection.ts index c475e273e24..b9fcc33dc7d 100644 --- a/app/gui2/src/util/selection.ts +++ b/app/gui2/src/util/selection.ts @@ -109,7 +109,7 @@ export function useSelection( overrideElemsToSelect.value = undefined } - const pointer = usePointer((pos, event, eventType) => { + const pointer = usePointer((_pos, event, eventType) => { if (eventType === 'start') { readInitiallySelected() } else if (pointer.dragging && anchor.value == null) { @@ -120,6 +120,7 @@ export function useSelection( } selectionEventHandler(event) }) + return proxyRefs({ selected, anchor,