From 3c4dfc72e4813c41735e1fbf08be90f9e167c8dd Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Wed, 1 Jul 2026 17:58:05 +0200 Subject: [PATCH] feat: refactor queue panel and split layout with deferred reveal and pointer events - Add PointerEvent support for queue toggle, edge drag, and splitter interactions - Implement deferred queue panel reveal animation (mr-queue-opening class) - Make queue panel updates demand-driven; skip refresh when panel is hidden - Replace per-item remove listeners with event delegation on queue list - Track queue split container width via ResizeObserver with safe fallbacks - Centralize split flex application and persist queue width/collapsed state with debounce - Fix missing browser split pane styles to use helper-track CSS variables - Add deferred UI cleanup for missing browser split drag end - Add prewarm frame fallback when ResizeObserver is unavailable --- web/css/resolver-main.css | 13 +- web/css/resolver-shell.css | 30 +- web/resolver/actions/queue_methods.js | 623 +++++++++++++++--- web/resolver/views/missing_browser_methods.js | 194 +++--- 4 files changed, 699 insertions(+), 161 deletions(-) diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index 48cff35..81d7316 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -502,10 +502,13 @@ --mr-missing-line: rgba(255, 255, 255, 0.075); --mr-missing-line-strong: rgba(255, 255, 255, 0.13); --mr-missing-radius: 6px; + --mr-missing-splitter-track: 10px; + --mr-missing-list-track: calc(100% - var(--mr-missing-splitter-track) - var(--mr-missing-detail-track, 42%)); display: flex; gap: 0; height: 100%; min-height: 0; + min-width: 0; overflow: hidden; border: 1px solid var(--mr-missing-line); background: var(--mr-missing-bg); @@ -517,12 +520,12 @@ min-width: 0; min-height: 0; overflow: hidden; - contain: layout paint style; + contain: size layout paint style; background: var(--mr-missing-bg); } .mr-missing-list-pane { - flex: 1 1 0; + flex: 0 0 var(--mr-missing-list-track); display: grid; grid-template-rows: auto minmax(0, 1fr); border-right: 0; @@ -531,9 +534,9 @@ .mr-missing-browser-splitter { position: relative; - flex: 0 0 10px; - min-width: 10px; - width: 10px; + flex: 0 0 var(--mr-missing-splitter-track); + min-width: var(--mr-missing-splitter-track); + width: var(--mr-missing-splitter-track); cursor: col-resize; user-select: none; touch-action: none; diff --git a/web/css/resolver-shell.css b/web/css/resolver-shell.css index 29103d1..3df5fab 100644 --- a/web/css/resolver-shell.css +++ b/web/css/resolver-shell.css @@ -179,16 +179,19 @@ padding: 8px 6px 6px; flex: 1; min-height: 0; + min-width: 0; align-items: stretch; position: relative; + overflow: hidden; background: var(--mr-bg, #121212); } #model-resolver-content { overflow-y: auto; - flex: 1; + flex: 1 1 0; min-height: 0; - contain: layout paint style; + min-width: 0; + contain: size layout paint style; background: var(--mr-bg, #222); } @@ -196,14 +199,34 @@ width: 320px; min-width: 240px; max-width: 70%; + flex: 0 0 320px; + box-sizing: border-box; display: flex; flex-direction: column; padding-left: 0; border-left: 0; - contain: layout paint style; + contain: size layout paint style; background: var(--mr-bg, #121212); } +#model-resolver-queue.mr-queue-opening .mr-queue-stack { + content-visibility: hidden; + contain-intrinsic-size: 320px 420px; +} + +#model-resolver-body.mr-queue-opening-layout #model-resolver-queue { + position: absolute; + top: 8px; + right: 6px; + bottom: 6px; + z-index: 998; + box-shadow: -12px 0 24px rgba(0, 0, 0, 0.24); +} + +#model-resolver-body.mr-queue-opening-layout #model-resolver-splitter { + display: none !important; +} + #model-resolver-body.mr-is-resizing-queue #model-resolver-content, #model-resolver-body.mr-is-resizing-queue #model-resolver-queue { pointer-events: none; @@ -292,6 +315,7 @@ display: flex; flex-direction: column; min-height: 0; + min-width: 0; flex: 1 1 auto; } diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index 6366d30..266d493 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -28,6 +28,7 @@ export const queueMethods = { body.appendChild(this.contentElement); body.appendChild(this.splitterElement); body.appendChild(this.queueElement); + this.observeQueueSplitContainer(body); // Restore saved queue width and wire splitter try { @@ -36,13 +37,16 @@ export const queueMethods = { const w = parseInt(savedSplit, 10); if (!isNaN(w) && w > 0) { this.queueElement.style.width = `${w}px`; + this.queueElement.style.flexBasis = `${w}px`; + this.queueElement.style.flexGrow = '0'; + this.queueElement.style.flexShrink = '0'; } } } catch (e) { } try { const onSplitMouseDown = (e) => this.startSplitDrag(e); - this.splitterElement.addEventListener('mousedown', onSplitMouseDown); + this.splitterElement.addEventListener(typeof PointerEvent === 'function' ? 'pointerdown' : 'mousedown', onSplitMouseDown); this._splitterMouseDown = onSplitMouseDown; } catch (e) { } @@ -52,9 +56,12 @@ export const queueMethods = { id: "queue-toggle-icon", type: "button", "aria-label": "Toggle queued selections", - onclick: (event) => this.onQueueEdgeClick(event), - onmousedown: (event) => this.startQueueEdgeDrag(event) + onclick: (event) => this.onQueueEdgeClick(event) }); + this.queueToggleIcon.addEventListener( + typeof PointerEvent === 'function' ? 'pointerdown' : 'mousedown', + (event) => this.startQueueEdgeDrag(event) + ); body.appendChild(this.queueToggleIcon); this.updateQueueToggleIcon(); } catch (e) { } @@ -62,7 +69,7 @@ export const queueMethods = { // Restore queue collapsed state try { const col = localStorage.getItem('model_resolver_queue_collapsed'); - if (col === '1') this.setQueueCollapsed(true); + if (col === '1') this.setQueueCollapsed(true, { persist: false, updatePanel: false }); } catch (e) { } return body; @@ -105,6 +112,17 @@ export const queueMethods = { // Scrollable list this.queueList = $el("div#queue-list.mr-queue-list"); + this.queueList.addEventListener('click', (event) => { + const button = event.target instanceof Element + ? event.target.closest('.mr-queue-remove') + : null; + if (!(button instanceof HTMLElement)) return; + const index = Number.parseInt(button.dataset.queueIndex || '-1', 10); + if (!Number.isInteger(index) || index < 0) return; + event.preventDefault(); + event.stopPropagation(); + this.removeQueuedByIndex(index); + }); const panel = $el("div.mr-queue-panel", {}, [ $el("div.mr-queue-stack", {}, [this.queueTabs, this.queueHeader, this.queueList]) @@ -126,16 +144,21 @@ export const queueMethods = { this.updateQueuePanel(); }, - updateQueuePanel() { + updateQueuePanel({ force = false } = {}) { if (!this.queueList || !this.queueHeader) return; if (this._queuePanelUpdateTimer) { clearTimeout(this._queuePanelUpdateTimer); this._queuePanelUpdateTimer = null; } + if (!force && !this.isQueuePanelVisible()) { + this._queuePanelDirty = true; + return; + } + this._queuePanelDirty = false; const list = Array.isArray(this.pendingResolutions) ? this.pendingResolutions : []; const downloads = this.getActiveQueuePanelDownloads(); - const history = this.getDownloadHistory(); const activeTab = this.queuePanelActiveTab === 'downloads' ? 'downloads' : 'queued'; + const history = activeTab === 'downloads' ? this.getDownloadHistory() : []; const clearBtn = this.queueClearButton || this.queueHeader.querySelector('#queue-clear'); const showActions = activeTab === 'queued'; this.queueHeader.style.display = showActions ? '' : 'none'; @@ -150,15 +173,140 @@ export const queueMethods = { this.renderQueuedSelections(list); }, - requestQueuePanelUpdate(delayMs = 180) { + requestQueuePanelUpdate(delayMs = 180, { force = false } = {}) { if (!this.queueList || !this.queueHeader) return; + if (!force && !this.isQueuePanelVisible()) { + this._queuePanelDirty = true; + return; + } if (this._queuePanelUpdateTimer) return; this._queuePanelUpdateTimer = setTimeout(() => { this._queuePanelUpdateTimer = null; - this.updateQueuePanel(); + this.updateQueuePanel({ force }); }, delayMs); }, + isQueuePanelVisible() { + return Boolean( + this.queueElement + && this.activeTab === 'missing' + && !this.queueCollapsed + && this.queueElement.style.display !== 'none' + ); + }, + + scheduleQueuePanelOpenUpdate(delayMs = 80) { + if (!this.queueElement || !this.queueList || !this.queueHeader) return; + if (this._queuePanelOpenFrame) { + cancelAnimationFrame(this._queuePanelOpenFrame); + this._queuePanelOpenFrame = null; + } + if (this._queuePanelOpenTimer) { + clearTimeout(this._queuePanelOpenTimer); + this._queuePanelOpenTimer = null; + } + + const runUpdate = () => { + this._queuePanelOpenTimer = null; + document.getElementById('model-resolver-body')?.classList.remove('mr-queue-opening-layout'); + this.updateQueuePanel({ force: true }); + this.queueElement?.classList?.remove('mr-queue-opening'); + this.stabilizeQueueSplitLayout(); + }; + const scheduleTimer = () => { + this._queuePanelOpenFrame = null; + this._queuePanelOpenTimer = setTimeout(runUpdate, delayMs); + }; + + this._queuePanelOpenFrame = requestAnimationFrame(scheduleTimer); + }, + + scheduleQueuePanelReveal() { + if (!this.queueElement || !this.queueList || !this.queueHeader) return; + this.cancelQueuePanelReveal(); + + const reveal = () => { + this._queuePanelRevealFrame = null; + if (this.queueCollapsed || this.activeTab !== 'missing') return; + this.queueElement?.classList?.add('mr-queue-opening'); + document.getElementById('model-resolver-body')?.classList.add('mr-queue-opening-layout'); + this.updateQueueVisibility(); + this.scheduleQueuePanelOpenUpdate(); + }; + + this._queuePanelRevealFrame = requestAnimationFrame(() => { + this._queuePanelRevealFrame = requestAnimationFrame(reveal); + }); + }, + + cancelQueuePanelReveal() { + if (this._queuePanelRevealFrame) { + cancelAnimationFrame(this._queuePanelRevealFrame); + this._queuePanelRevealFrame = null; + } + }, + + cancelQueuePanelOpenUpdate() { + this.cancelQueuePanelReveal(); + if (this._queuePanelOpenFrame) { + cancelAnimationFrame(this._queuePanelOpenFrame); + this._queuePanelOpenFrame = null; + } + if (this._queuePanelOpenTimer) { + clearTimeout(this._queuePanelOpenTimer); + this._queuePanelOpenTimer = null; + } + this.queueElement?.classList?.remove('mr-queue-opening'); + document.getElementById('model-resolver-body')?.classList.remove('mr-queue-opening-layout'); + }, + + clearQueueContentSplitFlex() { + if (!(this.contentElement instanceof HTMLElement)) return; + this.contentElement.style.removeProperty('flex-basis'); + this.contentElement.style.removeProperty('flex-grow'); + this.contentElement.style.removeProperty('flex-shrink'); + }, + + persistQueueCollapsedState(collapsed, delayMs = 250) { + if (this._queueCollapsedPersistTimer) { + clearTimeout(this._queueCollapsedPersistTimer); + this._queueCollapsedPersistTimer = null; + } + const writeState = () => { + this._queueCollapsedPersistTimer = null; + try { + localStorage.setItem('model_resolver_queue_collapsed', collapsed ? '1' : '0'); + } catch (e) { } + }; + const delay = Math.max(0, Number(delayMs) || 0); + if (delay > 0) { + this._queueCollapsedPersistTimer = setTimeout(writeState, delay); + } else { + writeState(); + } + }, + + persistQueueSplitWidth(width, delayMs = 250) { + const nextWidth = Math.round(Number(width)); + if (!Number.isFinite(nextWidth) || nextWidth <= 0) return; + if (this._queueSplitPersistTimer) { + clearTimeout(this._queueSplitPersistTimer); + this._queueSplitPersistTimer = null; + } + const writeWidth = () => { + this._queueSplitPersistTimer = null; + try { + localStorage.setItem('model_resolver_split_w', String(nextWidth)); + } catch (e) { } + }; + const delay = Math.max(0, Number(delayMs) || 0); + if (delay > 0) { + this._queueSplitPersistTimer = setTimeout(writeWidth, delay); + } else { + writeWidth(); + } + }, + updateQueuePanelTabs(queueCount, downloadCount, activeTab) { const queuedTab = this.queueQueuedTabButton || this.queueTabs?.querySelector?.('.mr-queue-tab[data-tab="queued"]'); const downloadsTab = this.queueDownloadsTabButton || this.queueTabs?.querySelector?.('.mr-queue-tab[data-tab="downloads"]'); @@ -194,25 +342,15 @@ export const queueMethods = { const label = (r.resolved_model?.relative_path || r.resolved_model?.filename || r.resolved_path || '').toString(); const nodeLabel = r.node_label || r.node_type || (r.subgraph_id ? 'Subgraph' : 'Node'); const orig = (r.original_path || '').toString(); - const rmId = `queue-remove-${i}`; html += `
`; html += `
${this.escapeHtml(nodeLabel)} #${this.escapeHtml(String(r.node_id))}
`; html += `
Original${this.escapeHtml(orig)}
`; html += `
Selected${this.escapeHtml(label)}
`; - html += `
`; + html += `
`; html += `
`; } html += ''; this.queueList.innerHTML = html; - - // Wire remove buttons - for (let i = 0; i < list.length; i++) { - const rmId = `queue-remove-${i}`; - const btn = this.queueList.querySelector(`#${rmId}`); - if (btn) { - btn.addEventListener('click', () => this.removeQueuedByIndex(i)); - } - } }, getActiveQueuePanelDownloads() { @@ -1470,15 +1608,34 @@ export const queueMethods = { this.setQueueCollapsed(!this.queueCollapsed); }, - setQueueCollapsed(collapsed) { - this.queueCollapsed = !!collapsed; - if (this.queueCollapsed) { - try { localStorage.setItem('model_resolver_queue_collapsed', '1'); } catch (e) { } + setQueueCollapsed(collapsed, { persist = true, updatePanel = true, deferOpen = false } = {}) { + const nextCollapsed = !!collapsed; + const wasCollapsed = !!this.queueCollapsed; + const opening = wasCollapsed && !nextCollapsed; + this.queueCollapsed = nextCollapsed; + if (persist) { + this.persistQueueCollapsedState(this.queueCollapsed, opening ? 500 : 0); + } + if (opening && deferOpen) { + this.updateQueueToggleIcon(); + this.scheduleQueuePanelReveal(); + return; + } + if (opening) { + this.queueElement?.classList?.add('mr-queue-opening'); + document.getElementById('model-resolver-body')?.classList.add('mr-queue-opening-layout'); } else { - try { localStorage.setItem('model_resolver_queue_collapsed', '0'); } catch (e) { } + this.cancelQueuePanelOpenUpdate(); } this.updateQueueVisibility(); - this.updateQueuePanel(); + if (!updatePanel) return; + if (opening) { + this.scheduleQueuePanelOpenUpdate(); + } else if (this.isQueuePanelVisible()) { + this.updateQueuePanel(); + } else { + this._queuePanelDirty = true; + } }, getResolutionQueueKey(resolution = {}) { @@ -1555,6 +1712,11 @@ export const queueMethods = { if (this.queueElement) { this.queueElement.style.display = showQueuePanel ? '' : 'none'; } + if (!showQueuePanel) { + this.clearQueueContentSplitFlex(); + } else if (!this._splitDragging && !this._queuePanelDirty) { + this.stabilizeQueueSplitLayout(); + } if (this.splitterElement) { this.splitterElement.style.display = showQueuePanel ? '' : 'none'; } @@ -1563,6 +1725,11 @@ export const queueMethods = { } this.updateQueueToggleIcon(); + if (showQueuePanel && this._queuePanelDirty) { + this.queueElement?.classList?.add('mr-queue-opening'); + document.getElementById('model-resolver-body')?.classList.add('mr-queue-opening-layout'); + this.scheduleQueuePanelOpenUpdate(); + } }, updateQueueToggleIcon() { @@ -1587,15 +1754,83 @@ export const queueMethods = { this._suppressQueueEdgeClick = false; return; } - this.toggleQueueCollapsed(); + event?.preventDefault?.(); + event?.stopPropagation?.(); + this.setQueueCollapsed(!this.queueCollapsed, { deferOpen: true }); }, startQueueEdgeDrag(event) { if (event.button !== 0 || !this.queueCollapsed || !this.queueElement) return; - const startX = event.clientX; - const previousCursor = document.body.style.cursor; + event.preventDefault?.(); + event.stopPropagation?.(); + this._suppressQueueEdgeClick = true; + this.setQueueCollapsed(false, { deferOpen: true }); + + this.clearQueueEdgeDragCandidate(); + const usingPointerEvents = event.type === 'pointerdown'; + this._queueEdgeDragCandidate = { + startX: event.clientX, + startY: event.clientY, + previousCursor: document.body.style.cursor, + openedOnPointerDown: true + }; + this._onQueueEdgeCandidateMove = (moveEvent) => this.onQueueEdgeDragCandidateMove(moveEvent); + this._onQueueEdgeCandidateUp = () => { + const shouldClearSuppress = !!this._queueEdgeDragCandidate?.openedOnPointerDown; + this.clearQueueEdgeDragCandidate(); + if (shouldClearSuppress) { + setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0); + } + }; + this._queueEdgeCandidateMoveEvent = usingPointerEvents ? 'pointermove' : 'mousemove'; + this._queueEdgeCandidateUpEvent = usingPointerEvents ? 'pointerup' : 'mouseup'; + this._queueEdgeCandidateCancelEvent = usingPointerEvents ? 'pointercancel' : null; + document.addEventListener(this._queueEdgeCandidateMoveEvent, this._onQueueEdgeCandidateMove, true); + document.addEventListener(this._queueEdgeCandidateUpEvent, this._onQueueEdgeCandidateUp, { once: true, capture: true }); + if (this._queueEdgeCandidateCancelEvent) { + document.addEventListener(this._queueEdgeCandidateCancelEvent, this._onQueueEdgeCandidateUp, { once: true, capture: true }); + } + }, + + onQueueEdgeDragCandidateMove(event) { + const candidate = this._queueEdgeDragCandidate; + if (!candidate) return; + + const dx = event.clientX - candidate.startX; + const dy = event.clientY - candidate.startY; + if ((dx * dx) + (dy * dy) < 16) return; + event.preventDefault(); + event.stopPropagation(); + this.clearQueueEdgeDragCandidate(); + this._suppressQueueEdgeClick = true; + this.beginQueueEdgeDrag(candidate.startX, candidate.previousCursor, event); + this.onSplitDrag(event); + }, + + clearQueueEdgeDragCandidate() { + if (this._onQueueEdgeCandidateMove && this._queueEdgeCandidateMoveEvent) { + document.removeEventListener(this._queueEdgeCandidateMoveEvent, this._onQueueEdgeCandidateMove, true); + } + if (this._onQueueEdgeCandidateUp && this._queueEdgeCandidateUpEvent) { + document.removeEventListener(this._queueEdgeCandidateUpEvent, this._onQueueEdgeCandidateUp, true); + } + if (this._onQueueEdgeCandidateUp && this._queueEdgeCandidateCancelEvent) { + document.removeEventListener(this._queueEdgeCandidateCancelEvent, this._onQueueEdgeCandidateUp, true); + } + this._queueEdgeDragCandidate = null; + this._onQueueEdgeCandidateMove = null; + this._onQueueEdgeCandidateUp = null; + this._queueEdgeCandidateMoveEvent = null; + this._queueEdgeCandidateUpEvent = null; + this._queueEdgeCandidateCancelEvent = null; + }, + + beginQueueEdgeDrag(startX, previousCursor = document.body.style.cursor, sourceEvent = null) { + const dragStartX = Number(startX); + if (!Number.isFinite(dragStartX)) return; + const usePointerEvents = String(sourceEvent?.type || '').startsWith('pointer'); document.body.style.cursor = 'col-resize'; this._suppressQueueEdgeClick = true; this.splitterElement.style.display = ''; @@ -1603,56 +1838,258 @@ export const queueMethods = { this.queueElement.style.display = 'none'; this.queueElement.style.width = '0px'; this.startSplitDrag( - { clientX: startX, preventDefault: () => {} }, + { + clientX: dragStartX, + type: usePointerEvents ? 'pointerdown' : 'mousedown', + pointerId: sourceEvent?.pointerId, + preventDefault: () => {}, + stopPropagation: () => {} + }, { startWidth: 0, edgeOpen: true, previousCursor, startCollapsedPreview: true } ); }, + getQueueSplitBody() { + return document.getElementById('model-resolver-body'); + }, + + observeQueueSplitContainer(body = this.getQueueSplitBody()) { + if (!(body instanceof HTMLElement)) return; + + const rememberWidth = (width) => { + const nextWidth = Number(width); + if (!Number.isFinite(nextWidth) || nextWidth <= 0) return; + this._queueSplitLastContainerWidth = nextWidth; + if (!this._splitDragging && this.isQueuePanelVisible?.() && !this._queuePanelDirty) { + this.stabilizeQueueSplitLayout({ containerWidth: nextWidth }); + } + }; + + this._queueSplitFallbackContainerWidth = Number(window.innerWidth || 720); + if (this._queueSplitResizeObserver) { + try { this._queueSplitResizeObserver.disconnect(); } catch (e) { } + this._queueSplitResizeObserver = null; + } + + if (typeof ResizeObserver === 'function') { + this._queueSplitResizeObserver = new ResizeObserver((entries) => { + rememberWidth(entries?.[0]?.contentRect?.width); + }); + this._queueSplitResizeObserver.observe(body); + } + + requestAnimationFrame(() => { + rememberWidth(body.clientWidth || body.getBoundingClientRect().width); + }); + }, + + getQueueSplitContainerWidth(body = null, { allowMeasure = false } = {}) { + const cached = Number(this._queueSplitLastContainerWidth); + if (Number.isFinite(cached) && cached > 0) return cached; + + if (allowMeasure && body instanceof HTMLElement) { + const measured = Number(body.clientWidth || body.getBoundingClientRect().width); + if (Number.isFinite(measured) && measured > 0) { + this._queueSplitLastContainerWidth = measured; + return measured; + } + } + + const width = Number(this._queueSplitFallbackContainerWidth || window.innerWidth || 0); + const safeWidth = Number.isFinite(width) && width > 0 ? width : 720; + this._queueSplitLastContainerWidth = safeWidth; + return safeWidth; + }, + + getQueueSplitterWidth() { + const cached = Number(this._queueSplitterWidth); + if (Number.isFinite(cached) && cached > 0) return cached; + + this._queueSplitterWidth = 10; + return 10; + }, + + getQueueSplitBounds(containerWidth, { edgeOpen = false } = {}) { + const safeWidth = Math.max(1, Number(containerWidth) || 1); + const splitterWidth = this.getQueueSplitterWidth(); + const available = Math.max(1, safeWidth - splitterWidth); + const min = edgeOpen ? 0 : 56; + const max = Math.max(min, Math.floor(available - 360)); + return { min, max, available }; + }, + + getQueuePaneWidth(element, { allowMeasure = false } = {}) { + if (!(element instanceof HTMLElement)) return null; + + const flexBasis = String(element.style.flexBasis || '').trim(); + if (flexBasis.endsWith('px')) { + const width = parseFloat(flexBasis); + if (Number.isFinite(width) && width >= 0) return width; + } + + const inlineWidth = String(element.style.width || '').trim(); + if (inlineWidth.endsWith('px')) { + const width = parseFloat(inlineWidth); + if (Number.isFinite(width) && width >= 0) return width; + } + + if (!allowMeasure) return null; + + const measured = Number(element.offsetWidth || element.getBoundingClientRect().width || 0); + return Number.isFinite(measured) && measured >= 0 ? measured : null; + }, + + applyQueueSplitWidth(width, { bounds = null, containerWidth = null, skipIfUnchanged = false } = {}) { + if (!(this.queueElement instanceof HTMLElement) || !(this.contentElement instanceof HTMLElement)) return false; + const resolvedBounds = bounds || this.getQueueSplitBounds(containerWidth || this.getQueueSplitContainerWidth(), { + edgeOpen: this._splitEdgeOpen + }); + const nextWidth = Math.round(Math.max(resolvedBounds.min, Math.min(resolvedBounds.max, Number(width) || 0))); + const contentWidth = Math.max(1, Math.round(resolvedBounds.available - nextWidth)); + const queueValue = `${nextWidth}px`; + const contentValue = `${contentWidth}px`; + + if ( + skipIfUnchanged + && this.queueElement.style.flexBasis === queueValue + && this.contentElement.style.flexBasis === contentValue + ) { + return false; + } + + this.contentElement.style.flexBasis = contentValue; + this.contentElement.style.flexGrow = '0'; + this.contentElement.style.flexShrink = '0'; + this.queueElement.style.width = queueValue; + this.queueElement.style.flexBasis = queueValue; + this.queueElement.style.flexGrow = '0'; + this.queueElement.style.flexShrink = '0'; + this._appliedSplitWidth = nextWidth; + return true; + }, + + stabilizeQueueSplitLayout({ containerWidth = null } = {}) { + if (!this.isQueuePanelVisible() || this._splitDragging) return false; + const measuredContainerWidth = Number(containerWidth); + this._queueSplitLastContainerWidth = Number.isFinite(measuredContainerWidth) && measuredContainerWidth > 0 + ? measuredContainerWidth + : this.getQueueSplitContainerWidth(); + const bounds = this.getQueueSplitBounds(this._queueSplitLastContainerWidth); + const queueWidth = this.getQueuePaneWidth(this.queueElement) || 320; + return this.applyQueueSplitWidth(queueWidth, { + bounds, + containerWidth: this._queueSplitLastContainerWidth, + skipIfUnchanged: true + }); + }, + + deferQueueSplitReleaseUi(body, pointerId, previousUserSelect, previousCursor, fallbackCursor) { + if (this._queueSplitReleaseCleanupFrame) { + cancelAnimationFrame(this._queueSplitReleaseCleanupFrame); + this._queueSplitReleaseCleanupFrame = null; + } + + const restore = () => { + this._queueSplitReleaseCleanupFrame = null; + body?.classList?.remove('mr-is-resizing-queue'); + body?.classList?.remove('mr-queue-preview-collapsed'); + if (pointerId !== undefined && this.splitterElement?.releasePointerCapture) { + try { + this.splitterElement.releasePointerCapture(pointerId); + } catch (error) { } + } + const cursor = previousCursor !== null && previousCursor !== undefined + ? previousCursor + : (fallbackCursor || ''); + try { + document.body.style.userSelect = previousUserSelect || ''; + document.body.style.cursor = cursor; + } catch (e) { } + }; + + this._queueSplitReleaseCleanupFrame = requestAnimationFrame(() => { + this._queueSplitReleaseCleanupFrame = requestAnimationFrame(restore); + }); + }, + // Begin split drag for resizable panels startSplitDrag(e, { startWidth = null, edgeOpen = false, previousCursor = null, startCollapsedPreview = false } = {}) { try { + if (e?.button !== undefined && e.button !== 0) return; e?.preventDefault?.(); - if (!this.queueElement) return; - const rect = this.queueElement.getBoundingClientRect(); - const body = document.getElementById('model-resolver-body'); - const bodyRect = body ? body.getBoundingClientRect() : { width: window.innerWidth }; + e?.stopPropagation?.(); + if (!this.queueElement || !this.contentElement) return; + + const body = this.getQueueSplitBody(); + this.cancelQueuePanelOpenUpdate(); + this._queueSplitLastContainerWidth = this.getQueueSplitContainerWidth(); + const bounds = this.getQueueSplitBounds(this._queueSplitLastContainerWidth, { edgeOpen }); + const currentWidth = this.getQueuePaneWidth(this.queueElement) || 320; + const initialWidth = Math.round(Math.max(bounds.min, Math.min(bounds.max, Number.isFinite(startWidth) ? startWidth : currentWidth))); + this._splitDragging = true; body?.classList.add('mr-is-resizing-queue'); + if (!startCollapsedPreview) { + this.applyQueueSplitWidth(initialWidth, { + bounds, + containerWidth: this._queueSplitLastContainerWidth, + skipIfUnchanged: true + }); + } this._splitStart = { x: e.clientX, - startWidth: Number.isFinite(startWidth) ? startWidth : rect.width, - containerWidth: bodyRect.width + startWidth: initialWidth, + containerWidth: this._queueSplitLastContainerWidth, + bounds }; - this._lastSplitDragApply = 0; + this._pendingSplitWidth = initialWidth; + this._appliedSplitWidth = initialWidth; this._splitPreviewCollapsed = !!startCollapsedPreview; this._splitEdgeOpen = !!edgeOpen; this._splitEdgeOpenedPastThreshold = false; this._splitPreviousCursor = previousCursor; this._prevUserSelect = document.body.style.userSelect; + this._prevQueueSplitCursor = document.body.style.cursor; document.body.style.userSelect = 'none'; + document.body.style.cursor = 'col-resize'; + if (e?.pointerId !== undefined && this.splitterElement?.setPointerCapture) { + try { + this.splitterElement.setPointerCapture(e.pointerId); + } catch (error) { } + } this._onSplitMove = (ev) => this.onSplitDrag(ev); - this._onSplitUp = () => this.endSplitDrag(); - document.addEventListener('mousemove', this._onSplitMove); - document.addEventListener('mouseup', this._onSplitUp, { once: true }); + this._onSplitUp = (ev) => this.endSplitDrag(ev); + this._splitMoveEvent = e?.type === 'pointerdown' ? 'pointermove' : 'mousemove'; + this._splitUpEvent = e?.type === 'pointerdown' ? 'pointerup' : 'mouseup'; + this._splitCancelEvent = e?.type === 'pointerdown' ? 'pointercancel' : null; + document.addEventListener(this._splitMoveEvent, this._onSplitMove, true); + document.addEventListener(this._splitUpEvent, this._onSplitUp, { once: true, capture: true }); + if (this._splitCancelEvent) { + document.addEventListener(this._splitCancelEvent, this._onSplitUp, { once: true, capture: true }); + } } catch (err) { /* ignore */ } }, onSplitDrag(e) { if (!this._splitDragging || !this._splitStart || !this.queueElement) return; + e?.preventDefault?.(); + e?.stopPropagation?.(); const dx = e.clientX - this._splitStart.x; let newW = this._splitStart.startWidth - dx; - const minW = this._splitEdgeOpen ? 0 : 56; - const maxW = Math.max(minW, Math.floor(this._splitStart.containerWidth - 360)); - if (newW < minW) newW = minW; - if (newW > maxW) newW = maxW; + const bounds = this._splitStart.bounds || this.getQueueSplitBounds(this._splitStart.containerWidth, { edgeOpen: this._splitEdgeOpen }); + if (newW < bounds.min) newW = bounds.min; + if (newW > bounds.max) newW = bounds.max; this._pendingSplitWidth = Math.round(newW); if (this._splitEdgeOpen && this._pendingSplitWidth > 140) { this._splitEdgeOpenedPastThreshold = true; if (this.queueCollapsed) { this.queueCollapsed = false; - try { localStorage.setItem('model_resolver_queue_collapsed', '0'); } catch (error) { } + this.persistQueueCollapsedState(false, 500); this.updateQueueToggleIcon(); - this.updateQueuePanel(); + } + if (this._queuePanelDirty) { + this.queueElement?.classList?.add('mr-queue-opening'); + this.scheduleQueuePanelOpenUpdate(); } } const previewCollapsed = this._splitEdgeOpen @@ -1664,50 +2101,69 @@ export const queueMethods = { body?.classList.toggle('mr-queue-preview-collapsed', previewCollapsed); this.queueElement.style.display = previewCollapsed ? 'none' : ''; } - const now = performance.now(); - if (this._lastSplitDragApply && now - this._lastSplitDragApply < 33) return; - this._lastSplitDragApply = now; - if (this._splitDragFrame) return; - this._splitDragFrame = requestAnimationFrame(() => { - this._splitDragFrame = null; - if (!this._splitDragging || !this.queueElement || !this._pendingSplitWidth || this._splitPreviewCollapsed) return; - this.queueElement.style.width = `${this._pendingSplitWidth}px`; + if (this._splitPreviewCollapsed) return; + this.applyQueueSplitWidth(this._pendingSplitWidth, { + bounds, + containerWidth: this._splitStart.containerWidth, + skipIfUnchanged: true }); }, - endSplitDrag() { + endSplitDrag(e = null) { if (!this._splitDragging) return; + e?.preventDefault?.(); + e?.stopPropagation?.(); + const releasePointerId = e?.pointerId; + const previousUserSelect = this._prevUserSelect; + const previousCursor = this._splitPreviousCursor; + const fallbackCursor = this._prevQueueSplitCursor; this._splitDragging = false; if (this._splitDragFrame) { cancelAnimationFrame(this._splitDragFrame); this._splitDragFrame = null; } if (this.queueElement && this._pendingSplitWidth && !this._splitPreviewCollapsed) { - this.queueElement.style.width = `${this._pendingSplitWidth}px`; + this.applyQueueSplitWidth(this._pendingSplitWidth, { + bounds: this._splitStart?.bounds, + containerWidth: this._splitStart?.containerWidth, + skipIfUnchanged: true + }); + } + const body = this.getQueueSplitBody(); + if (this._splitMoveEvent) { + document.removeEventListener(this._splitMoveEvent, this._onSplitMove, true); + } + if (this._splitUpEvent) { + document.removeEventListener(this._splitUpEvent, this._onSplitUp, true); + } + if (this._splitCancelEvent) { + document.removeEventListener(this._splitCancelEvent, this._onSplitUp, true); } - const body = document.getElementById('model-resolver-body'); - body?.classList.remove('mr-is-resizing-queue'); - body?.classList.remove('mr-queue-preview-collapsed'); - document.removeEventListener('mousemove', this._onSplitMove); if (this._splitEdgeOpen && !this._pendingSplitWidth) { try { const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320)); - this.queueElement.style.width = `${restoreWidth}px`; + this.applyQueueSplitWidth(restoreWidth, { + bounds: this._splitStart?.bounds, + containerWidth: this._splitStart?.containerWidth + }); this.queueElement.style.display = ''; if (this.splitterElement) this.splitterElement.style.display = ''; - localStorage.setItem('model_resolver_split_w', String(restoreWidth)); + this.persistQueueSplitWidth(restoreWidth, 500); } catch (e) { } this.queueCollapsed = false; - try { localStorage.setItem('model_resolver_queue_collapsed', '0'); } catch (e) { } + this.persistQueueCollapsedState(false, 500); + this.queueElement?.classList?.add('mr-queue-opening'); + document.getElementById('model-resolver-body')?.classList.add('mr-queue-opening-layout'); this.updateQueueVisibility(); - this.updateQueuePanel(); + this.scheduleQueuePanelOpenUpdate(); this._splitEdgeOpen = false; this._pendingSplitWidth = null; - this._lastSplitDragApply = 0; + this._appliedSplitWidth = null; this._splitPreviewCollapsed = false; - try { document.body.style.userSelect = this._prevUserSelect || ''; } catch (e) { } - try { document.body.style.cursor = this._splitPreviousCursor || ''; } catch (e) { } + this.deferQueueSplitReleaseUi(body, releasePointerId, previousUserSelect, previousCursor, fallbackCursor); this._splitPreviousCursor = null; + this._prevQueueSplitCursor = null; + this._prevUserSelect = null; this._splitEdgeOpenedPastThreshold = false; setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0); return; @@ -1716,37 +2172,46 @@ export const queueMethods = { if (shouldCollapse) { try { const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320)); - this.queueElement.style.width = `${restoreWidth}px`; + this.applyQueueSplitWidth(restoreWidth, { + bounds: this._splitStart?.bounds, + containerWidth: this._splitStart?.containerWidth + }); this.queueElement.style.display = ''; - localStorage.setItem('model_resolver_split_w', String(restoreWidth)); + this.persistQueueSplitWidth(restoreWidth); } catch (e) { } this._pendingSplitWidth = null; - this._lastSplitDragApply = 0; + this._appliedSplitWidth = null; this._splitPreviewCollapsed = false; this._splitEdgeOpen = false; this._splitEdgeOpenedPastThreshold = false; - try { document.body.style.userSelect = this._prevUserSelect || ''; } catch (e) { } - try { document.body.style.cursor = this._splitPreviousCursor || ''; } catch (e) { } + this.deferQueueSplitReleaseUi(body, releasePointerId, previousUserSelect, previousCursor, fallbackCursor); this._splitPreviousCursor = null; + this._prevQueueSplitCursor = null; + this._prevUserSelect = null; this.setQueueCollapsed(true); setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0); return; } if (this.queueElement) this.queueElement.style.display = ''; try { - const rect = this.queueElement.getBoundingClientRect(); - localStorage.setItem('model_resolver_split_w', String(Math.round(rect.width))); + const width = this._pendingSplitWidth + || this._appliedSplitWidth + || Number.parseInt(String(this.queueElement.style.width || ''), 10) + || this.getQueuePaneWidth(this.queueElement); + this.persistQueueSplitWidth(width); } catch (e) { } this._pendingSplitWidth = null; - this._lastSplitDragApply = 0; + this._appliedSplitWidth = null; this._splitPreviewCollapsed = false; this._splitEdgeOpen = false; this._splitEdgeOpenedPastThreshold = false; - try { document.body.style.userSelect = this._prevUserSelect || ''; } catch (e) { } - if (this._splitPreviousCursor !== null) { - try { document.body.style.cursor = this._splitPreviousCursor || ''; } catch (e) { } - this._splitPreviousCursor = null; - } + this.deferQueueSplitReleaseUi(body, releasePointerId, previousUserSelect, previousCursor, fallbackCursor); + this._splitPreviousCursor = null; + this._prevQueueSplitCursor = null; + this._prevUserSelect = null; + this._splitMoveEvent = null; + this._splitUpEvent = null; + this._splitCancelEvent = null; setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0); }, diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 66db38e..73edb46 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -531,12 +531,13 @@ export const missingBrowserMethods = { ? `--mr-missing-detail-track:${savedDetailWidth}px;` : ''; const listStyle = `--mr-missing-model-col:${listLayout.modelPx}px;--mr-missing-type-col:${listLayout.typePx}px;`; + const browserStyle = `${listStyle}${splitStyle}`; const titleText = this.showResolvedModels && resolvedCount > 0 ? `${totalMissing} shown (${missingCount} missing / ${resolvedCount} resolved)` : `${totalMissing} missing model${totalMissing === 1 ? '' : 's'}`; let html = ` -
+
@@ -628,7 +629,7 @@ export const missingBrowserMethods = {
-
+
${detailMissing ? this.renderMissingModel(detailMissing, detailIndex) : this.renderStatusMessage('Select a missing model to inspect details.', 'info')}
@@ -760,12 +761,13 @@ export const missingBrowserMethods = { const browser = container.querySelector('.mr-missing-browser'); const splitter = browser?.querySelector('.mr-missing-browser-splitter'); if (!(browser instanceof HTMLElement) || !(splitter instanceof HTMLElement)) return; - - this.restoreMissingBrowserSplitWidth(browser); + const listPane = browser.querySelector('.mr-missing-list-pane'); + const detailPane = browser.querySelector('.mr-missing-detail-pane'); + if (!(listPane instanceof HTMLElement) || !(detailPane instanceof HTMLElement)) return; const startSplitDrag = (event) => { if (event.button !== undefined && event.button !== 0) return; - this.startMissingBrowserSplitDrag(event, browser, splitter); + this.startMissingBrowserSplitDrag(event, browser, splitter, { listPane, detailPane }); }; if (typeof PointerEvent === 'function') { splitter.addEventListener('pointerdown', startSplitDrag); @@ -797,25 +799,37 @@ export const missingBrowserMethods = { if (typeof ResizeObserver === 'function') { this._missingBrowserResizeObserver = new ResizeObserver((entries) => { const observedWidth = Number(entries?.[0]?.contentRect?.width); - if (Number.isFinite(observedWidth) && observedWidth > 0) { - this._missingBrowserObservedWidth = observedWidth; - this._missingBrowserLastBrowserWidth = observedWidth; - if (!this._missingBrowserLastDetailWidth) { - this._missingBrowserLastDetailWidth = this.getDefaultMissingBrowserDetailWidth(observedWidth); - } - } + this.rememberMissingBrowserWidth(observedWidth); 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: true }); + this.scheduleMissingBrowserSplitRestore(browser, observedWidth, { useDefault: false }); } }); this._missingBrowserResizeObserver.observe(browser); + } else { + this.cancelMissingBrowserPrewarmFrame(); + this._missingBrowserPrewarmFrame = requestAnimationFrame(() => { + this._missingBrowserPrewarmFrame = null; + if (!browser.isConnected) return; + if (this._missingBrowserSplitDragging) return; + const measuredWidth = Number(browser.clientWidth || browser.getBoundingClientRect().width); + this.rememberMissingBrowserWidth(measuredWidth); + if (!this._missingBrowserSplitDragging) { + this.scheduleMissingBrowserSplitRestore(browser, measuredWidth, { useDefault: false }); + } + }); } }, + cancelMissingBrowserPrewarmFrame() { + if (!this._missingBrowserPrewarmFrame) return; + cancelAnimationFrame(this._missingBrowserPrewarmFrame); + this._missingBrowserPrewarmFrame = null; + }, + getStoredMissingBrowserSplitWidth() { if (this._missingBrowserStoredSplitWidthLoaded) { return this._missingBrowserStoredSplitWidth || null; @@ -832,23 +846,34 @@ export const missingBrowserMethods = { return savedWidth; }, - getFastMissingBrowserWidthEstimate(browser = null) { + rememberMissingBrowserWidth(width) { + const observedWidth = Number(width); + if (!Number.isFinite(observedWidth) || observedWidth <= 0) return null; + this._missingBrowserObservedWidth = observedWidth; + this._missingBrowserLastBrowserWidth = observedWidth; + if (!this._missingBrowserLastDetailWidth) { + this._missingBrowserLastDetailWidth = this.getDefaultMissingBrowserDetailWidth(observedWidth); + } + return observedWidth; + }, + + getFastMissingBrowserWidthEstimate(browser = null, { allowMeasure = false } = {}) { const observedWidth = Number(this._missingBrowserObservedWidth); if (Number.isFinite(observedWidth) && observedWidth > 0) return observedWidth; const lastWidth = Number(this._missingBrowserLastBrowserWidth); if (Number.isFinite(lastWidth) && lastWidth > 0) return lastWidth; - if (browser instanceof HTMLElement) { + if (allowMeasure && browser instanceof HTMLElement) { const clientWidth = Number(browser.clientWidth || browser.offsetWidth); if (Number.isFinite(clientWidth) && clientWidth > 0) { - this._missingBrowserObservedWidth = clientWidth; - this._missingBrowserLastBrowserWidth = clientWidth; + this.rememberMissingBrowserWidth(clientWidth); return clientWidth; } } - return 720; + const fallbackWidth = Number(this._queueSplitLastContainerWidth || window.innerWidth || 720); + return Number.isFinite(fallbackWidth) && fallbackWidth > 0 ? fallbackWidth : 720; }, getMissingBrowserSplitBoundsForWidth(width) { @@ -910,23 +935,26 @@ export const missingBrowserMethods = { }); }, - startMissingBrowserSplitDrag(event, browser, splitter = null) { + startMissingBrowserSplitDrag(event, browser, splitter = null, panes = {}) { if (!(browser instanceof HTMLElement)) return; event.preventDefault(); event.stopPropagation?.(); - if (this._missingBrowserReleaseCleanupFrame) { - cancelAnimationFrame(this._missingBrowserReleaseCleanupFrame); - this._missingBrowserReleaseCleanupFrame = null; - } + this.cancelMissingBrowserPrewarmFrame(); + this.flushMissingBrowserSplitReleaseUi(); - const detailPane = browser.querySelector('.mr-missing-detail-pane'); - const listPane = browser.querySelector('.mr-missing-list-pane'); + const detailPane = panes.detailPane instanceof HTMLElement + ? panes.detailPane + : browser.querySelector('.mr-missing-detail-pane'); + const listPane = panes.listPane instanceof HTMLElement + ? panes.listPane + : browser.querySelector('.mr-missing-list-pane'); if (!(detailPane instanceof HTMLElement) || !(listPane instanceof HTMLElement)) return; const browserWidth = this.getFastMissingBrowserWidthEstimate(browser); const bounds = this.getMissingBrowserSplitBoundsForWidth(browserWidth); const savedWidth = this.getStoredMissingBrowserSplitWidth(); const detailWidth = Math.round(Math.max(bounds.min, Math.min(bounds.max, this.getMissingBrowserDetailTrackWidth(detailPane) + || this._missingBrowserLastDetailWidth || savedWidth || this.getDefaultMissingBrowserDetailWidth(browserWidth)))); @@ -938,12 +966,12 @@ export const missingBrowserMethods = { this._pendingMissingBrowserRestoreWidth = null; this._pendingMissingBrowserRestoreUseDefault = false; this.cancelMissingBrowserSplitWidthPersist(); - this.applyMissingBrowserDetailWidth(detailPane, detailWidth, { - browserWidth, - availableWidth: bounds.available, - splitBounds: bounds, - skipIfUnchanged: true - }); + this._missingBrowserSplitBrowser = browser; + this._missingBrowserSplitListPane = listPane; + this._missingBrowserSplitDetailPane = detailPane; + this._missingBrowserSplitSplitter = splitter instanceof HTMLElement + ? splitter + : browser.querySelector('.mr-missing-browser-splitter'); this._missingBrowserSplitDragging = true; this._missingBrowserSplitStart = { x: event.clientX, @@ -953,22 +981,7 @@ export const missingBrowserMethods = { availableWidth: bounds.available }; this._appliedMissingBrowserSplitWidth = Math.round(this._missingBrowserSplitStart.width); - this._missingBrowserSplitBrowser = browser; - this._missingBrowserSplitListPane = listPane; - this._missingBrowserSplitDetailPane = detailPane; - this._missingBrowserSplitSplitter = splitter instanceof HTMLElement - ? splitter - : browser.querySelector('.mr-missing-browser-splitter'); - this._missingBrowserPrevUserSelect = document.body.style.userSelect; - this._missingBrowserPrevCursor = document.body.style.cursor; - document.body.style.userSelect = 'none'; - document.body.style.cursor = 'col-resize'; - this._missingBrowserSplitSplitter?.classList?.add('is-resizing'); - if (event.pointerId !== undefined && this._missingBrowserSplitSplitter?.setPointerCapture) { - try { - this._missingBrowserSplitSplitter.setPointerCapture(event.pointerId); - } catch (e) {} - } + this._missingBrowserSplitUiActive = false; this._onMissingBrowserSplitMove = (moveEvent) => this.onMissingBrowserSplitDrag(moveEvent); this._onMissingBrowserSplitUp = (upEvent) => this.endMissingBrowserSplitDrag(upEvent); @@ -982,6 +995,23 @@ export const missingBrowserMethods = { } }, + activateMissingBrowserSplitUi() { + if (this._missingBrowserSplitUiActive) return; + this._missingBrowserSplitUiActive = true; + if (this._missingBrowserResizeClassFrame) { + cancelAnimationFrame(this._missingBrowserResizeClassFrame); + this._missingBrowserResizeClassFrame = null; + } + this._missingBrowserResizeClassFrame = requestAnimationFrame(() => { + this._missingBrowserResizeClassFrame = requestAnimationFrame(() => { + this._missingBrowserResizeClassFrame = null; + if (this._missingBrowserSplitDragging) { + this._missingBrowserSplitSplitter?.classList?.add('is-resizing'); + } + }); + }); + }, + onMissingBrowserSplitDrag(event) { if (!this._missingBrowserSplitDragging || !this._missingBrowserSplitStart || !this._missingBrowserSplitBrowser) return; @@ -992,6 +1022,7 @@ export const missingBrowserMethods = { this._pendingMissingBrowserSplitWidth = Math.round(Math.max(bounds.min, Math.min(bounds.max, nextWidth))); if (this._pendingMissingBrowserSplitWidth === this._appliedMissingBrowserSplitWidth) return; if (!this._missingBrowserSplitDetailPane) return; + this.activateMissingBrowserSplitUi(); this.applyMissingBrowserDetailWidth(this._missingBrowserSplitDetailPane, this._pendingMissingBrowserSplitWidth, { browserWidth: this._missingBrowserSplitStart.browserWidth, availableWidth: this._missingBrowserSplitStart.availableWidth, @@ -1011,9 +1042,7 @@ export const missingBrowserMethods = { : Date.now(); this._missingBrowserSplitSettlingUntil = settlingNow + 350; const releaseSplitter = this._missingBrowserSplitSplitter; - const releasePointerId = event?.pointerId; - const previousUserSelect = this._missingBrowserPrevUserSelect; - const previousCursor = this._missingBrowserPrevCursor; + const wasUiActive = !!this._missingBrowserSplitUiActive; const finalWidth = this._pendingMissingBrowserSplitWidth || this._appliedMissingBrowserSplitWidth; this._missingBrowserSplitDragging = false; if (this._missingBrowserSplitMoveEvent) { @@ -1042,12 +1071,13 @@ export const missingBrowserMethods = { if (finalWidth) { this.persistMissingBrowserSplitWidth(finalWidth, { delay: 600 }); } - this.deferMissingBrowserSplitReleaseUi(releaseSplitter, releasePointerId, previousUserSelect, previousCursor); + this.deferMissingBrowserSplitReleaseUi(releaseSplitter, wasUiActive); this._missingBrowserSplitBrowser = null; this._missingBrowserSplitListPane = null; this._missingBrowserSplitDetailPane = null; this._missingBrowserSplitSplitter = null; this._missingBrowserSplitStart = null; + this._missingBrowserSplitUiActive = false; this._pendingMissingBrowserSplitWidth = null; this._appliedMissingBrowserSplitWidth = null; this._missingBrowserSplitMoveEvent = null; @@ -1055,22 +1085,40 @@ export const missingBrowserMethods = { this._missingBrowserSplitCancelEvent = null; }, - deferMissingBrowserSplitReleaseUi(splitter, pointerId, previousUserSelect, previousCursor) { + applyMissingBrowserSplitReleaseCleanup(cleanup) { + if (!cleanup) return; + cleanup.splitter?.classList?.remove('is-resizing'); + }, + + flushMissingBrowserSplitReleaseUi() { + if (this._missingBrowserReleaseCleanupFrame) { + cancelAnimationFrame(this._missingBrowserReleaseCleanupFrame); + this._missingBrowserReleaseCleanupFrame = null; + } + const cleanup = this._missingBrowserPendingReleaseCleanup; + this._missingBrowserPendingReleaseCleanup = null; + this.applyMissingBrowserSplitReleaseCleanup(cleanup); + }, + + deferMissingBrowserSplitReleaseUi(splitter, shouldCleanup = true) { + if (this._missingBrowserResizeClassFrame) { + cancelAnimationFrame(this._missingBrowserResizeClassFrame); + this._missingBrowserResizeClassFrame = null; + } + this._missingBrowserPendingReleaseCleanup = { + splitter, + shouldCleanup + }; const release = () => { this._missingBrowserReleaseCleanupFrame = null; - if (pointerId !== undefined && splitter?.releasePointerCapture) { - try { - splitter.releasePointerCapture(pointerId); - } catch (e) {} - } - splitter?.classList?.remove('is-resizing'); - try { - document.body.style.userSelect = previousUserSelect || ''; - document.body.style.cursor = previousCursor || ''; - } catch (e) {} + const cleanup = this._missingBrowserPendingReleaseCleanup; + this._missingBrowserPendingReleaseCleanup = null; + this.applyMissingBrowserSplitReleaseCleanup(cleanup); }; if (typeof requestAnimationFrame === 'function') { - this._missingBrowserReleaseCleanupFrame = requestAnimationFrame(release); + this._missingBrowserReleaseCleanupFrame = requestAnimationFrame(() => { + this._missingBrowserReleaseCleanupFrame = requestAnimationFrame(release); + }); } else { release(); } @@ -1120,14 +1168,13 @@ export const missingBrowserMethods = { } if (listPane instanceof HTMLElement) { - listPane.style.flexBasis = listValue; - listPane.style.flexGrow = '0'; - listPane.style.flexShrink = '0'; + if (listPane.style.flexBasis !== listValue) listPane.style.flexBasis = listValue; + if (listPane.style.flexGrow !== '0') listPane.style.flexGrow = '0'; + if (listPane.style.flexShrink !== '0') listPane.style.flexShrink = '0'; } - detailPane.style.setProperty('--mr-missing-detail-track', detailValue); - detailPane.style.flexBasis = detailValue; - detailPane.style.flexGrow = '0'; - detailPane.style.flexShrink = '0'; + if (detailPane.style.flexBasis !== detailValue) detailPane.style.flexBasis = detailValue; + if (detailPane.style.flexGrow !== '0') detailPane.style.flexGrow = '0'; + if (detailPane.style.flexShrink !== '0') detailPane.style.flexShrink = '0'; this._missingBrowserLastDetailWidth = nextWidth; return true; }, @@ -1215,14 +1262,13 @@ export const missingBrowserMethods = { } }, - getMissingBrowserSplitBounds(browser, browserWidth = null) { + getMissingBrowserSplitBounds(browser, browserWidth = null, { allowMeasure = false } = {}) { const measuredWidth = Number(browserWidth); const width = Number.isFinite(measuredWidth) && measuredWidth > 0 ? measuredWidth - : browser.getBoundingClientRect().width; + : this.getFastMissingBrowserWidthEstimate(browser, { allowMeasure }); if (Number.isFinite(width) && width > 0) { - this._missingBrowserObservedWidth = width; - this._missingBrowserLastBrowserWidth = width; + this.rememberMissingBrowserWidth(width); } return this.getMissingBrowserSplitBoundsForWidth(width); },