Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create node from port button #11836

Merged
merged 5 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Next Next Release

#### Enso IDE

- [Round ‘Add component’ button under the component menu replaced by a small
button protruding from the output port.][11836].

[11836]: https://github.com/enso-org/enso/pull/11836

#### Enso Language & Runtime

- [Intersection types & type checks][11600]
Expand Down
4 changes: 2 additions & 2 deletions app/gui/integration-test/project-view/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ export async function dragNodeByBinding(page: Page, nodeBinding: string, x: numb
}

/** Move mouse away to avoid random hover events and wait for any circular menus to disappear. */
export async function ensureNoCircularMenusVisibleDueToHovering(page: Page) {
export async function ensureNoComponentMenusVisibleDueToHovering(page: Page) {
await page.mouse.move(-1000, 0)
await expect(locate.circularMenu(page)).toBeHidden()
await expect(locate.componentMenu(page)).toBeHidden()
}

/** Ensure no nodes are selected. */
Expand Down
22 changes: 9 additions & 13 deletions app/gui/integration-test/project-view/componentBrowser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,21 @@ test('Different ways of opening Component Browser', async ({ page }) => {
await expectAndCancelBrowser(page, '', 'selected')
})

test('Opening Component Browser with small plus buttons', async ({ page }) => {
test('Opening Component Browser from output port buttons', async ({ page }) => {
await actions.goToGraph(page)

// Small (+) button shown when node is hovered
await page.keyboard.press('Escape')
await page.mouse.move(100, 80)
await expect(locate.smallPlusButton(page)).toBeHidden()
await locate.graphNodeIcon(locate.graphNodeByBinding(page, 'selected')).hover()
await expect(locate.smallPlusButton(page)).toBeVisible()
await locate.smallPlusButton(page).click()
const node = locate.graphNodeByBinding(page, 'selected')
await locate.graphNodeIcon(node).hover()
await expect(locate.createNodeFromPort(node)).toBeVisible()
await locate.createNodeFromPort(node).click({ force: true })
await expectAndCancelBrowser(page, '', 'selected')

// Small (+) button shown when node is sole selection
// Small (+) button shown when node is selected
await page.keyboard.press('Escape')
await page.mouse.move(300, 300)
await expect(locate.smallPlusButton(page)).toBeHidden()
await locate.graphNodeByBinding(page, 'selected').click()
await expect(locate.smallPlusButton(page)).toBeVisible()
await locate.smallPlusButton(page).click()
await node.click()
await expect(locate.createNodeFromPort(node)).toBeVisible()
await locate.createNodeFromPort(node).click({ force: true })
await expectAndCancelBrowser(page, '', 'selected')
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test('Node can open and load visualization', async ({ page }) => {
await actions.goToGraph(page)
const node = locate.graphNode(page).last()
await node.click({ position: { x: 8, y: 8 } })
await expect(locate.circularMenu(page)).toExist()
await expect(locate.componentMenu(page)).toExist()
await locate.toggleVisualizationButton(page).click()
await expect(locate.anyVisualization(page)).toExist()
await expect(locate.loadingVisualization(page)).toHaveCount(0)
Expand Down
4 changes: 2 additions & 2 deletions app/gui/integration-test/project-view/locate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ export const graphEditor = componentLocator('.GraphEditor')
export const codeEditor = componentLocator('.CodeEditor')
export const anyVisualization = componentLocator('.GraphVisualization')
export const loadingVisualization = componentLocator('.LoadingVisualization')
export const circularMenu = componentLocator('.CircularMenu')
export const componentMenu = componentLocator('.ComponentMenu')
export const addNewNodeButton = componentLocator('.PlusButton')
export const componentBrowser = componentLocator('.ComponentBrowser')
export const nodeOutputPort = componentLocator('.outputPortHoverArea')
export const smallPlusButton = componentLocator('.SmallPlusButton')
export const createNodeFromPort = componentLocator('.CreateNodeFromPortButton .plusIcon')
export const editorRoot = componentLocator('.CodeMirror')
export const nodeComment = componentLocator('.GraphNodeComment')
export const nodeCommentContent = componentLocator('.GraphNodeComment div[contentEditable]')
Expand Down
8 changes: 4 additions & 4 deletions app/gui/integration-test/project-view/nodeComments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ test('Start editing comment via menu', async ({ page }) => {
await actions.goToGraph(page)
const node = locate.graphNodeByBinding(page, 'final')
await node.click()
await locate.circularMenu(node).getByRole('button', { name: 'More' }).click()
await locate.circularMenu(node).getByRole('button', { name: 'Comment' }).click()
await locate.componentMenu(node).getByRole('button', { name: 'More' }).click()
await locate.componentMenu(node).getByRole('button', { name: 'Comment' }).click()
await expect(locate.nodeCommentContent(node)).toBeFocused()
})

Expand Down Expand Up @@ -60,8 +60,8 @@ test('Add new comment via menu', async ({ page }) => {
const nodeComment = locate.nodeCommentContent(node)

await node.click()
await locate.circularMenu(node).getByRole('button', { name: 'More' }).click()
await locate.circularMenu(node).getByRole('button', { name: 'Comment' }).click()
await locate.componentMenu(node).getByRole('button', { name: 'More' }).click()
await locate.componentMenu(node).getByRole('button', { name: 'Comment' }).click()
await expect(locate.nodeCommentContent(node)).toBeFocused()
const NEW_COMMENT = 'New comment text'
await nodeComment.fill(NEW_COMMENT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ async function assertTypeLabelOnNode(
) {
// Ensure the visualization button won't be covered by any other parts of another node (e.g. a comment).
await bringNodeToFront(page, node)
await node.hover({ position: { x: 8, y: 8 } })
await locate.toggleVisualizationButton(node).click()
await node.hover({ position: { x: 8, y: 8 }, force: true })
await locate.toggleVisualizationButton(node).click({ force: true })
const targetLabel = node.locator('.node-type').first()
await expect(targetLabel).toHaveText(type.short)
await expect(targetLabel).toHaveAttribute('title', type.full)
await locate.toggleVisualizationButton(node).click()
await locate.toggleVisualizationButton(node).click({ force: true })
await actions.deselectNodes(page)
}

async function bringNodeToFront(page: Page, node: Locator) {
await node.click({ position: { x: 8, y: 8 } })
await node.click({ position: { x: 0, y: 8 }, force: true })
await page.keyboard.press('Escape')
}

Expand Down
2 changes: 1 addition & 1 deletion app/gui/src/project-view/components/ComponentBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ const handler = componentBrowserBindings.handler({
:nodeSize="inputSize"
:nodePosition="nodePosition"
:scale="1"
:isCircularMenuVisible="false"
:isComponentMenuVisible="false"
:isFullscreen="false"
:isFullscreenAllowed="false"
:isResizable="false"
Expand Down
4 changes: 2 additions & 2 deletions app/gui/src/project-view/components/ComponentMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const isDropdownOpened = ref(false)

<template>
<div
class="CircularMenu"
class="ComponentMenu"
:class="{
menu: !componentButtons.pickColor.state,
openedDropdown: isDropdownOpened,
Expand Down Expand Up @@ -58,7 +58,7 @@ const isDropdownOpened = ref(false)
</template>

<style scoped>
.CircularMenu {
.ComponentMenu {
position: absolute;
left: -36px;
bottom: -36px;
Expand Down
15 changes: 9 additions & 6 deletions app/gui/src/project-view/components/GraphEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { keyboardBusy, keyboardBusyExceptIn, unrefElement, useEvent } from '@/co
import { groupColorVar } from '@/composables/nodeColors'
import type { PlacementStrategy } from '@/composables/nodeCreation'
import { provideGraphEditorLayers } from '@/providers/graphEditorLayers'
import { provideGraphEditorState } from '@/providers/graphEditorState'
import type { GraphNavigator } from '@/providers/graphNavigator'
import { provideGraphNavigator } from '@/providers/graphNavigator'
import { provideNodeColors } from '@/providers/graphNodeColors'
Expand Down Expand Up @@ -286,7 +287,7 @@ const graphBindingsHandler = graphBindings.handler({
projectStore.lsRpcConnection.profilingStop()
},
openComponentBrowser() {
if (graphNavigator.sceneMousePos != null && !componentBrowserVisible.value) {
if (graphNavigator.sceneMousePos != null && !componentBrowserOpened.value) {
createWithComponentBrowser(fromSelection() ?? { placement: { type: 'mouse' } })
}
},
Expand Down Expand Up @@ -390,23 +391,25 @@ const documentationEditorHandler = documentationEditorBindings.handler({
// === Component Browser ===
const componentBrowserVisible = ref(false)
const { componentBrowserOpened } = provideGraphEditorState({
componentBrowserOpened: ref(false),
})
const componentBrowserNodePosition = ref<Vec2>(Vec2.Zero)
const componentBrowserUsage = ref<Usage>({ type: 'newNode' })
watch(componentBrowserVisible, (v) =>
watch(componentBrowserOpened, (v) =>
rightDock.setStorageMode(v ? StorageMode.ComponentBrowser : StorageMode.Default),
)
function openComponentBrowser(usage: Usage, position: Vec2) {
componentBrowserUsage.value = usage
componentBrowserNodePosition.value = position
componentBrowserVisible.value = true
componentBrowserOpened.value = true
}
function hideComponentBrowser() {
graphStore.editedNodeInfo = undefined
componentBrowserVisible.value = false
componentBrowserOpened.value = false
displayedDocs.value = undefined
}
Expand Down Expand Up @@ -639,7 +642,7 @@ const groupColors = computed(() => {
/>
<GraphEdges :navigator="graphNavigator" @createNodeFromEdge="handleEdgeDrop" />
<ComponentBrowser
v-if="componentBrowserVisible"
v-if="componentBrowserOpened"
ref="componentBrowser"
:navigator="graphNavigator"
:nodePosition="componentBrowserNodePosition"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<script setup lang="ts">
import { ref } from 'vue'
import { AstId } from 'ydoc-shared/ast'
const props = defineProps<{ portId: AstId }>()
const emit = defineEmits<{ click: [] }>()
const hovered = ref(false)
</script>

<template>
<g :class="{ CreateNodeFromPortButton: true, hovered }" @click="emit('click')">
<rect :class="{ connection: true }" fill="currentColor"></rect>
<g :class="{ plusIcon: true }">
<mask :id="`${props.portId}_add_node_clip_path`">
<rect class="maskBackground"></rect>
<rect class="plusV"></rect>
<rect class="plusH"></rect>
</mask>
<circle :mask="`url(#${props.portId}_add_node_clip_path)`" fill="currentColor"></circle>
</g>
<rect class="hoverArea" @pointerenter="hovered = true" @pointerleave="hovered = false"></rect>
</g>
</template>

<style scoped>
.CreateNodeFromPortButton {
--radius: 6px;
--maskSize: calc(var(--radius) * 2);
--strokeWidth: 1.5px;
--leftOffset: 16px;
--topOffset: 40px;
--color-dimmed: color-mix(in oklab, var(--color-node-primary) 60%, white 40%);
--color: var(--color-node-primary);
}
.connection {
--width: 4px;
--direct-hover-offset: calc(
var(--output-port-hovered-extra-width) * var(--direct-hover-animation)
);
width: var(--width);
height: calc((var(--topOffset) - var(--direct-hover-offset) + 2px) * var(--hover-animation));
transform: translate(
calc(var(--port-clip-start) * (100% + 1px) + var(--leftOffset) - var(--width) / 2),
calc(var(--node-size-y) + var(--direct-hover-offset) - var(--output-port-overlap))
);
cursor: pointer;
color: var(--color-dimmed);
transition: color 0.2s ease;
}
.hovered * {
color: var(--color);
}
.plusIcon {
transform: translate(
calc(var(--port-clip-start) * (100% + 1px) + var(--leftOffset) - var(--radius)),
calc(
var(--node-size-y) + var(--output-port-max-width) + var(--node-vertical-gap) +
var(--topOffset)
)
);
color: var(--color-dimmed);
cursor: pointer;
& .maskBackground {
fill: white;
width: var(--maskSize);
height: var(--maskSize);
}
& .plusV {
x: calc(var(--maskSize) / 2 - var(--strokeWidth) / 2);
y: calc(var(--radius) / 2);
width: var(--strokeWidth);
height: var(--radius);
fill: black;
}
& .plusH {
x: calc(var(--radius) / 2);
y: calc(var(--maskSize) / 2 - var(--strokeWidth) / 2);
width: var(--radius);
height: var(--strokeWidth);
fill: black;
}
& circle {
cx: var(--radius);
cy: var(--radius);
r: calc(var(--radius) * var(--hover-animation));
transition: color 0.2s ease;
}
}
.hoverArea {
--margin: 4px;
--width: calc(var(--radius) * 2 + var(--margin) * 2);
fill: transparent;
width: var(--width);
height: calc(
var(--node-vertical-gap) + var(--output-port-max-width) + var(--margin) * 2 + var(--topOffset) +
var(--radius)
);
transform: translate(
calc(var(--port-clip-start) * (100% + 1px) + var(--leftOffset) - var(--width) / 2),
calc(var(--node-size-y) + var(--output-port-max-width))
);
cursor: pointer;
}
</style>
13 changes: 5 additions & 8 deletions app/gui/src/project-view/components/GraphEditor/GraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import GraphNodeOutputPorts from '@/components/GraphEditor/GraphNodeOutputPorts.
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation'
import PointFloatingMenu from '@/components/PointFloatingMenu.vue'
import SmallPlusButton from '@/components/SmallPlusButton.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { useDoubleClick } from '@/composables/doubleClick'
import { usePointer, useResizeObserver } from '@/composables/events'
Expand Down Expand Up @@ -500,7 +499,7 @@ const showMenuAt = ref<{ x: number; y: number }>()
:nodeSize="nodeSize"
:scale="navigator?.scale ?? 1"
:nodePosition="nodePosition"
:isCircularMenuVisible="menuVisible"
:isComponentMenuVisible="menuVisible"
:currentType="props.node.vis?.identifier"
:dataSource="dataSource"
:typename="expressionInfo?.typename"
Expand Down Expand Up @@ -562,17 +561,15 @@ const showMenuAt = ref<{ x: number; y: number }>()
v-if="props.node.type !== 'output'"
:nodeId="nodeId"
:forceVisible="nodeHovered"
@newNodeClick="
setSoleSelected(), emit('createNodes', [{ commit: false, content: undefined }])
"
@portClick="(...args) => emit('outputPortClick', ...args)"
@portDoubleClick="(...args) => emit('outputPortDoubleClick', ...args)"
@update:hoverAnim="emit('update:hoverAnim', $event)"
@update:nodeHovered="outputHovered = $event"
/>
</svg>
<SmallPlusButton
v-if="menuVisible"
:class="isVisualizationVisible ? 'afterNode' : 'belowMenu'"
@createNodes="setSoleSelected(), emit('createNodes', $event)"
/>
</div>
<PointFloatingMenu v-if="showMenuAt" :point="showMenuAt" @close="showMenuAt = undefined">
<ComponentContextMenu @close="showMenuAt = undefined" />
Expand Down Expand Up @@ -641,7 +638,7 @@ const showMenuAt = ref<{ x: number; y: number }>()
opacity: 1;
}
.CircularMenu {
.ComponentMenu {
z-index: 25;
&.partial {
z-index: 1;
Expand Down
Loading
Loading