From 149750ce72d2ce6ad80063e2609876cf59311456 Mon Sep 17 00:00:00 2001 From: Kaz Date: Thu, 27 Jun 2024 13:37:54 -0700 Subject: [PATCH 1/3] Doc editor and code editor are toggleable docks --- app/gui2/e2e/actions.ts | 11 +- app/gui2/src/App.vue | 2 +- app/gui2/src/assets/base.css | 1 + app/gui2/src/components/BottomPanel.vue | 79 ++++++++ app/gui2/src/components/CodeEditor.vue | 111 +---------- app/gui2/src/components/DockPanel.vue | 89 +++++---- .../src/components/DocumentationEditor.vue | 9 +- app/gui2/src/components/ExtendedMenu.vue | 24 ++- app/gui2/src/components/GraphEditor.vue | 186 +++++++++++------- .../components/GraphEditor/NodeWidgetTree.vue | 2 +- .../GraphEditor/widgets/WidgetVector.vue | 2 +- app/gui2/src/components/MarkdownEditor.vue | 1 + .../MarkdownEditor/FormattingToolbar.vue | 5 +- .../MarkdownEditor/MarkdownEditorImpl.vue | 5 +- app/gui2/src/components/PlusButton.vue | 2 +- app/gui2/src/components/ResizeHandles.vue | 26 +-- app/gui2/src/components/SlideIn.vue | 54 +++++ app/gui2/src/components/TopBar.vue | 11 +- app/gui2/src/composables/events.ts | 24 ++- .../__tests__/editHandler.test.ts | 2 +- 20 files changed, 383 insertions(+), 263 deletions(-) create mode 100644 app/gui2/src/components/BottomPanel.vue create mode 100644 app/gui2/src/components/SlideIn.vue diff --git a/app/gui2/e2e/actions.ts b/app/gui2/e2e/actions.ts index 79f0e0fd2d5f..40aecf65e467 100644 --- a/app/gui2/e2e/actions.ts +++ b/app/gui2/e2e/actions.ts @@ -12,11 +12,14 @@ export async function goToGraph(page: Page, closeDocPanel: boolean = true) { await page.goto('/') // Initial load through vite can take a while. Make sure that the first locator has enough time. await expect(page.locator('.GraphEditor')).toBeVisible({ timeout: 100000 }) - if (closeDocPanel) { - await page.locator('.rightDock > .closeButton').click() - } // Wait until nodes are loaded. await expect(locate.graphNode(page)).toExist() + if (closeDocPanel) { + await expect(page.getByTestId('rightDock')).toExist() + await page.getByRole('button', { name: 'Documentation Panel' }).click() + // Wait for the closing animation. + await expect(page.getByTestId('rightDock')).not.toBeVisible() + } // Wait for position initialization await expectNodePositionsInitialized(page, 72) } @@ -46,7 +49,7 @@ export async function exitFunction(page: Page, x = 300, y = 300) { /// Move node defined by the given binding by the given x and y. export async function dragNodeByBinding(page: Page, nodeBinding: string, x: number, y: number) { const node = graphNodeByBinding(page, nodeBinding) - const grabHandle = await node.locator('.grab-handle') + const grabHandle = node.locator('.grab-handle') await grabHandle.dragTo(grabHandle, { targetPosition: { x, y }, force: true, diff --git a/app/gui2/src/App.vue b/app/gui2/src/App.vue index ad1446574dfd..a12df0351f80 100644 --- a/app/gui2/src/App.vue +++ b/app/gui2/src/App.vue @@ -101,6 +101,6 @@ registerAutoBlurHandler() .enso-dashboard .App { /* Compensate for top bar, render the app below it. */ - margin-top: calc(0px - var(--row-height) - var(--top-level-gap) - var(--top-bar-margin)); + top: calc(var(--row-height) + var(--top-level-gap, 0px) + var(--top-bar-margin, 0px) + 16px); } diff --git a/app/gui2/src/assets/base.css b/app/gui2/src/assets/base.css index 521a3bb4cafd..103a7d310242 100644 --- a/app/gui2/src/assets/base.css +++ b/app/gui2/src/assets/base.css @@ -58,6 +58,7 @@ --visualization-resize-handle-inside: 3px; --visualization-resize-handle-outside: 3px; --right-dock-default-width: 40%; + --code-editor-default-height: 30%; } *, diff --git a/app/gui2/src/components/BottomPanel.vue b/app/gui2/src/components/BottomPanel.vue new file mode 100644 index 000000000000..65e48f4ffa0d --- /dev/null +++ b/app/gui2/src/components/BottomPanel.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/app/gui2/src/components/CodeEditor.vue b/app/gui2/src/components/CodeEditor.vue index 80755fb602d6..4872b518c006 100644 --- a/app/gui2/src/components/CodeEditor.vue +++ b/app/gui2/src/components/CodeEditor.vue @@ -1,7 +1,5 @@ diff --git a/app/gui2/src/components/DockPanel.vue b/app/gui2/src/components/DockPanel.vue index b6f80074a58a..e476093491c1 100644 --- a/app/gui2/src/components/DockPanel.vue +++ b/app/gui2/src/components/DockPanel.vue @@ -1,75 +1,74 @@ diff --git a/app/gui2/src/components/DocumentationEditor.vue b/app/gui2/src/components/DocumentationEditor.vue index b8e8931e8a3c..fcdb7e657037 100644 --- a/app/gui2/src/components/DocumentationEditor.vue +++ b/app/gui2/src/components/DocumentationEditor.vue @@ -9,6 +9,9 @@ import { Err, Ok, mapOk, withContext, type Result } from 'shared/util/data/resul import { toRef, toValue } from 'vue' const documentation = defineModel({ required: true }) +const _props = defineProps<{ + toolbarContainer: HTMLElement | undefined +}>() const graphStore = useGraphStore() const projectStore = useProjectStore() @@ -66,5 +69,9 @@ function useDocumentationImages( diff --git a/app/gui2/src/components/ExtendedMenu.vue b/app/gui2/src/components/ExtendedMenu.vue index 039685855ea2..779011015c61 100644 --- a/app/gui2/src/components/ExtendedMenu.vue +++ b/app/gui2/src/components/ExtendedMenu.vue @@ -30,8 +30,8 @@ const toggleDocumentationEditorShortcut = documentationEditorBindings.bindings.t /> @@ -68,7 +70,7 @@ const toggleDocumentationEditorShortcut = documentationEditorBindings.bindings.t .ExtendedMenu { background: var(--color-frame-bg); border-radius: var(--radius-full); - margin: 0 32px 0 auto; + margin: 0 12px 0 auto; } .moreIcon { @@ -82,7 +84,6 @@ const toggleDocumentationEditorShortcut = documentationEditorBindings.bindings.t > * { display: flex; - justify-content: space-between; align-items: center; padding-left: 8px; padding-right: 8px; @@ -93,7 +94,16 @@ const toggleDocumentationEditorShortcut = documentationEditorBindings.bindings.t background-color: var(--color-menu-entry-selected-bg); } -.label { +.rowIcon { + display: inline-block; + margin-right: 4px; +} + +.rightSide { + margin-left: auto; +} + +.nonInteractive { user-select: none; pointer-events: none; } diff --git a/app/gui2/src/components/GraphEditor.vue b/app/gui2/src/components/GraphEditor.vue index 3e19c45fbd0c..607c645c6375 100644 --- a/app/gui2/src/components/GraphEditor.vue +++ b/app/gui2/src/components/GraphEditor.vue @@ -6,6 +6,7 @@ import { interactionBindings, undoBindings, } from '@/bindings' +import BottomPanel from '@/components/BottomPanel.vue' import CodeEditor from '@/components/CodeEditor.vue' import ComponentBrowser from '@/components/ComponentBrowser.vue' import { type Usage } from '@/components/ComponentBrowser/input' @@ -369,7 +370,8 @@ function deleteSelected() { // === Code Editor === -const codeEditorArea = ref() +const codeEditor = shallowRef>() +const codeEditorArea = computed(() => unrefElement(codeEditor)) const showCodeEditor = ref(false) const codeEditorHandler = codeEditorBindings.handler({ toggle() { @@ -379,8 +381,8 @@ const codeEditorHandler = codeEditorBindings.handler({ // === Documentation Editor === -const rightDock = shallowRef>() -const documentationEditorArea = computed(() => unrefElement(rightDock)) +const docEditor = shallowRef>() +const documentationEditorArea = computed(() => unrefElement(docEditor)) const showDocumentationEditor = computedFallback( storedShowDocumentationEditor, // Show documentation editor when documentation exists on first graph visit. @@ -653,90 +655,122 @@ const groupColors = computed(() => { diff --git a/app/gui2/src/components/TopBar.vue b/app/gui2/src/components/TopBar.vue index 9a1394c6379d..8877fe23acfe 100644 --- a/app/gui2/src/components/TopBar.vue +++ b/app/gui2/src/components/TopBar.vue @@ -69,16 +69,19 @@ const barStyle = computed(() => { position: absolute; display: flex; gap: 8px; - top: 9px; - /* FIXME[sb]: Get correct offset from dashboard. */ - left: 9px; - width: 100%; + top: 8px; + left: 0; + right: 0; pointer-events: none; > * { pointer-events: auto; } } +.TopBar.extraRightSpace { + right: 32px; +} + .selection-menu-enter-active, .selection-menu-leave-active { transition: opacity 0.25s ease; diff --git a/app/gui2/src/composables/events.ts b/app/gui2/src/composables/events.ts index 4a7192a825bf..1d41cd649510 100644 --- a/app/gui2/src/composables/events.ts +++ b/app/gui2/src/composables/events.ts @@ -140,12 +140,32 @@ export function modKey(e: KeyboardEvent | MouseEvent): boolean { return isMacLike ? e.metaKey : e.ctrlKey } -/** A helper for getting Element out of VueInstance, it allows using `useResizeObserver` with Vue components. */ +/** A helper for getting Element out of VueInstance, it allows using `useResizeObserver` with Vue components. + * + * Note that this function is only shallowly reactive: It will trigger its reactive scope if the value of `element` + * changes, but not if the root `Element` of the provided `VueInstance` changes. This is because a + * `ComponentPublicInstance` is implicitly treated as if `markRaw` were applied[^1]. As a result, this function should + * not be used for any component that may have a dynamic root element; rather, the component can use `defineExpose` to + * provide access to a `ref`. + * + * [^1]: https://github.com/vuejs/core/blob/ae97e5053895eeaaa443306e72cd8f45da001179/packages/runtime-core/src/componentPublicInstance.ts#L312 + */ export function unrefElement( element: Ref, ): Element | undefined | null { const plain = toValue(element) - return (plain as VueInstance)?.$el ?? plain + const result = (plain as VueInstance)?.$el ?? plain + // A component's root can be a Node (if it's a fragment), TextNode, or Comment (if its root uses a v-if). + if (result != null && !(result instanceof Element)) { + if (result instanceof Comment && result.data.includes('v-if')) { + console.warn( + "unrefElement: Component root is a v-if, but a root element can't be watched reactively.", + result, + ) + } + return undefined + } + return result } interface ResizeObserverData { diff --git a/app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts b/app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts index 2cd1f6a2d679..290b7425ee3b 100644 --- a/app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts +++ b/app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts @@ -5,7 +5,7 @@ import { useCurrentEdit, type CurrentEdit } from '@/providers/widgetTree' import { assert } from 'shared/util/assert' import { expect, test, vi, type Mock } from 'vitest' import { proxyRefs } from 'vue' -import { WidgetEditHandler, type WidgetId } from '../editHandler' +import { WidgetEditHandler } from '../editHandler' // If widget's name is a prefix of another widget's name, then it is its ancestor. // The ancestor with longest name is a direct parent. From 21e1a24231e562b6a1187013a5d429917c45ad9a Mon Sep 17 00:00:00 2001 From: Kaz Date: Mon, 1 Jul 2024 09:57:07 -0700 Subject: [PATCH 2/3] Fix keyboardBusy bug --- app/gui2/src/composables/events.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/gui2/src/composables/events.ts b/app/gui2/src/composables/events.ts index 1d41cd649510..997bef4d285f 100644 --- a/app/gui2/src/composables/events.ts +++ b/app/gui2/src/composables/events.ts @@ -115,8 +115,20 @@ export function useEventConditional( } /** Whether any element currently has keyboard focus. */ -export function keyboardBusy() { - return document.activeElement != document.body +export function keyboardBusy(): boolean { + return ( + document.activeElement !== document.body && + document.activeElement instanceof HTMLElement && + isEditable(document.activeElement) + ) +} + +function isEditable(element: HTMLElement) { + return ( + element.isContentEditable || + element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement + ) } /** Whether focused element is within given element's subtree. */ From 836da9504169dfa26cca9c8527b1240743db8dce Mon Sep 17 00:00:00 2001 From: Kaz Date: Thu, 4 Jul 2024 08:52:15 -0700 Subject: [PATCH 3/3] Use existing SizeTransition component --- app/gui2/src/components/DockPanel.vue | 26 ++++++------- app/gui2/src/components/SlideIn.vue | 54 --------------------------- 2 files changed, 13 insertions(+), 67 deletions(-) delete mode 100644 app/gui2/src/components/SlideIn.vue diff --git a/app/gui2/src/components/DockPanel.vue b/app/gui2/src/components/DockPanel.vue index e476093491c1..52e4cf863dea 100644 --- a/app/gui2/src/components/DockPanel.vue +++ b/app/gui2/src/components/DockPanel.vue @@ -1,23 +1,22 @@ @@ -36,18 +35,19 @@ const style = computed(() => ({ icon="right_panel" class="toggleDock" /> - -
-
- + +
+
+
+ +
+
- - +