Skip to content

Commit

Permalink
Add-node buttons (#9247)
Browse files Browse the repository at this point in the history
Introduce add-node button below circular menu or open visualization.

https://github.com/enso-org/enso/assets/1047859/aa6cedba-ca7e-44c5-ab27-2f5d5f9421e8
  • Loading branch information
kazcw authored Mar 8, 2024
1 parent 9f9cf58 commit dcad48e
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 50 deletions.
4 changes: 2 additions & 2 deletions app/gui2/e2e/collapsingAndEntering.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ test('Collapsing nodes', async ({ page }) => {

// Widgets may "steal" clicks, so we always click at icon.
await locate
.graphNodeByBinding(page, 'ten')
.graphNodeByBinding(page, 'prod')
.locator('.icon')
.click({ modifiers: ['Shift'] })
await locate
.graphNodeByBinding(page, 'sum')
.locator('.icon')
.click({ modifiers: ['Shift'] })
await locate
.graphNodeByBinding(page, 'prod')
.graphNodeByBinding(page, 'ten')
.locator('.icon')
.click({ modifiers: ['Shift'] })

Expand Down
1 change: 1 addition & 0 deletions app/gui2/e2e/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ provideVisualizationConfig._mock(
},
],
updateType() {},
addNode() {},
},
app,
)
Expand Down
5 changes: 4 additions & 1 deletion app/gui2/e2e/selectingNodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import * as locate from './locate'
test('Selecting nodes by click', async ({ page }) => {
await actions.goToGraph(page)
const node1 = locate.graphNodeByBinding(page, 'five')
const node2 = locate.graphNodeByBinding(page, 'ten')
const node2 = locate.graphNodeByBinding(page, 'final')
await expect(node1).not.toBeSelected()
await expect(node2).not.toBeSelected()

await locate.graphNodeIcon(node1).click()
await expect(node1).toBeSelected()
await expect(node2).not.toBeSelected()

// Check that clicking an unselected node deselects replaces the previous selection.
await locate.graphNodeIcon(node2).click()
await expect(node1).not.toBeSelected()
await expect(node2).toBeSelected()
Expand All @@ -24,10 +25,12 @@ test('Selecting nodes by click', async ({ page }) => {
await expect(node1).toBeSelected()
await expect(node2).toBeSelected()

// Check that when two nodes are selected, clicking a selected node replaces the previous selection.
await locate.graphNodeIcon(node2).click()
await expect(node1).not.toBeSelected()
await expect(node2).toBeSelected()

// Check that clicking the background deselects all nodes.
await page.mouse.click(600, 200)
await expect(node1).not.toBeSelected()
await expect(node2).not.toBeSelected()
Expand Down
5 changes: 5 additions & 0 deletions app/gui2/src/assets/icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
103 changes: 60 additions & 43 deletions app/gui2/src/components/CircularMenu.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script setup lang="ts">
import SmallPlusButton from '@/components/SmallPlusButton.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import ToggleIcon from '@/components/ToggleIcon.vue'
import { Vec2 } from '@/util/data/vec2'
const props = defineProps<{
isRecordingEnabledGlobally: boolean
Expand All @@ -17,64 +19,71 @@ const emit = defineEmits<{
startEditingComment: []
openFullMenu: []
delete: []
addNode: [pos: Vec2 | undefined]
}>()
</script>

<template>
<div
:class="`${props.isFullMenuVisible ? 'CircularMenu full' : 'CircularMenu partial'}`"
@pointerdown.stop
@pointerup.stop
@click.stop
>
<div v-if="!isFullMenuVisible" class="More" @pointerdown.stop="emit('openFullMenu')"></div>
<SvgIcon
v-if="isFullMenuVisible"
name="comment"
class="icon-container button slot2"
:alt="`Edit comment`"
@click.stop="emit('startEditingComment')"
/>
<SvgIcon
v-if="isFullMenuVisible"
name="trash2"
class="icon-container button slot4"
:alt="`Delete component`"
@click.stop="emit('delete')"
/>
<ToggleIcon
icon="eye"
class="icon-container button slot5"
:alt="`${props.isVisualizationVisible ? 'Hide' : 'Show'} visualization`"
:modelValue="props.isVisualizationVisible"
@update:modelValue="emit('update:isVisualizationVisible', $event)"
/>
<SvgIcon
name="edit"
class="icon-container button slot6"
data-testid="edit-button"
@click.stop="emit('startEditing')"
/>
<ToggleIcon
icon="record"
class="icon-container button slot7"
:class="{ 'recording-overridden': props.isRecordingOverridden }"
:alt="`${props.isRecordingOverridden ? 'Disable' : 'Enable'} recording`"
:modelValue="props.isRecordingOverridden"
@update:modelValue="emit('update:isRecordingOverridden', $event)"
<div class="CircularMenu" @pointerdown.stop @pointerup.stop @click.stop>
<div class="circle" :class="`${props.isFullMenuVisible ? 'full' : 'partial'}`">
<div v-if="!isFullMenuVisible" class="More" @pointerdown.stop="emit('openFullMenu')"></div>
<SvgIcon
v-if="isFullMenuVisible"
name="comment"
class="icon-container button slot2"
:alt="`Edit comment`"
@click.stop="emit('startEditingComment')"
/>
<SvgIcon
v-if="isFullMenuVisible"
name="trash2"
class="icon-container button slot4"
:alt="`Delete component`"
@click.stop="emit('delete')"
/>
<ToggleIcon
icon="eye"
class="icon-container button slot5"
:alt="`${props.isVisualizationVisible ? 'Hide' : 'Show'} visualization`"
:modelValue="props.isVisualizationVisible"
@update:modelValue="emit('update:isVisualizationVisible', $event)"
/>
<SvgIcon
name="edit"
class="icon-container button slot6"
data-testid="edit-button"
@click.stop="emit('startEditing')"
/>
<ToggleIcon
icon="record"
class="icon-container button slot7"
:class="{ 'recording-overridden': props.isRecordingOverridden }"
:alt="`${props.isRecordingOverridden ? 'Disable' : 'Enable'} recording`"
:modelValue="props.isRecordingOverridden"
@update:modelValue="emit('update:isRecordingOverridden', $event)"
/>
</div>
<SmallPlusButton
v-if="!isVisualizationVisible"
class="below-slot5"
@addNode="emit('addNode', $event)"
/>
</div>
</template>

<style scoped>
.CircularMenu {
user-select: none;
position: absolute;
user-select: none;
pointer-events: none;
}
.circle {
position: relative;
left: -36px;
top: -36px;
width: 114px;
height: 114px;
pointer-events: none;
> * {
pointer-events: all;
Expand Down Expand Up @@ -122,6 +131,7 @@ const emit = defineEmits<{
backdrop-filter: var(--blur-app-bg);
background: var(--color-app-bg);
z-index: -2;
pointer-events: all;
&:after {
content: '...';
Expand All @@ -140,6 +150,7 @@ const emit = defineEmits<{
padding: 0;
border: none;
opacity: 30%;
pointer-events: all;
}
.toggledOn {
Expand Down Expand Up @@ -198,6 +209,12 @@ const emit = defineEmits<{
top: 80px;
}
.below-slot5 {
position: absolute;
top: calc(108px - 36px);
pointer-events: all;
}
.slot6 {
position: absolute;
top: 69.46px;
Expand Down
13 changes: 11 additions & 2 deletions app/gui2/src/components/GraphEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -380,13 +380,21 @@ function showComponentBrowser(nodePosition?: Vec2, usage?: Usage) {
componentBrowserVisible.value = true
}
function startCreatingNodeFromButton() {
/** Start creating a node, basing its inputs and position on the current selection, if any;
* or the current viewport, otherwise.
*/
function addNodeAuto() {
const targetPos =
placementPositionForSelection() ??
nonDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment.value).position
showComponentBrowser(targetPos)
}
function addNodeAt(pos: Vec2 | undefined) {
if (!pos) return addNodeAuto()
showComponentBrowser(pos)
}
function hideComponentBrowser() {
graphStore.editedNodeInfo = undefined
componentBrowserVisible.value = false
Expand Down Expand Up @@ -604,6 +612,7 @@ function handleEdgeDrop(source: AstId, position: Vec2) {
<GraphNodes
@nodeOutputPortDoubleClick="handleNodeOutputPortDoubleClick"
@nodeDoubleClick="(id) => stackNavigator.enterNode(id)"
@addNode="addNodeAt($event)"
/>
</div>
<GraphEdges :navigator="graphNavigator" @createNodeFromEdge="handleEdgeDrop" />
Expand Down Expand Up @@ -631,7 +640,7 @@ function handleEdgeDrop(source: AstId, position: Vec2) {
@zoomIn="graphNavigator.stepZoom(+1)"
@zoomOut="graphNavigator.stepZoom(-1)"
/>
<PlusButton @pointerdown.stop @click.stop="startCreatingNodeFromButton()" @pointerup.stop />
<PlusButton @pointerdown.stop @click.stop="addNodeAuto()" @pointerup.stop />
<Transition>
<Suspense ref="codeEditorArea">
<CodeEditor v-if="showCodeEditor" />
Expand Down
3 changes: 3 additions & 0 deletions app/gui2/src/components/GraphEditor/GraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const emit = defineEmits<{
outputPortClick: [portId: AstId]
outputPortDoubleClick: [portId: AstId]
doubleClick: []
addNode: [pos: Vec2 | undefined]
'update:edited': [cursorPosition: number]
'update:rect': [rect: Rect]
'update:visualizationId': [id: Opt<VisualizationIdentifier>]
Expand Down Expand Up @@ -383,6 +384,7 @@ const documentation = computed<string | undefined>({
@startEditingComment="editingComment = true"
@openFullMenu="openFullMenu"
@delete="emit('delete')"
@addNode="emit('addNode', $event)"
/>
<GraphVisualization
v-if="isVisualizationVisible"
Expand All @@ -401,6 +403,7 @@ const documentation = computed<string | undefined>({
@update:visible="emit('update:visualizationVisible', $event)"
@update:fullscreen="emit('update:visualizationFullscreen', $event)"
@update:width="emit('update:visualizationWidth', $event)"
@addNode="emit('addNode', $event)"
/>
<Suspense>
<GraphNodeComment
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 @@ -21,6 +21,7 @@ const navigator = injectGraphNavigator(true)
const emit = defineEmits<{
nodeOutputPortDoubleClick: [portId: AstId]
nodeDoubleClick: [nodeId: NodeId]
addNode: [pos: Vec2 | undefined]
}>()
function nodeIsDragged(movedId: NodeId, offset: Vec2) {
Expand Down Expand Up @@ -54,6 +55,7 @@ const uploadingFiles = computed<[FileName, File][]>(() => {
@outputPortClick="graphStore.createEdgeFromOutput($event)"
@outputPortDoubleClick="emit('nodeOutputPortDoubleClick', $event)"
@doubleClick="emit('nodeDoubleClick', id)"
@addNode="emit('addNode', $event)"
@update:edited="graphStore.setEditedNode(id, $event)"
@update:rect="graphStore.updateNodeRect(id, $event)"
@update:visualizationId="
Expand Down
2 changes: 2 additions & 0 deletions app/gui2/src/components/GraphEditor/GraphVisualization.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const emit = defineEmits<{
'update:visible': [visible: boolean]
'update:fullscreen': [fullscreen: boolean]
'update:width': [width: number]
addNode: [pos: Vec2 | undefined]
}>()
const visPreprocessor = ref(DEFAULT_VISUALIZATION_CONFIGURATION)
Expand Down Expand Up @@ -292,6 +293,7 @@ provideVisualizationConfig({
},
hide: () => emit('update:visible', false),
updateType: (id) => emit('update:id', id),
addNode: (pos) => emit('addNode', pos),
})
const effectiveVisualization = computed(() => {
Expand Down
65 changes: 65 additions & 0 deletions app/gui2/src/components/SmallPlusButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
import SvgIcon from '@/components/SvgIcon.vue'
import { injectGraphNavigator } from '@/providers/graphNavigator'
import { Vec2 } from '@/util/data/vec2'
import { ref } from 'vue'
const emit = defineEmits<{
addNode: [pos: Vec2 | undefined]
}>()
const navigator = injectGraphNavigator(true)
const addNodeButton = ref<HTMLElement>()
function addNode() {
const clientRect = addNodeButton.value?.getBoundingClientRect()
const pos = clientRect && navigator?.clientToScenePos(new Vec2(clientRect.left, clientRect.top))
emit('addNode', pos)
}
</script>

<template>
<div
ref="addNodeButton"
class="SmallPlusButton add-node"
@click.stop
@pointerdown.stop
@pointerup.stop
>
<SvgIcon name="add" class="icon button" @click.stop="addNode" />
</div>
</template>

<style scoped>
.SmallPlusButton {
width: var(--node-height);
height: var(--node-height);
&:before {
content: '';
position: absolute;
left: 0;
top: 0;
backdrop-filter: var(--blur-app-bg);
background: var(--color-app-bg);
border-radius: 16px;
width: var(--node-height);
height: var(--node-height);
}
&:hover:before {
background: rgb(230, 230, 255);
}
&:active:before {
background: rgb(158, 158, 255);
}
}
.icon {
display: inline-flex;
background: none;
margin: 8px;
padding: 0;
border: none;
opacity: 30%;
}
</style>
Loading

0 comments on commit dcad48e

Please sign in to comment.