diff --git a/src/musrview_web/musrview.html b/src/musrview_web/musrview.html index 9d454ab4..49a0932c 100644 --- a/src/musrview_web/musrview.html +++ b/src/musrview_web/musrview.html @@ -439,6 +439,13 @@ const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi); let sideWidth = 340; // px, width of the params/theory column let infoHeight = null; // px, height of the run-info panel; defaulted lazily // from the viewport once it is known +let infoHeightFixed = false; // true once infoHeight has its initial value for this + // page load (content-based, restored or dragged) so the + // run-info sizing doesn't run more than once +let savedInfoHeight = null; // px, height the user dragged to in an earlier session, + // if any; only honored for the non-compact (>=6 entries) + // case, since a short run list is always sized to its + // content regardless of what a longer one was resized to let paramsFrac = 0.5; // fraction of #side's height given to 'Fit parameters' function updateLayout() { @@ -486,11 +493,72 @@ function restoreLayout() { try { const saved = JSON.parse(localStorage.getItem('musrview.layout') || '{}'); if (typeof saved.sideWidth === 'number') sideWidth = saved.sideWidth; - if (typeof saved.infoHeight === 'number') infoHeight = saved.infoHeight; + if (typeof saved.infoHeight === 'number') savedInfoHeight = saved.infoHeight; if (typeof saved.paramsFrac === 'number') paramsFrac = saved.paramsFrac; } catch (e) { /* ignore */ } } +/** #info's true content height, independent of whatever height it currently + * happens to be given. Element.scrollHeight only reports the *overflowing* + * content height; when the content fits inside the box without a scrollbar + * (the common case right after the page's placeholder height is applied), + * it just reports back the box's own (unrelated) current height instead. + * Briefly switching the flex item to size itself ('flex: 0 0 auto; height: + * auto') gets its real intrinsic height, then the inline overrides are + * removed again -- this never paints, so there is nothing to see flicker. */ +function measureInfoContentHeight() { + const info = $('info'); + const prevFlex = info.style.flex; + const prevHeight = info.style.height; + info.style.flex = '0 0 auto'; + info.style.height = 'auto'; + const h = info.scrollHeight; + info.style.flex = prevFlex; + info.style.height = prevHeight; + return h; +} + +/** The row track in #app's grid covers the whole run-info panel, header + * included, so the header's own (fixed, content-driven) height has to be + * added to measureInfoContentHeight() -- omitting it clips the last line of + * content behind #info's scrollbar. */ +function measureInfoPanelHeight() { + const header = document.querySelector('#infopanel > h2'); + const headerHeight = header ? header.getBoundingClientRect().height : 0; + return headerHeight + measureInfoContentHeight(); +} + +/** Picks the run-info panel's initial height once its content (the number of + * run/legend entries) is actually known: 1-5 entries get just the room their + * content needs, instead of the same fixed share of the window that a long + * run list would need, which otherwise left mostly empty space for + * single-run/single-histo fits. That content-based size always wins for a + * short list, even if a previous, longer-list session had the pad dragged + * bigger -- a stale large size is exactly the "no improvement" bug this + * guards against. 6+ entries keep today's behavior: the size the user last + * dragged to, or the viewport-based default if there is none. + * + * Runs once per page (a live drag during this same load takes precedence + * and short-circuits it), so later mode changes (e.g. switching to the + * single-curve average view) don't reflow it. */ +function sizeInfoPanelToContent(entryCount) { + if (infoHeightFixed) return; + infoHeightFixed = true; + + // measureInfoContentHeight() would read 0 while the panel is hidden + // (display:none), so fall back to the non-compact path rather than + // collapsing it + const compact = entryCount > 0 && entryCount < 6 && panels.info.visible; + if (compact) + infoHeight = measureInfoPanelHeight() + 4; + else if (savedInfoHeight !== null) + infoHeight = savedInfoHeight; + else + infoHeight = Math.round(window.innerHeight * 0.28); + + updateLayout(); +} + /** Wires up a splitter bar: while it is dragged, onMove(pointerEvent) is * called to update the layout; on release the layout is persisted. */ function makeSplitter(handleEl, onMove) { @@ -519,6 +587,7 @@ makeSplitter($('vsplit'), ev => { makeSplitter($('hsplit'), ev => { const statusH = $('statusbar').getBoundingClientRect().height; infoHeight = window.innerHeight - statusH - ev.clientY - 3; + infoHeightFixed = true; updateLayout(); }); @@ -714,16 +783,21 @@ function renderRunInfo(state) { parts.push('
'); const runs = state.runs || []; + let entryCount; if (runs.length) { for (const r of runs) parts.push(`