Skip to content

Commit 059daff

Browse files
committed
add sources
1 parent e9704e1 commit 059daff

3 files changed

Lines changed: 401 additions & 0 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# ImGui Knobs
2+
This is a port/adaptation of [imgui-rs-knobs](https://github.com/DGriffin91/imgui-rs-knobs), for C++ with slightly changed API.
3+
4+
## Usage
5+
Include `imgui-knobs.cpp` and `imgui-knobs.h` in your project and include `imgui-knobs.h` in some source file, then:
6+
7+
```cpp
8+
static float value = 0;
9+
10+
if (ImGuiKnobs::WiperKnob("Volume", &value, -6.0f, 6.0f, "%.1fdB")) {
11+
// value was changed
12+
}
13+
```
14+
15+
Available knob variants are: `TickKnob`, `DotKnob`, `WiperKnob`, `WiperOnlyKnob`, `WiperDotKnob`, `SteppedKnob`, `SpaceKnob`. They differ only in their visual appearance.
16+

imgui-knobs.cpp

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
#include "knobs.h"
2+
3+
#include <cmath>
4+
#include <cstdlib>
5+
#include <imgui.h>
6+
#include <imgui_internal.h>
7+
#include <utility>
8+
9+
namespace ImGuiKnobs {
10+
namespace detail {
11+
std::pair<ImVec2, ImVec2> bezier_arc(ImVec2 center, ImVec2 start, ImVec2 end) {
12+
auto ax = start[0] - center[0];
13+
auto ay = start[1] - center[1];
14+
auto bx = end[0] - center[0];
15+
auto by = end[1] - center[1];
16+
auto q1 = ax * ax + ay * ay;
17+
auto q2 = q1 + ax * bx + ay * by;
18+
auto k2 = (4.0f / 3.0f) * (sqrtf((2.0f * q1 * q2)) - q2) / (ax * by - ay * bx);
19+
20+
return {
21+
{center[0] + ax - k2 * ay, center[1] + ay + k2 * ax},
22+
{center[0] + bx + k2 * by, center[1] + by - k2 * bx},
23+
};
24+
}
25+
26+
void draw_arc1(ImVec2 center, float radius, float start_angle, float end_angle, float thickness, ImColor color, int num_segments) {
27+
ImVec2 start = {
28+
center[0] + cosf(start_angle) * radius,
29+
center[1] + sinf(start_angle) * radius,
30+
};
31+
32+
ImVec2 end = {
33+
center[0] + cosf(end_angle) * radius,
34+
center[1] + sinf(end_angle) * radius,
35+
};
36+
37+
auto arc = bezier_arc(center, start, end);
38+
39+
auto *draw_list = ImGui::GetWindowDrawList();
40+
41+
draw_list->AddBezierCurve(start, arc.first, arc.second, end, color, thickness, num_segments);
42+
}
43+
44+
void draw_arc(ImVec2 center, float radius, float start_angle, float end_angle, float thickness, ImColor color, int num_segments, int bezier_count) {
45+
// Overlap and angle of ends of bezier curves needs work, only looks good when not transperant
46+
auto overlap = thickness * radius * 0.00001f * M_PI;
47+
auto delta = end_angle - start_angle;
48+
auto bez_step = 1.0f / bezier_count;
49+
auto mid_angle = start_angle + overlap;
50+
51+
for (auto i = 0; i < bezier_count - 1; i++) {
52+
auto mid_angle2 = delta * bez_step + mid_angle;
53+
draw_arc1(
54+
center,
55+
radius,
56+
mid_angle - overlap,
57+
mid_angle2 + overlap,
58+
thickness,
59+
color,
60+
num_segments);
61+
mid_angle = mid_angle2;
62+
}
63+
64+
draw_arc1(
65+
center,
66+
radius,
67+
mid_angle - overlap,
68+
end_angle,
69+
thickness,
70+
color,
71+
num_segments);
72+
}
73+
74+
bool knob_control(const char *id, float *p_value, float v_min, float v_max, float v_default, float radius) {
75+
ImGui::InvisibleButton(id, {radius * 2.0f, radius * 2.0f});
76+
77+
auto value_changed = false;
78+
79+
auto is_active = ImGui::IsItemActive();
80+
auto delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left, 0.0001f);
81+
82+
const auto &io = ImGui::GetIO();
83+
84+
// Maybe this should be configurable
85+
auto speed = io.KeyShift ? 2000.0f : 200.0f;
86+
87+
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && is_active) {
88+
*p_value = v_default;
89+
value_changed = true;
90+
} else if (is_active && delta[1] != 0.0) {
91+
auto step = (v_max - v_min) / speed;
92+
*p_value -= delta[1] * step;
93+
if (*p_value < v_min) {
94+
*p_value = v_min;
95+
}
96+
if (*p_value > v_max) {
97+
*p_value = v_max;
98+
}
99+
value_changed = true;
100+
101+
// There may be a way to do this without using this
102+
ImGui::ResetMouseDragDelta(ImGuiMouseButton_Left);
103+
}
104+
105+
return value_changed;
106+
}
107+
108+
knob::knob(const char *_label, float *_p_value, float _v_min, float _v_max, float _v_default, float _radius) {
109+
auto _angle_min = M_PI * 0.75;
110+
auto _angle_max = M_PI * 2.25;
111+
auto _t = (*_p_value - _v_min) / (_v_max - _v_min);
112+
auto _angle = _angle_min + (_angle_max - _angle_min) * _t;
113+
auto _value_changed = false;
114+
auto _screen_pos = ImGui::GetCursorScreenPos();
115+
116+
_value_changed = knob_control(_label, _p_value, _v_min, _v_max, _v_default, _radius);
117+
118+
label = _label;
119+
p_value = _p_value;
120+
v_min = _v_min;
121+
v_max = _v_max;
122+
v_default = _v_default;
123+
radius = _radius;
124+
screen_pos = _screen_pos;
125+
value_changed = _value_changed;
126+
center = {screen_pos[0] + radius, screen_pos[1] + radius};
127+
is_active = ImGui::IsItemActive();
128+
is_hovered = ImGui::IsItemHovered();
129+
angle_min = _angle_min;
130+
angle_max = _angle_max;
131+
t = _t;
132+
angle = _angle;
133+
angle_cos = cosf(angle);
134+
angle_sin = sinf(angle);
135+
}
136+
137+
void knob::draw_dot(float size, float radius, float angle, color_set color, bool filled, int segments) {
138+
auto dot_size = size * this->radius;
139+
auto dot_radius = radius * this->radius;
140+
141+
ImGui::GetWindowDrawList()->AddCircleFilled(
142+
{
143+
center[0] + cosf(angle) * dot_radius,
144+
center[1] + sinf(angle) * dot_radius,
145+
},
146+
dot_size,
147+
is_active ? color.active : (is_hovered ? color.hovered : color.base),
148+
segments);
149+
}
150+
151+
void knob::draw_tick(float start, float end, float width, float angle, color_set color) {
152+
auto tick_start = start * radius;
153+
auto tick_end = end * radius;
154+
auto angle_cos = cosf(angle);
155+
auto angle_sin = sinf(angle);
156+
157+
ImGui::GetWindowDrawList()->AddLine(
158+
{
159+
center[0] + angle_cos * tick_end,
160+
center[1] + angle_sin * tick_end,
161+
},
162+
{
163+
center[0] + angle_cos * tick_start,
164+
center[1] + angle_sin * tick_start,
165+
},
166+
is_active ? color.active : (is_hovered ? color.hovered : color.base),
167+
width * radius);
168+
}
169+
170+
void knob::draw_circle(float size, color_set color, bool filled, int segments) {
171+
auto circle_radius = size * radius;
172+
173+
ImGui::GetWindowDrawList()->AddCircleFilled(
174+
center,
175+
circle_radius,
176+
is_active ? color.active : (is_hovered ? color.hovered : color.base));
177+
}
178+
179+
void knob::draw_arc(float radius, float size, float start_angle, float end_angle, color_set color, int segments, int bezier_count) {
180+
auto track_radius = radius * this->radius;
181+
auto track_size = size * this->radius * 0.5f + 0.0001f;
182+
183+
detail::draw_arc(
184+
center,
185+
track_radius,
186+
start_angle,
187+
end_angle,
188+
track_size,
189+
is_active ? color.active : (is_hovered ? color.hovered : color.base),
190+
segments,
191+
bezier_count);
192+
}
193+
194+
knob knob_with_drag(const char *label, float *p_value, float v_min, float v_max, float v_default, const char *format, float size) {
195+
ImGui::PushID(label);
196+
auto width = size == 0 ? ImGui::GetTextLineHeight() * 4.0f : size * ImGui::GetIO().FontGlobalScale;
197+
ImGui::PushItemWidth(width);
198+
199+
// Draw title
200+
auto title_size = ImGui::CalcTextSize(label, NULL, false, width);
201+
auto old_cursor_pos = ImGui::GetCursorPos();
202+
203+
// Center title
204+
ImGui::SetCursorPos({old_cursor_pos[0] + (width - title_size[0]) * 0.5f, old_cursor_pos[1]});
205+
206+
ImGui::Text("%s", label);
207+
208+
// Reset cursor X
209+
ImGui::SetCursorPosX(old_cursor_pos[0]);
210+
211+
knob k(label, p_value, v_min, v_max, v_default, width * 0.5f);
212+
213+
ImGui::DragScalar("###knob_drag", ImGuiDataType_Float, p_value, (v_max - v_min) / 1000.f, &v_min, &v_max, format);
214+
215+
ImGui::PopItemWidth();
216+
ImGui::PopID();
217+
return k;
218+
}
219+
220+
color_set GetPrimaryColorSet() {
221+
auto *colors = ImGui::GetStyle().Colors;
222+
223+
return {colors[ImGuiCol_ButtonActive], colors[ImGuiCol_ButtonHovered], colors[ImGuiCol_ButtonHovered]};
224+
}
225+
226+
color_set GetSecondaryColorSet() {
227+
auto *colors = ImGui::GetStyle().Colors;
228+
auto active = colors[ImGuiCol_ButtonActive] * ImVec4(0.5f, 0.5f, 0.5f, 1.f);
229+
auto hovered = colors[ImGuiCol_ButtonHovered] * ImVec4(0.5f, 0.5f, 0.5f, 1.f);
230+
return {active, hovered, hovered};
231+
}
232+
233+
color_set GetTrackColorSet() {
234+
auto *colors = ImGui::GetStyle().Colors;
235+
236+
return {colors[ImGuiCol_FrameBg], colors[ImGuiCol_FrameBg], colors[ImGuiCol_FrameBg]};
237+
}
238+
}// namespace detail
239+
240+
241+
// Knob implementations
242+
KNOB_WIDGET(WiperKnob) {
243+
auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size);
244+
knob.draw_circle(0.7, detail::GetSecondaryColorSet(), true, 32);
245+
knob.draw_arc(0.8, 0.41, knob.angle_min, knob.angle_max, detail::GetTrackColorSet(), 16, 2);
246+
247+
if (knob.t > 0.01) {
248+
knob.draw_arc(0.8, 0.43, knob.angle_min, knob.angle, detail::GetPrimaryColorSet(), 16, 2);
249+
}
250+
return knob.value_changed;
251+
}
252+
253+
KNOB_WIDGET(WiperOnlyKnob) {
254+
auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size);
255+
knob.draw_arc(0.8, 0.41, knob.angle_min, knob.angle_max, detail::GetTrackColorSet(), 32, 2);
256+
257+
if (knob.t > 0.01) {
258+
knob.draw_arc(0.8, 0.43, knob.angle_min, knob.angle, detail::GetPrimaryColorSet(), 16, 2);
259+
}
260+
return knob.value_changed;
261+
}
262+
263+
KNOB_WIDGET(WiperDotKnob) {
264+
auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size);
265+
knob.draw_circle(0.6, detail::GetSecondaryColorSet(), true, 32);
266+
knob.draw_arc(0.85, 0.41, knob.angle_min, knob.angle_max, detail::GetTrackColorSet(), 16, 2);
267+
knob.draw_dot(0.1, 0.85, knob.angle, detail::GetPrimaryColorSet(), true, 12);
268+
return knob.value_changed;
269+
}
270+
271+
272+
KNOB_WIDGET(TickKnob) {
273+
auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size);
274+
knob.draw_circle(0.7, detail::GetSecondaryColorSet(), true, 32);
275+
knob.draw_tick(0.4, 0.7, 0.08, knob.angle, detail::GetPrimaryColorSet());
276+
return knob.value_changed;
277+
}
278+
279+
KNOB_WIDGET(DotKnob) {
280+
auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size);
281+
knob.draw_circle(0.85, detail::GetSecondaryColorSet(), true, 32);
282+
knob.draw_dot(0.12, 0.6, knob.angle, detail::GetPrimaryColorSet(), true, 12);
283+
return knob.value_changed;
284+
}
285+
286+
KNOB_WIDGET(SpaceKnob) {
287+
auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size);
288+
knob.draw_circle(0.3 - knob.t * 0.1, detail::GetSecondaryColorSet(), true, 16);
289+
290+
if (knob.t > 0.01) {
291+
knob.draw_arc(0.4, 0.15, knob.angle_min - 1.0, knob.angle - 1.0, detail::GetPrimaryColorSet(), 16, 2);
292+
knob.draw_arc(0.6, 0.15, knob.angle_min + 1.0, knob.angle + 1.0, detail::GetPrimaryColorSet(), 16, 2);
293+
knob.draw_arc(0.8, 0.15, knob.angle_min + 3.0, knob.angle + 3.0, detail::GetPrimaryColorSet(), 16, 2);
294+
}
295+
296+
return knob.value_changed;
297+
}
298+
299+
KNOB_WIDGET(SteppedKnob, int steps) {
300+
auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size);
301+
for (auto n = 0.f; n < steps; n++) {
302+
auto a = n / (steps - 1);
303+
auto angle = knob.angle_min + (knob.angle_max - knob.angle_min) * a;
304+
knob.draw_tick(0.7, 0.9, 0.04, angle, detail::GetPrimaryColorSet());
305+
}
306+
knob.draw_circle(0.6, detail::GetSecondaryColorSet(), true, 32);
307+
knob.draw_dot(0.12, 0.4, knob.angle, detail::GetPrimaryColorSet(), true, 12);
308+
return knob.value_changed;
309+
}
310+
}// namespace ImGuiKnobs

0 commit comments

Comments
 (0)