From 083d009aa5a84ed3813686f0188ea4271f14a2f3 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Thu, 5 Mar 2026 18:17:34 +0100 Subject: [PATCH] [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 Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/app.js | 125 +++++++++++++----- .../trx-frontend-http/assets/web/index.html | 4 + .../trx-frontend-http/assets/web/style.css | 57 +++++++- 3 files changed, 152 insertions(+), 34 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 698fccf..31a11f0 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -284,10 +284,12 @@ function applyCapabilities(caps) { if (spectrumPanel) { if (caps.filter_controls) { spectrumPanel.style.display = ""; + setSignalSplitControlVisible(true); if (centerFreqField) centerFreqField.style.display = ""; startSpectrumStreaming(); } else { spectrumPanel.style.display = "none"; + setSignalSplitControlVisible(false); if (centerFreqField) centerFreqField.style.display = "none"; stopSpectrumStreaming(); } @@ -325,6 +327,9 @@ const loadingSub = document.getElementById("loading-sub"); const overviewCanvas = document.getElementById("overview-canvas"); const signalOverlayCanvas = document.getElementById("signal-overlay-canvas"); 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 themeToggleBtn = document.getElementById("theme-toggle"); const headerRigSwitchSelect = document.getElementById("header-rig-switch-select"); @@ -1932,10 +1937,17 @@ let spectrumCoverageMarginHz = 50_000; let spectrumUsableSpanRatio = 0.92; const DEFAULT_OVERVIEW_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 DEFAULT_SIGNAL_SPLIT_PERCENT = 50; +const MIN_SIGNAL_SPLIT_PERCENT = 20; +const MAX_SIGNAL_SPLIT_PERCENT = 80; let spectrumLayoutPending = false; -let spectrumManualPlotHeightPx = null; +let spectrumManualTotalPlotHeightPx = null; let spectrumResizeState = null; +let signalSplitPercent = clampSignalSplitPercent( + Number(loadSetting("signalSplitPercent", DEFAULT_SIGNAL_SPLIT_PERCENT)), +); function updateFooterBuildInfo() { 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) { return Math.max( 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 currentTotalHeight = currentOverviewHeight + currentSpectrumHeight; const tabBottom = tabMainEl.getBoundingClientRect().bottom; const contentBottom = contentEl.getBoundingClientRect().bottom; const slackPx = Math.floor(tabBottom - contentBottom); - const maxHeight = Math.max( - MIN_SPECTRUM_PLOT_HEIGHT_PX, - currentSpectrumHeight + slackPx - 2, + const minTotalHeight = MIN_OVERVIEW_PLOT_HEIGHT_PX + MIN_SPECTRUM_PLOT_HEIGHT_PX; + const maxAutoTotalHeight = Math.max( + minTotalHeight, + currentTotalHeight + slackPx - 2, ); return { - min: MIN_SPECTRUM_PLOT_HEIGHT_PX, - max: maxHeight, + minTotal: minTotalHeight, + autoMaxTotal: maxAutoTotalHeight, }; } @@ -1988,16 +2028,11 @@ function updateSpectrumAutoHeight() { const mainVisible = getComputedStyle(tabMainEl).display !== "none"; const contentVisible = getComputedStyle(contentEl).display !== "none"; const spectrumVisible = getComputedStyle(spectrumPanelEl).display !== "none"; - const currentOverviewHeight = Math.max( - DEFAULT_OVERVIEW_PLOT_HEIGHT_PX, - 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), - ); + const currentOverviewHeight = currentOverviewHeightPx(overviewCanvasEl); + const currentSpectrumHeight = currentSpectrumHeightPx(spectrumCanvasEl); if (!mainVisible || !contentVisible || !spectrumVisible) { + setSignalSplitControlVisible(false); root.style.setProperty("--overview-plot-height", `${DEFAULT_OVERVIEW_PLOT_HEIGHT_PX}px`); root.style.setProperty("--spectrum-plot-height", `${DEFAULT_SPECTRUM_PLOT_HEIGHT_PX}px`); if ( @@ -2011,19 +2046,29 @@ function updateSpectrumAutoHeight() { return; } - const bounds = spectrumHeightBoundsPx(tabMainEl, contentEl, spectrumCanvasEl); - const nextSpectrumHeight = spectrumManualPlotHeightPx == null - ? bounds.max - : Math.max(bounds.min, Math.round(spectrumManualPlotHeightPx)); - if (spectrumManualPlotHeightPx != null) { - spectrumManualPlotHeightPx = nextSpectrumHeight; + setSignalSplitControlVisible(true); + const bounds = spectrumHeightBoundsPx(tabMainEl, contentEl, overviewCanvasEl, spectrumCanvasEl); + const nextTotalHeight = spectrumManualTotalPlotHeightPx == null + ? bounds.autoMaxTotal + : Math.max(bounds.minTotal, Math.round(spectrumManualTotalPlotHeightPx)); + 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 ( - Math.abs(DEFAULT_OVERVIEW_PLOT_HEIGHT_PX - currentOverviewHeight) < 2 + Math.abs(nextOverviewHeight - currentOverviewHeight) < 2 && Math.abs(nextSpectrumHeight - currentSpectrumHeight) < 2 ) 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`); if (lastSpectrumData) { scheduleSpectrumDraw(); @@ -2034,16 +2079,20 @@ function updateSpectrumAutoHeight() { function beginSpectrumResize(clientY) { const tabMainEl = document.getElementById("tab-main"); const contentEl = document.getElementById("content"); + const overviewCanvasEl = document.getElementById("overview-canvas"); const spectrumCanvasEl = document.getElementById("spectrum-canvas"); 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; - const bounds = spectrumHeightBoundsPx(tabMainEl, contentEl, spectrumCanvasEl); - const startHeight = Math.max(bounds.min, currentSpectrumHeightPx(spectrumCanvasEl)); + const bounds = spectrumHeightBoundsPx(tabMainEl, contentEl, overviewCanvasEl, spectrumCanvasEl); + const startTotalHeight = Math.max( + bounds.minTotal, + currentOverviewHeightPx(overviewCanvasEl) + currentSpectrumHeightPx(spectrumCanvasEl), + ); spectrumResizeState = { startY: clientY, - startHeight, - minHeight: bounds.min, + startTotalHeight, + minTotalHeight: bounds.minTotal, }; document.body.classList.add("spectrum-resizing"); return true; @@ -2052,9 +2101,9 @@ function beginSpectrumResize(clientY) { function updateSpectrumResize(clientY) { if (!spectrumResizeState) return; const deltaY = clientY - spectrumResizeState.startY; - spectrumManualPlotHeightPx = Math.max( - spectrumResizeState.minHeight, - Math.round(spectrumResizeState.startHeight + deltaY), + spectrumManualTotalPlotHeightPx = Math.max( + spectrumResizeState.minTotalHeight, + Math.round(spectrumResizeState.startTotalHeight + deltaY), ); updateSpectrumAutoHeight(); } @@ -2088,11 +2137,23 @@ if (spectrumSizeGrip) { spectrumSizeGrip.addEventListener("pointerup", finishResize); spectrumSizeGrip.addEventListener("pointercancel", finishResize); spectrumSizeGrip.addEventListener("dblclick", () => { - spectrumManualPlotHeightPx = null; + spectrumManualTotalPlotHeightPx = null; 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() { const titleEl = document.getElementById("rig-title"); if (titleEl) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 65e6537..8a9d6bb 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -120,6 +120,10 @@
Scroll to zoom · Ctrl+Scroll to tune · Drag to pan · Drag BW edges to resize
Pinch to zoom · Drag to pan · Drag BW edges to resize
+
+ + 50/50 +
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index bbb150c..6ad5103 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -529,6 +529,43 @@ small { color: var(--text-muted); } gap: 0; 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 { position: absolute; 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 select { min-width: 6.5rem; } .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-col-wfm { 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 { position: absolute; - top: calc(var(--spectrum-plot-height) / 2); + top: 50%; transform: translateY(-50%); z-index: 8; width: 1.7rem; @@ -2261,7 +2314,7 @@ button:focus-visible, input:focus-visible, select:focus-visible { } .spectrum-bookmark-side { position: absolute; - top: calc(var(--spectrum-plot-height) / 2); + top: 50%; transform: translateY(-50%); z-index: 7; width: 7.25rem;