Docking action bar on top menu bar (#1119)

* Teleport when docked

* Docking logic

* Remove unnecessary v-show

* Docked panel style

* Drop zone highlight

* Rename test

* Add playwright test
This commit is contained in:
Chenlei Hu
2024-10-05 14:06:29 -04:00
committed by GitHub
parent 9c118c8e37
commit ad55722662
5 changed files with 112 additions and 18 deletions

View File

@@ -113,4 +113,15 @@ test.describe('Actionbar', () => {
).toBe(END) ).toBe(END)
expect(promptNumber, 'queued prompt count should be 2').toBe(2) expect(promptNumber, 'queued prompt count should be 2').toBe(2)
}) })
test('Can dock actionbar into top menu', async ({ comfyPage }) => {
await comfyPage.page.dragAndDrop(
'.actionbar .drag-handle',
'.comfyui-menu',
{
targetPosition: { x: 0, y: 0 }
}
)
expect(await comfyPage.actionbar.isDocked()).toBe(true)
})
}) })

View File

@@ -9,6 +9,11 @@ export class ComfyActionbar {
this.root = page.locator('.actionbar') this.root = page.locator('.actionbar')
this.queueButton = new ComfyQueueButton(this) this.queueButton = new ComfyQueueButton(this)
} }
async isDocked() {
const className = await this.root.getAttribute('class')
return className?.includes('is-docked') ?? false
}
} }
class ComfyQueueButton { class ComfyQueueButton {

View File

@@ -1,9 +1,8 @@
<template> <template>
<Panel <Panel
v-show="visible"
class="actionbar w-fit" class="actionbar w-fit"
:style="style" :style="style"
:class="{ 'is-dragging': isDragging }" :class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
> >
<div class="actionbar-content flex items-center" ref="panelRef"> <div class="actionbar-content flex items-center" ref="panelRef">
<span class="drag-handle cursor-move mr-2 p-0!" ref="dragHandleRef"> <span class="drag-handle cursor-move mr-2 p-0!" ref="dragHandleRef">
@@ -24,7 +23,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue' import { computed, inject, nextTick, onMounted, Ref, ref, watch } from 'vue'
import Panel from 'primevue/panel' import Panel from 'primevue/panel'
import Divider from 'primevue/divider' import Divider from 'primevue/divider'
import Button from 'primevue/button' import Button from 'primevue/button'
@@ -32,34 +31,48 @@ import ButtonGroup from 'primevue/buttongroup'
import ComfyQueueButton from './ComfyQueueButton.vue' import ComfyQueueButton from './ComfyQueueButton.vue'
import { useSettingStore } from '@/stores/settingStore' import { useSettingStore } from '@/stores/settingStore'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { useDraggable, useEventListener, useLocalStorage } from '@vueuse/core' import {
import { debounce, clamp } from 'lodash' useDraggable,
useElementBounding,
useEventBus,
useEventListener,
useLocalStorage,
watchDebounced
} from '@vueuse/core'
import { clamp } from 'lodash'
const settingsStore = useSettingStore() const settingsStore = useSettingStore()
const commandStore = useCommandStore() const commandStore = useCommandStore()
const visible = computed( const visible = computed(
() => settingsStore.get('Comfy.UseNewMenu') === 'Floating' () => settingsStore.get('Comfy.UseNewMenu') !== 'Disabled'
) )
const panelRef = ref<HTMLElement | null>(null) const panelRef = ref<HTMLElement | null>(null)
const dragHandleRef = ref<HTMLElement | null>(null) const dragHandleRef = ref<HTMLElement | null>(null)
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', false)
const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', { const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
x: 0, x: 0,
y: 0 y: 0
}) })
const { x, y, style, isDragging } = useDraggable(panelRef, { const {
x,
y,
style: style,
isDragging
} = useDraggable(panelRef, {
initialValue: { x: 0, y: 0 }, initialValue: { x: 0, y: 0 },
handle: dragHandleRef, handle: dragHandleRef,
containerElement: document.body containerElement: document.body
}) })
// Update storedPosition when x or y changes // Update storedPosition when x or y changes
watch( watchDebounced(
[x, y], [x, y],
debounce(([newX, newY]) => { ([newX, newY]) => {
storedPosition.value = { x: newX, y: newY } storedPosition.value = { x: newX, y: newY }
}, 300) },
{ debounce: 300 }
) )
// Set initial position to bottom center // Set initial position to bottom center
@@ -109,6 +122,41 @@ const adjustMenuPosition = () => {
} }
useEventListener(window, 'resize', adjustMenuPosition) useEventListener(window, 'resize', adjustMenuPosition)
const topMenuRef = inject<Ref<HTMLDivElement | null>>('topMenuRef')
const topMenuBounds = useElementBounding(topMenuRef)
const overlapThreshold = 20 // pixels
const isOverlappingWithTopMenu = computed(() => {
if (!panelRef.value) {
return false
}
const { height } = panelRef.value.getBoundingClientRect()
const actionbarBottom = y.value + height
const topMenuBottom = topMenuBounds.bottom.value
const overlapPixels =
Math.min(actionbarBottom, topMenuBottom) -
Math.max(y.value, topMenuBounds.top.value)
return overlapPixels > overlapThreshold
})
watch(isDragging, (newIsDragging) => {
if (!newIsDragging) {
// Stop dragging
isDocked.value = isOverlappingWithTopMenu.value
} else {
// Start dragging
isDocked.value = false
}
})
const eventBus = useEventBus<string>('topMenu')
watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
eventBus.emit('updateHighlight', {
isDragging: dragging,
isOverlapping: overlapping
})
})
</script> </script>
<style scoped> <style scoped>
@@ -118,6 +166,11 @@ useEventListener(window, 'resize', adjustMenuPosition)
z-index: 1000; z-index: 1000;
} }
.actionbar.is-docked {
position: static;
@apply bg-transparent border-none p-0;
}
.actionbar.is-dragging { .actionbar.is-dragging {
user-select: none; user-select: none;
} }

View File

@@ -1,6 +1,11 @@
<template> <template>
<teleport to=".comfyui-body-top"> <teleport to=".comfyui-body-top">
<div class="comfyui-menu flex items-center" v-show="betaMenuEnabled"> <div
ref="topMenuRef"
class="comfyui-menu flex items-center"
v-show="betaMenuEnabled"
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
>
<h1 class="comfyui-logo mx-2">ComfyUI</h1> <h1 class="comfyui-logo mx-2">ComfyUI</h1>
<Menubar <Menubar
:model="items" :model="items"
@@ -10,11 +15,11 @@
}" }"
/> />
<Divider layout="vertical" class="mx-2" /> <Divider layout="vertical" class="mx-2" />
<WorkflowTabs <div class="flex-grow">
v-if="workflowTabsPosition === 'Topbar'" <WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
class="flex-grow" </div>
/>
<div class="comfyui-menu-right" ref="menuRight"></div> <div class="comfyui-menu-right" ref="menuRight"></div>
<Actionbar />
</div> </div>
</teleport> </teleport>
</template> </template>
@@ -23,10 +28,12 @@
import Menubar from 'primevue/menubar' import Menubar from 'primevue/menubar'
import Divider from 'primevue/divider' import Divider from 'primevue/divider'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue' import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
import { useMenuItemStore } from '@/stores/menuItemStore' import { useMenuItemStore } from '@/stores/menuItemStore'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, provide, ref } from 'vue'
import { useSettingStore } from '@/stores/settingStore' import { useSettingStore } from '@/stores/settingStore'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
import { useEventBus } from '@vueuse/core'
const settingStore = useSettingStore() const settingStore = useSettingStore()
const workflowTabsPosition = computed(() => const workflowTabsPosition = computed(() =>
@@ -45,6 +52,18 @@ onMounted(() => {
menuRight.value.appendChild(app.menu.element) menuRight.value.appendChild(app.menu.element)
} }
}) })
const topMenuRef = ref<HTMLDivElement | null>(null)
provide('topMenuRef', topMenuRef)
const eventBus = useEventBus<string>('topMenu')
const isDropZone = ref(false)
const isDroppable = ref(false)
eventBus.on((event: string, payload: any) => {
if (event === 'updateHighlight') {
isDropZone.value = payload.isDragging
isDroppable.value = payload.isOverlapping && payload.isDragging
}
})
</script> </script>
<style scoped> <style scoped>
@@ -61,6 +80,14 @@ onMounted(() => {
max-height: 90vh; max-height: 90vh;
} }
.comfyui-menu.dropzone {
background: var(--p-highlight-background);
}
.comfyui-menu.dropzone-active {
background: var(--p-highlight-background-focus);
}
.comfyui-logo { .comfyui-logo {
font-size: 1.2em; font-size: 1.2em;
user-select: none; user-select: none;

View File

@@ -6,7 +6,6 @@
<GlobalToast /> <GlobalToast />
<UnloadWindowConfirmDialog /> <UnloadWindowConfirmDialog />
<BrowserTabTitle /> <BrowserTabTitle />
<Actionbar />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -41,7 +40,6 @@ import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySideba
import GlobalToast from '@/components/toast/GlobalToast.vue' import GlobalToast from '@/components/toast/GlobalToast.vue'
import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue' import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'
import BrowserTabTitle from '@/components/BrowserTabTitle.vue' import BrowserTabTitle from '@/components/BrowserTabTitle.vue'
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue' import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
import TopMenubar from '@/components/topbar/TopMenubar.vue' import TopMenubar from '@/components/topbar/TopMenubar.vue'
import { setupAutoQueueHandler } from '@/services/autoQueueService' import { setupAutoQueueHandler } from '@/services/autoQueueService'