LCOV - code coverage report
Current view: top level - src/gui/components - screen.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 82.0 % 687 563
Test Date: 2026-06-03 09:13:19 Functions: 100.0 % 7 7

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

Generated by: LCOV version 2.0-1