[feat](trx-frontend): add signal split slider and align spectrum side controls

Add a right-side slider to control the waterfall/waveform split and\npersist the selected ratio locally.\n\nRework spectrum height layout so manual resize adjusts total plot height\nwhile split controls the overview/spectrum ratio.\n\nKeep center-frequency arrows and side bookmark stacks vertically centered\nwithin the full spectrum view container.\n\nCo-authored-by: Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-05 18:17:34 +01:00
parent 4c69e2bcde
commit 083d009aa5
3 changed files with 152 additions and 34 deletions
@@ -284,10 +284,12 @@ function applyCapabilities(caps) {
if (spectrumPanel) { if (spectrumPanel) {
if (caps.filter_controls) { if (caps.filter_controls) {
spectrumPanel.style.display = ""; spectrumPanel.style.display = "";
setSignalSplitControlVisible(true);
if (centerFreqField) centerFreqField.style.display = ""; if (centerFreqField) centerFreqField.style.display = "";
startSpectrumStreaming(); startSpectrumStreaming();
} else { } else {
spectrumPanel.style.display = "none"; spectrumPanel.style.display = "none";
setSignalSplitControlVisible(false);
if (centerFreqField) centerFreqField.style.display = "none"; if (centerFreqField) centerFreqField.style.display = "none";
stopSpectrumStreaming(); stopSpectrumStreaming();
} }
@@ -325,6 +327,9 @@ const loadingSub = document.getElementById("loading-sub");
const overviewCanvas = document.getElementById("overview-canvas"); const overviewCanvas = document.getElementById("overview-canvas");
const signalOverlayCanvas = document.getElementById("signal-overlay-canvas"); const signalOverlayCanvas = document.getElementById("signal-overlay-canvas");
const signalVisualBlockEl = document.querySelector(".signal-visual-block"); const signalVisualBlockEl = document.querySelector(".signal-visual-block");
const signalSplitControlEl = document.getElementById("signal-split-control");
const signalSplitSliderEl = document.getElementById("signal-split-slider");
const signalSplitValueEl = document.getElementById("signal-split-value");
const overviewPeakHoldEl = document.getElementById("overview-peak-hold"); const overviewPeakHoldEl = document.getElementById("overview-peak-hold");
const themeToggleBtn = document.getElementById("theme-toggle"); const themeToggleBtn = document.getElementById("theme-toggle");
const headerRigSwitchSelect = document.getElementById("header-rig-switch-select"); const headerRigSwitchSelect = document.getElementById("header-rig-switch-select");
@@ -1932,10 +1937,17 @@ let spectrumCoverageMarginHz = 50_000;
let spectrumUsableSpanRatio = 0.92; let spectrumUsableSpanRatio = 0.92;
const DEFAULT_OVERVIEW_PLOT_HEIGHT_PX = 160; const DEFAULT_OVERVIEW_PLOT_HEIGHT_PX = 160;
const DEFAULT_SPECTRUM_PLOT_HEIGHT_PX = 160; const DEFAULT_SPECTRUM_PLOT_HEIGHT_PX = 160;
const MIN_OVERVIEW_PLOT_HEIGHT_PX = 90;
const MIN_SPECTRUM_PLOT_HEIGHT_PX = 130; const MIN_SPECTRUM_PLOT_HEIGHT_PX = 130;
const DEFAULT_SIGNAL_SPLIT_PERCENT = 50;
const MIN_SIGNAL_SPLIT_PERCENT = 20;
const MAX_SIGNAL_SPLIT_PERCENT = 80;
let spectrumLayoutPending = false; let spectrumLayoutPending = false;
let spectrumManualPlotHeightPx = null; let spectrumManualTotalPlotHeightPx = null;
let spectrumResizeState = null; let spectrumResizeState = null;
let signalSplitPercent = clampSignalSplitPercent(
Number(loadSetting("signalSplitPercent", DEFAULT_SIGNAL_SPLIT_PERCENT)),
);
function updateFooterBuildInfo() { function updateFooterBuildInfo() {
const serverEl = document.getElementById("footer-server-build"); const serverEl = document.getElementById("footer-server-build");
@@ -1954,6 +1966,31 @@ function scheduleSpectrumLayout() {
}); });
} }
function clampSignalSplitPercent(value) {
const numeric = Number.isFinite(value) ? value : DEFAULT_SIGNAL_SPLIT_PERCENT;
return Math.max(
MIN_SIGNAL_SPLIT_PERCENT,
Math.min(MAX_SIGNAL_SPLIT_PERCENT, Math.round(numeric)),
);
}
function updateSignalSplitControlText() {
if (!signalSplitValueEl) return;
signalSplitValueEl.textContent = `${signalSplitPercent}/${100 - signalSplitPercent}`;
}
function setSignalSplitControlVisible(visible) {
if (!signalSplitControlEl) return;
signalSplitControlEl.style.display = visible ? "flex" : "none";
}
function currentOverviewHeightPx(overviewCanvasEl) {
return Math.max(
MIN_OVERVIEW_PLOT_HEIGHT_PX,
Math.round(overviewCanvasEl?.clientHeight || DEFAULT_OVERVIEW_PLOT_HEIGHT_PX),
);
}
function currentSpectrumHeightPx(spectrumCanvasEl) { function currentSpectrumHeightPx(spectrumCanvasEl) {
return Math.max( return Math.max(
MIN_SPECTRUM_PLOT_HEIGHT_PX, MIN_SPECTRUM_PLOT_HEIGHT_PX,
@@ -1961,18 +1998,21 @@ function currentSpectrumHeightPx(spectrumCanvasEl) {
); );
} }
function spectrumHeightBoundsPx(tabMainEl, contentEl, spectrumCanvasEl) { function spectrumHeightBoundsPx(tabMainEl, contentEl, overviewCanvasEl, spectrumCanvasEl) {
const currentOverviewHeight = currentOverviewHeightPx(overviewCanvasEl);
const currentSpectrumHeight = currentSpectrumHeightPx(spectrumCanvasEl); const currentSpectrumHeight = currentSpectrumHeightPx(spectrumCanvasEl);
const currentTotalHeight = currentOverviewHeight + currentSpectrumHeight;
const tabBottom = tabMainEl.getBoundingClientRect().bottom; const tabBottom = tabMainEl.getBoundingClientRect().bottom;
const contentBottom = contentEl.getBoundingClientRect().bottom; const contentBottom = contentEl.getBoundingClientRect().bottom;
const slackPx = Math.floor(tabBottom - contentBottom); const slackPx = Math.floor(tabBottom - contentBottom);
const maxHeight = Math.max( const minTotalHeight = MIN_OVERVIEW_PLOT_HEIGHT_PX + MIN_SPECTRUM_PLOT_HEIGHT_PX;
MIN_SPECTRUM_PLOT_HEIGHT_PX, const maxAutoTotalHeight = Math.max(
currentSpectrumHeight + slackPx - 2, minTotalHeight,
currentTotalHeight + slackPx - 2,
); );
return { return {
min: MIN_SPECTRUM_PLOT_HEIGHT_PX, minTotal: minTotalHeight,
max: maxHeight, autoMaxTotal: maxAutoTotalHeight,
}; };
} }
@@ -1988,16 +2028,11 @@ function updateSpectrumAutoHeight() {
const mainVisible = getComputedStyle(tabMainEl).display !== "none"; const mainVisible = getComputedStyle(tabMainEl).display !== "none";
const contentVisible = getComputedStyle(contentEl).display !== "none"; const contentVisible = getComputedStyle(contentEl).display !== "none";
const spectrumVisible = getComputedStyle(spectrumPanelEl).display !== "none"; const spectrumVisible = getComputedStyle(spectrumPanelEl).display !== "none";
const currentOverviewHeight = Math.max( const currentOverviewHeight = currentOverviewHeightPx(overviewCanvasEl);
DEFAULT_OVERVIEW_PLOT_HEIGHT_PX, const currentSpectrumHeight = currentSpectrumHeightPx(spectrumCanvasEl);
Math.round(overviewCanvasEl.clientHeight || DEFAULT_OVERVIEW_PLOT_HEIGHT_PX),
);
const currentSpectrumHeight = Math.max(
DEFAULT_SPECTRUM_PLOT_HEIGHT_PX,
Math.round(spectrumCanvasEl.clientHeight || DEFAULT_SPECTRUM_PLOT_HEIGHT_PX),
);
if (!mainVisible || !contentVisible || !spectrumVisible) { if (!mainVisible || !contentVisible || !spectrumVisible) {
setSignalSplitControlVisible(false);
root.style.setProperty("--overview-plot-height", `${DEFAULT_OVERVIEW_PLOT_HEIGHT_PX}px`); root.style.setProperty("--overview-plot-height", `${DEFAULT_OVERVIEW_PLOT_HEIGHT_PX}px`);
root.style.setProperty("--spectrum-plot-height", `${DEFAULT_SPECTRUM_PLOT_HEIGHT_PX}px`); root.style.setProperty("--spectrum-plot-height", `${DEFAULT_SPECTRUM_PLOT_HEIGHT_PX}px`);
if ( if (
@@ -2011,19 +2046,29 @@ function updateSpectrumAutoHeight() {
return; return;
} }
const bounds = spectrumHeightBoundsPx(tabMainEl, contentEl, spectrumCanvasEl); setSignalSplitControlVisible(true);
const nextSpectrumHeight = spectrumManualPlotHeightPx == null const bounds = spectrumHeightBoundsPx(tabMainEl, contentEl, overviewCanvasEl, spectrumCanvasEl);
? bounds.max const nextTotalHeight = spectrumManualTotalPlotHeightPx == null
: Math.max(bounds.min, Math.round(spectrumManualPlotHeightPx)); ? bounds.autoMaxTotal
if (spectrumManualPlotHeightPx != null) { : Math.max(bounds.minTotal, Math.round(spectrumManualTotalPlotHeightPx));
spectrumManualPlotHeightPx = nextSpectrumHeight; if (spectrumManualTotalPlotHeightPx != null) {
spectrumManualTotalPlotHeightPx = nextTotalHeight;
} }
const requestedOverviewHeight = Math.round((nextTotalHeight * signalSplitPercent) / 100);
const nextOverviewHeight = Math.max(
MIN_OVERVIEW_PLOT_HEIGHT_PX,
Math.min(nextTotalHeight - MIN_SPECTRUM_PLOT_HEIGHT_PX, requestedOverviewHeight),
);
const nextSpectrumHeight = Math.max(
MIN_SPECTRUM_PLOT_HEIGHT_PX,
nextTotalHeight - nextOverviewHeight,
);
if ( if (
Math.abs(DEFAULT_OVERVIEW_PLOT_HEIGHT_PX - currentOverviewHeight) < 2 Math.abs(nextOverviewHeight - currentOverviewHeight) < 2
&& Math.abs(nextSpectrumHeight - currentSpectrumHeight) < 2 && Math.abs(nextSpectrumHeight - currentSpectrumHeight) < 2
) return; ) return;
root.style.setProperty("--overview-plot-height", `${DEFAULT_OVERVIEW_PLOT_HEIGHT_PX}px`); root.style.setProperty("--overview-plot-height", `${nextOverviewHeight}px`);
root.style.setProperty("--spectrum-plot-height", `${nextSpectrumHeight}px`); root.style.setProperty("--spectrum-plot-height", `${nextSpectrumHeight}px`);
if (lastSpectrumData) { if (lastSpectrumData) {
scheduleSpectrumDraw(); scheduleSpectrumDraw();
@@ -2034,16 +2079,20 @@ function updateSpectrumAutoHeight() {
function beginSpectrumResize(clientY) { function beginSpectrumResize(clientY) {
const tabMainEl = document.getElementById("tab-main"); const tabMainEl = document.getElementById("tab-main");
const contentEl = document.getElementById("content"); const contentEl = document.getElementById("content");
const overviewCanvasEl = document.getElementById("overview-canvas");
const spectrumCanvasEl = document.getElementById("spectrum-canvas"); const spectrumCanvasEl = document.getElementById("spectrum-canvas");
const spectrumPanelEl = document.getElementById("spectrum-panel"); const spectrumPanelEl = document.getElementById("spectrum-panel");
if (!tabMainEl || !contentEl || !spectrumCanvasEl || !spectrumPanelEl) return false; if (!tabMainEl || !contentEl || !overviewCanvasEl || !spectrumCanvasEl || !spectrumPanelEl) return false;
if (getComputedStyle(spectrumPanelEl).display === "none") return false; if (getComputedStyle(spectrumPanelEl).display === "none") return false;
const bounds = spectrumHeightBoundsPx(tabMainEl, contentEl, spectrumCanvasEl); const bounds = spectrumHeightBoundsPx(tabMainEl, contentEl, overviewCanvasEl, spectrumCanvasEl);
const startHeight = Math.max(bounds.min, currentSpectrumHeightPx(spectrumCanvasEl)); const startTotalHeight = Math.max(
bounds.minTotal,
currentOverviewHeightPx(overviewCanvasEl) + currentSpectrumHeightPx(spectrumCanvasEl),
);
spectrumResizeState = { spectrumResizeState = {
startY: clientY, startY: clientY,
startHeight, startTotalHeight,
minHeight: bounds.min, minTotalHeight: bounds.minTotal,
}; };
document.body.classList.add("spectrum-resizing"); document.body.classList.add("spectrum-resizing");
return true; return true;
@@ -2052,9 +2101,9 @@ function beginSpectrumResize(clientY) {
function updateSpectrumResize(clientY) { function updateSpectrumResize(clientY) {
if (!spectrumResizeState) return; if (!spectrumResizeState) return;
const deltaY = clientY - spectrumResizeState.startY; const deltaY = clientY - spectrumResizeState.startY;
spectrumManualPlotHeightPx = Math.max( spectrumManualTotalPlotHeightPx = Math.max(
spectrumResizeState.minHeight, spectrumResizeState.minTotalHeight,
Math.round(spectrumResizeState.startHeight + deltaY), Math.round(spectrumResizeState.startTotalHeight + deltaY),
); );
updateSpectrumAutoHeight(); updateSpectrumAutoHeight();
} }
@@ -2088,11 +2137,23 @@ if (spectrumSizeGrip) {
spectrumSizeGrip.addEventListener("pointerup", finishResize); spectrumSizeGrip.addEventListener("pointerup", finishResize);
spectrumSizeGrip.addEventListener("pointercancel", finishResize); spectrumSizeGrip.addEventListener("pointercancel", finishResize);
spectrumSizeGrip.addEventListener("dblclick", () => { spectrumSizeGrip.addEventListener("dblclick", () => {
spectrumManualPlotHeightPx = null; spectrumManualTotalPlotHeightPx = null;
scheduleSpectrumLayout(); scheduleSpectrumLayout();
}); });
} }
if (signalSplitSliderEl) {
signalSplitSliderEl.value = String(signalSplitPercent);
signalSplitSliderEl.addEventListener("input", () => {
signalSplitPercent = clampSignalSplitPercent(Number(signalSplitSliderEl.value));
signalSplitSliderEl.value = String(signalSplitPercent);
updateSignalSplitControlText();
saveSetting("signalSplitPercent", signalSplitPercent);
scheduleSpectrumLayout();
});
}
updateSignalSplitControlText();
function updateTitle() { function updateTitle() {
const titleEl = document.getElementById("rig-title"); const titleEl = document.getElementById("rig-title");
if (titleEl) { if (titleEl) {
@@ -120,6 +120,10 @@
<div id="spectrum-hint" class="spectrum-hint-mouse">Scroll to zoom &middot; Ctrl+Scroll to tune &middot; Drag to pan &middot; Drag BW edges to resize</div> <div id="spectrum-hint" class="spectrum-hint-mouse">Scroll to zoom &middot; Ctrl+Scroll to tune &middot; Drag to pan &middot; Drag BW edges to resize</div>
<div id="spectrum-hint-touch" class="spectrum-hint-touch">Pinch to zoom &middot; Drag to pan &middot; Drag BW edges to resize</div> <div id="spectrum-hint-touch" class="spectrum-hint-touch">Pinch to zoom &middot; Drag to pan &middot; Drag BW edges to resize</div>
</div> </div>
<div id="signal-split-control" title="Set waterfall and waveform split" aria-label="Set waterfall and waveform split">
<input id="signal-split-slider" type="range" min="20" max="80" step="1" value="50" aria-label="Set waterfall and waveform split">
<span id="signal-split-value" aria-live="polite">50/50</span>
</div>
<canvas id="signal-overlay-canvas" aria-hidden="true"></canvas> <canvas id="signal-overlay-canvas" aria-hidden="true"></canvas>
</div> </div>
<div class="status"> <div class="status">
@@ -529,6 +529,43 @@ small { color: var(--text-muted); }
gap: 0; gap: 0;
margin-bottom: 0.9rem; margin-bottom: 0.9rem;
} }
#signal-split-control {
position: absolute;
top: 50%;
right: 0.32rem;
transform: translateY(-50%);
z-index: 9;
display: none;
flex-direction: column;
align-items: center;
gap: 0.3rem;
padding: 0.32rem 0.25rem;
border: 1px solid color-mix(in srgb, var(--border-light) 78%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--card-bg) 78%, transparent);
backdrop-filter: blur(8px) saturate(125%);
-webkit-backdrop-filter: blur(8px) saturate(125%);
box-shadow:
0 8px 16px color-mix(in srgb, #000000 14%, transparent),
inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
}
#signal-split-slider {
writing-mode: vertical-lr;
direction: rtl;
width: 0.95rem;
height: 7.2rem;
margin: 0;
accent-color: var(--accent-yellow);
cursor: ns-resize;
}
#signal-split-value {
min-width: 2.9rem;
text-align: center;
font-size: 0.64rem;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--text-muted);
}
#signal-overlay-canvas { #signal-overlay-canvas {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -1805,6 +1842,22 @@ button:focus-visible, input:focus-visible, select:focus-visible {
.header-rig-switch { width: auto; justify-content: flex-end; } .header-rig-switch { width: auto; justify-content: flex-end; }
.header-rig-switch select { min-width: 6.5rem; } .header-rig-switch select { min-width: 6.5rem; }
.overview-toolbar { top: calc(var(--header-waterfall-overlap) + 0.15rem); } .overview-toolbar { top: calc(var(--header-waterfall-overlap) + 0.15rem); }
#signal-split-control {
top: 0.35rem;
right: 0.35rem;
transform: none;
flex-direction: row;
gap: 0.4rem;
border-radius: 0.7rem;
padding: 0.28rem 0.42rem;
}
#signal-split-slider {
writing-mode: horizontal-tb;
direction: ltr;
width: 5.5rem;
height: 1rem;
cursor: ew-resize;
}
.controls-row { grid-template-columns: 1fr auto; } .controls-row { grid-template-columns: 1fr auto; }
.controls-col-wfm { grid-column: 1 / -1; } .controls-col-wfm { grid-column: 1 / -1; }
.controls-col-power { grid-column: 1 / -1; } .controls-col-power { grid-column: 1 / -1; }
@@ -2132,7 +2185,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
} }
.spectrum-edge-shift { .spectrum-edge-shift {
position: absolute; position: absolute;
top: calc(var(--spectrum-plot-height) / 2); top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
z-index: 8; z-index: 8;
width: 1.7rem; width: 1.7rem;
@@ -2261,7 +2314,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
} }
.spectrum-bookmark-side { .spectrum-bookmark-side {
position: absolute; position: absolute;
top: calc(var(--spectrum-plot-height) / 2); top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
z-index: 7; z-index: 7;
width: 7.25rem; width: 7.25rem;