|
| 1 | +#include "imgui-knobs.h" |
| 2 | + |
| 3 | +#include <cmath> |
| 4 | +#include <cstdlib> |
| 5 | +#include <imgui.h> |
| 6 | +#include <imgui_internal.h> |
| 7 | + |
| 8 | +#define IMGUIKNOBS_PI 3.14159265358979323846f |
| 9 | + |
| 10 | +namespace ImGuiKnobs { |
| 11 | + namespace detail { |
| 12 | + void draw_arc1(ImVec2 center, float radius, float start_angle, float end_angle, float thickness, ImColor color, int num_segments) { |
| 13 | + ImVec2 start = { |
| 14 | + center[0] + cosf(start_angle) * radius, |
| 15 | + center[1] + sinf(start_angle) * radius, |
| 16 | + }; |
| 17 | + |
| 18 | + ImVec2 end = { |
| 19 | + center[0] + cosf(end_angle) * radius, |
| 20 | + center[1] + sinf(end_angle) * radius, |
| 21 | + }; |
| 22 | + |
| 23 | + // Calculate bezier arc points |
| 24 | + auto ax = start[0] - center[0]; |
| 25 | + auto ay = start[1] - center[1]; |
| 26 | + auto bx = end[0] - center[0]; |
| 27 | + auto by = end[1] - center[1]; |
| 28 | + auto q1 = ax * ax + ay * ay; |
| 29 | + auto q2 = q1 + ax * bx + ay * by; |
| 30 | + auto k2 = (4.0f / 3.0f) * (sqrtf((2.0f * q1 * q2)) - q2) / (ax * by - ay * bx); |
| 31 | + auto arc1 = ImVec2{center[0] + ax - k2 * ay, center[1] + ay + k2 * ax}; |
| 32 | + auto arc2 = ImVec2{center[0] + bx + k2 * by, center[1] + by - k2 * bx}; |
| 33 | + |
| 34 | + auto *draw_list = ImGui::GetWindowDrawList(); |
| 35 | + |
| 36 | + draw_list->AddBezierCurve(start, arc1, arc2, end, color, thickness, num_segments); |
| 37 | + } |
| 38 | + |
| 39 | + void draw_arc(ImVec2 center, float radius, float start_angle, float end_angle, float thickness, ImColor color, int num_segments, int bezier_count) { |
| 40 | + // Overlap and angle of ends of bezier curves needs work, only looks good when not transperant |
| 41 | + auto overlap = thickness * radius * 0.00001f * IMGUIKNOBS_PI; |
| 42 | + auto delta = end_angle - start_angle; |
| 43 | + auto bez_step = 1.0f / bezier_count; |
| 44 | + auto mid_angle = start_angle + overlap; |
| 45 | + |
| 46 | + for (auto i = 0; i < bezier_count - 1; i++) { |
| 47 | + auto mid_angle2 = delta * bez_step + mid_angle; |
| 48 | + draw_arc1(center, radius, mid_angle - overlap, mid_angle2 + overlap, thickness, color, num_segments); |
| 49 | + mid_angle = mid_angle2; |
| 50 | + } |
| 51 | + |
| 52 | + draw_arc1(center, radius, mid_angle - overlap, end_angle, thickness, color, num_segments); |
| 53 | + } |
| 54 | + |
| 55 | + template<typename DataType> |
| 56 | + struct knob { |
| 57 | + float radius; |
| 58 | + bool value_changed; |
| 59 | + ImVec2 center; |
| 60 | + bool is_active; |
| 61 | + bool is_hovered; |
| 62 | + float angle_min; |
| 63 | + float angle_max; |
| 64 | + float t; |
| 65 | + float angle; |
| 66 | + float angle_cos; |
| 67 | + float angle_sin; |
| 68 | + |
| 69 | + knob(const char *_label, ImGuiDataType data_type, DataType *p_value, DataType v_min, DataType v_max, float speed, float _radius, const char *format, ImGuiKnobFlags flags) { |
| 70 | + radius = _radius; |
| 71 | + t = ((float) *p_value - v_min) / (v_max - v_min); |
| 72 | + auto screen_pos = ImGui::GetCursorScreenPos(); |
| 73 | + |
| 74 | + // Handle dragging |
| 75 | + ImGui::InvisibleButton(_label, {radius * 2.0f, radius * 2.0f}); |
| 76 | + auto gid = ImGui::GetID(_label); |
| 77 | + ImGuiSliderFlags drag_flags = 0; |
| 78 | + if (!(flags & ImGuiKnobFlags_DragHorizontal)) { |
| 79 | + drag_flags |= ImGuiSliderFlags_Vertical; |
| 80 | + } |
| 81 | + value_changed = ImGui::DragBehavior(gid, data_type, p_value, speed, &v_min, &v_max, format, 1, drag_flags); |
| 82 | + |
| 83 | + angle_min = IMGUIKNOBS_PI * 0.75f; |
| 84 | + angle_max = IMGUIKNOBS_PI * 2.25f; |
| 85 | + center = {screen_pos[0] + radius, screen_pos[1] + radius}; |
| 86 | + is_active = ImGui::IsItemActive(); |
| 87 | + is_hovered = ImGui::IsItemHovered(); |
| 88 | + angle = angle_min + (angle_max - angle_min) * t; |
| 89 | + angle_cos = cosf(angle); |
| 90 | + angle_sin = sinf(angle); |
| 91 | + } |
| 92 | + |
| 93 | + void draw_dot(float size, float radius, float angle, color_set color, bool filled, int segments) { |
| 94 | + auto dot_size = size * this->radius; |
| 95 | + auto dot_radius = radius * this->radius; |
| 96 | + |
| 97 | + ImGui::GetWindowDrawList()->AddCircleFilled( |
| 98 | + {center[0] + cosf(angle) * dot_radius, center[1] + sinf(angle) * dot_radius}, |
| 99 | + dot_size, |
| 100 | + is_active ? color.active : (is_hovered ? color.hovered : color.base), |
| 101 | + segments); |
| 102 | + } |
| 103 | + |
| 104 | + void draw_tick(float start, float end, float width, float angle, color_set color) { |
| 105 | + auto tick_start = start * radius; |
| 106 | + auto tick_end = end * radius; |
| 107 | + auto angle_cos = cosf(angle); |
| 108 | + auto angle_sin = sinf(angle); |
| 109 | + |
| 110 | + ImGui::GetWindowDrawList()->AddLine( |
| 111 | + {center[0] + angle_cos * tick_end, center[1] + angle_sin * tick_end}, |
| 112 | + {center[0] + angle_cos * tick_start, center[1] + angle_sin * tick_start}, |
| 113 | + is_active ? color.active : (is_hovered ? color.hovered : color.base), |
| 114 | + width * radius); |
| 115 | + } |
| 116 | + |
| 117 | + void draw_circle(float size, color_set color, bool filled, int segments) { |
| 118 | + auto circle_radius = size * radius; |
| 119 | + |
| 120 | + ImGui::GetWindowDrawList()->AddCircleFilled( |
| 121 | + center, |
| 122 | + circle_radius, |
| 123 | + is_active ? color.active : (is_hovered ? color.hovered : color.base)); |
| 124 | + } |
| 125 | + |
| 126 | + void draw_arc(float radius, float size, float start_angle, float end_angle, color_set color, int segments, int bezier_count) { |
| 127 | + auto track_radius = radius * this->radius; |
| 128 | + auto track_size = size * this->radius * 0.5f + 0.0001f; |
| 129 | + |
| 130 | + detail::draw_arc( |
| 131 | + center, |
| 132 | + track_radius, |
| 133 | + start_angle, |
| 134 | + end_angle, |
| 135 | + track_size, |
| 136 | + is_active ? color.active : (is_hovered ? color.hovered : color.base), |
| 137 | + segments, |
| 138 | + bezier_count); |
| 139 | + } |
| 140 | + }; |
| 141 | + |
| 142 | + template<typename DataType> |
| 143 | + knob<DataType> knob_with_drag(const char *label, ImGuiDataType data_type, DataType *p_value, DataType v_min, DataType v_max, float _speed, const char *format, float size, ImGuiKnobFlags flags) { |
| 144 | + auto speed = _speed == 0 ? (v_max - v_min) / 250.f : _speed; |
| 145 | + ImGui::PushID(label); |
| 146 | + auto width = size == 0 ? ImGui::GetTextLineHeight() * 4.0f : size * ImGui::GetIO().FontGlobalScale; |
| 147 | + ImGui::PushItemWidth(width); |
| 148 | + |
| 149 | + ImGui::BeginGroup(); |
| 150 | + |
| 151 | + // There's an issue with `SameLine` and Groups, see https://github.com/ocornut/imgui/issues/4190. |
| 152 | + // This is probably not the best solution, but seems to work for now |
| 153 | + ImGui::GetCurrentWindow()->DC.CurrLineTextBaseOffset = 0; |
| 154 | + |
| 155 | + // Draw title |
| 156 | + if (!(flags & ImGuiKnobFlags_NoTitle)) { |
| 157 | + auto title_size = ImGui::CalcTextSize(label, NULL, false, width); |
| 158 | + |
| 159 | + // Center title |
| 160 | + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (width - title_size[0]) * 0.5f); |
| 161 | + |
| 162 | + ImGui::Text("%s", label); |
| 163 | + } |
| 164 | + |
| 165 | + // Draw knob |
| 166 | + knob<DataType> k(label, data_type, p_value, v_min, v_max, speed, width * 0.5f, format, flags); |
| 167 | + |
| 168 | + // Draw tooltip |
| 169 | + if (flags & ImGuiKnobFlags_ValueTooltip && (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) || ImGui::IsItemActive())) { |
| 170 | + ImGui::BeginTooltip(); |
| 171 | + ImGui::Text(format, *p_value); |
| 172 | + ImGui::EndTooltip(); |
| 173 | + } |
| 174 | + |
| 175 | + // Draw input |
| 176 | + if (!(flags & ImGuiKnobFlags_NoInput)) { |
| 177 | + ImGuiSliderFlags drag_flags = 0; |
| 178 | + if (!(flags & ImGuiKnobFlags_DragHorizontal)) { |
| 179 | + drag_flags |= ImGuiSliderFlags_Vertical; |
| 180 | + } |
| 181 | + auto changed = ImGui::DragScalar("###knob_drag", data_type, p_value, speed, &v_min, &v_max, format, drag_flags); |
| 182 | + if (changed) { |
| 183 | + k.value_changed = true; |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + ImGui::EndGroup(); |
| 188 | + ImGui::PopItemWidth(); |
| 189 | + ImGui::PopID(); |
| 190 | + |
| 191 | + return k; |
| 192 | + } |
| 193 | + |
| 194 | + color_set GetPrimaryColorSet() { |
| 195 | + auto *colors = ImGui::GetStyle().Colors; |
| 196 | + |
| 197 | + return {colors[ImGuiCol_ButtonActive], colors[ImGuiCol_ButtonHovered], colors[ImGuiCol_ButtonHovered]}; |
| 198 | + } |
| 199 | + |
| 200 | + color_set GetSecondaryColorSet() { |
| 201 | + auto *colors = ImGui::GetStyle().Colors; |
| 202 | + auto active = ImVec4( |
| 203 | + colors[ImGuiCol_ButtonActive].x * 0.5f, |
| 204 | + colors[ImGuiCol_ButtonActive].y * 0.5f, |
| 205 | + colors[ImGuiCol_ButtonActive].z * 0.5f, |
| 206 | + colors[ImGuiCol_ButtonActive].w); |
| 207 | + |
| 208 | + auto hovered = ImVec4( |
| 209 | + colors[ImGuiCol_ButtonHovered].x * 0.5f, |
| 210 | + colors[ImGuiCol_ButtonHovered].y * 0.5f, |
| 211 | + colors[ImGuiCol_ButtonHovered].z * 0.5f, |
| 212 | + colors[ImGuiCol_ButtonHovered].w); |
| 213 | + |
| 214 | + return {active, hovered, hovered}; |
| 215 | + } |
| 216 | + |
| 217 | + color_set GetTrackColorSet() { |
| 218 | + auto *colors = ImGui::GetStyle().Colors; |
| 219 | + |
| 220 | + return {colors[ImGuiCol_FrameBg], colors[ImGuiCol_FrameBg], colors[ImGuiCol_FrameBg]}; |
| 221 | + } |
| 222 | + |
| 223 | + color_set GetCustomColorSet() { |
| 224 | + |
| 225 | + return {ImColor(255,255,120,255), ImColor(255,255,120,255), ImColor(255,255,120,255)}; |
| 226 | + } |
| 227 | + }// namespace detail |
| 228 | + |
| 229 | + |
| 230 | + template<typename DataType> |
| 231 | + bool BaseKnob(const char *label, ImGuiDataType data_type, DataType *p_value, DataType v_min, DataType v_max, float speed, const char *format, ImGuiKnobVariant variant, float size, ImGuiKnobFlags flags, int steps = 10) { |
| 232 | + auto knob = detail::knob_with_drag(label, data_type, p_value, v_min, v_max, speed, format, size, flags); |
| 233 | + |
| 234 | + switch (variant) { |
| 235 | + case ImGuiKnobVariant_Tick: { |
| 236 | + knob.draw_circle(0.85f, detail::GetSecondaryColorSet(), true, 32); |
| 237 | + knob.draw_tick(0.5f, 0.85f, 0.08f, knob.angle, detail::GetPrimaryColorSet()); |
| 238 | + break; |
| 239 | + } |
| 240 | + case ImGuiKnobVariant_Dot: { |
| 241 | + knob.draw_circle(0.85f, detail::GetSecondaryColorSet(), true, 32); |
| 242 | + knob.draw_dot(0.12f, 0.6f, knob.angle, detail::GetPrimaryColorSet(), true, 12); |
| 243 | + break; |
| 244 | + } |
| 245 | + |
| 246 | + case ImGuiKnobVariant_Wiper: { |
| 247 | + knob.draw_circle(0.7f, detail::GetSecondaryColorSet(), true, 32); |
| 248 | + knob.draw_arc(0.8f, 0.41f, knob.angle_min, knob.angle_max, detail::GetTrackColorSet(), 16, 2); |
| 249 | + |
| 250 | + if (knob.t > 0.01f) { |
| 251 | + knob.draw_arc(0.8f, 0.43f, knob.angle_min, knob.angle, detail::GetCustomColorSet(), 16, 2); |
| 252 | + } |
| 253 | + knob.draw_tick(0.5f, 0.88f, 0.16f, knob.angle, detail::GetCustomColorSet()); |
| 254 | + break; |
| 255 | + } |
| 256 | + case ImGuiKnobVariant_WiperOnly: { |
| 257 | + knob.draw_arc(0.8f, 0.41f, knob.angle_min, knob.angle_max, detail::GetTrackColorSet(), 32, 2); |
| 258 | + |
| 259 | + if (knob.t > 0.01) { |
| 260 | + knob.draw_arc(0.8f, 0.43f, knob.angle_min, knob.angle, detail::GetPrimaryColorSet(), 16, 2); |
| 261 | + } |
| 262 | + break; |
| 263 | + } |
| 264 | + case ImGuiKnobVariant_WiperDot: { |
| 265 | + knob.draw_circle(0.6f, detail::GetSecondaryColorSet(), true, 32); |
| 266 | + knob.draw_arc(0.85f, 0.41f, knob.angle_min, knob.angle_max, detail::GetTrackColorSet(), 16, 2); |
| 267 | + knob.draw_dot(0.1f, 0.85f, knob.angle, detail::GetPrimaryColorSet(), true, 12); |
| 268 | + break; |
| 269 | + } |
| 270 | + case ImGuiKnobVariant_Stepped: { |
| 271 | + for (auto n = 0.f; n < steps; n++) { |
| 272 | + auto a = n / (steps - 1); |
| 273 | + auto angle = knob.angle_min + (knob.angle_max - knob.angle_min) * a; |
| 274 | + knob.draw_tick(0.7f, 0.9f, 0.04f, angle, detail::GetPrimaryColorSet()); |
| 275 | + } |
| 276 | + |
| 277 | + knob.draw_circle(0.6f, detail::GetSecondaryColorSet(), true, 32); |
| 278 | + knob.draw_dot(0.12f, 0.4f, knob.angle, detail::GetPrimaryColorSet(), true, 12); |
| 279 | + break; |
| 280 | + } |
| 281 | + case ImGuiKnobVariant_Space: { |
| 282 | + knob.draw_circle(0.3f - knob.t * 0.1f, detail::GetSecondaryColorSet(), true, 16); |
| 283 | + |
| 284 | + if (knob.t > 0.01f) { |
| 285 | + knob.draw_arc(0.4f, 0.15f, knob.angle_min - 1.0f, knob.angle - 1.0f, detail::GetPrimaryColorSet(), 16, 2); |
| 286 | + knob.draw_arc(0.6f, 0.15f, knob.angle_min + 1.0f, knob.angle + 1.0f, detail::GetPrimaryColorSet(), 16, 2); |
| 287 | + knob.draw_arc(0.8f, 0.15f, knob.angle_min + 3.0f, knob.angle + 3.0f, detail::GetPrimaryColorSet(), 16, 2); |
| 288 | + } |
| 289 | + break; |
| 290 | + } |
| 291 | + } |
| 292 | + |
| 293 | + return knob.value_changed; |
| 294 | + } |
| 295 | + |
| 296 | + bool Knob(const char *label, float *p_value, float v_min, float v_max, float speed, const char *format, ImGuiKnobVariant variant, float size, ImGuiKnobFlags flags, int steps) { |
| 297 | + const char *_format = format == NULL ? "%.3f" : format; |
| 298 | + return BaseKnob(label, ImGuiDataType_Float, p_value, v_min, v_max, speed, _format, variant, size, flags, steps); |
| 299 | + } |
| 300 | + |
| 301 | + bool KnobInt(const char *label, int *p_value, int v_min, int v_max, float speed, const char *format, ImGuiKnobVariant variant, float size, ImGuiKnobFlags flags, int steps) { |
| 302 | + const char *_format = format == NULL ? "%i" : format; |
| 303 | + return BaseKnob(label, ImGuiDataType_S32, p_value, v_min, v_max, speed, _format, variant, size, flags, steps); |
| 304 | + } |
| 305 | + |
| 306 | +}// namespace ImGuiKnobs |
0 commit comments