LCOV - code coverage report
Current view: top level - src/gui/components - knob.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 97.9 % 192 188
Test Date: 2026-06-03 09:13:19 Functions: 100.0 % 1 1

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

Generated by: LCOV version 2.0-1