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
|