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:
1 parent
1366e46c34
commit
e7e16dd793
1 file changed
+78
-3
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user