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
|