diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index 9ec8abb..38ffed6 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -22,6 +22,10 @@ EM_JS(int, js_isFullscreen, (), { return document.fullscreenElement ? 1 : 0; }); +EM_JS(float, js_devicePixelRatio, (), { + return window.devicePixelRatio || 1.0; +}); + #else #include #endif @@ -34,6 +38,34 @@ namespace baudmine { Application::Application() = default; +void Application::applyUIScale(float scale) { + scale = std::clamp(scale, 0.5f, 4.0f); + if (std::abs(scale - appliedScale_) < 0.01f) return; + appliedScale_ = scale; + + // Snapshot the 1x base style once. + static ImGuiStyle baseStyle = [] { + ImGuiStyle s; + ImGui::StyleColorsDark(&s); + s.WindowRounding = 4.0f; + s.FrameRounding = 2.0f; + s.GrabRounding = 2.0f; + return s; + }(); + + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->Clear(); + ImFontConfig fc; + fc.SizePixels = 13.0f * scale; + io.Fonts->AddFontDefault(&fc); + io.Fonts->Build(); + ImGui_ImplOpenGL3_DestroyFontsTexture(); + + // Restore base style, then scale from 1x. + ImGui::GetStyle() = baseStyle; + ImGui::GetStyle().ScaleAllSizes(scale); +} + Application::~Application() { shutdown(); } @@ -112,6 +144,20 @@ bool Application::init(int argc, char** argv) { // Load saved config (overwrites defaults for FFT size, overlap, window, etc.) loadConfig(); + // Apply DPI-aware UI scaling + { + float dpiScale = 1.0f; +#ifdef __EMSCRIPTEN__ + dpiScale = js_devicePixelRatio(); +#else + float ddpi = 0; + if (SDL_GetDisplayDPI(0, &ddpi, nullptr, nullptr) == 0 && ddpi > 0) + dpiScale = ddpi / 96.0f; +#endif + float scale = (uiScale_ > 0.0f) ? uiScale_ : dpiScale; + applyUIScale(scale); + } + // Apply loaded settings settings_.fftSize = kFFTSizes[fftSizeIdx_]; settings_.overlap = overlapPct_ / 100.0f; @@ -370,6 +416,34 @@ void Application::render() { saveConfig(); } + if (ImGui::BeginMenu("UI scale")) { + static constexpr int kScales[] = {100, 150, 175, 200, 225, 250, 300}; + int curPct = static_cast(appliedScale_ * 100.0f + 0.5f); + if (ImGui::MenuItem("Auto", nullptr, uiScale_ == 0.0f)) { + uiScale_ = 0.0f; + float dpiScale = 1.0f; +#ifdef __EMSCRIPTEN__ + dpiScale = js_devicePixelRatio(); +#else + float ddpi = 0; + if (SDL_GetDisplayDPI(0, &ddpi, nullptr, nullptr) == 0 && ddpi > 0) + dpiScale = ddpi / 96.0f; +#endif + applyUIScale(dpiScale); + saveConfig(); + } + for (int s : kScales) { + char label[16]; + std::snprintf(label, sizeof(label), "%d%%", s); + if (ImGui::MenuItem(label, nullptr, uiScale_ > 0.0f && std::abs(curPct - s) <= 2)) { + uiScale_ = s / 100.0f; + applyUIScale(uiScale_); + saveConfig(); + } + } + ImGui::EndMenu(); + } + ImGui::EndMenu(); } @@ -409,7 +483,7 @@ void Application::render() { // Layout float totalW = ImGui::GetContentRegionAvail().x; float contentH = ImGui::GetContentRegionAvail().y; - float controlW = showSidebar_ ? 270.0f : 0.0f; + float controlW = showSidebar_ ? 270.0f * appliedScale_ : 0.0f; float contentW = totalW - (showSidebar_ ? controlW + 8 : 0); // Control panel (sidebar) @@ -686,6 +760,7 @@ void Application::renderControlPanel() { } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Reset to 2x zoom"); } + } // ── Channels ── @@ -695,7 +770,7 @@ void Application::renderControlPanel() { bool isMulti = waterfallMultiCh_ && nCh > 1; // Header with inline Single/Multi toggle - float widgetW = (nCh > 1) ? 60.0f : 0.0f; + float widgetW = (nCh > 1) ? ImGui::CalcTextSize(" Multi ").x + ImGui::GetStyle().FramePadding.x * 2 : 0.0f; float gap = ImGui::GetStyle().ItemSpacing.x * 0.25f; ImVec2 hdrMin = ImGui::GetCursorScreenPos(); float winLeft = ImGui::GetWindowPos().x; @@ -1536,6 +1611,7 @@ void Application::loadConfig() { int fs = config_.getInt("freq_scale", static_cast(freqScale_)); freqScale_ = static_cast(fs); vsync_ = config_.getBool("vsync", vsync_); + uiScale_ = config_.getFloat("ui_scale", uiScale_); spectrumFrac_ = config_.getFloat("spectrum_frac", spectrumFrac_); showSidebar_ = config_.getBool("show_sidebar", showSidebar_); specDisplay_.peakHoldEnable = config_.getBool("peak_hold", specDisplay_.peakHoldEnable); @@ -1579,6 +1655,7 @@ void Application::saveConfig() const { cfg.setFloat("max_db", maxDB_); cfg.setInt("freq_scale", static_cast(freqScale_)); cfg.setBool("vsync", vsync_); + cfg.setFloat("ui_scale", uiScale_); cfg.setFloat("spectrum_frac", spectrumFrac_); cfg.setBool("show_sidebar", showSidebar_); cfg.setBool("peak_hold", specDisplay_.peakHoldEnable); diff --git a/src/ui/Application.h b/src/ui/Application.h index 5deb90d..40d0b9a 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -121,6 +121,9 @@ private: FreqScale freqScale_ = FreqScale::Linear; bool paused_ = false; bool vsync_ = true; + float uiScale_ = 0.0f; // 0 = auto (use DPI), >0 = manual override + float appliedScale_ = 0.0f; // currently applied scale (0 = not yet applied) + void applyUIScale(float scale); // (waterfallW_ removed — texture width tracks bin count automatically) // (waterfallH_ removed — fixed history depth of 1024 rows) diff --git a/src/ui/Cursors.cpp b/src/ui/Cursors.cpp index e855fa8..e439652 100644 --- a/src/ui/Cursors.cpp +++ b/src/ui/Cursors.cpp @@ -123,7 +123,9 @@ void Cursors::draw(const SpectrumDisplay& specDisplay, fmtFreqDB(deltaBuf, sizeof(deltaBuf), "D", dFreq, dDB); ImVec2 dSz = ImGui::CalcTextSize(deltaBuf); - float tx = posX + sizeX - dSz.x - 168; + // Reserve space for hover label to the right. + float reserveW = ImGui::CalcTextSize(" 00.000 kHz 000.0 dB").x; + float tx = posX + sizeX - dSz.x - reserveW; float lineH = ImGui::GetTextLineHeight(); float ty = posY + 4; ImU32 col = IM_COL32(255, 200, 100, 255); diff --git a/src/ui/Measurements.cpp b/src/ui/Measurements.cpp index 4e96515..497cf3a 100644 --- a/src/ui/Measurements.cpp +++ b/src/ui/Measurements.cpp @@ -119,7 +119,9 @@ void Measurements::draw(const SpectrumDisplay& specDisplay, char pkBuf[128]; fmtFreqDB(pkBuf, sizeof(pkBuf), "Peak", globalPeak_.freq, globalPeak_.dB); ImVec2 pkSz = ImGui::CalcTextSize(pkBuf); - float pkX = posX + sizeX - pkSz.x - 8 - 350; + // Reserve space for delta + hover labels to the right. + float reserveW = ImGui::CalcTextSize("D: 00.000 kHz 000.0 dB 00.000 kHz 000.0 dB").x; + float pkX = posX + sizeX - pkSz.x - 8 - reserveW; float pkY = posY + 4; ImGui::GetWindowDrawList()->AddText({pkX, pkY}, IM_COL32(180, 180, 200, 200), pkBuf); } diff --git a/src/ui/SpectrumDisplay.cpp b/src/ui/SpectrumDisplay.cpp index 219a0a8..bf67b18 100644 --- a/src/ui/SpectrumDisplay.cpp +++ b/src/ui/SpectrumDisplay.cpp @@ -127,7 +127,7 @@ void SpectrumDisplay::draw(const std::vector>& spectra, dl->AddLine({posX, y}, {posX + sizeX, y}, gridCol); char label[16]; std::snprintf(label, sizeof(label), "%.0f", db); - dl->AddText({posX + 2, y - 12}, textCol, label); + dl->AddText({posX + 2, y - ImGui::GetTextLineHeight()}, textCol, label); } // ── Vertical (frequency) grid — adapt count to available width ── @@ -148,7 +148,7 @@ void SpectrumDisplay::draw(const std::vector>& spectra, std::snprintf(label, sizeof(label), "%.1fk", freq / 1e3); else std::snprintf(label, sizeof(label), "%.0f", freq); - dl->AddText({x + 2, posY + sizeY - 14}, textCol, label); + dl->AddText({x + 2, posY + sizeY - ImGui::GetTextLineHeight()}, textCol, label); } } diff --git a/web/shell.html b/web/shell.html index 7d632b9..5471a04 100644 --- a/web/shell.html +++ b/web/shell.html @@ -48,12 +48,13 @@ setStatus: function(text) { document.getElementById('status').textContent = text; }, - // Auto-resize canvas to fill the window. + // Auto-resize canvas at full device resolution. preRun: [function() { function resize() { var canvas = document.getElementById('canvas'); - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; + var dpr = window.devicePixelRatio || 1; + canvas.width = Math.round(window.innerWidth * dpr); + canvas.height = Math.round(window.innerHeight * dpr); } resize(); window.addEventListener('resize', resize);