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<{ 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' + } +}