diff --git a/web/css/resolver-shell.css b/web/css/resolver-shell.css index 1a34eaf..6375b20 100644 --- a/web/css/resolver-shell.css +++ b/web/css/resolver-shell.css @@ -122,17 +122,76 @@ .mr-modal-resize-handle { position: absolute; - right: 0; - bottom: 0; z-index: 2; - width: 18px; - height: 18px; - cursor: nwse-resize; touch-action: none; user-select: none; } -.mr-modal-resize-handle::after { +.mr-modal-resize-handle--n, +.mr-modal-resize-handle--s { + left: 12px; + right: 12px; + height: 10px; + cursor: ns-resize; +} + +.mr-modal-resize-handle--n { + top: 0; +} + +.mr-modal-resize-handle--s { + bottom: 0; +} + +.mr-modal-resize-handle--e, +.mr-modal-resize-handle--w { + top: 12px; + bottom: 12px; + width: 10px; + cursor: ew-resize; +} + +.mr-modal-resize-handle--e { + right: 0; +} + +.mr-modal-resize-handle--w { + left: 0; +} + +.mr-modal-resize-handle--ne, +.mr-modal-resize-handle--se, +.mr-modal-resize-handle--sw, +.mr-modal-resize-handle--nw { + width: 18px; + height: 18px; +} + +.mr-modal-resize-handle--ne { + top: 0; + right: 0; + cursor: nesw-resize; +} + +.mr-modal-resize-handle--se { + right: 0; + bottom: 0; + cursor: nwse-resize; +} + +.mr-modal-resize-handle--sw { + left: 0; + bottom: 0; + cursor: nesw-resize; +} + +.mr-modal-resize-handle--nw { + top: 0; + left: 0; + cursor: nwse-resize; +} + +.mr-modal-resize-handle--se::after { content: ""; position: absolute; right: 4px; @@ -147,7 +206,7 @@ user-select: none; } -.model-resolver-modal.is-resizing .mr-modal-resize-handle::after { +.model-resolver-modal.is-resizing .mr-modal-resize-handle--se::after { opacity: 1; } diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index 53d9b50..3ef2a72 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -106,6 +106,7 @@ export class ResolverManagerDialog extends ComfyDialog { this.dockButton = null; this.undockButton = null; this._floatingRectBeforeDock = null; + this._modalResizeHandles = []; this._dragging = false; this._dragStart = null; this._dragBounds = null; diff --git a/web/resolver/shell/dialog_shell_methods.js b/web/resolver/shell/dialog_shell_methods.js index e167a92..6720bfe 100644 --- a/web/resolver/shell/dialog_shell_methods.js +++ b/web/resolver/shell/dialog_shell_methods.js @@ -3,6 +3,10 @@ import { $el } from "../../../../../scripts/ui.js"; import { getSvgIcon } from "../../utils/icon_utils.js"; import { isSidebarButtonActive } from "../utils/dom_patch_utils.js"; import { safeStorage } from "../utils/html_utils.js"; + +const MODAL_RESIZE_EDGES = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']; +const MODAL_RESIZE_EDGE_SET = new Set(MODAL_RESIZE_EDGES); + export const dialogShellMethods = { createHeader() { // Create tabs @@ -162,25 +166,35 @@ export const dialogShellMethods = { }, installModalResizeHandle() { - if (!this.element || this._modalResizeHandle?.isConnected) return; + if (!this.element) return; + if (this._modalResizeHandles?.some((handle) => handle?.isConnected)) return; - const handle = $el("div.mr-modal-resize-handle", { - id: "model-resolver-resize-handle", - ariaHidden: "true" - }); const eventName = typeof PointerEvent === 'function' ? 'pointerdown' : 'mousedown'; - this._modalResizeStartHandler = (event) => this.startModalResize(event); - handle.addEventListener(eventName, this._modalResizeStartHandler, true); - this._modalResizeHandle = handle; + const handles = MODAL_RESIZE_EDGES.map((edge) => { + const handle = $el(`div.mr-modal-resize-handle.mr-modal-resize-handle--${edge}`, { + id: `model-resolver-resize-handle-${edge}`, + ariaHidden: "true" + }); + handle.addEventListener(eventName, (event) => this.startModalResize(event, edge), true); + this.element.appendChild(handle); + return handle; + }); + + this._modalResizeHandles = handles; this._modalResizeStartEventName = eventName; - this.element.appendChild(handle); }, - startModalResize(event) { + startModalResize(event, edge = 'se') { if (!this.element || this.docked || this.fullscreen) return; if (event.button !== undefined && event.button !== 0) return; if (event.isPrimary === false) return; + const resizeEdge = MODAL_RESIZE_EDGE_SET.has(edge) ? edge : 'se'; + const resizesLeft = resizeEdge.includes('w'); + const resizesRight = resizeEdge.includes('e'); + const resizesTop = resizeEdge.includes('n'); + const resizesBottom = resizeEdge.includes('s'); + this.finishModalResize({ flush: true }); event.preventDefault?.(); event.stopPropagation?.(); @@ -195,21 +209,41 @@ export const dialogShellMethods = { const minHeight = Math.max(1, Number.parseFloat(computed?.minHeight) || 420); const startWidth = Math.max(minWidth, Number(rect.width) || this.element.offsetWidth || minWidth); const startHeight = Math.max(minHeight, Number(rect.height) || this.element.offsetHeight || minHeight); + const startLeft = Number.isFinite(Number(rect.left)) ? Number(rect.left) : 0; + const startTop = Number.isFinite(Number(rect.top)) ? Number(rect.top) : 0; + const startRight = startLeft + startWidth; + const startBottom = startTop + startHeight; + const viewportPadding = 4; const state = { + edge: resizeEdge, startX: Number(event.clientX) || 0, startY: Number(event.clientY) || 0, + startLeft, + startTop, + startRight, + startBottom, startWidth, startHeight, minWidth, minHeight, - maxWidth: Math.max(minWidth, startWidth, viewportWidth - rect.left), - maxHeight: Math.max(minHeight, startHeight, viewportHeight - rect.top), + maxWidth: resizesLeft + ? Math.max(minWidth, startWidth, startRight - viewportPadding) + : resizesRight + ? Math.max(minWidth, startWidth, viewportWidth - startLeft - viewportPadding) + : startWidth, + maxHeight: resizesTop + ? Math.max(minHeight, startHeight, startBottom - viewportPadding) + : resizesBottom + ? Math.max(minHeight, startHeight, viewportHeight - startTop - viewportPadding) + : startHeight, pointerId: event.pointerId, moveEventName: typeof PointerEvent === 'function' ? 'pointermove' : 'mousemove', endEventName: typeof PointerEvent === 'function' ? 'pointerup' : 'mouseup', cancelEventName: typeof PointerEvent === 'function' ? 'pointercancel' : null, - pendingWidth: Number(rect.width) || minWidth, - pendingHeight: Number(rect.height) || minHeight, + pendingLeft: startLeft, + pendingTop: startTop, + pendingWidth: startWidth, + pendingHeight: startHeight, animationFrame: null, moveHandler: null, endHandler: null, @@ -221,15 +255,42 @@ export const dialogShellMethods = { this.element.style.left = `${Math.round(rect.left)}px`; this.element.style.transform = 'none'; this.element.style.resize = 'none'; - this.element.style.willChange = 'width, height'; + this.element.style.willChange = 'top, left, width, height'; this.element.classList.add('is-resizing'); state.moveHandler = (moveEvent) => { if (state.pointerId !== undefined && moveEvent.pointerId !== state.pointerId) return; - const nextWidth = state.startWidth + ((Number(moveEvent.clientX) || state.startX) - state.startX); - const nextHeight = state.startHeight + ((Number(moveEvent.clientY) || state.startY) - state.startY); - state.pendingWidth = Math.min(state.maxWidth, Math.max(state.minWidth, nextWidth)); - state.pendingHeight = Math.min(state.maxHeight, Math.max(state.minHeight, nextHeight)); + const clientX = Number(moveEvent.clientX); + const clientY = Number(moveEvent.clientY); + const deltaX = (Number.isFinite(clientX) ? clientX : state.startX) - state.startX; + const deltaY = (Number.isFinite(clientY) ? clientY : state.startY) - state.startY; + let nextLeft = state.startLeft; + let nextTop = state.startTop; + let nextWidth = state.startWidth; + let nextHeight = state.startHeight; + + if (resizesLeft) { + nextWidth = state.startWidth - deltaX; + } else if (resizesRight) { + nextWidth = state.startWidth + deltaX; + } + if (resizesTop) { + nextHeight = state.startHeight - deltaY; + } else if (resizesBottom) { + nextHeight = state.startHeight + deltaY; + } + + if (resizesLeft || resizesRight) { + state.pendingWidth = Math.min(state.maxWidth, Math.max(state.minWidth, nextWidth)); + nextLeft = resizesLeft ? state.startRight - state.pendingWidth : state.startLeft; + } + if (resizesTop || resizesBottom) { + state.pendingHeight = Math.min(state.maxHeight, Math.max(state.minHeight, nextHeight)); + nextTop = resizesTop ? state.startBottom - state.pendingHeight : state.startTop; + } + + state.pendingLeft = nextLeft; + state.pendingTop = nextTop; if (state.animationFrame) return; state.animationFrame = requestAnimationFrame(() => { state.animationFrame = null; @@ -251,6 +312,8 @@ export const dialogShellMethods = { applyModalResize(state) { if (!state || this._modalResizeState !== state || !this.element) return; + this.element.style.left = `${Math.round(state.pendingLeft)}px`; + this.element.style.top = `${Math.round(state.pendingTop)}px`; this.element.style.width = `${Math.round(state.pendingWidth)}px`; this.element.style.height = `${Math.round(state.pendingHeight)}px`; }, @@ -743,7 +806,7 @@ export const dialogShellMethods = { el.style.maxHeight = '100vh'; el.style.borderRadius = '0'; el.style.resize = 'none'; - this._modalResizeHandle?.style.setProperty('display', 'none'); + this._modalResizeHandles?.forEach((handle) => handle.style.setProperty('display', 'none')); if (btn) { btn.innerHTML = getSvgIcon('windowRestore', 'currentColor', 'mr-window-btn-icon'); btn.setAttribute('aria-label', 'Exit full screen'); @@ -757,7 +820,7 @@ export const dialogShellMethods = { el.style.maxHeight = '100vh'; el.style.borderRadius = '8px'; el.style.resize = 'none'; - this._modalResizeHandle?.style.removeProperty('display'); + this._modalResizeHandles?.forEach((handle) => handle.style.removeProperty('display')); // Restore saved pre-FS size if available let wh = null; try { wh = JSON.parse(safeStorage.getItem('model_resolver_modal_size_before_fs') || 'null'); } catch (_e) {}