mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-01 03:31:58 +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)
|
).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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user