mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +00:00
## Summary A common bit of feedback is that users want to be able to drag the actionbar into the very top of their window. Currently the actionbar is clamped to not allow it to overlap the tabs, this update removes that restriction & fixes padding for the top menu section when the UI elements are hidden within it adding additional gaps. ## Changes - Remove clamping on actionbar position - Fix padding on top menu section ## Screenshots (if applicable) Before: <img width="192" height="119" alt="image" src="https://github.com/user-attachments/assets/c35c89ed-ec30-45ff-8ebd-8ad68dbd4212" /> After: <img width="134" height="120" alt="image" src="https://github.com/user-attachments/assets/adc32c43-e3ab-4c7b-a8ff-360fd39d6bf1" /> Actionbar over tabs: <img width="465" height="173" alt="image" src="https://github.com/user-attachments/assets/d1502911-1e15-4082-ba2e-b8906e329b70" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8068-Allow-users-to-drag-actionbar-over-tabs-2e96d73d365081708491f3c54968df3a) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
301 lines
8.7 KiB
Vue
301 lines
8.7 KiB
Vue
<template>
|
|
<div class="flex h-full items-center" :class="cn(!isDocked && '-ml-2')">
|
|
<div
|
|
v-if="isDragging && !isDocked"
|
|
:class="actionbarClass"
|
|
@mouseenter="onMouseEnterDropZone"
|
|
@mouseleave="onMouseLeaveDropZone"
|
|
>
|
|
{{ t('actionbar.dockToTop') }}
|
|
</div>
|
|
|
|
<Panel
|
|
class="pointer-events-auto"
|
|
:style="style"
|
|
:class="panelClass"
|
|
:pt="{
|
|
header: { class: 'hidden' },
|
|
content: { class: isDocked ? 'p-0' : 'p-1' }
|
|
}"
|
|
>
|
|
<div ref="panelRef" class="flex items-center select-none gap-2">
|
|
<span
|
|
ref="dragHandleRef"
|
|
:class="
|
|
cn(
|
|
'drag-handle cursor-grab w-3 h-max',
|
|
isDragging && 'cursor-grabbing'
|
|
)
|
|
"
|
|
/>
|
|
<Suspense @resolve="comfyRunButtonResolved">
|
|
<ComfyRunButton />
|
|
</Suspense>
|
|
<Button
|
|
v-tooltip.bottom="cancelJobTooltipConfig"
|
|
variant="destructive"
|
|
size="icon"
|
|
:disabled="isExecutionIdle"
|
|
:aria-label="t('menu.interrupt')"
|
|
@click="cancelCurrentJob"
|
|
>
|
|
<i class="icon-[lucide--x] size-4" />
|
|
</Button>
|
|
</div>
|
|
</Panel>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import {
|
|
useDraggable,
|
|
useEventListener,
|
|
useLocalStorage,
|
|
watchDebounced
|
|
} from '@vueuse/core'
|
|
import { clamp } from 'es-toolkit/compat'
|
|
import { storeToRefs } from 'pinia'
|
|
import Panel from 'primevue/panel'
|
|
import { computed, nextTick, ref, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { useTelemetry } from '@/platform/telemetry'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { useExecutionStore } from '@/stores/executionStore'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
import ComfyRunButton from './ComfyRunButton'
|
|
|
|
const settingsStore = useSettingStore()
|
|
const commandStore = useCommandStore()
|
|
const { t } = useI18n()
|
|
const { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore())
|
|
|
|
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
|
|
const visible = computed(() => position.value !== 'Disabled')
|
|
|
|
const panelRef = ref<HTMLElement | null>(null)
|
|
const dragHandleRef = ref<HTMLElement | null>(null)
|
|
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
|
|
const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
|
|
x: 0,
|
|
y: 0
|
|
})
|
|
const { x, y, style, isDragging } = useDraggable(panelRef, {
|
|
initialValue: { x: 0, y: 0 },
|
|
handle: dragHandleRef,
|
|
containerElement: document.body
|
|
})
|
|
|
|
// Update storedPosition when x or y changes
|
|
watchDebounced(
|
|
[x, y],
|
|
([newX, newY]) => {
|
|
storedPosition.value = { x: newX, y: newY }
|
|
},
|
|
{ debounce: 300 }
|
|
)
|
|
|
|
// Set initial position to bottom center
|
|
const setInitialPosition = () => {
|
|
if (panelRef.value) {
|
|
const screenWidth = window.innerWidth
|
|
const screenHeight = window.innerHeight
|
|
const menuWidth = panelRef.value.offsetWidth
|
|
const menuHeight = panelRef.value.offsetHeight
|
|
|
|
if (menuWidth === 0 || menuHeight === 0) {
|
|
return
|
|
}
|
|
|
|
// Check if stored position exists and is within bounds
|
|
if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {
|
|
// Ensure stored position is within screen bounds
|
|
x.value = clamp(storedPosition.value.x, 0, screenWidth - menuWidth)
|
|
y.value = clamp(storedPosition.value.y, 0, screenHeight - menuHeight)
|
|
captureLastDragState()
|
|
return
|
|
}
|
|
|
|
// If no stored position or current position, set to bottom center
|
|
if (x.value === 0 && y.value === 0) {
|
|
x.value = clamp((screenWidth - menuWidth) / 2, 0, screenWidth - menuWidth)
|
|
y.value = clamp(
|
|
screenHeight - menuHeight - 10,
|
|
0,
|
|
screenHeight - menuHeight
|
|
)
|
|
captureLastDragState()
|
|
}
|
|
}
|
|
}
|
|
|
|
//The ComfyRunButton is a dynamic import. Which means it will not be loaded onMount in this component.
|
|
//So we must use suspense resolve to ensure that is has loaded and updated the DOM before calling setInitialPosition()
|
|
async function comfyRunButtonResolved() {
|
|
await nextTick()
|
|
setInitialPosition()
|
|
}
|
|
|
|
watch(visible, async (newVisible) => {
|
|
if (newVisible) {
|
|
await nextTick(setInitialPosition)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* Track run button handle drag start using mousedown on the drag handle.
|
|
*/
|
|
useEventListener(dragHandleRef, 'mousedown', () => {
|
|
useTelemetry()?.trackUiButtonClicked({
|
|
button_id: 'actionbar_run_handle_drag_start'
|
|
})
|
|
})
|
|
|
|
const lastDragState = ref({
|
|
x: x.value,
|
|
y: y.value,
|
|
windowWidth: window.innerWidth,
|
|
windowHeight: window.innerHeight
|
|
})
|
|
const captureLastDragState = () => {
|
|
lastDragState.value = {
|
|
x: x.value,
|
|
y: y.value,
|
|
windowWidth: window.innerWidth,
|
|
windowHeight: window.innerHeight
|
|
}
|
|
}
|
|
watch(
|
|
isDragging,
|
|
(newIsDragging) => {
|
|
if (!newIsDragging) {
|
|
// Stop dragging
|
|
captureLastDragState()
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
const adjustMenuPosition = () => {
|
|
if (panelRef.value) {
|
|
const screenWidth = window.innerWidth
|
|
const screenHeight = window.innerHeight
|
|
const menuWidth = panelRef.value.offsetWidth
|
|
const menuHeight = panelRef.value.offsetHeight
|
|
|
|
// Calculate distances to all edges
|
|
const distanceLeft = lastDragState.value.x
|
|
const distanceRight =
|
|
lastDragState.value.windowWidth - (lastDragState.value.x + menuWidth)
|
|
const distanceTop = lastDragState.value.y
|
|
const distanceBottom =
|
|
lastDragState.value.windowHeight - (lastDragState.value.y + menuHeight)
|
|
|
|
// Find the smallest distance to determine which edge to anchor to
|
|
const distances = [
|
|
{ edge: 'left', distance: distanceLeft },
|
|
{ edge: 'right', distance: distanceRight },
|
|
{ edge: 'top', distance: distanceTop },
|
|
{ edge: 'bottom', distance: distanceBottom }
|
|
]
|
|
const closestEdge = distances.reduce((min, curr) =>
|
|
curr.distance < min.distance ? curr : min
|
|
)
|
|
|
|
// Calculate vertical position as a percentage of screen height
|
|
const verticalRatio =
|
|
lastDragState.value.y / lastDragState.value.windowHeight
|
|
const horizontalRatio =
|
|
lastDragState.value.x / lastDragState.value.windowWidth
|
|
|
|
// Apply positioning based on closest edge
|
|
if (closestEdge.edge === 'left') {
|
|
x.value = closestEdge.distance // Maintain exact distance from left
|
|
y.value = verticalRatio * screenHeight
|
|
} else if (closestEdge.edge === 'right') {
|
|
x.value = screenWidth - menuWidth - closestEdge.distance // Maintain exact distance from right
|
|
y.value = verticalRatio * screenHeight
|
|
} else if (closestEdge.edge === 'top') {
|
|
x.value = horizontalRatio * screenWidth
|
|
y.value = closestEdge.distance // Maintain exact distance from top
|
|
} else {
|
|
// bottom
|
|
x.value = horizontalRatio * screenWidth
|
|
y.value = screenHeight - menuHeight - closestEdge.distance // Maintain exact distance from bottom
|
|
}
|
|
|
|
// Ensure the menu stays within the screen bounds
|
|
x.value = clamp(x.value, 0, screenWidth - menuWidth)
|
|
y.value = clamp(y.value, 0, screenHeight - menuHeight)
|
|
}
|
|
}
|
|
|
|
useEventListener(window, 'resize', adjustMenuPosition)
|
|
|
|
// Drop zone state
|
|
const isMouseOverDropZone = ref(false)
|
|
|
|
// Mouse event handlers for self-contained drop zone
|
|
const onMouseEnterDropZone = () => {
|
|
if (isDragging.value) {
|
|
isMouseOverDropZone.value = true
|
|
}
|
|
}
|
|
|
|
const onMouseLeaveDropZone = () => {
|
|
if (isDragging.value) {
|
|
isMouseOverDropZone.value = false
|
|
}
|
|
}
|
|
|
|
// Handle drag state changes
|
|
watch(isDragging, (dragging) => {
|
|
if (dragging) {
|
|
// Starting to drag - undock if docked
|
|
if (isDocked.value) {
|
|
isDocked.value = false
|
|
}
|
|
} else {
|
|
// Stopped dragging - dock if mouse is over drop zone
|
|
if (isMouseOverDropZone.value) {
|
|
isDocked.value = true
|
|
}
|
|
// Reset drop zone state
|
|
isMouseOverDropZone.value = false
|
|
}
|
|
})
|
|
|
|
const cancelJobTooltipConfig = computed(() =>
|
|
buildTooltipConfig(t('menu.interrupt'))
|
|
)
|
|
|
|
const cancelCurrentJob = async () => {
|
|
if (isExecutionIdle.value) return
|
|
await commandStore.execute('Comfy.Interrupt')
|
|
}
|
|
|
|
const actionbarClass = computed(() =>
|
|
cn(
|
|
'w-[200px] border-dashed border-blue-500 opacity-80',
|
|
'm-1.5 flex items-center justify-center self-stretch',
|
|
'rounded-md before:w-50 before:-ml-50 before:h-full',
|
|
'pointer-events-auto',
|
|
isMouseOverDropZone.value &&
|
|
'border-[3px] opacity-100 scale-105 shadow-[0_0_20px] shadow-blue-500'
|
|
)
|
|
)
|
|
const panelClass = computed(() =>
|
|
cn(
|
|
'actionbar pointer-events-auto z-1300',
|
|
isDragging.value && 'select-none pointer-events-none',
|
|
isDocked.value
|
|
? 'p-0 static border-none bg-transparent'
|
|
: 'fixed shadow-interface'
|
|
)
|
|
)
|
|
</script>
|