musrview: make the Fit parameters, Theory and Run info panels resizable
Add drag handles between the plot and the side column, between the Fit parameters/Theory panels, and above the Run info panel, so their width/height can be adjusted interactively. Sizes are clamped to the viewport and persisted in localStorage, reusing the pattern already used for panel visibility. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjACu9ZD3FGfp1Db32DoiQ
This commit is contained in:
1 parent
174d58ea6a
commit
16febf62e2
1 file changed
+146
-29
+146
-29
@@ -30,24 +30,40 @@
|
||||
}
|
||||
|
||||
/* ---- overall layout ------------------------------------------------ */
|
||||
/* row/column sizes are the defaults for the very first paint; from then
|
||||
on updateLayout() (below, in the script) drives them via inline
|
||||
styles, so that the side column width and the run-info height can be
|
||||
dragged by the user and the title/params/theory/info rows/columns
|
||||
collapse to 0 when their panel is hidden. */
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr auto auto;
|
||||
grid-template-columns: 1fr 340px;
|
||||
grid-template-rows: auto auto 1fr 6px auto auto;
|
||||
grid-template-columns: 1fr 6px 340px;
|
||||
grid-template-areas:
|
||||
"toolbar toolbar"
|
||||
"title title"
|
||||
"plot side"
|
||||
"info info"
|
||||
"status status";
|
||||
"toolbar toolbar toolbar"
|
||||
"title title title"
|
||||
"plot vsplit side"
|
||||
"hsplit hsplit hsplit"
|
||||
"info info info"
|
||||
"status status status";
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
/* the side column disappears completely when both side panels are off */
|
||||
#app.no-side { grid-template-columns: 1fr 0px; }
|
||||
#app.no-title { grid-template-rows: auto 0px 1fr auto auto; }
|
||||
#app.no-info { grid-template-rows: auto auto 1fr 0px auto; }
|
||||
#app.no-title.no-info { grid-template-rows: auto 0px 1fr 0px auto; }
|
||||
|
||||
/* ---- drag handles ---------------------------------------------------
|
||||
thin bars between the plot and the side column (width) and between
|
||||
the plot row and the run-info panel (height); the params/theory
|
||||
split handle lives inside #side below. */
|
||||
#vsplit, #hsplit, #side-split {
|
||||
background: var(--border);
|
||||
transition: background .15s;
|
||||
touch-action: none;
|
||||
}
|
||||
#vsplit { grid-area: vsplit; cursor: col-resize; }
|
||||
#hsplit { grid-area: hsplit; cursor: row-resize; }
|
||||
#vsplit:hover, #vsplit.dragging,
|
||||
#hsplit:hover, #hsplit.dragging,
|
||||
#side-split:hover, #side-split.dragging { background: var(--accent); }
|
||||
|
||||
/* ---- toolbar ------------------------------------------------------- */
|
||||
#toolbar {
|
||||
@@ -112,10 +128,11 @@
|
||||
|
||||
#side {
|
||||
grid-area: side;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 6px 1fr;
|
||||
border-left: 1px solid var(--border);
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -123,10 +140,9 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1 1 50%;
|
||||
border-bottom: 1px solid var(--border);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.panel:last-child { border-bottom: none; }
|
||||
.panel > h2 {
|
||||
margin: 0;
|
||||
padding: 4px 8px;
|
||||
@@ -179,11 +195,11 @@
|
||||
grid-area: info;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 28vh;
|
||||
min-height: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
#infopanel > #info { max-height: 22vh; }
|
||||
#infopanel > #info { flex: 1 1 auto; min-height: 0; }
|
||||
|
||||
#statusbar {
|
||||
grid-area: status;
|
||||
@@ -279,17 +295,22 @@
|
||||
|
||||
<div id="plot"></div>
|
||||
|
||||
<div id="vsplit" title="drag to resize the side panels"></div>
|
||||
|
||||
<div id="side">
|
||||
<div class="panel" id="panel-params">
|
||||
<h2>Fit parameters</h2>
|
||||
<pre id="params"></pre>
|
||||
</div>
|
||||
<div id="side-split" title="drag to resize"></div>
|
||||
<div class="panel" id="panel-theory">
|
||||
<h2>Theory</h2>
|
||||
<pre id="theory"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hsplit" title="drag to resize the run info panel"></div>
|
||||
|
||||
<div id="infopanel">
|
||||
<h2 style="margin:0;padding:4px 8px;font-size:11px;font-weight:700;letter-spacing:.06em;
|
||||
text-transform:uppercase;color:var(--muted);background:var(--panel-bg);
|
||||
@@ -365,29 +386,25 @@ function isTyping() {
|
||||
// shown and hidden individually)
|
||||
//--------------------------------------------------------------------------
|
||||
const panels = {
|
||||
title: { btn: 'pan-title', el: 'titlebar', cls: 'no-title', visible: true },
|
||||
params: { btn: 'pan-params', el: 'panel-params', cls: null, visible: true },
|
||||
theory: { btn: 'pan-theory', el: 'panel-theory', cls: null, visible: true },
|
||||
info: { btn: 'pan-info', el: 'infopanel', cls: 'no-info', visible: true }
|
||||
title: { btn: 'pan-title', el: 'titlebar', visible: true },
|
||||
params: { btn: 'pan-params', el: 'panel-params', visible: true },
|
||||
theory: { btn: 'pan-theory', el: 'panel-theory', visible: true },
|
||||
info: { btn: 'pan-info', el: 'infopanel', visible: true }
|
||||
};
|
||||
|
||||
function applyPanels() {
|
||||
const app = $('app');
|
||||
for (const key of Object.keys(panels)) {
|
||||
const p = panels[key];
|
||||
$(p.el).classList.toggle('hidden', !p.visible);
|
||||
$(p.btn).classList.toggle('active', p.visible);
|
||||
if (p.cls) app.classList.toggle(p.cls, !p.visible);
|
||||
}
|
||||
// collapse the whole side column when neither side panel is shown
|
||||
app.classList.toggle('no-side', !panels.params.visible && !panels.theory.visible);
|
||||
|
||||
try {
|
||||
localStorage.setItem('musrview.panels',
|
||||
JSON.stringify(Object.fromEntries(Object.entries(panels).map(([k, v]) => [k, v.visible]))));
|
||||
} catch (e) { /* private mode, ignore */ }
|
||||
|
||||
resizePlot();
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
function togglePanel(key) {
|
||||
@@ -404,6 +421,105 @@ function restorePanels() {
|
||||
applyPanels();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// layout: the side column width, the fit-parameters/theory split and the
|
||||
// run-info height are all user-draggable (via #vsplit / #side-split /
|
||||
// #hsplit) and persisted the same way panel visibility is.
|
||||
//--------------------------------------------------------------------------
|
||||
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 paramsFrac = 0.5; // fraction of #side's height given to 'Fit parameters'
|
||||
|
||||
function updateLayout() {
|
||||
const app = $('app');
|
||||
const sideVisible = panels.params.visible || panels.theory.visible;
|
||||
const infoVisible = panels.info.visible;
|
||||
|
||||
if (infoHeight === null) infoHeight = Math.round(window.innerHeight * 0.28);
|
||||
infoHeight = clamp(infoHeight, 60, Math.max(60, Math.round(window.innerHeight * 0.75)));
|
||||
sideWidth = clamp(sideWidth, 200, Math.max(200, Math.round(window.innerWidth * 0.75)));
|
||||
|
||||
app.style.gridTemplateColumns =
|
||||
`1fr ${sideVisible ? '6px' : '0px'} ${sideVisible ? sideWidth + 'px' : '0px'}`;
|
||||
app.style.gridTemplateRows =
|
||||
`auto ${panels.title.visible ? 'auto' : '0px'} 1fr ` +
|
||||
`${infoVisible ? '6px' : '0px'} ${infoVisible ? infoHeight + 'px' : '0px'} auto`;
|
||||
|
||||
$('vsplit').classList.toggle('hidden', !sideVisible);
|
||||
$('hsplit').classList.toggle('hidden', !infoVisible);
|
||||
|
||||
updateSideLayout();
|
||||
resizePlot();
|
||||
}
|
||||
|
||||
function updateSideLayout() {
|
||||
const side = $('side');
|
||||
const pVis = panels.params.visible, tVis = panels.theory.visible;
|
||||
|
||||
if (pVis && tVis) {
|
||||
side.style.gridTemplateRows = `${paramsFrac}fr 6px ${1 - paramsFrac}fr`;
|
||||
$('side-split').classList.remove('hidden');
|
||||
} else {
|
||||
side.style.gridTemplateRows = pVis ? '1fr 0px 0px' : (tVis ? '0px 0px 1fr' : '0px 0px 0px');
|
||||
$('side-split').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function saveLayout() {
|
||||
try {
|
||||
localStorage.setItem('musrview.layout', JSON.stringify({ sideWidth, infoHeight, paramsFrac }));
|
||||
} catch (e) { /* private mode, ignore */ }
|
||||
}
|
||||
|
||||
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.paramsFrac === 'number') paramsFrac = saved.paramsFrac;
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
handleEl.addEventListener('pointerdown', ev => {
|
||||
ev.preventDefault();
|
||||
handleEl.setPointerCapture(ev.pointerId);
|
||||
handleEl.classList.add('dragging');
|
||||
|
||||
const onPointerMove = mv => onMove(mv);
|
||||
const onPointerUp = () => {
|
||||
handleEl.classList.remove('dragging');
|
||||
handleEl.removeEventListener('pointermove', onPointerMove);
|
||||
handleEl.removeEventListener('pointerup', onPointerUp);
|
||||
saveLayout();
|
||||
};
|
||||
handleEl.addEventListener('pointermove', onPointerMove);
|
||||
handleEl.addEventListener('pointerup', onPointerUp);
|
||||
});
|
||||
}
|
||||
|
||||
makeSplitter($('vsplit'), ev => {
|
||||
sideWidth = window.innerWidth - ev.clientX - 3;
|
||||
updateLayout();
|
||||
});
|
||||
|
||||
makeSplitter($('hsplit'), ev => {
|
||||
const statusH = $('statusbar').getBoundingClientRect().height;
|
||||
infoHeight = window.innerHeight - statusH - ev.clientY - 3;
|
||||
updateLayout();
|
||||
});
|
||||
|
||||
makeSplitter($('side-split'), ev => {
|
||||
const rect = $('side').getBoundingClientRect();
|
||||
paramsFrac = clamp((ev.clientY - rect.top) / rect.height, 0.1, 0.9);
|
||||
updateSideLayout();
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// plotting
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -680,7 +796,7 @@ $('pan-params').onclick = () => togglePanel('params');
|
||||
$('pan-theory').onclick = () => togglePanel('theory');
|
||||
$('pan-info').onclick = () => togglePanel('info');
|
||||
|
||||
window.addEventListener('resize', resizePlot);
|
||||
window.addEventListener('resize', updateLayout);
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// websocket towards the C++ side
|
||||
@@ -724,6 +840,7 @@ const receiver = {
|
||||
}
|
||||
};
|
||||
|
||||
restoreLayout();
|
||||
restorePanels();
|
||||
|
||||
connectWebWindow({
|
||||
|
||||
Reference in new issue
Block a user