LCOV - code coverage report
Current view: top level - src/gui/components - screen.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 71.5 % 743 531
Test Date: 2026-06-07 15:51:50 Functions: 100.0 % 7 7

            Line data    Source code
       1              : #include "gui/components/screen.h"
       2              : 
       3              : #include <algorithm>
       4              : #include <cmath>
       5              : #include <cstdio>
       6              : 
       7              : #include "audio/effects/amp_cab/cabinet_sim.h"
       8              : #include "audio/effects/dynamics/multiband_compressor.h"
       9              : #include "audio/effects/utility/looper.h"
      10              : #include "audio/effects/utility/tuner.h"
      11              : #include "audio/engine/audio_engine.h"
      12              : #include "common.h"
      13              : #include "gui/dialogs/file_dialog.h"
      14              : #include "gui/theme/theme.h"
      15              : #include "gui/views/gui_midi.h"
      16              : #include "midi/midi_manager.h"
      17              : 
      18              : #ifdef __EMSCRIPTEN__
      19              : #include <emscripten.h>
      20              : extern "C" {
      21              : EMSCRIPTEN_KEEPALIVE void load_ir_callback_screen(uintptr_t cab_ptr, const char* path) {
      22              :     if (cab_ptr && path) {
      23              :         auto* cab = reinterpret_cast<Amplitron::CabinetSim*>(cab_ptr);
      24              :         cab->load_ir(path);
      25              :     }
      26              : }
      27              : }
      28              : #endif
      29              : 
      30              : namespace Amplitron {
      31              : 
      32              : // Static variables to track active knob drag states across frames for accurate undo commitment
      33              : static bool s_knob_was_active = false;
      34              : static int s_active_param_index = -1;
      35              : static float s_param_value_before_drag = 0.0f;
      36            2 : static std::string s_active_knob_id = "";
      37              : 
      38              : static int s_popup_active_param_index = -1;
      39              : static float s_popup_param_value_before_edit = 0.0f;
      40            2 : static std::string s_active_popup_id = "";
      41              : 
      42          342 : void ScreenComponent::render(ImDrawList* dl, ImVec2 p0, float pedal_width, float zoom,
      43              :                              const ScreenProps& props) {
      44          342 :     if (!props.effect) return;
      45              : 
      46          342 :     switch (props.type) {
      47           24 :         case ScreenType::Tuner:
      48           36 :             render_tuner_display(dl, p0, pedal_width, zoom, props);
      49           36 :             break;
      50           36 :         case ScreenType::Cabinet:
      51           54 :             render_ir_cabinet_display(dl, p0, pedal_width, zoom, props);
      52           54 :             break;
      53           48 :         case ScreenType::Looper:
      54           72 :             render_looper_display(dl, p0, pedal_width, zoom, props);
      55           72 :             break;
      56          120 :         case ScreenType::MultiBandCompressor:
      57          180 :             render_multiband_compressor_display(dl, p0, pedal_width, zoom, props);
      58          180 :             break;
      59              :     }
      60          114 : }
      61              : 
      62           36 : void ScreenComponent::render_tuner_display(ImDrawList* dl, ImVec2 p0, float pedal_width, float zoom,
      63              :                                            const ScreenProps& props) {
      64           36 :     auto* tuner = dynamic_cast<TunerPedal*>(props.effect.get());
      65           36 :     if (tuner) {
      66           36 :         float cx = p0.x + pedal_width * 0.5f;
      67              : 
      68           36 :         bool has_signal = tuner->signal_detected.load(std::memory_order_relaxed);
      69           36 :         int note_idx = tuner->detected_note.load(std::memory_order_relaxed);
      70           36 :         int octave = tuner->detected_octave.load(std::memory_order_relaxed);
      71           36 :         float cents = tuner->detected_cents.load(std::memory_order_relaxed);
      72           36 :         float freq = tuner->detected_freq.load(std::memory_order_relaxed);
      73              : 
      74           36 :         float display_y = p0.y + 55 * zoom;
      75              : 
      76           36 :         if (has_signal && note_idx >= 0) {
      77            3 :             char note_buf[16];
      78            9 :             snprintf(note_buf, sizeof(note_buf), "%s%d", TunerPedal::note_name(note_idx), octave);
      79            9 :             ImVec2 note_size = ImGui::CalcTextSize(note_buf);
      80            9 :             float note_x = cx - note_size.x * 1.5f;
      81           12 :             dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 2.0f, ImVec2(note_x, display_y),
      82            3 :                         Theme::TEXT_PRIMARY, note_buf);
      83              : 
      84            9 :             display_y += 45 * zoom;
      85              : 
      86            3 :             char cents_buf[32];
      87            9 :             snprintf(cents_buf, sizeof(cents_buf), "%+.1f cents", cents);
      88            9 :             ImVec2 cents_text_size = ImGui::CalcTextSize(cents_buf);
      89            9 :             ImGui::SetCursorScreenPos(ImVec2(cx - cents_text_size.x * 0.5f, display_y));
      90            9 :             float abs_cents = std::fabs(cents);
      91           12 :             ImVec4 cents_col = (abs_cents < 2.0f)    ? ImVec4(0.2f, 0.9f, 0.3f, 1.0f)
      92            9 :                                : (abs_cents < 15.0f) ? ImVec4(0.9f, 0.8f, 0.2f, 1.0f)
      93            6 :                                                      : ImVec4(0.9f, 0.2f, 0.2f, 1.0f);
      94            9 :             ImGui::PushStyleColor(ImGuiCol_Text, cents_col);
      95            9 :             ImGui::TextUnformatted(cents_buf);
      96            9 :             ImGui::PopStyleColor();
      97              : 
      98            9 :             display_y += 22 * zoom;
      99              : 
     100            9 :             float bar_w = pedal_width - 30 * zoom;
     101            9 :             float bar_h = 10 * zoom;
     102            9 :             float bar_x = p0.x + 15 * zoom;
     103            9 :             float bar_y = display_y;
     104           12 :             dl->AddRectFilled(ImVec2(bar_x, bar_y), ImVec2(bar_x + bar_w, bar_y + bar_h),
     105            3 :                               Theme::KNOB_BG, 3.0f * zoom);
     106            9 :             float center_x = bar_x + bar_w * 0.5f;
     107           15 :             dl->AddLine(ImVec2(center_x, bar_y - 1 * zoom),
     108            9 :                         ImVec2(center_x, bar_y + bar_h + 1 * zoom), Theme::TEXT_DIM, 1.5f * zoom);
     109            9 :             float needle_norm = clamp(cents / 50.0f, -1.0f, 1.0f);
     110            9 :             float needle_x = center_x + needle_norm * (bar_w * 0.5f);
     111            9 :             ImU32 needle_col = ImGui::ColorConvertFloat4ToU32(cents_col);
     112           15 :             dl->AddRectFilled(ImVec2(needle_x - 3 * zoom, bar_y - 2 * zoom),
     113            9 :                               ImVec2(needle_x + 3 * zoom, bar_y + bar_h + 2 * zoom), needle_col,
     114            3 :                               2.0f * zoom);
     115              : 
     116            9 :             display_y += bar_h + 14 * zoom;
     117              : 
     118            3 :             char freq_buf[32];
     119            9 :             snprintf(freq_buf, sizeof(freq_buf), "%.1f Hz", freq);
     120            9 :             ImVec2 freq_size = ImGui::CalcTextSize(freq_buf);
     121            9 :             ImGui::SetCursorScreenPos(ImVec2(cx - freq_size.x * 0.5f, display_y));
     122            9 :             ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextSecondary());
     123            9 :             ImGui::TextUnformatted(freq_buf);
     124            9 :             ImGui::PopStyleColor();
     125              : 
     126            9 :             display_y += 22 * zoom;
     127            6 :         } else {
     128           27 :             const char* no_sig = "---";
     129           27 :             ImVec2 ns_size = ImGui::CalcTextSize(no_sig);
     130           36 :             dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 2.0f,
     131           27 :                         ImVec2(cx - ns_size.x * 1.5f, display_y), Theme::TEXT_DIM, no_sig);
     132           27 :             display_y += 45 * zoom;
     133              : 
     134           27 :             const char* waiting = "Play a note...";
     135           27 :             ImVec2 wt_size = ImGui::CalcTextSize(waiting);
     136           27 :             ImGui::SetCursorScreenPos(ImVec2(cx - wt_size.x * 0.5f, display_y));
     137           27 :             ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextDim());
     138           27 :             ImGui::TextUnformatted(waiting);
     139           27 :             ImGui::PopStyleColor();
     140              : 
     141           27 :             display_y += 22 * zoom;
     142              :         }
     143              : 
     144           36 :         display_y += 8 * zoom;
     145           36 :         bool mute_on = props.effect->params()[0].value >= 0.5f;
     146           36 :         const char* mute_label = mute_on ? "[MUTE ON]" : "[MUTE OFF]";
     147           36 :         ImVec2 ml_size = ImGui::CalcTextSize(mute_label);
     148           36 :         ImGui::SetCursorScreenPos(ImVec2(cx - ml_size.x * 0.5f, display_y));
     149           36 :         ImGui::PushStyleColor(ImGuiCol_Text, mute_on ? ImVec4(0.9f, 0.3f, 0.3f, 1.0f)
     150            3 :                                                      : ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
     151           36 :         ImGui::TextUnformatted(mute_label);
     152           36 :         ImGui::PopStyleColor();
     153              : 
     154           36 :         ImGui::SetCursorScreenPos(ImVec2(cx - ml_size.x * 0.5f, display_y));
     155           36 :         ImGui::SetNextItemAllowOverlap();
     156           36 :         ImGui::InvisibleButton("##tuner_mute_toggle", ml_size);
     157           36 :         if (ImGui::IsItemClicked()) {
     158            0 :             float new_val = mute_on ? 0.0f : 1.0f;
     159            0 :             props.effect->params()[0].value = new_val;
     160            0 :             if (props.engine) {
     161            0 :                 props.engine->push_param_change(props.index, 0, new_val);
     162            0 :             }
     163            0 :         }
     164           36 :         if (ImGui::IsItemHovered()) {
     165            0 :             if (!props.effect->params()[0].tooltip.empty()) {
     166            0 :                 ImGui::SetTooltip("Click to toggle mute\n\n%s",
     167            0 :                                   props.effect->params()[0].tooltip.c_str());
     168            0 :             } else {
     169            0 :                 ImGui::SetTooltip("Click to toggle mute");
     170              :             }
     171            0 :         }
     172           12 :     }
     173           36 : }
     174              : 
     175           54 : void ScreenComponent::render_ir_cabinet_display(ImDrawList* dl, ImVec2 p0, float pedal_width,
     176              :                                                 float zoom, const ScreenProps& props) {
     177           54 :     auto* ir_cab = dynamic_cast<CabinetSim*>(props.effect.get());
     178           54 :     if (ir_cab) {
     179           54 :         float cx = p0.x + pedal_width * 0.5f;
     180           54 :         float display_y = p0.y + 50 * zoom;
     181              : 
     182           54 :         float btn_w = pedal_width - 30 * zoom;
     183           54 :         ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
     184           54 :         ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.22f, 0.20f, 0.16f, 1.0f));
     185           54 :         ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.30f, 0.18f, 1.0f));
     186           54 :         ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.42f, 0.20f, 1.0f));
     187           18 :         char load_id[64];
     188           54 :         snprintf(load_id, sizeof(load_id), "Load IR##ir_load_%d", props.index);
     189           54 :         if (ImGui::Button(load_id, ImVec2(btn_w, 22 * zoom))) {
     190              : #ifdef __EMSCRIPTEN__
     191              :             EM_ASM(
     192              :                 {
     193              :                     var cab_ptr = $0;
     194              :                     var input = document.createElement('input');
     195              :                     input.type = 'file';
     196              :                     input.accept = '.wav';
     197              :                     input.onchange = function(e) {
     198              :                         var file = e.target.files[0];
     199              :                         var reader = new FileReader();
     200              :                         reader.onload = function(re) {
     201              :                             var data = new Uint8Array(re.target.result);
     202              :                             var path = "/ir_" + file.name;
     203              :                             FS.writeFile(path, data);
     204              :                             Module.ccall('load_ir_callback_screen', 'v', [ 'number', 'string' ],
     205              :                                          [ cab_ptr, path ]);
     206              :                         };
     207              :                         reader.readAsArrayBuffer(file);
     208              :                     };
     209              :                     input.click();
     210              :                 },
     211              :                 (uintptr_t)ir_cab);
     212              : #else
     213            0 :             std::string path = show_open_dialog("Load Impulse Response", "WAV Audio", "wav");
     214            0 :             if (!path.empty()) {
     215            0 :                 ir_cab->load_ir(path);
     216            0 :             }
     217              : #endif
     218            0 :         }
     219           54 :         ImGui::PopStyleColor(3);
     220              : 
     221           54 :         display_y += 28 * zoom;
     222              : 
     223           54 :         if (ir_cab->has_ir()) {
     224            9 :             const std::string& ir_name = ir_cab->ir_name();
     225            9 :             std::string display_name = ir_name;
     226            9 :             if (display_name.size() > 20) {
     227            0 :                 display_name = display_name.substr(0, 17) + "...";
     228            0 :             }
     229            9 :             ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str());
     230            9 :             ImGui::SetCursorScreenPos(ImVec2(cx - name_size.x * 0.5f, display_y));
     231            9 :             ImGui::PushStyleColor(ImGuiCol_Text, Theme::TEXT_PRIMARY);
     232            9 :             ImGui::TextUnformatted(display_name.c_str());
     233            9 :             ImGui::PopStyleColor();
     234              : 
     235            9 :             display_y += 18 * zoom;
     236              : 
     237            3 :             char dur_buf[32];
     238            9 :             snprintf(dur_buf, sizeof(dur_buf), "%.1f ms", ir_cab->ir_duration_ms());
     239            9 :             ImVec2 dur_size = ImGui::CalcTextSize(dur_buf);
     240            9 :             ImGui::SetCursorScreenPos(ImVec2(cx - dur_size.x * 0.5f, display_y));
     241            9 :             ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextSecondary());
     242            9 :             ImGui::TextUnformatted(dur_buf);
     243            9 :             ImGui::PopStyleColor();
     244              : 
     245            9 :             display_y += 22 * zoom;
     246              : 
     247            9 :             ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
     248            9 :             ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.12f, 0.10f, 1.0f));
     249            9 :             ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.15f, 0.12f, 1.0f));
     250            9 :             ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.20f, 0.15f, 1.0f));
     251            3 :             char clear_id[64];
     252            9 :             snprintf(clear_id, sizeof(clear_id), "Clear##ir_clear_%d", props.index);
     253            9 :             if (ImGui::Button(clear_id, ImVec2(btn_w, 20 * zoom))) {
     254            0 :                 ir_cab->clear_ir();
     255            0 :             }
     256            9 :             ImGui::PopStyleColor(3);
     257            9 :         } else {
     258           45 :             const char* no_ir = "No IR loaded";
     259           45 :             ImVec2 ni_size = ImGui::CalcTextSize(no_ir);
     260           45 :             ImGui::SetCursorScreenPos(ImVec2(cx - ni_size.x * 0.5f, display_y));
     261           45 :             ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextDim());
     262           45 :             ImGui::TextUnformatted(no_ir);
     263           45 :             ImGui::PopStyleColor();
     264              :         }
     265           18 :     }
     266           54 : }
     267              : 
     268           72 : void ScreenComponent::render_looper_display(ImDrawList* dl, ImVec2 p0, float pedal_width,
     269              :                                             float zoom, const ScreenProps& props) {
     270           72 :     auto* looper = dynamic_cast<Looper*>(props.effect.get());
     271           72 :     if (!looper) return;
     272              : 
     273           72 :     float cx = p0.x + pedal_width * 0.5f;
     274           72 :     float display_y = p0.y + 55 * zoom;
     275              : 
     276           72 :     Looper::State st = looper->state();
     277           72 :     bool has_loop = looper->has_loop();
     278           72 :     int loop_len = looper->loop_length_samples();
     279           72 :     int play_pos = looper->playhead_samples();
     280              : 
     281           72 :     const char* state_label = "EMPTY";
     282           72 :     ImVec4 state_col = Theme::TextDim();
     283           72 :     switch (st) {
     284           21 :         case Looper::State::Empty:
     285           42 :             state_label = "EMPTY";
     286           42 :             state_col = Theme::TextDim();
     287           42 :             break;
     288            0 :         case Looper::State::Idle:
     289            0 :             state_label = "STOP";
     290            0 :             state_col = Theme::TextSecondary();
     291            0 :             break;
     292            2 :         case Looper::State::Recording:
     293            3 :             state_label = "REC";
     294            3 :             state_col = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
     295            3 :             break;
     296            2 :         case Looper::State::Playing:
     297            3 :             state_label = "PLAY";
     298            3 :             state_col = ImVec4(0.2f, 0.9f, 0.3f, 1.0f);
     299            3 :             break;
     300            2 :         case Looper::State::Overdubbing:
     301            3 :             state_label = "DUB";
     302            3 :             state_col = ImVec4(0.95f, 0.80f, 0.25f, 1.0f);
     303            3 :             break;
     304              :     }
     305              : 
     306           72 :     ImVec2 st_size = ImGui::CalcTextSize(state_label);
     307           72 :     ImGui::SetCursorScreenPos(ImVec2(cx - st_size.x * 0.5f, display_y));
     308           72 :     ImGui::PushStyleColor(ImGuiCol_Text, state_col);
     309           72 :     ImGui::TextUnformatted(state_label);
     310           72 :     ImGui::PopStyleColor();
     311              : 
     312           72 :     display_y += 18 * zoom;
     313              : 
     314           72 :     float bar_w = pedal_width - 30 * zoom;
     315           72 :     float progress = 0.0f;
     316           72 :     if (has_loop && loop_len > 0) {
     317            6 :         progress = clamp(static_cast<float>(play_pos) / static_cast<float>(loop_len), 0.0f, 1.0f);
     318            2 :     }
     319           72 :     ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
     320           72 :     ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.12f, 0.11f, 0.10f, 1.0f));
     321           72 :     ImGui::PushStyleColor(ImGuiCol_PlotHistogram, state_col);
     322           72 :     ImGui::ProgressBar(progress, ImVec2(bar_w, 8 * zoom), "");
     323           72 :     ImGui::PopStyleColor(2);
     324              : 
     325           72 :     display_y += 16 * zoom;
     326              : 
     327           72 :     float btn_w_total = bar_w;
     328           72 :     float btn_gap = 8.0f * zoom;
     329           72 :     float btn_w = (btn_w_total - btn_gap) * 0.5f;
     330           72 :     float btn_h = 22.0f * zoom;
     331              : 
     332              :     // Row 1: Record / Play
     333           72 :     ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
     334           72 :     ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.12f, 0.12f, 1.0f));
     335           72 :     ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.50f, 0.18f, 0.18f, 1.0f));
     336           72 :     ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.65f, 0.22f, 0.22f, 1.0f));
     337           24 :     char rec_id[64];
     338           72 :     std::snprintf(rec_id, sizeof(rec_id), "Record##looper_rec_%d", props.index);
     339           72 :     if (ImGui::Button(rec_id, ImVec2(btn_w, btn_h))) {
     340            0 :         looper->request_record_toggle();
     341            0 :     }
     342           72 :     ImGui::PopStyleColor(3);
     343           72 :     if (ImGui::IsItemHovered()) ImGui::SetTooltip("Start/stop recording (new loop)");
     344              : 
     345           72 :     ImGui::SameLine(0.0f, btn_gap);
     346           72 :     ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.14f, 0.30f, 0.18f, 1.0f));
     347           72 :     ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.18f, 0.42f, 0.22f, 1.0f));
     348           72 :     ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.22f, 0.55f, 0.28f, 1.0f));
     349           24 :     char play_id[64];
     350           72 :     std::snprintf(play_id, sizeof(play_id), "Play/Stop##looper_play_%d", props.index);
     351           72 :     if (ImGui::Button(play_id, ImVec2(btn_w, btn_h))) {
     352            0 :         looper->request_play_toggle();
     353            0 :     }
     354           72 :     ImGui::PopStyleColor(3);
     355           72 :     if (ImGui::IsItemHovered()) ImGui::SetTooltip("Toggle playback (keeps loop in memory)");
     356              : 
     357           72 :     display_y += btn_h + 6 * zoom;
     358              : 
     359              :     // Row 2: Overdub / Clear
     360           72 :     ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
     361           72 :     ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.30f, 0.26f, 0.10f, 1.0f));
     362           72 :     ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.40f, 0.34f, 0.12f, 1.0f));
     363           72 :     ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.52f, 0.45f, 0.15f, 1.0f));
     364           24 :     char dub_id[64];
     365           72 :     std::snprintf(dub_id, sizeof(dub_id), "Overdub##looper_dub_%d", props.index);
     366           72 :     if (ImGui::Button(dub_id, ImVec2(btn_w, btn_h))) {
     367            0 :         looper->request_overdub_toggle();
     368            0 :     }
     369           72 :     ImGui::PopStyleColor(3);
     370           72 :     if (ImGui::IsItemHovered())
     371            3 :         ImGui::SetTooltip("Toggle overdub mode (record over existing loop)");
     372              : 
     373           72 :     ImGui::SameLine(0.0f, btn_gap);
     374           72 :     ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.22f, 0.12f, 0.10f, 1.0f));
     375           72 :     ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.15f, 0.12f, 1.0f));
     376           72 :     ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.20f, 0.15f, 1.0f));
     377           24 :     char clr_id[64];
     378           72 :     std::snprintf(clr_id, sizeof(clr_id), "Clear##looper_clear_%d", props.index);
     379           72 :     if (ImGui::Button(clr_id, ImVec2(btn_w, btn_h))) {
     380            0 :         looper->request_clear();
     381            0 :     }
     382           72 :     ImGui::PopStyleColor(3);
     383           72 :     if (ImGui::IsItemHovered()) ImGui::SetTooltip("Clear loop from memory");
     384              : 
     385           72 :     display_y += btn_h + 8 * zoom;
     386              : 
     387              :     // Loop Level slider (param 0)
     388           72 :     if (!props.effect->params().empty()) {
     389           72 :         float& level = props.effect->params()[0].value;
     390           72 :         ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
     391           72 :         ImGui::SetNextItemWidth(bar_w);
     392           24 :         char slider_id[64];
     393           72 :         std::snprintf(slider_id, sizeof(slider_id), "##looper_level_%d", props.index);
     394           72 :         if (ImGui::SliderFloat(slider_id, &level, 0.0f, 1.0f, "Loop Level: %.2f")) {
     395            0 :             level = clamp(level, 0.0f, 1.0f);
     396            0 :             if (props.engine) {
     397            0 :                 props.engine->push_param_change(props.index, 0, level);
     398            0 :             }
     399            0 :         }
     400              : 
     401           24 :         char popup_id[128];
     402           72 :         std::snprintf(popup_id, sizeof(popup_id), "Popup_%s", slider_id);
     403              : 
     404           72 :         if (ImGui::IsItemActivated()) {
     405            0 :             s_active_popup_id = popup_id;
     406            0 :             s_popup_active_param_index = 0;
     407            0 :             s_popup_param_value_before_edit = level;
     408            0 :         }
     409           72 :         if (ImGui::IsItemDeactivatedAfterEdit() && s_popup_active_param_index == 0 &&
     410            0 :             s_active_popup_id == popup_id) {
     411            0 :             if (level != s_popup_param_value_before_edit && props.on_commit_param_change) {
     412            0 :                 props.on_commit_param_change(0, s_popup_param_value_before_edit, level);
     413            0 :             }
     414            0 :             s_popup_active_param_index = -1;
     415            0 :             s_active_popup_id = "";
     416            0 :         }
     417           72 :         if (ImGui::IsItemHovered() && !props.effect->params()[0].tooltip.empty()) {
     418            0 :             ImGui::SetTooltip("%s", props.effect->params()[0].tooltip.c_str());
     419            0 :         }
     420           24 :     }
     421           24 : }
     422              : 
     423          180 : void ScreenComponent::render_multiband_compressor_display(ImDrawList* dl, ImVec2 p0,
     424              :                                                           float pedal_width, float zoom,
     425              :                                                           const ScreenProps& props) {
     426          180 :     auto* mb_comp = dynamic_cast<MultiBandCompressor*>(props.effect.get());
     427          180 :     if (!mb_comp) return;
     428              : 
     429          180 :     auto& params = props.effect->params();
     430          180 :     if (params.size() < 18) return;
     431              : 
     432          180 :     const auto* entry = get_effect_color(props.effect->name());
     433          180 :     ImVec4 led_color = entry ? entry->led_color : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
     434              : 
     435              :     // Outer boundaries
     436          180 :     ImVec2 p1 = ImVec2(p0.x + pedal_width, p0.y + Theme::PEDAL_HEIGHT * zoom);
     437              : 
     438              :     // Dynamic horizontal divider separating header/plate and controls
     439          300 :     dl->AddLine(ImVec2(p0.x + 8.0f * zoom, p0.y + 48.0f * zoom),
     440          180 :                 ImVec2(p1.x - 8.0f * zoom, p0.y + 48.0f * zoom), Theme::BORDER_DARK, 1.0f * zoom);
     441              : 
     442          180 :     float col_width = (pedal_width - 24.0f * zoom) / 3.0f;
     443              : 
     444              :     // --- REUSABLE KNOB HELPER (LAMBDA) ---
     445         3000 :     auto render_mb_knob = [&](ImDrawList* dl, ImVec2 center, int pi, float radius,
     446              :                               const char* label_prefix) {
     447         2880 :         auto& param = params[pi];
     448          960 :         char label[64];
     449         3840 :         std::snprintf(label, sizeof(label), "##knob_%s_%d_%d_%s", props.effect->name(), props.index,
     450          960 :                       pi, label_prefix);
     451              : 
     452         2880 :         float r = radius * zoom;
     453         2880 :         float knob_hit_size = r * Theme::KNOB_HIT_MULT;
     454              : 
     455         2880 :         ImGui::SetCursorScreenPos(
     456         2880 :             ImVec2(center.x - knob_hit_size * 0.5f, center.y - knob_hit_size * 0.5f));
     457         2880 :         ImGui::SetNextItemAllowOverlap();
     458         2880 :         ImGui::InvisibleButton(label, ImVec2(knob_hit_size, knob_hit_size));
     459              : 
     460         2880 :         bool is_hovered = ImGui::IsItemHovered();
     461         2880 :         bool is_active = ImGui::IsItemActive();
     462              : 
     463         2880 :         float range = param.max_val - param.min_val;
     464         2880 :         if (range <= 0.0001f) range = 1.0f;
     465              : 
     466         2880 :         if (is_active && !s_knob_was_active) {
     467            3 :             s_knob_was_active = true;
     468            3 :             s_active_param_index = pi;
     469            3 :             s_param_value_before_drag = param.value;
     470            3 :             s_active_knob_id = label;
     471            1 :         }
     472              : 
     473         1921 :         if (is_active && s_active_knob_id == label) {
     474            3 :             float mdy = ImGui::GetIO().MousePos.y - ImGui::GetIO().MousePosPrev.y;
     475            3 :             if (mdy != 0.0f) {
     476            0 :                 float sensitivity = 0.005f;
     477            0 :                 float value_delta = -mdy * sensitivity * range;
     478            0 :                 if (ImGui::GetIO().KeyShift) value_delta *= 0.2f;
     479            0 :                 if (ImGui::GetIO().KeyCtrl) value_delta *= 3.0f;
     480              : 
     481            0 :                 float new_val = clamp(param.value + value_delta, param.min_val, param.max_val);
     482            0 :                 if (new_val != param.value) {
     483            0 :                     param.value = new_val;
     484            0 :                     if (props.engine) {
     485            0 :                         props.engine->push_param_change(props.index, pi, new_val);
     486            0 :                     }
     487            0 :                 }
     488            0 :             }
     489            1 :         }
     490              : 
     491         2881 :         if (s_knob_was_active && !is_active && s_active_param_index == pi &&
     492            3 :             s_active_knob_id == label) {
     493            3 :             float new_val = param.value;
     494            3 :             if (new_val != s_param_value_before_drag && props.on_commit_param_change) {
     495            0 :                 props.on_commit_param_change(pi, s_param_value_before_drag, new_val);
     496            0 :             }
     497            3 :             s_active_param_index = -1;
     498            3 :             s_knob_was_active = false;
     499            3 :             s_active_knob_id = "";
     500            1 :         }
     501              : 
     502         2880 :         if (is_hovered && std::fabs(ImGui::GetIO().MouseWheel) > 0.0f) {
     503            3 :             float old_val = param.value;
     504            3 :             float step = range * 0.03f;
     505            3 :             if (ImGui::GetIO().KeyShift) step *= 0.2f;
     506            2 :             float new_val =
     507            3 :                 clamp(param.value + ImGui::GetIO().MouseWheel * step, param.min_val, param.max_val);
     508            3 :             if (new_val != old_val) {
     509            3 :                 param.value = new_val;
     510            3 :                 if (props.engine) {
     511            3 :                     props.engine->push_param_change(props.index, pi, new_val);
     512            1 :                 }
     513            3 :                 if (props.on_commit_param_change) {
     514            3 :                     props.on_commit_param_change(pi, old_val, new_val);
     515            1 :                 }
     516            1 :             }
     517            1 :         }
     518              : 
     519         2880 :         if (is_hovered && ImGui::IsMouseDoubleClicked(0)) {
     520            0 :             float old_val = param.value;
     521            0 :             float new_val = param.default_val;
     522            0 :             if (new_val != old_val) {
     523            0 :                 param.value = new_val;
     524            0 :                 if (props.engine) {
     525            0 :                     props.engine->push_param_change(props.index, pi, new_val);
     526            0 :                 }
     527            0 :                 if (props.on_commit_param_change) {
     528            0 :                     props.on_commit_param_change(pi, old_val, new_val);
     529            0 :                 }
     530            0 :             }
     531            0 :         }
     532              : 
     533         2880 :         if (is_hovered && ImGui::IsMouseClicked(1)) {
     534            0 :             ImGui::OpenPopup(label);
     535            0 :         }
     536         2880 :         if (ImGui::BeginPopup(label)) {
     537            0 :             ImGui::Text("%s", param.name.c_str());
     538            0 :             ImGui::SetNextItemWidth(120);
     539            0 :             float slider_val = param.value;
     540            0 :             if (ImGui::SliderFloat("##edit", &slider_val, param.min_val, param.max_val, "%.2f")) {
     541            0 :                 param.value = slider_val;
     542            0 :                 if (props.engine) {
     543            0 :                     props.engine->push_param_change(props.index, pi, slider_val);
     544            0 :                 }
     545            0 :             }
     546            0 :             if (ImGui::IsItemActivated()) {
     547            0 :                 s_active_popup_id = label;
     548            0 :                 s_popup_active_param_index = pi;
     549            0 :                 s_popup_param_value_before_edit = param.value;
     550            0 :             }
     551            0 :             if (ImGui::IsItemDeactivatedAfterEdit() && s_popup_active_param_index == pi &&
     552            0 :                 s_active_popup_id == label) {
     553            0 :                 if (param.value != s_popup_param_value_before_edit &&
     554            0 :                     props.on_commit_param_change) {
     555            0 :                     props.on_commit_param_change(pi, s_popup_param_value_before_edit, param.value);
     556            0 :                 }
     557            0 :                 s_popup_active_param_index = -1;
     558            0 :                 s_active_popup_id = "";
     559            0 :             }
     560            0 :             if (ImGui::Button("Reset")) {
     561            0 :                 float old_val = param.value;
     562            0 :                 float new_val = param.default_val;
     563            0 :                 if (new_val != old_val) {
     564            0 :                     param.value = new_val;
     565            0 :                     if (props.engine) {
     566            0 :                         props.engine->push_param_change(props.index, pi, new_val);
     567            0 :                     }
     568            0 :                     if (props.on_commit_param_change) {
     569            0 :                         props.on_commit_param_change(pi, old_val, new_val);
     570            0 :                     }
     571            0 :                 }
     572            0 :                 ImGui::CloseCurrentPopup();
     573            0 :             }
     574            0 :             ImGui::Separator();
     575            0 :             ImGui::TextColored(Theme::Gold(), "MIDI Control");
     576            0 :             if (props.gui_midi) {
     577            0 :                 if (props.gui_midi->render_remove_mapping_item(props.effect->name(), param.name)) {
     578            0 :                     ImGui::CloseCurrentPopup();
     579            0 :                 }
     580            0 :                 if (props.gui_midi->render_learn_menu_item(props.effect->name(), param.name)) {
     581            0 :                     ImGui::CloseCurrentPopup();
     582            0 :                 }
     583            0 :                 ImGui::Spacing();
     584            0 :                 if (props.gui_midi->render_remove_bypass_item(props.effect->name())) {
     585            0 :                     ImGui::CloseCurrentPopup();
     586            0 :                 }
     587            0 :                 if (props.gui_midi->render_learn_bypass_item(props.effect->name())) {
     588            0 :                     ImGui::CloseCurrentPopup();
     589            0 :                 }
     590            0 :             } else {
     591            0 :                 ImGui::TextDisabled("MIDI manager not available");
     592              :             }
     593            0 :             ImGui::EndPopup();
     594            0 :         }
     595              : 
     596              :         // Draw track
     597         2880 :         float normalized = (param.value - param.min_val) / range;
     598         2880 :         constexpr float ARC_START = 2.356f;
     599         2880 :         constexpr float ARC_RANGE = 4.712f;
     600         2880 :         float track_radius = r + 2.5f * zoom;
     601         2880 :         int segments = 30;
     602        89280 :         for (int s = 0; s < segments; ++s) {
     603        86400 :             float t0 = static_cast<float>(s) / segments;
     604        86400 :             float t1 = static_cast<float>(s + 1) / segments;
     605        86400 :             float a0 = ARC_START + t0 * ARC_RANGE;
     606        86400 :             float a1 = ARC_START + t1 * ARC_RANGE;
     607              : 
     608        86400 :             bool filled = t0 <= normalized;
     609        57600 :             ImU32 seg_color =
     610        86400 :                 filled ? ImGui::ColorConvertFloat4ToU32(led_color) : Theme::KNOB_TRACK_OFF;
     611              : 
     612       172800 :             dl->AddLine(ImVec2(center.x + std::cos(a0) * track_radius,
     613        86400 :                                center.y + std::sin(a0) * track_radius),
     614       144000 :                         ImVec2(center.x + std::cos(a1) * track_radius,
     615        86400 :                                center.y + std::sin(a1) * track_radius),
     616        86400 :                         seg_color, 2.0f * zoom);
     617        28800 :         }
     618              : 
     619         2872 :         ImU32 knob_bg =
     620         2880 :             is_active ? Theme::KNOB_ACTIVE : (is_hovered ? Theme::KNOB_HOVER : Theme::KNOB_FACE);
     621         2880 :         dl->AddCircleFilled(center, r, Theme::KNOB_BG);
     622         2880 :         dl->AddCircleFilled(center, r - 1.0f * zoom, knob_bg);
     623              : 
     624              : #ifndef AMPLITRON_NO_MIDI
     625         1568 :         if (props.gui_midi && props.gui_midi->midi().is_learning() &&
     626         2224 :             props.gui_midi->midi().learn_effect_name() == props.effect->name() &&
     627            0 :             props.gui_midi->midi().learn_param_name() == param.name) {
     628            0 :             float time = static_cast<float>(ImGui::GetTime());
     629            0 :             float alpha = (std::sin(time * 2.0f * 3.14159f * 10.0f) + 1.0f) * 0.5f;
     630            0 :             ImU32 outline_col =
     631            0 :                 ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.6f, 1.0f, 0.4f + alpha * 0.6f));
     632            0 :             dl->AddCircle(center, r + 3.0f * zoom, outline_col, 0, 2.0f * zoom);
     633            0 :         }
     634              : #endif
     635              : 
     636         2880 :         float pointer_angle = ARC_START + normalized * ARC_RANGE;
     637         2880 :         float ptr_inner = r * 0.25f;
     638         2880 :         float ptr_outer = r - 2.0f * zoom;
     639         4800 :         ImVec2 ptr_from = ImVec2(center.x + std::cos(pointer_angle) * ptr_inner,
     640         2880 :                                  center.y + std::sin(pointer_angle) * ptr_inner);
     641         4800 :         ImVec2 ptr_to = ImVec2(center.x + std::cos(pointer_angle) * ptr_outer,
     642         2880 :                                center.y + std::sin(pointer_angle) * ptr_outer);
     643         2880 :         ImU32 ptr_color = is_active ? Theme::ACCENT_GOLD_HOT : Theme::ACCENT_GOLD;
     644         2880 :         dl->AddLine(ptr_from, ptr_to, ptr_color, 2.0f * zoom);
     645              : 
     646              :         // Tooltip
     647         2880 :         if (is_hovered || is_active) {
     648           24 :             std::string val_str = Theme::formatParameterValue(param.value, param.unit);
     649           24 :             std::string min_str = Theme::formatParameterValue(param.min_val, param.unit);
     650           24 :             std::string max_str = Theme::formatParameterValue(param.max_val, param.unit);
     651            8 :             std::string midi_info =
     652           32 :                 props.gui_midi ? props.gui_midi->get_mapping_info(props.effect->name(), param.name)
     653           24 :                                : "";
     654              : 
     655           24 :             if (param.tooltip.empty()) {
     656            0 :                 ImGui::SetTooltip("%s: %s\nRange: [%s, %s]%s", param.name.c_str(), val_str.c_str(),
     657            0 :                                   min_str.c_str(), max_str.c_str(), midi_info.c_str());
     658            0 :             } else {
     659           32 :                 ImGui::SetTooltip("%s: %s\nRange: [%s, %s]\n\n%s%s", param.name.c_str(),
     660            8 :                                   val_str.c_str(), min_str.c_str(), max_str.c_str(),
     661            8 :                                   param.tooltip.c_str(), midi_info.c_str());
     662              :             }
     663           24 :         }
     664              : 
     665              :         // Labels
     666         2880 :         const char* short_name = param.name.c_str();
     667         2880 :         if (std::strncmp(short_name, "Low ", 4) == 0)
     668          900 :             short_name += 4;
     669         1980 :         else if (std::strncmp(short_name, "Mid ", 4) == 0)
     670          900 :             short_name += 4;
     671         1080 :         else if (std::strncmp(short_name, "High ", 5) == 0)
     672          900 :             short_name += 5;
     673              : 
     674         2880 :         ImVec2 text_size = ImGui::CalcTextSize(short_name);
     675         3840 :         dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.85f,
     676         2880 :                     ImVec2(center.x - text_size.x * 0.5f, center.y + r + 5.0f * zoom),
     677          960 :                     Theme::TEXT_SECONDARY, short_name);
     678              : 
     679         2880 :         std::string val_display = Theme::formatParameterValue(param.value, param.unit);
     680         2880 :         ImVec2 val_size = ImGui::CalcTextSize(val_display.c_str());
     681         4799 :         dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.75f,
     682         3840 :                     ImVec2(center.x - val_size.x * 0.5f, center.y - r - 13.0f * zoom),
     683          960 :                     is_active ? Theme::ACCENT_GOLD_HOT : Theme::TEXT_DIM, val_display.c_str());
     684         2880 :     };
     685              : 
     686              :     // --- REUSABLE SLIDER HELPER (LAMBDA) ---
     687          480 :     auto render_xover_slider = [&](ImDrawList* dl, float track_x, int pi, const char* label_prefix,
     688              :                                    bool ticks_on_left) {
     689          120 :         (void)ticks_on_left;
     690          360 :         auto& param = params[pi];
     691          120 :         char label[64];
     692          480 :         std::snprintf(label, sizeof(label), "##slider_%s_%d_%d_%s", props.effect->name(),
     693          360 :                       props.index, pi, label_prefix);
     694              : 
     695          360 :         float track_top = p0.y + 90.0f * zoom;
     696          360 :         float track_bottom = p0.y + 260.0f * zoom;
     697          360 :         float range = param.max_val - param.min_val;
     698          360 :         if (range <= 0.0001f) range = 1.0f;
     699          360 :         float normalized = (param.value - param.min_val) / range;
     700          360 :         float handle_y = track_bottom - normalized * (track_bottom - track_top);
     701              : 
     702              :         // Click-and-drag detection box
     703          360 :         ImGui::SetCursorScreenPos(ImVec2(track_x - 12.0f * zoom, track_top));
     704          360 :         ImGui::SetNextItemAllowOverlap();
     705          360 :         ImGui::InvisibleButton(label, ImVec2(24.0f * zoom, track_bottom - track_top));
     706              : 
     707          360 :         bool is_hovered = ImGui::IsItemHovered();
     708          360 :         bool is_active = ImGui::IsItemActive();
     709              : 
     710          360 :         if (is_active && !s_knob_was_active) {
     711            0 :             s_knob_was_active = true;
     712            0 :             s_active_param_index = pi;
     713            0 :             s_param_value_before_drag = param.value;
     714            0 :             s_active_knob_id = label;
     715            0 :         }
     716              : 
     717          240 :         if (is_active && s_active_knob_id == label) {
     718            0 :             float my = ImGui::GetIO().MousePos.y;
     719            0 :             float norm = (track_bottom - my) / (track_bottom - track_top);
     720            0 :             norm = clamp(norm, 0.0f, 1.0f);
     721            0 :             float new_val = param.min_val + norm * range;
     722              : 
     723              :             // Prevent crossover overlap
     724            0 :             if (pi == 0) {
     725            0 :                 float high_val = params[1].value;
     726            0 :                 if (new_val >= high_val) new_val = high_val - 10.0f;
     727            0 :             } else if (pi == 1) {
     728            0 :                 float low_val = params[0].value;
     729            0 :                 if (new_val <= low_val) new_val = low_val + 10.0f;
     730            0 :             }
     731              : 
     732            0 :             if (new_val != param.value) {
     733            0 :                 param.value = new_val;
     734            0 :                 if (props.engine) {
     735            0 :                     props.engine->push_param_change(props.index, pi, new_val);
     736            0 :                 }
     737            0 :             }
     738            0 :         }
     739              : 
     740          360 :         if (s_knob_was_active && !is_active && s_active_param_index == pi &&
     741            0 :             s_active_knob_id == label) {
     742            0 :             float new_val = param.value;
     743            0 :             if (new_val != s_param_value_before_drag && props.on_commit_param_change) {
     744            0 :                 props.on_commit_param_change(pi, s_param_value_before_drag, new_val);
     745            0 :             }
     746            0 :             s_active_param_index = -1;
     747            0 :             s_knob_was_active = false;
     748            0 :             s_active_knob_id = "";
     749            0 :         }
     750              : 
     751          360 :         if (is_hovered && std::fabs(ImGui::GetIO().MouseWheel) > 0.0f) {
     752            3 :             float old_val = param.value;
     753            3 :             float step = range * 0.02f;
     754            3 :             if (ImGui::GetIO().KeyShift) step *= 0.2f;
     755            2 :             float new_val =
     756            3 :                 clamp(param.value + ImGui::GetIO().MouseWheel * step, param.min_val, param.max_val);
     757              : 
     758              :             // Prevent crossover overlap
     759            3 :             if (pi == 0) {
     760            3 :                 float high_val = params[1].value;
     761            3 :                 if (new_val >= high_val) new_val = high_val - 10.0f;
     762            1 :             } else if (pi == 1) {
     763            0 :                 float low_val = params[0].value;
     764            0 :                 if (new_val <= low_val) new_val = low_val + 10.0f;
     765            0 :             }
     766              : 
     767            3 :             if (new_val != old_val) {
     768            3 :                 param.value = new_val;
     769            3 :                 if (props.engine) {
     770            3 :                     props.engine->push_param_change(props.index, pi, new_val);
     771            1 :                 }
     772            3 :                 if (props.on_commit_param_change) {
     773            3 :                     props.on_commit_param_change(pi, old_val, new_val);
     774            1 :                 }
     775            1 :             }
     776            1 :         }
     777              : 
     778          360 :         if (is_hovered && ImGui::IsMouseDoubleClicked(0)) {
     779            0 :             float old_val = param.value;
     780            0 :             float new_val = param.default_val;
     781              : 
     782              :             // Prevent crossover overlap on reset
     783            0 :             if (pi == 0) {
     784            0 :                 float high_val = params[1].value;
     785            0 :                 if (new_val >= high_val) new_val = high_val - 10.0f;
     786            0 :             } else if (pi == 1) {
     787            0 :                 float low_val = params[0].value;
     788            0 :                 if (new_val <= low_val) new_val = low_val + 10.0f;
     789            0 :             }
     790              : 
     791            0 :             if (new_val != old_val) {
     792            0 :                 param.value = new_val;
     793            0 :                 if (props.engine) {
     794            0 :                     props.engine->push_param_change(props.index, pi, new_val);
     795            0 :                 }
     796            0 :                 if (props.on_commit_param_change) {
     797            0 :                     props.on_commit_param_change(pi, old_val, new_val);
     798            0 :                 }
     799            0 :             }
     800            0 :         }
     801              : 
     802              :         // Draw track vertical line
     803          600 :         dl->AddRectFilled(ImVec2(track_x - 1.5f * zoom, track_top),
     804          360 :                           ImVec2(track_x + 1.5f * zoom, track_bottom), Theme::KNOB_TRACK_OFF,
     805          360 :                           1.5f * zoom);
     806              : 
     807              :         // Draw Ticks & Labels
     808          360 :         if (pi == 0) {  // Low crossover (50 to 1000 Hz)
     809          180 :             float tick_hzs[] = {50.0f, 200.0f, 500.0f, 1000.0f};
     810          900 :             for (float hz : tick_hzs) {
     811          720 :                 float norm = (hz - param.min_val) / range;
     812          720 :                 float ty = track_bottom - norm * (track_bottom - track_top);
     813          960 :                 dl->AddLine(ImVec2(track_x - 4.0f * zoom, ty), ImVec2(track_x, ty),
     814          240 :                             Theme::BORDER_LIGHT, 1.0f * zoom);
     815              : 
     816          240 :                 char tick_lbl[16];
     817          720 :                 if (hz >= 1000.0f)
     818          180 :                     std::snprintf(tick_lbl, sizeof(tick_lbl), "1k");
     819              :                 else
     820          540 :                     std::snprintf(tick_lbl, sizeof(tick_lbl), "%.0f", hz);
     821              : 
     822          720 :                 ImVec2 tsz = ImGui::CalcTextSize(tick_lbl);
     823          960 :                 dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.65f,
     824         1200 :                             ImVec2(track_x - 6.0f * zoom - tsz.x, ty - tsz.y * 0.5f),
     825          240 :                             Theme::TEXT_DIM, tick_lbl);
     826              :             }
     827           60 :         } else {  // High crossover (1000 to 15000 Hz)
     828          180 :             float tick_hzs[] = {1000.0f, 4000.0f, 8000.0f, 12000.0f, 15000.0f};
     829         1080 :             for (float hz : tick_hzs) {
     830          900 :                 float norm = (hz - param.min_val) / range;
     831          900 :                 float ty = track_bottom - norm * (track_bottom - track_top);
     832         1200 :                 dl->AddLine(ImVec2(track_x, ty), ImVec2(track_x + 4.0f * zoom, ty),
     833          300 :                             Theme::BORDER_LIGHT, 1.0f * zoom);
     834              : 
     835          300 :                 char tick_lbl[16];
     836          900 :                 if (hz >= 1000.0f)
     837          900 :                     std::snprintf(tick_lbl, sizeof(tick_lbl), "%.0fk", hz / 1000.0f);
     838              :                 else
     839            0 :                     std::snprintf(tick_lbl, sizeof(tick_lbl), "%.0f", hz);
     840              : 
     841          900 :                 ImVec2 tsz = ImGui::CalcTextSize(tick_lbl);
     842         1200 :                 dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.65f,
     843         1500 :                             ImVec2(track_x + 6.0f * zoom, ty - tsz.y * 0.5f), Theme::TEXT_DIM,
     844          300 :                             tick_lbl);
     845              :             }
     846              :         }
     847              : 
     848              :         // Draw pill handle
     849          360 :         ImVec2 handle_center = ImVec2(track_x, handle_y);
     850          360 :         ImU32 handle_bg =
     851          360 :             is_active ? Theme::KNOB_ACTIVE : (is_hovered ? Theme::KNOB_HOVER : Theme::KNOB_FACE);
     852          357 :         ImU32 border_col = (is_active || is_hovered) ? Theme::ACCENT_GOLD_HOT : Theme::ACCENT_GOLD;
     853              : 
     854          600 :         dl->AddRectFilled(ImVec2(track_x - 8.0f * zoom, handle_y - 5.0f * zoom),
     855          360 :                           ImVec2(track_x + 8.0f * zoom, handle_y + 5.0f * zoom), Theme::KNOB_BG,
     856          360 :                           3.0f * zoom);
     857          600 :         dl->AddRectFilled(ImVec2(track_x - 7.0f * zoom, handle_y - 4.0f * zoom),
     858          360 :                           ImVec2(track_x + 7.0f * zoom, handle_y + 4.0f * zoom), handle_bg,
     859          360 :                           2.0f * zoom);
     860          600 :         dl->AddRect(ImVec2(track_x - 8.0f * zoom, handle_y - 5.0f * zoom),
     861          360 :                     ImVec2(track_x + 8.0f * zoom, handle_y + 5.0f * zoom), border_col, 3.0f * zoom,
     862          360 :                     0, 1.5f * zoom);
     863          360 :         dl->AddCircleFilled(handle_center, 2.0f * zoom, border_col);
     864              : 
     865              :         // Value text at the top of the track
     866          360 :         std::string val_str = Theme::formatParameterValue(param.value, param.unit);
     867          360 :         ImVec2 vsz = ImGui::CalcTextSize(val_str.c_str());
     868          600 :         dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.75f,
     869          360 :                     ImVec2(track_x - vsz.x * 0.5f, track_top - vsz.y - 4.0f * zoom),
     870          120 :                     is_active ? Theme::ACCENT_GOLD_HOT : Theme::TEXT_SECONDARY, val_str.c_str());
     871              : 
     872              :         // Header text above the value
     873          360 :         const char* s_name = (pi == 0) ? "Low X" : "High X";
     874          360 :         ImVec2 nsz = ImGui::CalcTextSize(s_name);
     875          360 :         dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.75f,
     876          360 :                     ImVec2(track_x - nsz.x * 0.5f, track_top - vsz.y - nsz.y - 6.0f * zoom),
     877          120 :                     Theme::TEXT_DIM, s_name);
     878              : 
     879          360 :         if (is_hovered || is_active) {
     880            3 :             std::string midi_info =
     881           12 :                 props.gui_midi ? props.gui_midi->get_mapping_info(props.effect->name(), param.name)
     882            9 :                                : "";
     883           18 :             ImGui::SetTooltip(
     884              :                 "%s: %s\nRange: [%s, %s]%s\n\nDrag vertically to adjust\nShift=fine, "
     885              :                 "Ctrl=coarse\nDbl-click to reset",
     886            3 :                 param.name.c_str(), val_str.c_str(),
     887           15 :                 Theme::formatParameterValue(param.min_val, param.unit).c_str(),
     888           15 :                 Theme::formatParameterValue(param.max_val, param.unit).c_str(), midi_info.c_str());
     889            9 :         }
     890          360 :     };
     891              : 
     892              :     // --- RENDER 3 COLUMNS & THEIR METERS/KNOBS ---
     893          180 :     const char* titles[3] = {"LOW BAND", "MID BAND", "HIGH BAND"};
     894          180 :     int band_param_offsets[3] = {2, 7, 12};
     895              : 
     896          720 :     for (int b = 0; b < 3; ++b) {
     897          540 :         float col_left = p0.x + 12.0f * zoom + b * col_width;
     898          540 :         float col_center = col_left + col_width * 0.5f;
     899              : 
     900              :         // Title
     901          540 :         ImVec2 tsz = ImGui::CalcTextSize(titles[b]);
     902          720 :         dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.9f,
     903          540 :                     ImVec2(col_center - tsz.x * 0.5f, p0.y + 55.0f * zoom), Theme::TEXT_PRIMARY,
     904          180 :                     titles[b]);
     905              : 
     906              :         // Horizontal Gain Reduction Meter
     907          540 :         float meter_y = p0.y + 76.0f * zoom;
     908          540 :         float meter_h = 7.0f * zoom;
     909          540 :         float meter_w = col_width - 24.0f * zoom;
     910          540 :         float meter_x = col_left + 12.0f * zoom;
     911              : 
     912          720 :         dl->AddRectFilled(ImVec2(meter_x, meter_y), ImVec2(meter_x + meter_w, meter_y + meter_h),
     913          180 :                           Theme::METER_BG, 3.0f * zoom);
     914          720 :         dl->AddRect(ImVec2(meter_x, meter_y), ImVec2(meter_x + meter_w, meter_y + meter_h),
     915          180 :                     Theme::BORDER_DARK, 3.0f * zoom, 0, 1.0f * zoom);
     916              : 
     917          540 :         float gr_db = mb_comp->get_gain_reduction_db(b);
     918          540 :         float norm_gr = clamp(gr_db / 20.0f, 0.0f, 1.0f);
     919              : 
     920          540 :         if (norm_gr > 0.0f) {
     921            0 :             float fill_x1 = meter_x + meter_w;
     922            0 :             float fill_x0 = meter_x + meter_w - norm_gr * meter_w;
     923            0 :             ImU32 fill_color = Theme::METER_GREEN;
     924            0 :             if (gr_db > 12.0f)
     925            0 :                 fill_color = Theme::METER_RED;
     926            0 :             else if (gr_db > 6.0f)
     927            0 :                 fill_color = Theme::METER_YELLOW;
     928              : 
     929            0 :             dl->AddRectFilled(ImVec2(fill_x0, meter_y + 1.0f * zoom),
     930            0 :                               ImVec2(fill_x1 - 1.0f * zoom, meter_y + meter_h - 1.0f * zoom),
     931            0 :                               fill_color, 2.0f * zoom);
     932            0 :         }
     933              : 
     934              :         // GR Meter Ticks
     935          540 :         float tick_dbs[] = {0.0f, -3.0f, -6.0f, -12.0f, -20.0f};
     936         3240 :         for (float db : tick_dbs) {
     937         2700 :             float t_norm = -db / 20.0f;
     938         2700 :             float tx = meter_x + meter_w * (1.0f - t_norm);
     939         3600 :             dl->AddLine(ImVec2(tx, meter_y), ImVec2(tx, meter_y + meter_h + 2.0f * zoom),
     940          900 :                         Theme::BORDER_MID, 1.0f * zoom);
     941              :         }
     942              : 
     943              :         // Render Knobs
     944          540 :         int p_offset = band_param_offsets[b];
     945          540 :         float k_radius = 12.0f;
     946              : 
     947          540 :         float kx_left = col_left + col_width * 0.28f;
     948          540 :         float kx_right = col_left + col_width * 0.72f;
     949              : 
     950              :         // Row 1: Threshold & Ratio
     951          540 :         render_mb_knob(dl, ImVec2(kx_left, p0.y + 120.0f * zoom), p_offset + 0, k_radius, "thresh");
     952          540 :         render_mb_knob(dl, ImVec2(kx_right, p0.y + 120.0f * zoom), p_offset + 1, k_radius, "ratio");
     953              : 
     954              :         // Row 2: Attack & Release
     955          540 :         render_mb_knob(dl, ImVec2(kx_left, p0.y + 185.0f * zoom), p_offset + 2, k_radius, "attack");
     956          540 :         render_mb_knob(dl, ImVec2(kx_right, p0.y + 185.0f * zoom), p_offset + 3, k_radius,
     957              :                        "release");
     958              : 
     959              :         // Row 3: Makeup (Centered)
     960          540 :         render_mb_knob(dl, ImVec2(col_center, p0.y + 248.0f * zoom), p_offset + 4, k_radius,
     961              :                        "makeup");
     962          180 :     }
     963              : 
     964              :     // --- RENDER 2 INTERACTIVE CROSSOVER SLIDERS ---
     965          180 :     float x1 = p0.x + 12.0f * zoom + col_width;
     966          180 :     float x2 = p0.x + 12.0f * zoom + 2.0f * col_width;
     967              : 
     968          180 :     render_xover_slider(dl, x1, 0, "low", true);
     969          180 :     render_xover_slider(dl, x2, 1, "high", false);
     970              : 
     971              :     // --- RENDER GLOBAL OUT GAIN ---
     972          240 :     render_mb_knob(dl,
     973          240 :                    ImVec2(p0.x + pedal_width - 40.0f * zoom,
     974          240 :                           p0.y + Theme::PEDAL_HEIGHT * zoom - Theme::SWITCH_BOTTOM_OFFSET * zoom +
     975          180 :                               10.0f * zoom),
     976              :                    17, 13.0f, "outgain");
     977           60 : }
     978              : 
     979              : }  // namespace Amplitron
        

Generated by: LCOV version 2.0-1