diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 463ec6a..a21afe8 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -172,6 +172,8 @@ test('floating dialog drag stays on the compositor without forced style reads', assert.match(resolverDialogSource, /mr-undock-drop-preview/); assert.match(startDockedDrag, /getRememberedFloatingSize\(\)/); assert.match(onDockedDrag, /Math\.hypot\(dx,\s*dy\)\s*<\s*5/); + assert.match(onDockedDrag, /e\.clientX\s*>\s*this\.getDockSnapThreshold\(\)/); + assert.match(onDockedDrag, /setUndockDropPreviewActive\(shouldUndock\)/); assert.match(onDockedDrag, /requestAnimationFrame/); assert.match(endDockedDrag, /this\.undockToFloating\(\{\s*persist:\s*false\s*\}\)/); assert.match(endDockedDrag, /this\._floatingRectBeforeDock\s*=\s*\{\s*\.\.\.finalRect\s*\}/); @@ -241,6 +243,70 @@ test('floating dialog drag stays on the compositor without forced style reads', } }); +test('docked dialog drag stays docked after returning to the dock edge', () => { + const onDockedDrag = eval(`(${extractMethod(dialogShellMethodsSource, 'onDockedDrag')})`); + const endDockedDrag = eval(`(${extractMethod(dialogShellMethodsSource, 'endDockedDrag')})`); + const previousDocument = globalThis.document; + const previousRequestAnimationFrame = globalThis.requestAnimationFrame; + const previousCancelAnimationFrame = globalThis.cancelAnimationFrame; + let undockCalls = 0; + let cancelledFrame = null; + + globalThis.document = { + removeEventListener() {}, + }; + globalThis.requestAnimationFrame = () => 23; + globalThis.cancelAnimationFrame = frame => { + cancelledFrame = frame; + }; + + try { + const dialog = { + docked: true, + fullscreen: false, + element: { style: {} }, + undockDropPreview: { style: {} }, + _dockedDragStart: { x: 20, y: 20 }, + _dockedDragPendingRect: null, + _dockedDragAnimationFrame: null, + _dragUndockCandidate: false, + getDockSnapThreshold: () => 64, + getDockedDragPreviewRect: (clientX, clientY) => ({ + top: clientY, + left: clientX, + width: 800, + height: 600, + }), + setUndockDropPreviewActive(active) { + this._dragUndockCandidate = Boolean(active && this.docked && !this.fullscreen); + }, + undockToFloating() { + undockCalls += 1; + }, + saveModalPosition() {}, + }; + + onDockedDrag.call(dialog, { clientX: 120, clientY: 40 }); + assert.equal(dialog._dragUndockCandidate, true); + assert.equal(dialog._dockedDragAnimationFrame, 23); + + onDockedDrag.call(dialog, { clientX: 20, clientY: 40 }); + assert.equal(dialog._dragUndockCandidate, false); + assert.equal(dialog._dockedDragPendingRect, null); + + endDockedDrag.call(dialog); + + assert.equal(undockCalls, 0); + assert.equal(dialog.docked, true); + assert.equal(cancelledFrame, 23); + assert.equal(dialog._dockedDragStart, null); + } finally { + globalThis.document = previousDocument; + globalThis.requestAnimationFrame = previousRequestAnimationFrame; + globalThis.cancelAnimationFrame = previousCancelAnimationFrame; + } +}); + test('missing browser splitter keeps a compositor preview between live layout frames', () => { const previousDocument = globalThis.document; const previousRequestAnimationFrame = globalThis.requestAnimationFrame; @@ -419,6 +485,61 @@ test('docked ComfyUI splitter coalesces live layout while its gutter stays respo assert.doesNotMatch(dialogShellMethodsSource, /mr-resolver-external-resizing/); }); +test('queue splitter suppresses tooltips while resizing', () => { + const activateQueueSplitUi = eval( + `(${extractMethod(queueMethodsSource, 'activateQueueSplitUi')})` + ); + const deactivateQueueSplitUi = eval( + `(${extractMethod(queueMethodsSource, 'deactivateQueueSplitUi')})` + ); + const classes = new Set(); + const splitterClasses = new Set(); + let hideTooltipCalls = 0; + const body = { + classList: { + add: value => classes.add(value), + remove: value => classes.delete(value), + }, + }; + const dialog = { + _queueSplitUiActive: false, + splitterElement: { + classList: { + add: value => splitterClasses.add(value), + remove: value => splitterClasses.delete(value), + }, + }, + hideTooltip() { + hideTooltipCalls += 1; + }, + }; + + activateQueueSplitUi.call(dialog, body); + activateQueueSplitUi.call(dialog, body); + + assert.equal(hideTooltipCalls, 1); + assert.equal(classes.has('is-resizing'), true); + assert.equal(splitterClasses.has('is-resizing'), true); + + deactivateQueueSplitUi.call(dialog, body); + + assert.equal(dialog._queueSplitUiActive, false); + assert.equal(classes.has('is-resizing'), false); + assert.equal(splitterClasses.has('is-resizing'), false); + assert.match( + queueMethodsSource, + /onBeforeDrag:\s*\(newW\)\s*=>\s*\{\s*this\.activateQueueSplitUi\(body\)/ + ); + assert.match( + queueMethodsSource, + /onEnd:\s*\(finalWidth\)\s*=>\s*\{[^]*this\.deactivateQueueSplitUi\(body\)/ + ); + assert.match( + resolverMainCssSource, + /#model-resolver-body\.is-resizing::after\s*\{[^}]*pointer-events:\s*auto/s + ); +}); + test('missing browser detail remains available at narrow widths', () => { assert.doesNotMatch(missingBrowserMethodsSource, /updateMissingBrowserDetailCollapse/); assert.doesNotMatch(missingBrowserMethodsSource, /is-detail-collapsed/); diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index cdb1053..304683c 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -742,6 +742,15 @@ pointer-events: auto; } +#model-resolver-body.is-resizing::after { + content: ""; + position: absolute; + z-index: 9999; + inset: 0; + cursor: col-resize; + pointer-events: auto; +} + #model-resolver-modal .mr-missing-list-row { content-visibility: auto; contain-intrinsic-size: 70px; diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index c513418..d6c9157 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -1955,6 +1955,23 @@ export const queueMethods = { }); }, + activateQueueSplitUi(body) { + if (this._queueSplitUiActive) return; + + const targetBody = body || this.getQueueSplitBody(); + this._queueSplitUiActive = true; + this.hideTooltip?.(); + targetBody?.classList?.add('is-resizing'); + this.splitterElement?.classList?.add('is-resizing'); + }, + + deactivateQueueSplitUi(body) { + const targetBody = body || this.getQueueSplitBody(); + this._queueSplitUiActive = false; + targetBody?.classList?.remove('is-resizing'); + this.splitterElement?.classList?.remove('is-resizing'); + }, + // Begin split drag for resizable panels startSplitDrag(e, { startWidth = null, edgeOpen = false, startCollapsedPreview = false } = {}) { try { @@ -1987,12 +2004,14 @@ export const queueMethods = { this._splitPreviewCollapsed = !!startCollapsedPreview; this._splitEdgeOpen = !!edgeOpen; this._splitEdgeOpenedPastThreshold = false; + const body = this.getQueueSplitBody(); this._splitInteraction = startSplitterDrag(e, { anchor: 'right', startWidth: initialWidth, bounds, onBeforeDrag: (newW) => { + this.activateQueueSplitUi(body); if (newW < bounds.min) newW = bounds.min; if (newW > bounds.max) newW = bounds.max; return newW; @@ -2041,6 +2060,7 @@ export const queueMethods = { }, onEnd: (finalWidth) => { this._splitDragging = false; + this.deactivateQueueSplitUi(body); if (this.queueElement && finalWidth && !this._splitPreviewCollapsed) { this.applyQueueSplitWidth(finalWidth, { bounds: this._splitStart?.bounds, @@ -2049,8 +2069,6 @@ export const queueMethods = { }); } - const body = this.getQueueSplitBody(); - if (this._splitEdgeOpen && !this._pendingSplitWidth) { try { const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320)); diff --git a/web/resolver/shell/dialog_shell_methods.js b/web/resolver/shell/dialog_shell_methods.js index f94a412..65e1c3a 100644 --- a/web/resolver/shell/dialog_shell_methods.js +++ b/web/resolver/shell/dialog_shell_methods.js @@ -906,8 +906,14 @@ export const dialogShellMethods = { const dy = e.clientY - start.y; if (!this._dragUndockCandidate && Math.hypot(dx, dy) < 5) return; + const shouldUndock = e.clientX > this.getDockSnapThreshold(); + this.setUndockDropPreviewActive(shouldUndock); + if (!shouldUndock) { + this._dockedDragPendingRect = null; + return; + } + this._dockedDragPendingRect = this.getDockedDragPreviewRect(e.clientX, e.clientY); - this.setUndockDropPreviewActive(true); if (this._dockedDragAnimationFrame || !this._dockedDragPendingRect) return; this._dockedDragAnimationFrame = requestAnimationFrame(() => {