Improve resolver docked-splitter responsiveness and external resize behavior

This commit is contained in:
Dariusz L
2026-07-28 12:12:09 +02:00
parent 8b2f9e81cb
commit 37d31b6782
6 changed files with 440 additions and 25 deletions
+131
View File
@@ -329,6 +329,137 @@ test('missing browser splitter keeps a compositor preview between live layout fr
/\.mr-missing-browser\.is-resizing \.mr-missing-(?:list|detail)-pane/
);
assert.doesNotMatch(missingBrowserMethodsSource, /deferMissingBrowserSplitReleaseUi/);
assert.match(missingBrowserMethodsSource, /observedWidth\s*<\s*760/);
assert.match(missingBrowserMethodsSource, /observedWidth\s*<\s*700/);
assert.match(
missingBrowserMethodsSource,
/scheduleMissingBrowserExternalResizeRestore\(browser,\s*observedWidth\)/
);
assert.match(
resolverMainCssSource,
/\.mr-missing-browser\.is-detail-collapsed \.mr-missing-detail-pane\s*\{[^}]*display:\s*none/s
);
assert.doesNotMatch(missingBrowserMethodsSource, /startMissingBrowserExternalResizeLock/);
assert.doesNotMatch(resolverMainCssSource, /mr-resolver-external-resizing/);
assert.doesNotMatch(
resolverMainCssSource,
/@media\s*\(max-width:\s*980px\)\s*\{\s*\.mr-missing-browser/
);
});
test('docked ComfyUI splitter coalesces live layout while its gutter stays responsive', () => {
const queueResize = eval(
`(${extractMethod(dialogShellMethodsSource, 'queueSidebarSplitterResize')})`
);
const flushResize = eval(
`(${extractMethod(dialogShellMethodsSource, 'flushSidebarSplitterResize')})`
);
const previousRequestAnimationFrame = globalThis.requestAnimationFrame;
const previousWindow = globalThis.window;
const frames = [];
const originalCalls = [];
globalThis.requestAnimationFrame = callback => {
frames.push(callback);
return frames.length;
};
globalThis.window = {
setTimeout(callback) {
callback();
return 1;
},
};
try {
const gutter = {
style: {
transform: '',
willChange: '',
},
};
const state = {
proxy: {},
originalResize(...args) {
originalCalls.push(args);
},
gutter,
originalTransform: '',
pendingArgs: null,
animationFrame: null,
delayTimer: null,
lastLayoutAt: Number.NEGATIVE_INFINITY,
minLayoutInterval: 40,
appliedPageX: 100,
appliedPageY: 0,
vertical: false,
hasMoved: false,
};
const context = {
_sidebarSplitterDragState: state,
flushSidebarSplitterResize: flushResize,
};
queueResize.call(context, state, [{ pageX: 112, pageY: 0 }]);
queueResize.call(context, state, [{ pageX: 128, pageY: 0 }]);
assert.equal(frames.length, 1);
assert.equal(gutter.style.transform, 'translate3d(28px, 0px, 0)');
assert.equal(originalCalls.length, 0);
frames.shift()();
assert.equal(originalCalls.length, 1);
assert.equal(originalCalls[0][0].pageX, 128);
assert.equal(state.appliedPageX, 128);
assert.equal(gutter.style.transform, '');
} finally {
globalThis.requestAnimationFrame = previousRequestAnimationFrame;
globalThis.window = previousWindow;
}
assert.match(dialogShellMethodsSource, /minLayoutInterval:\s*40/);
assert.match(dialogShellMethodsSource, /setPointerCapture\(event\.pointerId\)/);
assert.match(dialogShellMethodsSource, /translate3d\(/);
assert.match(dialogShellMethodsSource, /requestAnimationFrame\(/);
assert.doesNotMatch(dialogShellMethodsSource, /mr-resolver-external-resizing/);
});
test('missing browser detail collapse follows container width with hysteresis', () => {
const previousHTMLElement = globalThis.HTMLElement;
class FakeHTMLElement {}
globalThis.HTMLElement = FakeHTMLElement;
try {
const classes = new Set();
const browser = new FakeHTMLElement();
browser.classList = {
contains(name) {
return classes.has(name);
},
toggle(name, enabled) {
if (enabled) classes.add(name);
else classes.delete(name);
},
};
const context = {
_missingBrowserRestoreFrame: null,
cancelMissingBrowserExternalResizeRestore() {},
};
const updateDetailCollapse = eval(
`(${extractMethod(missingBrowserMethodsSource, 'updateMissingBrowserDetailCollapse')})`
);
assert.equal(updateDetailCollapse.call(context, browser, 690), true);
assert.equal(classes.has('is-detail-collapsed'), true);
assert.equal(updateDetailCollapse.call(context, browser, 730), false);
assert.equal(classes.has('is-detail-collapsed'), true);
assert.equal(updateDetailCollapse.call(context, browser, 770), true);
assert.equal(classes.has('is-detail-collapsed'), false);
} finally {
globalThis.HTMLElement = previousHTMLElement;
}
});
test('local and loaded model tooltips include preview image routes above full names', () => {
+8 -19
View File
@@ -1156,26 +1156,15 @@
box-shadow: none;
}
@media (max-width: 980px) {
.mr-missing-browser {
flex-direction: column;
overflow-y: auto;
}
.mr-missing-browser.is-detail-collapsed .mr-missing-browser-splitter,
.mr-missing-browser.is-detail-collapsed .mr-missing-detail-pane {
display: none;
}
.mr-missing-browser-splitter {
display: none;
}
.mr-missing-list-pane {
max-height: 46vh;
border-right: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.mr-missing-detail-pane {
flex: 0 0 auto;
overflow: visible;
}
.mr-missing-browser.is-detail-collapsed .mr-missing-list-pane {
flex: 1 1 100% !important;
width: 100%;
border-right: 0;
}
@media (max-width: 760px) {
+7
View File
@@ -73,6 +73,10 @@ export class ResolverManagerDialog extends ComfyDialog {
this.sidebarTabId = "comfyui-model-resolver";
this.sidebarOpenModeStorageKey = "model_resolver_sidebar_open_mode";
this.missingBrowserSplitStorageKey = "model_resolver_missing_browser_detail_w";
this._missingBrowserExternalResizeTimer = null;
this._pendingMissingBrowserExternalResizeBrowser = null;
this._pendingMissingBrowserExternalResizeWidth = null;
this._missingBrowserObservedHostWidth = null;
this.showResolvedModelsStorageKey = "model_resolver_show_resolved_models";
this.showAutoDownloadModelsStorageKey = "model_resolver_show_auto_download_models";
this.localMatchAlternativesCollapsedStorageKey = "model_resolver_local_match_alternatives_collapsed";
@@ -100,6 +104,9 @@ export class ResolverManagerDialog extends ComfyDialog {
this._dockedDragPendingRect = null;
this._dockedDragAnimationFrame = null;
this._dragUndockCandidate = false;
this._sidebarSplitterPointerDownHandler = null;
this._sidebarSplitterMouseDownHandler = null;
this._sidebarSplitterDragState = null;
this._analysisProgressToken = null;
this._workflowDataLoadToken = null;
this._loadedModelsLoadToken = null;
+205
View File
@@ -207,6 +207,210 @@ export const dialogShellMethods = {
if (this.element.parentNode !== container) {
container.appendChild(this.element);
}
this.installSidebarSplitterOptimization();
},
installSidebarSplitterOptimization() {
if (this._sidebarSplitterPointerDownHandler || this._sidebarSplitterMouseDownHandler) return;
this._sidebarSplitterPointerDownHandler = (event) => {
if (event.button !== undefined && event.button !== 0) return;
const gutter = this.getResolverSidebarGutter(event.target);
if (!gutter || typeof gutter.setPointerCapture !== 'function') return;
try {
gutter.setPointerCapture(event.pointerId);
} catch {
// Pointer capture is an optional optimization and can fail for synthetic events.
}
};
this._sidebarSplitterMouseDownHandler = (event) => {
if (event.button !== undefined && event.button !== 0) return;
const gutter = this.getResolverSidebarGutter(event.target);
if (gutter) this.startSidebarSplitterDragOptimization(event, gutter);
};
document.addEventListener('pointerdown', this._sidebarSplitterPointerDownHandler, true);
document.addEventListener('mousedown', this._sidebarSplitterMouseDownHandler, true);
},
removeSidebarSplitterOptimization() {
this.finishSidebarSplitterDragOptimization({ flush: true });
if (this._sidebarSplitterPointerDownHandler) {
document.removeEventListener('pointerdown', this._sidebarSplitterPointerDownHandler, true);
this._sidebarSplitterPointerDownHandler = null;
}
if (this._sidebarSplitterMouseDownHandler) {
document.removeEventListener('mousedown', this._sidebarSplitterMouseDownHandler, true);
this._sidebarSplitterMouseDownHandler = null;
}
},
getResolverSidebarGutter(target) {
if (!this.docked || !(this.dockContainer instanceof HTMLElement) || !this.dockContainer.isConnected) {
return null;
}
const element = target instanceof Element ? target : null;
const gutter = element?.closest?.('.p-splitter-gutter');
const panel = this.dockContainer.closest?.('[data-pc-name="splitterpanel"]');
if (!(gutter instanceof HTMLElement) || !(panel instanceof HTMLElement)) return null;
return gutter.previousElementSibling === panel || gutter.nextElementSibling === panel
? gutter
: null;
},
findPrimeVueSplitterResizeOwner(gutter) {
if (!(gutter instanceof HTMLElement)) return null;
let instance = gutter.__vueParentComponent || gutter.parentElement?.__vueParentComponent || null;
for (let depth = 0; instance && depth < 10; depth += 1, instance = instance.parent) {
const owner = instance.ctx;
if (
owner
&& typeof owner.onResize === 'function'
&& typeof owner.onResizeStart === 'function'
) {
return {
owner,
proxy: instance.proxy || owner
};
}
}
return null;
},
startSidebarSplitterDragOptimization(event, gutter) {
if (this._sidebarSplitterDragState) {
this.finishSidebarSplitterDragOptimization({ flush: true });
}
const resizeTarget = this.findPrimeVueSplitterResizeOwner(gutter);
if (!resizeTarget) return false;
const { owner, proxy } = resizeTarget;
const originalResize = owner.onResize;
const state = {
owner,
proxy,
originalResize,
wrappedResize: null,
gutter,
originalTransform: gutter.style.transform,
originalWillChange: gutter.style.willChange,
pendingArgs: null,
animationFrame: null,
delayTimer: null,
lastLayoutAt: typeof performance === 'object' ? performance.now() : Date.now(),
minLayoutInterval: 40,
appliedPageX: Number(event.pageX ?? event.clientX) || 0,
appliedPageY: Number(event.pageY ?? event.clientY) || 0,
vertical: proxy?.horizontal === false,
hasMoved: false,
mouseUpHandler: null,
blurHandler: null
};
state.wrappedResize = (...args) => this.queueSidebarSplitterResize(state, args);
try {
owner.onResize = state.wrappedResize;
} catch {
return false;
}
if (owner.onResize !== state.wrappedResize) return false;
state.mouseUpHandler = (mouseEvent) => {
this.finishSidebarSplitterDragOptimization({
flush: true,
finalEvent: mouseEvent
});
};
state.blurHandler = () => this.finishSidebarSplitterDragOptimization({ flush: true });
this._sidebarSplitterDragState = state;
document.addEventListener('mouseup', state.mouseUpHandler, true);
window.addEventListener('blur', state.blurHandler, { once: true });
return true;
},
queueSidebarSplitterResize(state, args) {
if (!state || this._sidebarSplitterDragState !== state) {
return state?.originalResize?.apply(state.proxy, args);
}
const event = args?.[0];
const pageX = Number(event?.pageX ?? event?.clientX);
const pageY = Number(event?.pageY ?? event?.clientY);
state.pendingArgs = args;
state.hasMoved = true;
const offset = state.vertical
? pageY - state.appliedPageY
: pageX - state.appliedPageX;
if (Number.isFinite(offset)) {
state.gutter.style.willChange = 'transform';
state.gutter.style.transform = offset
? `translate3d(${state.vertical ? 0 : Math.round(offset)}px, ${state.vertical ? Math.round(offset) : 0}px, 0)`
: state.originalTransform;
}
if (state.animationFrame || state.delayTimer) return;
const now = typeof performance === 'object' ? performance.now() : Date.now();
const delay = Math.max(0, state.minLayoutInterval - (now - state.lastLayoutAt));
const requestFlush = () => {
state.delayTimer = null;
state.animationFrame = requestAnimationFrame(() => {
state.animationFrame = null;
this.flushSidebarSplitterResize(state);
});
};
if (delay > 0) {
state.delayTimer = window.setTimeout(requestFlush, delay);
} else {
requestFlush();
}
},
flushSidebarSplitterResize(state) {
if (!state?.pendingArgs) return false;
const args = state.pendingArgs;
state.pendingArgs = null;
const event = args[0];
state.originalResize.apply(state.proxy, args);
state.appliedPageX = Number(event?.pageX ?? event?.clientX) || state.appliedPageX;
state.appliedPageY = Number(event?.pageY ?? event?.clientY) || state.appliedPageY;
state.lastLayoutAt = typeof performance === 'object' ? performance.now() : Date.now();
state.gutter.style.transform = state.originalTransform;
return true;
},
finishSidebarSplitterDragOptimization({ flush = false, finalEvent = null } = {}) {
const state = this._sidebarSplitterDragState;
if (!state) return false;
this._sidebarSplitterDragState = null;
if (state.delayTimer) {
clearTimeout(state.delayTimer);
state.delayTimer = null;
}
if (state.animationFrame) {
cancelAnimationFrame(state.animationFrame);
state.animationFrame = null;
}
if (flush && state.hasMoved && finalEvent) {
state.pendingArgs = [finalEvent];
}
if (flush) this.flushSidebarSplitterResize(state);
if (state.owner.onResize === state.wrappedResize) {
state.owner.onResize = state.originalResize;
}
state.gutter.style.transform = state.originalTransform;
state.gutter.style.willChange = state.originalWillChange;
document.removeEventListener('mouseup', state.mouseUpHandler, true);
window.removeEventListener('blur', state.blurHandler);
return true;
},
dockToSidebar() {
@@ -306,6 +510,7 @@ export const dialogShellMethods = {
undockToFloating({ persist = true, closeSidebar = true } = {}) {
if (!this.element) return;
this.removeSidebarSplitterOptimization();
this.setDockDropPreviewActive(false);
this.setUndockDropPreviewActive(false);
this._pendingDragDockRect = null;
@@ -98,6 +98,7 @@ export const lifecycleGraphMethods = {
close({ collapseSidebar = true } = {}) {
const wasDocked = this.docked;
const dockContainer = this.dockContainer;
this.removeSidebarSplitterOptimization?.();
this.setDockDropPreviewActive(false);
this.setUndockDropPreviewActive(false);
this.rememberSidebarOpenMode(this.docked ? 'docked' : 'floating');
+88 -6
View File
@@ -959,20 +959,39 @@ export const missingBrowserMethods = {
this.resizeMissingBrowserDetailBy(browser, event.key === 'ArrowLeft' ? 32 : -32);
});
this.cancelMissingBrowserExternalResizeRestore();
this._missingBrowserResizeObserver?.disconnect?.();
if (typeof ResizeObserver === 'function') {
const resizeHost = browser.parentElement instanceof HTMLElement
? browser.parentElement
: browser;
this._missingBrowserObservedHostWidth = null;
this._missingBrowserResizeObserver = new ResizeObserver((entries) => {
const observedWidth = Number(entries?.[0]?.contentRect?.width);
if (!Number.isFinite(observedWidth) || observedWidth <= 0) return;
const previousHostWidth = Number(this._missingBrowserObservedHostWidth);
this._missingBrowserObservedHostWidth = observedWidth;
this.rememberMissingBrowserWidth(observedWidth);
if (!Number.isFinite(previousHostWidth) || previousHostWidth <= 0) {
this.updateMissingBrowserDetailCollapse(browser, observedWidth);
return;
}
if (Math.abs(previousHostWidth - observedWidth) < 0.5) return;
const settlingUntil = Number(this._missingBrowserSplitSettlingUntil || 0);
const isSettling = typeof performance === 'object'
&& typeof performance.now === 'function'
&& performance.now() < settlingUntil;
if (!this._missingBrowserSplitDragging && !isSettling) {
this.scheduleMissingBrowserSplitRestore(browser, observedWidth, { useDefault: false });
if (
!this._missingBrowserSplitDragging
&& !isSettling
) {
this.scheduleMissingBrowserExternalResizeRestore(browser, observedWidth);
}
});
this._missingBrowserResizeObserver.observe(browser);
this._missingBrowserResizeObserver.observe(resizeHost);
} else {
this.cancelMissingBrowserPrewarmFrame();
this._missingBrowserPrewarmFrame = requestAnimationFrame(() => {
@@ -981,7 +1000,8 @@ export const missingBrowserMethods = {
if (this._missingBrowserSplitDragging) return;
const measuredWidth = Number(browser.clientWidth || browser.getBoundingClientRect().width);
this.rememberMissingBrowserWidth(measuredWidth);
if (!this._missingBrowserSplitDragging) {
this.updateMissingBrowserDetailCollapse(browser, measuredWidth);
if (!this._missingBrowserSplitDragging && !browser.classList.contains('is-detail-collapsed')) {
this.scheduleMissingBrowserSplitRestore(browser, measuredWidth, { useDefault: false });
}
});
@@ -994,6 +1014,68 @@ export const missingBrowserMethods = {
this._missingBrowserPrewarmFrame = null;
},
updateMissingBrowserDetailCollapse(browser, width) {
if (!(browser instanceof HTMLElement)) return false;
const observedWidth = Number(width);
if (!Number.isFinite(observedWidth) || observedWidth <= 0) return false;
const wasCollapsed = browser.classList.contains('is-detail-collapsed');
const shouldCollapse = wasCollapsed
? observedWidth < 760
: observedWidth < 700;
if (shouldCollapse === wasCollapsed) return false;
browser.classList.toggle('is-detail-collapsed', shouldCollapse);
if (shouldCollapse) {
this.cancelMissingBrowserExternalResizeRestore();
if (this._missingBrowserRestoreFrame) {
cancelAnimationFrame(this._missingBrowserRestoreFrame);
this._missingBrowserRestoreFrame = null;
}
this._pendingMissingBrowserRestoreBrowser = null;
this._pendingMissingBrowserRestoreWidth = null;
this._pendingMissingBrowserRestoreUseDefault = false;
}
return true;
},
cancelMissingBrowserExternalResizeRestore() {
if (this._missingBrowserExternalResizeTimer) {
clearTimeout(this._missingBrowserExternalResizeTimer);
}
this._missingBrowserExternalResizeTimer = null;
this._pendingMissingBrowserExternalResizeBrowser = null;
this._pendingMissingBrowserExternalResizeWidth = null;
},
scheduleMissingBrowserExternalResizeRestore(browser, browserWidth, delay = 120) {
if (!(browser instanceof HTMLElement)) return;
this._pendingMissingBrowserExternalResizeBrowser = browser;
this._pendingMissingBrowserExternalResizeWidth = Number(browserWidth) || null;
if (this._missingBrowserExternalResizeTimer) {
clearTimeout(this._missingBrowserExternalResizeTimer);
}
this._missingBrowserExternalResizeTimer = setTimeout(() => {
this._missingBrowserExternalResizeTimer = null;
const targetBrowser = this._pendingMissingBrowserExternalResizeBrowser;
const targetWidth = this._pendingMissingBrowserExternalResizeWidth;
this._pendingMissingBrowserExternalResizeBrowser = null;
this._pendingMissingBrowserExternalResizeWidth = null;
if (
!targetBrowser?.isConnected
|| this._missingBrowserSplitDragging
) {
return;
}
this.updateMissingBrowserDetailCollapse(targetBrowser, targetWidth);
if (targetBrowser.classList.contains('is-detail-collapsed')) return;
this.scheduleMissingBrowserSplitRestore(targetBrowser, targetWidth, { useDefault: false });
}, Math.max(0, Number(delay) || 0));
},
getStoredMissingBrowserSplitWidth() {
if (this._missingBrowserStoredSplitWidthLoaded) {
return this._missingBrowserStoredSplitWidth || null;
@@ -1060,7 +1142,7 @@ export const missingBrowserMethods = {
},
scheduleMissingBrowserSplitRestore(browser, browserWidth = null, { useDefault = false } = {}) {
if (!(browser instanceof HTMLElement)) return;
if (!(browser instanceof HTMLElement) || browser.classList.contains('is-detail-collapsed')) return;
this._pendingMissingBrowserRestoreBrowser = browser;
this._pendingMissingBrowserRestoreWidth = Number.isFinite(Number(browserWidth)) && Number(browserWidth) > 0
@@ -1100,7 +1182,7 @@ export const missingBrowserMethods = {
},
startMissingBrowserSplitDrag(event, browser, splitter = null, panes = {}) {
if (!(browser instanceof HTMLElement)) return;
if (!(browser instanceof HTMLElement) || browser.classList.contains('is-detail-collapsed')) return;
event.preventDefault();
event.stopPropagation?.();