mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-10 23:50:00 +00:00
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:
@@ -113,4 +113,15 @@ test.describe('Actionbar', () => {
|
||||
).toBe(END)
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,11 @@ export class ComfyActionbar {
|
||||
this.root = page.locator('.actionbar')
|
||||
this.queueButton = new ComfyQueueButton(this)
|
||||
}
|
||||
|
||||
async isDocked() {
|
||||
const className = await this.root.getAttribute('class')
|
||||
return className?.includes('is-docked') ?? false
|
||||
}
|
||||
}
|
||||
|
||||
class ComfyQueueButton {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<Panel
|
||||
v-show="visible"
|
||||
class="actionbar w-fit"
|
||||
:style="style"
|
||||
:class="{ 'is-dragging': isDragging }"
|
||||
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
|
||||
>
|
||||
<div class="actionbar-content flex items-center" ref="panelRef">
|
||||
<span class="drag-handle cursor-move mr-2 p-0!" ref="dragHandleRef">
|
||||
@@ -24,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<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 Divider from 'primevue/divider'
|
||||
import Button from 'primevue/button'
|
||||
@@ -32,34 +31,48 @@ import ButtonGroup from 'primevue/buttongroup'
|
||||
import ComfyQueueButton from './ComfyQueueButton.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDraggable, useEventListener, useLocalStorage } from '@vueuse/core'
|
||||
import { debounce, clamp } from 'lodash'
|
||||
import {
|
||||
useDraggable,
|
||||
useElementBounding,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useLocalStorage,
|
||||
watchDebounced
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'lodash'
|
||||
|
||||
const settingsStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const visible = computed(
|
||||
() => settingsStore.get('Comfy.UseNewMenu') === 'Floating'
|
||||
() => settingsStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const dragHandleRef = ref<HTMLElement | null>(null)
|
||||
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', false)
|
||||
const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
|
||||
x: 0,
|
||||
y: 0
|
||||
})
|
||||
const { x, y, style, isDragging } = useDraggable(panelRef, {
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
style: style,
|
||||
isDragging
|
||||
} = useDraggable(panelRef, {
|
||||
initialValue: { x: 0, y: 0 },
|
||||
handle: dragHandleRef,
|
||||
containerElement: document.body
|
||||
})
|
||||
|
||||
// Update storedPosition when x or y changes
|
||||
watch(
|
||||
watchDebounced(
|
||||
[x, y],
|
||||
debounce(([newX, newY]) => {
|
||||
([newX, newY]) => {
|
||||
storedPosition.value = { x: newX, y: newY }
|
||||
}, 300)
|
||||
},
|
||||
{ debounce: 300 }
|
||||
)
|
||||
|
||||
// Set initial position to bottom center
|
||||
@@ -109,6 +122,41 @@ const 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>
|
||||
|
||||
<style scoped>
|
||||
@@ -118,6 +166,11 @@ useEventListener(window, 'resize', adjustMenuPosition)
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.actionbar.is-docked {
|
||||
position: static;
|
||||
@apply bg-transparent border-none p-0;
|
||||
}
|
||||
|
||||
.actionbar.is-dragging {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<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>
|
||||
<Menubar
|
||||
:model="items"
|
||||
@@ -10,11 +15,11 @@
|
||||
}"
|
||||
/>
|
||||
<Divider layout="vertical" class="mx-2" />
|
||||
<WorkflowTabs
|
||||
v-if="workflowTabsPosition === 'Topbar'"
|
||||
class="flex-grow"
|
||||
/>
|
||||
<div class="flex-grow">
|
||||
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
|
||||
</div>
|
||||
<div class="comfyui-menu-right" ref="menuRight"></div>
|
||||
<Actionbar />
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
@@ -23,10 +28,12 @@
|
||||
import Menubar from 'primevue/menubar'
|
||||
import Divider from 'primevue/divider'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, provide, ref } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useEventBus } from '@vueuse/core'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const workflowTabsPosition = computed(() =>
|
||||
@@ -45,6 +52,18 @@ onMounted(() => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@@ -61,6 +80,14 @@ onMounted(() => {
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.comfyui-menu.dropzone {
|
||||
background: var(--p-highlight-background);
|
||||
}
|
||||
|
||||
.comfyui-menu.dropzone-active {
|
||||
background: var(--p-highlight-background-focus);
|
||||
}
|
||||
|
||||
.comfyui-logo {
|
||||
font-size: 1.2em;
|
||||
user-select: none;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<GlobalToast />
|
||||
<UnloadWindowConfirmDialog />
|
||||
<BrowserTabTitle />
|
||||
<Actionbar />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -41,7 +40,6 @@ import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySideba
|
||||
import GlobalToast from '@/components/toast/GlobalToast.vue'
|
||||
import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'
|
||||
import BrowserTabTitle from '@/components/BrowserTabTitle.vue'
|
||||
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
||||
import TopMenubar from '@/components/topbar/TopMenubar.vue'
|
||||
import { setupAutoQueueHandler } from '@/services/autoQueueService'
|
||||
|
||||
Reference in New Issue
Block a user