diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9dc4cdb4998..c1f6b7eaa2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,9 +3,14 @@
#### Enso IDE
- [Table Editor Widget][10774] displayed in `Table.new` component.
+- [New design of Component Browser][10814] - the component list is under the
+ input and shown only in the initial "component browsing" mode - after picking
+ any suggestion with Tab or new button the mode is switched to "code editing",
+ where visualization preview is displayed instead.
- [Drilldown for XML][10824]
[10774]: https://github.com/enso-org/enso/pull/10774
+[10814]: https://github.com/enso-org/enso/pull/10814
[10824]: https://github.com/enso-org/enso/pull/10824
#### Enso Standard Library
diff --git a/app/gui2/e2e/componentBrowser.spec.ts b/app/gui2/e2e/componentBrowser.spec.ts
index 86e5272edc0..63eb2f27e6f 100644
--- a/app/gui2/e2e/componentBrowser.spec.ts
+++ b/app/gui2/e2e/componentBrowser.spec.ts
@@ -106,7 +106,7 @@ test('Graph Editor pans to Component Browser', async ({ page }) => {
// Dragging out an edge to the bottom of the viewport; when the CB pans into view, some nodes are out of view.
await page.mouse.move(100, 1100)
await page.mouse.down({ button: 'middle' })
- await page.mouse.move(100, 80)
+ await page.mouse.move(100, 280)
await page.mouse.up({ button: 'middle' })
await expect(locate.graphNodeByBinding(page, 'five')).toBeInViewport()
const outputPort = await locate.outputPortCoordinates(locate.graphNodeByBinding(page, 'final'))
@@ -240,14 +240,10 @@ test('Visualization preview: type-based visualization selection', async ({ page
await expect(locate.componentBrowser(page)).toExist()
await expect(locate.componentBrowserEntry(page)).toExist()
const input = locate.componentBrowserInput(page).locator('input')
- await input.fill('4')
- await expect(input).toHaveValue('4')
- await expect(locate.jsonVisualization(page)).toExist()
await input.fill('Table.ne')
await expect(input).toHaveValue('Table.ne')
- // The table visualization is not currently working with `executeExpression` (#9194), but we can test that the JSON
- // visualization is no longer selected.
- await expect(locate.jsonVisualization(page)).toBeHidden()
+ await locate.componentBrowser(page).getByTestId('switchToEditMode').click()
+ await expect(locate.tableVisualization(page)).toBeVisible()
await page.keyboard.press('Escape')
await expect(locate.componentBrowser(page)).toBeHidden()
await expect(locate.graphNode(page)).toHaveCount(nodeCount)
@@ -258,17 +254,15 @@ test('Visualization preview: user visualization selection', async ({ page }) =>
const nodeCount = await locate.graphNode(page).count()
await locate.addNewNodeButton(page).click()
await expect(locate.componentBrowser(page)).toExist()
- await expect(locate.componentBrowserEntry(page)).toExist()
const input = locate.componentBrowserInput(page).locator('input')
await input.fill('4')
await expect(input).toHaveValue('4')
- await expect(locate.jsonVisualization(page)).toExist()
+ await locate.componentBrowser(page).getByTestId('switchToEditMode').click()
+ await expect(locate.jsonVisualization(page)).toBeVisible()
await expect(locate.jsonVisualization(page)).toContainText('"visualizedExpr": "4"')
await locate.toggleVisualizationSelectorButton(page).click()
await page.getByRole('button', { name: 'Table' }).click()
- // The table visualization is not currently working with `executeExpression` (#9194), but we can test that the JSON
- // visualization is no longer selected.
- await expect(locate.jsonVisualization(page)).toBeHidden()
+ await expect(locate.tableVisualization(page)).toBeVisible()
await page.keyboard.press('Escape')
await expect(locate.componentBrowser(page)).toBeHidden()
await expect(locate.graphNode(page)).toHaveCount(nodeCount)
diff --git a/app/gui2/src/bindings.ts b/app/gui2/src/bindings.ts
index f36234d2ec0..99cc69a11ca 100644
--- a/app/gui2/src/bindings.ts
+++ b/app/gui2/src/bindings.ts
@@ -19,8 +19,9 @@ export const interactionBindings = defineKeybinds('current-interaction', {
})
export const componentBrowserBindings = defineKeybinds('component-browser', {
- applySuggestion: ['Tab'],
+ applySuggestionAndSwitchToEditMode: ['Tab'],
acceptSuggestion: ['Enter'],
+ acceptCode: ['Enter'],
acceptInput: ['Mod+Enter'],
acceptAIPrompt: ['Tab', 'Enter'],
moveUp: ['ArrowUp'],
diff --git a/app/gui2/src/components/ComponentBrowser.vue b/app/gui2/src/components/ComponentBrowser.vue
index 32184a2d32c..bb35ef45b61 100644
--- a/app/gui2/src/components/ComponentBrowser.vue
+++ b/app/gui2/src/components/ComponentBrowser.vue
@@ -1,15 +1,24 @@
+
+
@@ -463,7 +362,7 @@ const handler = componentBrowserBindings.handler({
diff --git a/app/gui2/src/components/ComponentBrowser/ComponentList.vue b/app/gui2/src/components/ComponentBrowser/ComponentList.vue
new file mode 100644
index 00000000000..777e3f5c00b
--- /dev/null
+++ b/app/gui2/src/components/ComponentBrowser/ComponentList.vue
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
diff --git a/app/gui2/src/components/ComponentBrowser/scrolling.ts b/app/gui2/src/components/ComponentBrowser/scrolling.ts
index 0400540621b..b0b3256bb6a 100644
--- a/app/gui2/src/components/ComponentBrowser/scrolling.ts
+++ b/app/gui2/src/components/ComponentBrowser/scrolling.ts
@@ -1,24 +1,20 @@
import { useApproach } from '@/composables/animation'
-import { computed, ref } from 'vue'
+import { ToValue } from '@/util/reactivity'
+import { computed, ref, toValue } from 'vue'
export type ScrollTarget =
- | { type: 'bottom' }
+ | { type: 'top' }
| { type: 'selected' }
| { type: 'offset'; offset: number }
-export function useScrolling(
- selectedPos: { value: number },
- scrollerSize: { value: number },
- contentSize: { value: number },
- entrySize: number,
-) {
- const targetScroll = ref({ type: 'bottom' })
+export function useScrolling(selectedPos: ToValue) {
+ const targetScroll = ref({ type: 'top' })
const targetScrollPosition = computed(() => {
switch (targetScroll.value.type) {
case 'selected':
- return Math.max(selectedPos.value - scrollerSize.value + entrySize, 0)
- case 'bottom':
- return contentSize.value - scrollerSize.value
+ return toValue(selectedPos)
+ case 'top':
+ return 0.0
case 'offset':
return targetScroll.value.offset
}
diff --git a/app/gui2/src/components/GraphEditor.vue b/app/gui2/src/components/GraphEditor.vue
index cb95fc6c671..59cbc0e6b4f 100644
--- a/app/gui2/src/components/GraphEditor.vue
+++ b/app/gui2/src/components/GraphEditor.vue
@@ -53,7 +53,7 @@ import { colorFromString } from '@/util/colors'
import { partition } from '@/util/data/array'
import { every, filterDefined } from '@/util/data/iterable'
import { Rect } from '@/util/data/rect'
-import { Err, Ok, unwrapOr, type Result } from '@/util/data/result'
+import { Err, Ok, unwrapOr } from '@/util/data/result'
import { Vec2 } from '@/util/data/vec2'
import { computedFallback } from '@/util/reactivity'
import { until } from '@vueuse/core'
diff --git a/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue b/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue
index a8a35f73a14..c16fd3fc12c 100644
--- a/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue
+++ b/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue
@@ -8,7 +8,7 @@ import { useGraphStore, type NodeId } from '@/stores/graph'
import type { NodeType } from '@/stores/graph/graphDatabase'
import { Ast } from '@/util/ast'
import type { Vec2 } from '@/util/data/vec2'
-import { displayedIconOf } from '@/util/getIconName'
+import { iconOfNode } from '@/util/getIconName'
import { computed, toRef, watch } from 'vue'
import { DisplayIcon } from './widgets/WidgetIcon.vue'
@@ -100,21 +100,7 @@ const widgetTree = provideWidgetTree(
() => emit('openFullMenu'),
)
-const expressionInfo = computed(() => graph.db.getExpressionInfo(props.ast.externalId))
-const suggestionEntry = computed(() => graph.db.getNodeMainSuggestion(props.nodeId))
-const topLevelIcon = computed(() => {
- switch (props.nodeType) {
- default:
- case 'component':
- return displayedIconOf(
- suggestionEntry.value,
- expressionInfo.value?.methodCall?.methodPointer,
- expressionInfo.value?.typename ?? 'Unknown',
- )
- case 'output':
- return 'data_output'
- }
-})
+const topLevelIcon = computed(() => iconOfNode(props.nodeId, graph.db))
watch(toRef(widgetTree, 'currentEdit'), (edit) => edit && selectNode())
diff --git a/app/gui2/src/components/SvgButton.vue b/app/gui2/src/components/SvgButton.vue
index eea8f245a87..481668c709c 100644
--- a/app/gui2/src/components/SvgButton.vue
+++ b/app/gui2/src/components/SvgButton.vue
@@ -15,7 +15,7 @@ const _props = defineProps<{
-
+ {{ label }}
diff --git a/app/gui2/src/stores/graph/graphDatabase.ts b/app/gui2/src/stores/graph/graphDatabase.ts
index b4d89e78387..5cfdad76244 100644
--- a/app/gui2/src/stores/graph/graphDatabase.ts
+++ b/app/gui2/src/stores/graph/graphDatabase.ts
@@ -21,7 +21,7 @@ import {
} from '@/util/reactivity'
import * as objects from 'enso-common/src/utilities/data/object'
import * as set from 'lib0/set'
-import { reactive, ref, shallowReactive, watchEffect, WatchStopHandle, type Ref } from 'vue'
+import { reactive, ref, shallowReactive, WatchStopHandle, type Ref } from 'vue'
import type { MethodCall, StackItem } from 'ydoc-shared/languageServerTypes'
import type { Opt } from 'ydoc-shared/util/data/opt'
import type { ExternalId, SourceRange, VisualizationMetadata } from 'ydoc-shared/yjsModel'
diff --git a/app/gui2/src/util/autoBlur.ts b/app/gui2/src/util/autoBlur.ts
index ae61f5004d2..5a8df1e9374 100644
--- a/app/gui2/src/util/autoBlur.ts
+++ b/app/gui2/src/util/autoBlur.ts
@@ -55,18 +55,43 @@ export function isNodeOutside(element: any, area: Opt): boolean {
}
/** Returns a new interaction based on the given `interaction`. The new interaction will be ended if a pointerdown event
- * occurs outside the given `area` element. */
+ * occurs outside the given `area` element.
+ *
+ * See also {@link cancelOnClickOutside}.
+ */
export function endOnClickOutside(
- area: Ref,
+ area: Ref>,
interaction: Interaction,
): Interaction {
- const chainedPointerdown = interaction.pointerdown
const handler = injectInteractionHandler()
+ return handleClickOutside(area, interaction, handler.end.bind(handler))
+}
+
+/** Returns a new interaction based on the given `interaction`. The new interaction will be canceled if a pointerdown event
+ * occurs outside the given `area` element.
+ *
+ * See also {@link endOnClickOutside}.
+ */
+export function cancelOnClickOutside(
+ area: Ref>,
+ interaction: Interaction,
+) {
+ const handler = injectInteractionHandler()
+ return handleClickOutside(area, interaction, handler.cancel.bind(handler))
+}
+
+/** Common part of {@link cancelOnClickOutside} and {@link endOnClickOutside}. */
+function handleClickOutside(
+ area: Ref>,
+ interaction: Interaction,
+ handler: (interaction: Interaction) => void,
+) {
+ const chainedPointerdown = interaction.pointerdown
const wrappedInteraction: Interaction = {
...interaction,
pointerdown: (e: PointerEvent, ...args) => {
if (targetIsOutside(e, unrefElement(area))) {
- handler.end(wrappedInteraction)
+ handler(wrappedInteraction)
return false
}
return chainedPointerdown ? chainedPointerdown(e, ...args) : false
diff --git a/app/gui2/src/util/getIconName.ts b/app/gui2/src/util/getIconName.ts
index 50f9c4327ea..a12a237b831 100644
--- a/app/gui2/src/util/getIconName.ts
+++ b/app/gui2/src/util/getIconName.ts
@@ -1,3 +1,5 @@
+import { NodeId } from '@/stores/graph'
+import { GraphDb } from '@/stores/graph/graphDatabase'
import {
SuggestionKind,
type SuggestionEntry,
@@ -43,3 +45,20 @@ export function displayedIconOf(
return DEFAULT_ICON
}
}
+
+export function iconOfNode(node: NodeId, graphDb: GraphDb) {
+ const expressionInfo = graphDb.getExpressionInfo(node)
+ const suggestionEntry = graphDb.getNodeMainSuggestion(node)
+ const nodeType = graphDb.nodeIdToNode.get(node)?.type
+ switch (nodeType) {
+ default:
+ case 'component':
+ return displayedIconOf(
+ suggestionEntry,
+ expressionInfo?.methodCall?.methodPointer,
+ expressionInfo?.typename ?? 'Unknown',
+ )
+ case 'output':
+ return 'data_output'
+ }
+}