musrview: size the RUN INFO panel to its content for short run lists

The panel used a fixed 28% of the window height regardless of content,
which left mostly empty space for the common single-run/single-histo case.
Now, for plots with 1-5 run/legend entries, the panel is sized to what its
own content actually needs; 6+ entries keep the previous behavior (the
size the user last dragged to, or the 28%-of-viewport default).

Measuring the true content height needs care: Element.scrollHeight only
reports the *overflowing* content height, so while the content still fits
inside the page's initial placeholder box it just echoes that box's
(unrelated) size back. measureInfoContentHeight() briefly frees the
element from its flex/height constraint to read its real intrinsic size,
and measureInfoPanelHeight() adds the "RUN INFO" header's own height on
top, since the sized grid row covers header and content together.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
suter_aandClaude Sonnet 5 committed 2026-09-17 12:54:20 +02:00
1 parent 1366e46c34
commit e7e16dd793
1 file changed
+78 -3
+78 -3
View File
@@ -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('<div class="spacer"></div>');
const runs = state.runs || [];
let entryCount;
if (runs.length) {
for (const r of runs)
parts.push(`<div class="run">${markerSvg(r.shape, r.color, r.filled)}` +
`<span>${rootLatexToHtml(r.label)}</span></div>`);
entryCount = runs.length;
} else {
for (const line of (state.info || []))
const lines = state.info || [];
for (const line of lines)
parts.push(`<div class="plain">${rootLatexToHtml(line)}</div>`);
entryCount = lines.length;
}
el.innerHTML = parts.join('');
return entryCount;
}
function applyState(state) {
@@ -734,7 +808,8 @@ function applyState(state) {
$('params').textContent = (state.parameters || []).join('\n');
$('theory').textContent = (state.theory || []).join('\n');
renderRunInfo(state);
const infoEntryCount = renderRunInfo(state);
sizeInfoPanelToContent(infoEntryCount);
renderToolbar();