LCOV - code coverage report
Current view: top level - src/gui/components - knob.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 82.6 % 201 166
Test Date: 2026-06-07 15:51:50 Functions: 100.0 % 1 1

            Line data    Source code
       1              : #include "gui/components/knob.h"
       2              : 
       3              : #include <algorithm>
       4              : #include <cmath>
       5              : #include <cstdio>
       6              : 
       7              : #include "common.h"
       8              : #include "gui/theme/theme.h"
       9              : 
      10              : namespace Amplitron {
      11              : 
      12              : // Static variables to track active knob drag states across frames for accurate undo commitment
      13              : static bool s_knob_was_active = false;
      14              : static float s_param_value_before_drag = 0.0f;
      15            2 : static std::string s_active_knob_id = "";
      16              : 
      17              : static float s_popup_param_value_before_edit = 0.0f;
      18            2 : static std::string s_active_popup_id = "";
      19              : 
      20          741 : void KnobComponent::render(const char* imgui_id, const KnobProps& props, float zoom,
      21              :                            ImVec2 center) {
      22          741 :     ImDrawList* dl = ImGui::GetWindowDrawList();
      23              : 
      24          741 :     float knob_radius = Theme::KNOB_RADIUS * zoom;
      25          741 :     float knob_hit_size = knob_radius * Theme::KNOB_HIT_MULT;
      26              : 
      27          741 :     constexpr float PI = 3.14159265f;
      28          741 :     constexpr float TWO_PI = 6.28318530f;
      29          741 :     constexpr float ARC_START = 2.356f;
      30          741 :     constexpr float ARC_RANGE = 4.712f;
      31              : 
      32          741 :     ImGui::SetCursorScreenPos(
      33          741 :         ImVec2(center.x - knob_hit_size * 0.5f, center.y - knob_hit_size * 0.5f));
      34          741 :     ImGui::SetNextItemAllowOverlap();
      35          741 :     ImGui::InvisibleButton(imgui_id, ImVec2(knob_hit_size, knob_hit_size));
      36              : 
      37          741 :     bool is_hovered = ImGui::IsItemHovered();
      38          741 :     bool is_active = ImGui::IsItemActive();
      39              : 
      40          741 :     float range = props.max_val - props.min_val;
      41          741 :     if (range <= 0.0001f) range = 1.0f;
      42              : 
      43              :     // 1. Mouse Drag Start
      44          741 :     if (is_active && !s_knob_was_active) {
      45           12 :         s_knob_was_active = true;
      46           12 :         s_param_value_before_drag = props.value;
      47           12 :         s_active_knob_id = imgui_id;
      48            4 :     }
      49              : 
      50              :     // 2. Mouse Panning/Rotary Drag Action
      51          512 :     if (is_active && s_active_knob_id == imgui_id) {
      52           54 :         float mdx = ImGui::GetIO().MousePos.x - ImGui::GetIO().MousePosPrev.x;
      53           54 :         float mdy = ImGui::GetIO().MousePos.y - ImGui::GetIO().MousePosPrev.y;
      54              : 
      55           54 :         if (mdx != 0.0f || mdy != 0.0f) {
      56           54 :             ImVec2 mouse = ImGui::GetIO().MousePos;
      57           54 :             float dx = mouse.x - center.x;
      58           54 :             float dy = mouse.y - center.y;
      59           54 :             float dist = std::sqrt(dx * dx + dy * dy);
      60              : 
      61           54 :             float value_delta = 0.0f;
      62           54 :             if (dist > 5.0f && dist < knob_radius * 5.0f) {
      63           33 :                 float prev_x = mouse.x - mdx;
      64           33 :                 float prev_y = mouse.y - mdy;
      65           33 :                 float curr_angle = std::atan2(mouse.y - center.y, mouse.x - center.x);
      66           33 :                 float prev_angle = std::atan2(prev_y - center.y, prev_x - center.x);
      67              : 
      68           33 :                 float angle_delta = curr_angle - prev_angle;
      69           33 :                 if (angle_delta > PI) angle_delta -= TWO_PI;
      70           33 :                 if (angle_delta < -PI) angle_delta += TWO_PI;
      71              : 
      72           33 :                 value_delta = (angle_delta / ARC_RANGE) * range;
      73           33 :             } else {
      74           21 :                 float sensitivity = 0.007f;
      75           21 :                 value_delta = -mdy * sensitivity * range;
      76              :             }
      77              : 
      78           54 :             if (ImGui::GetIO().KeyShift) value_delta *= 0.2f;
      79           54 :             if (ImGui::GetIO().KeyCtrl) value_delta *= 3.0f;
      80              : 
      81           54 :             float new_val = clamp(props.value + value_delta, props.min_val, props.max_val);
      82           54 :             if (new_val != props.value && props.on_value_changed) {
      83           27 :                 props.on_value_changed(new_val);
      84            9 :             }
      85           18 :         }
      86           18 :     }
      87              : 
      88              :     // 3. Mouse Drag Stop & Commit Undo
      89          741 :     if (s_knob_was_active && !is_active && s_active_knob_id == imgui_id) {
      90           12 :         float new_val = props.value;
      91           12 :         if (new_val != s_param_value_before_drag && props.on_value_committed) {
      92            3 :             props.on_value_committed(s_param_value_before_drag, new_val);
      93            1 :         }
      94           12 :         s_knob_was_active = false;
      95           12 :         s_active_knob_id = "";
      96            4 :     }
      97              : 
      98              :     // 4. Scroll Wheel Adjustments
      99          741 :     if (is_hovered && std::fabs(ImGui::GetIO().MouseWheel) > 0.0f) {
     100            6 :         float old_val = props.value;
     101            6 :         float step = range * 0.03f;
     102            6 :         if (ImGui::GetIO().KeyShift) step *= 0.2f;
     103            4 :         float new_val =
     104            6 :             clamp(props.value + ImGui::GetIO().MouseWheel * step, props.min_val, props.max_val);
     105            6 :         if (new_val != old_val) {
     106            6 :             if (props.on_value_changed) props.on_value_changed(new_val);
     107            6 :             if (props.on_value_committed) props.on_value_committed(old_val, new_val);
     108            2 :         }
     109            2 :     }
     110              : 
     111              :     // 5. Double Click Reset to Default
     112          741 :     if (is_hovered && ImGui::IsMouseDoubleClicked(0)) {
     113            3 :         float old_val = props.value;
     114            3 :         float new_val = props.default_val;
     115            3 :         if (new_val != old_val) {
     116            3 :             if (props.on_value_changed) props.on_value_changed(new_val);
     117            3 :             if (props.on_value_committed) props.on_value_committed(old_val, new_val);
     118            1 :         }
     119            1 :     }
     120              : 
     121              :     // 6. Right-Click context popup sliders and MIDI options
     122          247 :     char popup_id[128];
     123          741 :     std::snprintf(popup_id, sizeof(popup_id), "Popup_%s", imgui_id);
     124              : 
     125          741 :     if (is_hovered && ImGui::IsMouseClicked(1)) {
     126            0 :         ImGui::OpenPopup(popup_id);
     127            0 :     }
     128              : 
     129          741 :     if (ImGui::BeginPopup(popup_id)) {
     130            6 :         ImGui::Text("%s", props.name.c_str());
     131            6 :         ImGui::SetNextItemWidth(120);
     132              : 
     133            6 :         float slider_val = props.value;
     134            6 :         ImGui::SliderFloat("##edit", &slider_val, props.min_val, props.max_val, "%.2f");
     135            6 :         if (slider_val != props.value && props.on_value_changed) {
     136            0 :             props.on_value_changed(slider_val);
     137            0 :         }
     138              : 
     139            6 :         if (ImGui::IsItemActivated()) {
     140            0 :             s_active_popup_id = popup_id;
     141            0 :             s_popup_param_value_before_edit = props.value;
     142            0 :         }
     143              : 
     144            6 :         if (ImGui::IsItemDeactivatedAfterEdit() && s_active_popup_id == popup_id) {
     145            0 :             if (props.value != s_popup_param_value_before_edit && props.on_value_committed) {
     146            0 :                 props.on_value_committed(s_popup_param_value_before_edit, props.value);
     147            0 :             }
     148            0 :             s_active_popup_id = "";
     149            0 :         }
     150              : 
     151            6 :         if (ImGui::Button("Reset")) {
     152            0 :             float old_val = props.value;
     153            0 :             float new_val = props.default_val;
     154            0 :             if (new_val != old_val) {
     155            0 :                 if (props.on_value_changed) props.on_value_changed(new_val);
     156            0 :                 if (props.on_value_committed) props.on_value_committed(old_val, new_val);
     157            0 :             }
     158            0 :             ImGui::CloseCurrentPopup();
     159            0 :         }
     160              : 
     161              :         // MIDI learn integrations
     162            6 :         ImGui::Separator();
     163            6 :         ImGui::TextColored(Theme::Gold(), "MIDI Control");
     164              : 
     165            6 :         if (props.on_midi_learn_param) {
     166            6 :             if (props.midi_info.empty()) {
     167            0 :                 if (ImGui::MenuItem("MIDI Learn Parameter")) {
     168            0 :                     props.on_midi_learn_param();
     169            0 :                     ImGui::CloseCurrentPopup();
     170            0 :                 }
     171            0 :             } else {
     172            6 :                 if (ImGui::MenuItem("Remove MIDI Mapping")) {
     173            0 :                     props.on_midi_clear_param();
     174            0 :                     ImGui::CloseCurrentPopup();
     175            0 :                 }
     176              :             }
     177            6 :             ImGui::Spacing();
     178            6 :             if (ImGui::MenuItem("MIDI Learn Bypass Toggle")) {
     179            0 :                 props.on_midi_learn_bypass();
     180            0 :                 ImGui::CloseCurrentPopup();
     181            0 :             }
     182            6 :             if (ImGui::MenuItem("Remove Bypass Mapping")) {
     183            0 :                 props.on_midi_clear_bypass();
     184            0 :                 ImGui::CloseCurrentPopup();
     185            0 :             }
     186            2 :         } else {
     187            0 :             ImGui::TextDisabled("MIDI mapping not available");
     188              :         }
     189              : 
     190            6 :         ImGui::EndPopup();
     191            2 :     }
     192              : 
     193              :     // 7. Visual calculations and drawings
     194          741 :     float normalized = (props.value - props.min_val) / range;
     195          741 :     float track_radius = knob_radius + 3 * zoom;
     196          741 :     int segments = 40;
     197              : 
     198        30381 :     for (int s = 0; s < segments; ++s) {
     199        29640 :         float t0 = static_cast<float>(s) / segments;
     200        29640 :         float t1 = static_cast<float>(s + 1) / segments;
     201        29640 :         float a0 = ARC_START + t0 * ARC_RANGE;
     202        29640 :         float a1 = ARC_START + t1 * ARC_RANGE;
     203              : 
     204        29640 :         bool filled = t0 <= normalized;
     205        19760 :         ImU32 seg_color =
     206        29640 :             filled ? ImGui::ColorConvertFloat4ToU32(props.led_color) : Theme::KNOB_TRACK_OFF;
     207              : 
     208        39520 :         dl->AddLine(
     209        29640 :             ImVec2(center.x + std::cos(a0) * track_radius, center.y + std::sin(a0) * track_radius),
     210        39520 :             ImVec2(center.x + std::cos(a1) * track_radius, center.y + std::sin(a1) * track_radius),
     211         9880 :             seg_color, 3.0f * zoom);
     212         9880 :     }
     213              : 
     214          709 :     ImU32 knob_bg =
     215          741 :         is_active ? Theme::KNOB_ACTIVE : (is_hovered ? Theme::KNOB_HOVER : Theme::KNOB_FACE);
     216          741 :     dl->AddCircleFilled(center, knob_radius, Theme::KNOB_BG);
     217          741 :     dl->AddCircleFilled(center, knob_radius - 1 * zoom, knob_bg);
     218              : 
     219              :     // Flash blue outline if learning MIDI
     220          741 :     if (props.is_learning) {
     221            9 :         float time = static_cast<float>(ImGui::GetTime());
     222            9 :         float alpha = (std::sin(time * 2.0f * 3.14159f * 10.0f) + 1.0f) * 0.5f;
     223            6 :         ImU32 outline_col =
     224            9 :             ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.6f, 1.0f, 0.4f + alpha * 0.6f));
     225            9 :         dl->AddCircle(center, knob_radius + 4.0f * zoom, outline_col, 0, 3.0f * zoom);
     226            3 :     }
     227              : 
     228              :     // Drawing the pointer pointer dot/line
     229          741 :     float pointer_angle = ARC_START + normalized * ARC_RANGE;
     230          741 :     float ptr_inner = knob_radius * 0.25f;
     231          741 :     float ptr_outer = knob_radius - 3.0f * zoom;
     232         1235 :     ImVec2 ptr_from = ImVec2(center.x + std::cos(pointer_angle) * ptr_inner,
     233          741 :                              center.y + std::sin(pointer_angle) * ptr_inner);
     234         1235 :     ImVec2 ptr_to = ImVec2(center.x + std::cos(pointer_angle) * ptr_outer,
     235          741 :                            center.y + std::sin(pointer_angle) * ptr_outer);
     236              : 
     237          741 :     ImU32 ptr_color = is_active ? Theme::ACCENT_GOLD_HOT : Theme::ACCENT_GOLD;
     238          741 :     dl->AddLine(ptr_from, ptr_to, ptr_color, 2.5f * zoom);
     239          741 :     dl->AddCircleFilled(ptr_to, 3.0f * zoom, ptr_color);
     240              : 
     241              :     // Tooltips
     242          741 :     if (is_hovered || is_active) {
     243           96 :         std::string val_str = Theme::formatParameterValue(props.value, props.unit);
     244           96 :         std::string min_str = Theme::formatParameterValue(props.min_val, props.unit);
     245           96 :         std::string max_str = Theme::formatParameterValue(props.max_val, props.unit);
     246              : 
     247           96 :         if (props.tooltip.empty()) {
     248           63 :             ImGui::SetTooltip(
     249              :                 "%s: %s\nRange: [%s, %s]%s\n\nRotate or drag to adjust\nScroll wheel also "
     250              :                 "works\nShift=fine  Ctrl=coarse\nDbl-click=reset  Right-click=edit/MIDI",
     251           21 :                 props.name.c_str(), val_str.c_str(), min_str.c_str(), max_str.c_str(),
     252           21 :                 props.midi_info.c_str());
     253           21 :         } else {
     254           33 :             ImGui::SetTooltip(
     255              :                 "%s: %s\nRange: [%s, %s]\n\n%s%s\n\nRotate or drag to adjust\nScroll wheel also "
     256              :                 "works\nShift=fine  Ctrl=coarse\nDbl-click=reset  Right-click=edit/MIDI",
     257           11 :                 props.name.c_str(), val_str.c_str(), min_str.c_str(), max_str.c_str(),
     258           11 :                 props.tooltip.c_str(), props.midi_info.c_str());
     259              :         }
     260           96 :     }
     261              : 
     262              :     // Parameter names and values text
     263          741 :     ImVec2 text_size = ImGui::CalcTextSize(props.name.c_str());
     264          741 :     ImGui::SetCursorScreenPos(
     265          988 :         ImVec2(center.x - text_size.x * 0.5f, center.y + knob_radius + 8 * zoom));
     266          741 :     ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextSecondary());
     267          741 :     ImGui::TextUnformatted(props.name.c_str());
     268          741 :     ImGui::PopStyleColor();
     269              : 
     270          741 :     std::string val_display = Theme::formatParameterValue(props.value, props.unit);
     271          741 :     ImVec2 val_size = ImGui::CalcTextSize(val_display.c_str());
     272          741 :     ImGui::SetCursorScreenPos(
     273          741 :         ImVec2(center.x - val_size.x * 0.5f, center.y - knob_radius - 20 * zoom));
     274          988 :     ImGui::PushStyleColor(ImGuiCol_Text, is_active ? Theme::GoldHot() : Theme::TextDim());
     275          741 :     ImGui::TextUnformatted(val_display.c_str());
     276          741 :     ImGui::PopStyleColor();
     277          741 : }
     278              : 
     279              : }  // namespace Amplitron
        

Generated by: LCOV version 2.0-1