From f9accd794d9e66787275a24c882c91b4ac8b0b78 Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Mon, 28 Jul 2025 15:06:43 +0800 Subject: [PATCH 01/13] - Added PlotLines and PlotHistogram I am about to test these functions, so I am going to commit those for a backup. --- src/gui.zig | 20 ++++++++++++++++++++ src/zgui.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/gui.zig b/src/gui.zig index 896d2e6..635a252 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -1828,6 +1828,26 @@ pub const textLink = zguiTextLink; extern fn zguiTextLinkOpenURL(label: [*:0]const u8, url: ?[*:0]const u8) void; pub const textLinkOpenURL = zguiTextLinkOpenURL; +//-------------------------------------------------------------------------------------------------- +const PlotNative = struct { + v: *f32, + v_count: c_int, + v_offset: c_int = 0, + overlay_text: ?[:0]const u8 = null, + scale_min: f32 = f32_max, + scale_max: f32 = f32_min, + graph_size: [2]f32 = .{ 0, 0 }, + stride: c_int = @sizeOf(f32), +}; +pub fn plotLines(label: [*:0]const u8, args: PlotNative) void { + zguiPlotLines(label, args.v, args.v_count, args.v_offset, args.overlay_text, args.scale_min, args.scale_max, args.graph_size, args.graph_size); +} +extern fn zguiPlotLines(label: [*:0]const u8, v: *f32, v_count: c_int, v_offset: c_int, overlay_text: ?[:0]const u8, scale_min: f32, scale_max: f32, graph_size: [2]f32, stride: c_int) void; + +pub fn plotHistogram(label: [*:0]const u8, args: PlotNative) void { + zguiPlotHistogram(label, args.v, args.v_count, args.v_offset, args.overlay_text, args.scale_min, args.scale_max, args.graph_size, args.graph_size); +} +extern fn zguiPlotHistogram(label: [*:0]const u8, v: *f32, v_count: c_int, v_offset: c_int, overlay_text: ?[:0]const u8, scale_min: f32, scale_max: f32, graph_size: [2]f32, stride: c_int) void; //-------------------------------------------------------------------------------------------------- // diff --git a/src/zgui.cpp b/src/zgui.cpp index 6f79c11..b96f21c 100644 --- a/src/zgui.cpp +++ b/src/zgui.cpp @@ -1977,6 +1977,36 @@ extern "C" { ImGui::CloseCurrentPopup(); } + + ZGUI_API void zguiPlotLines( + const char* label, + const float* values, + int values_count, + int values_offset, + const char* overlay_text, + float scale_min, + float scale_max, + float graph_size[2], + int stride) + { + ImGui::PlotLines(label, values, values_count, values_offset, overlay_text, scale_min, scale_max, ImVec2(graph_size[0], graph_size[1]), stride); + } + + + ZGUI_API void zguiPlotHistogram( + const char* label, + const float* values, + int values_count, + int values_offset, + const char* overlay_text, + float scale_min, + float scale_max, + float graph_size[2], + int stride) + { + ImGui::PlotHistogram(label, values, values_count, values_offset, overlay_text, scale_min, scale_max, ImVec2(graph_size[0], graph_size[1]), stride); + } + //-------------------------------------------------------------------------------------------------- // // Tables From d9c2b309aab3f6584572f6f2842fc6dd9765249c Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Mon, 28 Jul 2025 16:29:33 +0800 Subject: [PATCH 02/13] - Tested the two native plotting function The PlotLines() and PlotHistogram() are now tested and they can produce the exact same result from the web imgui demo. --- .gitignore | 3 +++ src/gui.zig | 54 ++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 7b1675a..5260ac9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ zig-out # Ignore some special OS files *.DS_Store + +# Ignore local example builds +example/* \ No newline at end of file diff --git a/src/gui.zig b/src/gui.zig index 635a252..7416bb2 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -1830,24 +1830,64 @@ extern fn zguiTextLinkOpenURL(label: [*:0]const u8, url: ?[*:0]const u8) void; pub const textLinkOpenURL = zguiTextLinkOpenURL; //-------------------------------------------------------------------------------------------------- const PlotNative = struct { - v: *f32, + v: [*]f32, v_count: c_int, v_offset: c_int = 0, - overlay_text: ?[:0]const u8 = null, + overlay: ?[:0]const u8 = null, scale_min: f32 = f32_max, - scale_max: f32 = f32_min, + scale_max: f32 = f32_max, graph_size: [2]f32 = .{ 0, 0 }, stride: c_int = @sizeOf(f32), }; pub fn plotLines(label: [*:0]const u8, args: PlotNative) void { - zguiPlotLines(label, args.v, args.v_count, args.v_offset, args.overlay_text, args.scale_min, args.scale_max, args.graph_size, args.graph_size); + zguiPlotLines( + label, + args.v, + args.v_count, + args.v_offset, + if (args.overlay) |o| o else null, + args.scale_min, + args.scale_max, + &args.graph_size, + args.stride, + ); } -extern fn zguiPlotLines(label: [*:0]const u8, v: *f32, v_count: c_int, v_offset: c_int, overlay_text: ?[:0]const u8, scale_min: f32, scale_max: f32, graph_size: [2]f32, stride: c_int) void; +extern fn zguiPlotLines( + label: [*:0]const u8, + v: [*]f32, + v_count: c_int, + v_offset: c_int, + overlay: ?[*:0]const u8, + scale_min: f32, + scale_max: f32, + graph_size: *const [2]f32, + stride: c_int, +) void; pub fn plotHistogram(label: [*:0]const u8, args: PlotNative) void { - zguiPlotHistogram(label, args.v, args.v_count, args.v_offset, args.overlay_text, args.scale_min, args.scale_max, args.graph_size, args.graph_size); + zguiPlotHistogram( + label, + args.v, + args.v_count, + args.v_offset, + if (args.overlay) |o| o else null, + args.scale_min, + args.scale_max, + &args.graph_size, + args.stride, + ); } -extern fn zguiPlotHistogram(label: [*:0]const u8, v: *f32, v_count: c_int, v_offset: c_int, overlay_text: ?[:0]const u8, scale_min: f32, scale_max: f32, graph_size: [2]f32, stride: c_int) void; +extern fn zguiPlotHistogram( + label: [*:0]const u8, + v: [*]f32, + v_count: c_int, + v_offset: c_int, + overlay: ?[*:0]const u8, + scale_min: f32, + scale_max: f32, + graph_size: *const [2]f32, + stride: c_int, +) void; //-------------------------------------------------------------------------------------------------- // From 7339c92eed9eae059350dafd0a5c74359d637cad Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Mon, 28 Jul 2025 19:50:57 +0800 Subject: [PATCH 03/13] - Removed My Private Example gitignore item --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 5260ac9..da42c1f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,4 @@ zig-out # Ignore some special OS files -*.DS_Store - -# Ignore local example builds -example/* \ No newline at end of file +*.DS_Store \ No newline at end of file From 08cb42e1d5b37ba4786ccaed8db46a29de31b8ce Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Mon, 28 Jul 2025 19:54:50 +0800 Subject: [PATCH 04/13] - Renamed PlotNative with PlotArgs --- src/gui.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gui.zig b/src/gui.zig index 7416bb2..5f1e885 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -1829,7 +1829,7 @@ pub const textLink = zguiTextLink; extern fn zguiTextLinkOpenURL(label: [*:0]const u8, url: ?[*:0]const u8) void; pub const textLinkOpenURL = zguiTextLinkOpenURL; //-------------------------------------------------------------------------------------------------- -const PlotNative = struct { +const PlotArgs = struct { v: [*]f32, v_count: c_int, v_offset: c_int = 0, @@ -1839,7 +1839,7 @@ const PlotNative = struct { graph_size: [2]f32 = .{ 0, 0 }, stride: c_int = @sizeOf(f32), }; -pub fn plotLines(label: [*:0]const u8, args: PlotNative) void { +pub fn plotLines(label: [*:0]const u8, args: PlotArgs) void { zguiPlotLines( label, args.v, @@ -1864,7 +1864,7 @@ extern fn zguiPlotLines( stride: c_int, ) void; -pub fn plotHistogram(label: [*:0]const u8, args: PlotNative) void { +pub fn plotHistogram(label: [*:0]const u8, args: PlotArgs) void { zguiPlotHistogram( label, args.v, From 1f846a9235dea4afb00eb053360a410554a995c3 Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Tue, 29 Jul 2025 14:16:30 +0800 Subject: [PATCH 05/13] - Added extended AddTextFunction The code is done, and testing is in sight. --- src/gui.zig | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/zgui.cpp | 14 ++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/gui.zig b/src/gui.zig index 5f1e885..d824233 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -4551,6 +4551,53 @@ pub const DrawList = *opaque { text: [*]const u8, text_end: [*]const u8, ) void; + const AddTextArgs = struct { + font: Font, + font_size: f32, + wrap_width: f32 = 0, + cpu_fine_clip_rect: ?[]const [4]f32 = null, + }; + pub fn addTextExtended( + draw_list: DrawList, + pos: [2]f32, + col: u32, + comptime fmt: []const u8, + args: anytype, + add_text_args: AddTextArgs, + ) void { + const txt = format(fmt, args); + addTextExtendedUnformatted(draw_list, pos, col, txt, add_text_args); + } + pub fn addTextExtendedUnformatted( + draw_list: DrawList, + pos: [2]f32, + col: u32, + txt: []const u8, + add_text_args: AddTextArgs, + ) void { + zguiDrawList_AddTextExtended( + draw_list, + add_text_args.font, + add_text_args.font_size, + &pos, + col, + txt.ptr, + txt.ptr + txt.len, + add_text_args.wrap_width, + add_text_args.cpu_fine_clip_rect, + ); + } + extern fn zguiDrawList_AddTextExtended( + draw_list: DrawList, + font: ?Font, + font_size: f32, + pos: *const [2]f32, + col: u32, + text: [*]const u8, + text_end: [*]const u8, + wrap_width: f32, + cpu_fine_clip_rect: ?[]const [4]f32, + ) void; //---------------------------------------------------------------------------------------------- pub fn addPolyline(draw_list: DrawList, points: []const [2]f32, args: struct { col: u32, diff --git a/src/zgui.cpp b/src/zgui.cpp index b96f21c..a0d53d2 100644 --- a/src/zgui.cpp +++ b/src/zgui.cpp @@ -2519,6 +2519,20 @@ extern "C" draw_list->AddText({pos[0], pos[1]}, col, text_begin, text_end); } + ZGUI_API void zguiDrawList_AddTextExtended( + ImDrawList *draw_list, + ImFont* font, + float font_size, + const float pos[2], + ImU32 col, + const char* text_begin, + const char* text_end, + float wrap_width, + const float cpu_fine_clip_rect[][4]) + { + draw_list->AddText(font, font_size, {pos[0], pos[1]}, col, text_begin, text_end, wrap_width, (const ImVec4 *)&cpu_fine_clip_rect[0][0]); + } + ZGUI_API void zguiDrawList_AddPolyline( ImDrawList *draw_list, const float points[][2], From 0aef6466413775875d48f6db05bbc1894b981dd8 Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Tue, 29 Jul 2025 16:49:54 +0800 Subject: [PATCH 06/13] - Fixed errors appeared during testing --- src/gui.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gui.zig b/src/gui.zig index d824233..eedf5f2 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -4552,10 +4552,10 @@ pub const DrawList = *opaque { text_end: [*]const u8, ) void; const AddTextArgs = struct { - font: Font, + font: ?Font, font_size: f32, wrap_width: f32 = 0, - cpu_fine_clip_rect: ?[]const [4]f32 = null, + cpu_fine_clip_rect: ?[*]const [4]f32 = null, }; pub fn addTextExtended( draw_list: DrawList, @@ -4596,7 +4596,7 @@ pub const DrawList = *opaque { text: [*]const u8, text_end: [*]const u8, wrap_width: f32, - cpu_fine_clip_rect: ?[]const [4]f32, + cpu_fine_clip_rect: ?[*]const [4]f32, ) void; //---------------------------------------------------------------------------------------------- pub fn addPolyline(draw_list: DrawList, points: []const [2]f32, args: struct { From bb26e056dcf8b24506f90330848a3ab349dcc7af Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Tue, 29 Jul 2025 20:08:10 +0800 Subject: [PATCH 07/13] - Updated Code to prevent null pointer dereference As suggested by the code review, I have replaced my direct array access with a null pointer check. --- src/zgui.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/zgui.cpp b/src/zgui.cpp index a0d53d2..ae3bb3b 100644 --- a/src/zgui.cpp +++ b/src/zgui.cpp @@ -2529,8 +2529,9 @@ extern "C" const char* text_end, float wrap_width, const float cpu_fine_clip_rect[][4]) - { - draw_list->AddText(font, font_size, {pos[0], pos[1]}, col, text_begin, text_end, wrap_width, (const ImVec4 *)&cpu_fine_clip_rect[0][0]); + { + const ImVec4* clip_rect = (cpu_fine_clip_rect != nullptr) ? (const ImVec4 *)&cpu_fine_clip_rect[0][0] : nullptr; + draw_list->AddText(font, font_size, {pos[0], pos[1]}, col, text_begin, text_end, wrap_width, clip_rect); } ZGUI_API void zguiDrawList_AddPolyline( From 9c94909b46906fccd42c6f3c7817486bf04c9d02 Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Tue, 12 Aug 2025 17:48:28 +0800 Subject: [PATCH 08/13] - Added Knobs Now we finally have the knobs for zgui which is based on the altschuler's imgui-knobs. Although it seemsingly works fully, I will do more tests before doing a pull request, along with put some images for the README section to demonstrate how to use the elements. --- build.zig | 21 ++ libs/imgui_knobs/LICENSE | 21 ++ libs/imgui_knobs/imgui-knobs.cpp | 400 +++++++++++++++++++++++++++++++ libs/imgui_knobs/imgui-knobs.h | 74 ++++++ src/gui.zig | 1 + src/knobs.zig | 99 ++++++++ src/zknobs.cpp | 55 +++++ 7 files changed, 671 insertions(+) create mode 100644 libs/imgui_knobs/LICENSE create mode 100644 libs/imgui_knobs/imgui-knobs.cpp create mode 100644 libs/imgui_knobs/imgui-knobs.h create mode 100644 src/knobs.zig create mode 100644 src/zknobs.cpp diff --git a/build.zig b/build.zig index bbbf6fb..5471fbb 100644 --- a/build.zig +++ b/build.zig @@ -54,6 +54,11 @@ pub fn build(b: *std.Build) void { "with_freetype", "Build with system FreeType engine support", ) orelse false, + .with_knobs = b.option( + bool, + "with_knobs", + "Build with bundled Imgui-Knobs", + ) orelse false, .use_wchar32 = b.option( bool, "use_wchar32", @@ -193,6 +198,22 @@ pub fn build(b: *std.Build) void { }); } + if (options.with_knobs) { + imgui.addIncludePath(b.path("libs/imgui_knobs/")); + + imgui.addCSourceFile(.{ + .file = b.path("src/zknobs.cpp"), + .flags = cflags, + }); + + imgui.addCSourceFiles(.{ + .files = &.{ + "libs/imgui_knobs/imgui-knobs.cpp", + }, + .flags = cflags, + }); + } + if (options.with_node_editor) { imgui.addCSourceFile(.{ .file = b.path("src/znode_editor.cpp"), diff --git a/libs/imgui_knobs/LICENSE b/libs/imgui_knobs/LICENSE new file mode 100644 index 0000000..c504396 --- /dev/null +++ b/libs/imgui_knobs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Simon Altschuler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/imgui_knobs/imgui-knobs.cpp b/libs/imgui_knobs/imgui-knobs.cpp new file mode 100644 index 0000000..31b27d3 --- /dev/null +++ b/libs/imgui_knobs/imgui-knobs.cpp @@ -0,0 +1,400 @@ +#include "imgui-knobs.h" + +#include +#include +#include +#include + +#define IMGUIKNOBS_PI 3.14159265358979323846f + +static inline float ImLog(int x) { return ImLog(static_cast(x)); } + +namespace ImGuiKnobs { + namespace detail { + void draw_arc(ImVec2 center, float radius, float start_angle, float end_angle, float thickness, ImColor color) { + auto *draw_list = ImGui::GetWindowDrawList(); + + draw_list->PathArcTo(center, radius, start_angle, end_angle); + draw_list->PathStroke(color, 0, thickness); + } + + template + struct knob { + float radius; + bool value_changed; + ImVec2 center; + bool is_active; + bool is_hovered; + float angle_min; + float angle_max; + float t; + float angle; + float angle_cos; + float angle_sin; + + 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, + float _angle_min, + float _angle_max) { + radius = _radius; + if (flags & ImGuiKnobFlags_Logarithmic) { + float v = ImMax(ImMin(*p_value, v_max), v_min); + t = (ImLog(ImAbs(v)) - ImLog(ImAbs(v_min))) / (ImLog(ImAbs(v_max)) - ImLog(ImAbs(v_min))); + } else { + t = ((float) *p_value - v_min) / (v_max - v_min); + } + auto screen_pos = ImGui::GetCursorScreenPos(); + + // Handle dragging + ImGui::InvisibleButton(_label, {radius * 2.0f, radius * 2.0f}); + + // Handle drag: if DragVertical or DragHorizontal flags are set, only the given direction is + // used, otherwise use the drag direction with the highest delta + ImGuiIO &io = ImGui::GetIO(); + bool drag_vertical = + !(flags & ImGuiKnobFlags_DragHorizontal) && + (flags & ImGuiKnobFlags_DragVertical || ImAbs(io.MouseDelta[ImGuiAxis_Y]) > ImAbs(io.MouseDelta[ImGuiAxis_X])); + + auto gid = ImGui::GetID(_label); + ImGuiSliderFlags drag_behaviour_flags = 0; + if (drag_vertical) { + drag_behaviour_flags |= ImGuiSliderFlags_Vertical; + } + if (flags & ImGuiKnobFlags_AlwaysClamp) { + drag_behaviour_flags |= ImGuiSliderFlags_AlwaysClamp; + } + if (flags & ImGuiKnobFlags_Logarithmic) { + drag_behaviour_flags |= ImGuiSliderFlags_Logarithmic; + } + value_changed = ImGui::DragBehavior( + gid, + data_type, + p_value, + speed, + &v_min, + &v_max, + format, + drag_behaviour_flags); + + angle_min = _angle_min < 0 ? IMGUIKNOBS_PI * 0.75f : _angle_min; + angle_max = _angle_max < 0 ? IMGUIKNOBS_PI * 2.25f : _angle_max; + + center = {screen_pos[0] + radius, screen_pos[1] + radius}; + is_active = ImGui::IsItemActive(); + is_hovered = ImGui::IsItemHovered(); + angle = angle_min + (angle_max - angle_min) * t; + angle_cos = cosf(angle); + angle_sin = sinf(angle); + } + + void draw_dot(float size, float radius, float angle, color_set color, bool filled, int segments) { + auto dot_size = size * this->radius; + auto dot_radius = radius * this->radius; + + ImGui::GetWindowDrawList()->AddCircleFilled( + {center[0] + cosf(angle) * dot_radius, + center[1] + sinf(angle) * dot_radius}, + dot_size, + is_active ? color.active : (is_hovered ? color.hovered : color.base), + segments); + } + + void draw_tick(float start, float end, float width, float angle, color_set color) { + auto tick_start = start * radius; + auto tick_end = end * radius; + auto angle_cos = cosf(angle); + auto angle_sin = sinf(angle); + + ImGui::GetWindowDrawList()->AddLine( + {center[0] + angle_cos * tick_end, center[1] + angle_sin * tick_end}, + {center[0] + angle_cos * tick_start, + center[1] + angle_sin * tick_start}, + is_active ? color.active : (is_hovered ? color.hovered : color.base), + width * radius); + } + + void draw_circle(float size, color_set color, bool filled, int segments) { + auto circle_radius = size * radius; + + ImGui::GetWindowDrawList()->AddCircleFilled( + center, + circle_radius, + is_active ? color.active : (is_hovered ? color.hovered : color.base)); + } + + void draw_arc(float radius, float size, float start_angle, float end_angle, color_set color) { + auto track_radius = radius * this->radius; + auto track_size = size * this->radius * 0.5f + 0.0001f; + + detail::draw_arc(center, track_radius, start_angle, end_angle, track_size, is_active ? color.active : (is_hovered ? color.hovered : color.base)); + } + }; + + template + knob 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, + float angle_min, + float angle_max) { + if (flags & ImGuiKnobFlags_Logarithmic && v_min <= 0.0 && v_max >= 0.0) { + // we must handle the cornercase if a client specifies a logarithmic range that contains zero + // for this we clamp lower limit to avoid hitting zero like it is done in ImGui::SliderBehaviorT + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1; + v_min = ImPow(0.1f, (float) decimal_precision); + v_max = ImMax(v_min, v_max); // this ensures that in the cornercase v_max is still at least ge v_min + *p_value = ImMax(ImMin(*p_value, v_max), v_min); // this ensures that in the cornercase p_value is within the range + } + + auto speed = _speed == 0 ? (v_max - v_min) / 250.f : _speed; + ImGui::PushID(label); + +#if IMGUI_VERSION_NUM < 19197 + auto font_scale = ImGui::GetIO().FontGlobalScale; +#else + auto font_scale = ImGui::GetStyle().FontScaleMain; +#endif + auto width = size == 0 ? ImGui::GetTextLineHeight() * 4.0f : size * font_scale; + ImGui::PushItemWidth(width); + + ImGui::BeginGroup(); + + // There's an issue with `SameLine` and Groups, see + // https://github.com/ocornut/imgui/issues/4190. This is probably not the best + // solution, but seems to work for now + ImGui::GetCurrentWindow()->DC.CurrLineTextBaseOffset = 0; + + // Draw title + if (!(flags & ImGuiKnobFlags_NoTitle)) { + auto title_size = ImGui::CalcTextSize(label, NULL, false, width); + + // Center title + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + + (width - title_size[0]) * 0.5f); + + ImGui::Text("%s", label); + } + + // Draw knob + knob k(label, data_type, p_value, v_min, v_max, speed, width * 0.5f, format, flags, angle_min, angle_max); + + // Draw tooltip + if (flags & ImGuiKnobFlags_ValueTooltip && + (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) || + ImGui::IsItemActive())) { + ImGui::BeginTooltip(); + ImGui::Text(format, *p_value); + ImGui::EndTooltip(); + } + + // Draw input + if (!(flags & ImGuiKnobFlags_NoInput)) { + ImGuiSliderFlags drag_scalar_flags = 0; + if (flags & ImGuiKnobFlags_AlwaysClamp) { + drag_scalar_flags |= ImGuiSliderFlags_AlwaysClamp; + } + if (flags & ImGuiKnobFlags_Logarithmic) { + drag_scalar_flags |= ImGuiSliderFlags_Logarithmic; + } + auto changed = ImGui::DragScalar("###knob_drag", data_type, p_value, speed, &v_min, &v_max, format, drag_scalar_flags); + if (changed) { + k.value_changed = true; + } + } + + ImGui::EndGroup(); + ImGui::PopItemWidth(); + ImGui::PopID(); + + return k; + } + + color_set GetPrimaryColorSet() { + auto *colors = ImGui::GetStyle().Colors; + + return {colors[ImGuiCol_ButtonActive], colors[ImGuiCol_ButtonHovered], colors[ImGuiCol_ButtonHovered]}; + } + + color_set GetSecondaryColorSet() { + auto *colors = ImGui::GetStyle().Colors; + auto active = ImVec4(colors[ImGuiCol_ButtonActive].x * 0.5f, + colors[ImGuiCol_ButtonActive].y * 0.5f, + colors[ImGuiCol_ButtonActive].z * 0.5f, + colors[ImGuiCol_ButtonActive].w); + + auto hovered = ImVec4(colors[ImGuiCol_ButtonHovered].x * 0.5f, + colors[ImGuiCol_ButtonHovered].y * 0.5f, + colors[ImGuiCol_ButtonHovered].z * 0.5f, + colors[ImGuiCol_ButtonHovered].w); + + return {active, hovered, hovered}; + } + + color_set GetTrackColorSet() { + auto *colors = ImGui::GetStyle().Colors; + + return {colors[ImGuiCol_Button], colors[ImGuiCol_Button], colors[ImGuiCol_Button]}; + } + }// namespace detail + + template + 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, + float angle_min, + float angle_max) { + auto knob = detail::knob_with_drag( + label, + data_type, + p_value, + v_min, + v_max, + speed, + format, + size, + flags, + angle_min, + angle_max); + + switch (variant) { + case ImGuiKnobVariant_Tick: { + knob.draw_circle(0.85f, detail::GetSecondaryColorSet(), true, 32); + knob.draw_tick(0.5f, 0.85f, 0.08f, knob.angle, detail::GetPrimaryColorSet()); + break; + } + case ImGuiKnobVariant_Dot: { + knob.draw_circle(0.85f, detail::GetSecondaryColorSet(), true, 32); + knob.draw_dot(0.12f, 0.6f, knob.angle, detail::GetPrimaryColorSet(), true, 12); + break; + } + + case ImGuiKnobVariant_Wiper: { + knob.draw_circle(0.7f, detail::GetSecondaryColorSet(), true, 32); + knob.draw_arc(0.8f, 0.41f, knob.angle_min, knob.angle_max, detail::GetTrackColorSet()); + + if (knob.t > 0.01f) { + knob.draw_arc(0.8f, 0.43f, knob.angle_min, knob.angle, detail::GetPrimaryColorSet()); + } + break; + } + case ImGuiKnobVariant_WiperOnly: { + knob.draw_arc(0.8f, 0.41f, knob.angle_min, knob.angle_max, detail::GetTrackColorSet()); + + if (knob.t > 0.01) { + knob.draw_arc(0.8f, 0.43f, knob.angle_min, knob.angle, detail::GetPrimaryColorSet()); + } + break; + } + case ImGuiKnobVariant_WiperDot: { + knob.draw_circle(0.6f, detail::GetSecondaryColorSet(), true, 32); + knob.draw_arc(0.85f, 0.41f, knob.angle_min, knob.angle_max, detail::GetTrackColorSet()); + knob.draw_dot(0.1f, 0.85f, knob.angle, detail::GetPrimaryColorSet(), true, 12); + break; + } + case ImGuiKnobVariant_Stepped: { + for (auto n = 0.f; n < steps; n++) { + auto a = n / (steps - 1); + auto angle = knob.angle_min + (knob.angle_max - knob.angle_min) * a; + knob.draw_tick(0.7f, 0.9f, 0.04f, angle, detail::GetPrimaryColorSet()); + } + + knob.draw_circle(0.6f, detail::GetSecondaryColorSet(), true, 32); + knob.draw_dot(0.12f, 0.4f, knob.angle, detail::GetPrimaryColorSet(), true, 12); + break; + } + case ImGuiKnobVariant_Space: { + knob.draw_circle(0.3f - knob.t * 0.1f, detail::GetSecondaryColorSet(), true, 16); + + if (knob.t > 0.01f) { + knob.draw_arc(0.4f, 0.15f, knob.angle_min - 1.0f, knob.angle - 1.0f, detail::GetPrimaryColorSet()); + knob.draw_arc(0.6f, 0.15f, knob.angle_min + 1.0f, knob.angle + 1.0f, detail::GetPrimaryColorSet()); + knob.draw_arc(0.8f, 0.15f, knob.angle_min + 3.0f, knob.angle + 3.0f, detail::GetPrimaryColorSet()); + } + break; + } + } + + return knob.value_changed; + } + + 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, + float angle_min, + float angle_max) { + return BaseKnob( + label, + ImGuiDataType_Float, + p_value, + v_min, + v_max, + speed, + format, + variant, + size, + flags, + steps, + angle_min, + angle_max); + } + + 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, + float angle_min, + float angle_max) { + return BaseKnob( + label, + ImGuiDataType_S32, + p_value, + v_min, + v_max, + speed, + format, + variant, + size, + flags, + steps, + angle_min, + angle_max); + } +}// namespace ImGuiKnobs diff --git a/libs/imgui_knobs/imgui-knobs.h b/libs/imgui_knobs/imgui-knobs.h new file mode 100644 index 0000000..a132c7b --- /dev/null +++ b/libs/imgui_knobs/imgui-knobs.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +typedef int ImGuiKnobFlags; + +enum ImGuiKnobFlags_ { + ImGuiKnobFlags_NoTitle = 1 << 0, + ImGuiKnobFlags_NoInput = 1 << 1, + ImGuiKnobFlags_ValueTooltip = 1 << 2, + ImGuiKnobFlags_DragHorizontal = 1 << 3, + ImGuiKnobFlags_DragVertical = 1 << 4, + ImGuiKnobFlags_Logarithmic = 1 << 5, + ImGuiKnobFlags_AlwaysClamp = 1 << 6 +}; + +typedef int ImGuiKnobVariant; + +enum ImGuiKnobVariant_ { + ImGuiKnobVariant_Tick = 1 << 0, + ImGuiKnobVariant_Dot = 1 << 1, + ImGuiKnobVariant_Wiper = 1 << 2, + ImGuiKnobVariant_WiperOnly = 1 << 3, + ImGuiKnobVariant_WiperDot = 1 << 4, + ImGuiKnobVariant_Stepped = 1 << 5, + ImGuiKnobVariant_Space = 1 << 6, +}; + +namespace ImGuiKnobs { + + struct color_set { + ImColor base; + ImColor hovered; + ImColor active; + + color_set(ImColor base, ImColor hovered, ImColor active) + : base(base), hovered(hovered), active(active) {} + + color_set(ImColor color) { + base = color; + hovered = color; + active = color; + } + }; + + bool Knob( + const char *label, + float *p_value, + float v_min, + float v_max, + float speed = 0, + const char *format = "%.3f", + ImGuiKnobVariant variant = ImGuiKnobVariant_Tick, + float size = 0, + ImGuiKnobFlags flags = 0, + int steps = 10, + float angle_min = -1, + float angle_max = -1); + bool KnobInt( + const char *label, + int *p_value, + int v_min, + int v_max, + float speed = 0, + const char *format = "%i", + ImGuiKnobVariant variant = ImGuiKnobVariant_Tick, + float size = 0, + ImGuiKnobFlags flags = 0, + int steps = 10, + float angle_min = -1, + float angle_max = -1); + +}// namespace ImGuiKnobs diff --git a/src/gui.zig b/src/gui.zig index 00c69c5..2fec19d 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -8,6 +8,7 @@ pub const plot = @import("plot.zig"); pub const gizmo = @import("gizmo.zig"); pub const node_editor = @import("node_editor.zig"); pub const te = @import("te.zig"); +pub const knobs = @import("knobs.zig"); pub const backend = switch (@import("zgui_options").backend) { .glfw_wgpu => @import("backend_glfw_wgpu.zig"), diff --git a/src/knobs.zig b/src/knobs.zig new file mode 100644 index 0000000..ea221ec --- /dev/null +++ b/src/knobs.zig @@ -0,0 +1,99 @@ +const gui = @import("gui.zig"); + +pub const KnobFlags = packed struct(u32) { + no_title: bool = false, + no_input: bool = false, + value_tooltip: bool = false, + drag_horizontal: bool = false, + drag_vertical: bool = false, + logarithmic: bool = false, + always_clamp: bool = false, + _padding: u25 = 0, +}; + +pub const KnobVariant = packed struct(u32) { + tick: bool = false, + dot: bool = false, + wiper: bool = false, + wiper_only: bool = false, + wiper_dot: bool = false, + stepped: bool = false, + space: bool = false, + _padding: u25 = 0, +}; + +//---------------------------------------------------------------------------------------------------------------------| +fn KnobTypeGen(comptime T: type) type { + const cfmt = switch (T) { + f32 => "%.3f", + i32 => "%i", + else => { + @panic("Unspported Knob Type"); + }, + }; + + return struct { + v: *T, + v_min: T, + v_max: T, + speed: f32 = 0, + comptime cfmt: [:0]const u8 = cfmt, + variant: KnobVariant = .{ .tick = true }, // an enum + size: f32 = 0, + flags: KnobFlags = .{}, + steps: i32 = 10, + angle_min: f32 = -1, + angle_max: f32 = -1, + }; +} + +/// Remarks: +/// - angle_min and angle_max works in radian with the starting position at the right side of the circle +/// - steps only affects the visuals for stepped KnobVariant +pub fn knob( + label: [*:0]const u8, + args: KnobTypeGen(f32), +) bool { + return zknobs_knob(label, args.v, args.v_min, args.v_max, args.speed, args.cfmt, args.variant, args.size, args.flags, args.steps, args.angle_min, args.angle_max); +} + +/// Remarks: +/// - angle_min and angle_max works in radian with the starting position at the right side of the circle +/// - steps only affects the visuals for stepped KnobVariant +pub fn knob_int( + label: [*:0]const u8, + args: KnobTypeGen(i32), +) bool { + return zknobs_knob_int(label, args.v, args.v_min, args.v_max, args.speed, args.cfmt, args.variant, args.size, args.flags, args.steps, args.angle_min, args.angle_max); +} + +//---------------------------------------------------------------------------------------------------------------------| +extern fn zknobs_knob( + label: [*:0]const u8, + v: *f32, + v_min: f32, + v_max: f32, + speed: f32, + cfmt: [*:0]const u8, + variant: KnobVariant, + size: f32, + flags: KnobFlags, + steps: c_int, + angle_min: f32, + angle_max: f32, +) bool; + +extern fn zknobs_knob_int( + label: [*:0]const u8, + v: *c_int, + v_min: c_int, + v_max: c_int, + speed: f32, + cfmt: [*:0]const u8, + variant: KnobVariant, + size: f32, + flags: KnobFlags, + steps: c_int, + angle_min: f32, + angle_max: f32, +) bool; diff --git a/src/zknobs.cpp b/src/zknobs.cpp new file mode 100644 index 0000000..78b1c5c --- /dev/null +++ b/src/zknobs.cpp @@ -0,0 +1,55 @@ +#include "imgui.h" + +#include "imgui-knobs.h" + +#include "imgui_internal.h" + +#ifndef ZGUI_API +#define ZGUI_API +#endif + +//-------------------------------------------------------------------------------------------------- +// +// imgui-knobs +// +//-------------------------------------------------------------------------------------------------- +extern "C" +{ + ZGUI_API bool zknobs_knob( + const char *label, + float *v, + float v_min, + float v_max, + float speed, + const char *format, + ImGuiKnobVariant variant, + float size, + ImGuiKnobFlags flags, + int steps, + float angle_min, + float angle_max + ){ + return ImGuiKnobs::Knob( + label, v, v_min, v_max, speed, format, variant, size, flags, steps, angle_min, angle_max + ); + } + + ZGUI_API bool zknobs_knob_int( + const char *label, + int *v, + int v_min, + int v_max, + float speed, + const char *format, + ImGuiKnobVariant variant, + float size, + ImGuiKnobFlags flags, + int steps, + float angle_min, + float angle_max + ){ + return ImGuiKnobs::KnobInt( + label, v, v_min, v_max, speed, format, variant, size, flags, steps, angle_min, angle_max + ); + } +} \ No newline at end of file From 1a65490f336f60df77ef4b4368c576eeb1b3f6bc Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Tue, 12 Aug 2025 22:53:50 +0800 Subject: [PATCH 09/13] - Updated README A simple example is added for knobs such that to demostrate how to create knobs for your imgui programs. --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index f6af9b8..37fb045 100644 --- a/README.md +++ b/README.md @@ -240,3 +240,47 @@ defer zgui.node_editor.setCurrentEditor(null); } } ``` + +### Imgui-Knobs Api. +zig wrapper for [imgui-knobs](https://github.com/altschuler/imgui-knobs) + +``` zig +// Minimal knob function call +_ = zgui.knobs.knob("Minimal", .{ + .v = &v_knob, + .v_min = 0, + .v_max = 1.0, +}); +zgui.sameLine(.{}); + +// Styled f32 knob +_ = zgui.knobs.knob("f32 Knob", .{ + .v = &v_knob, + .v_min = 0, + .v_max = 1.0, + .size = 200, + .speed = 0.0005, + .angle_min = std.math.pi, + .angle_max = 2 * std.math.pi, + .variant = .{ .stepped = true }, + .steps = 5, +}); +zgui.sameLine(.{}); + +// Styled i32 knob (applied color) +zgui.pushStyleColor4f(.{ .idx = zgui.StyleCol.button_active, .c = .{ 0.6, 0.2, 0.2, 1 } }); +zgui.pushStyleColor4f(.{ .idx = zgui.StyleCol.button_hovered, .c = .{ 0.6, 0.4, 0.4, 1 } }); +zgui.pushStyleColor4f(.{ .idx = zgui.StyleCol.button, .c = .{ 0.4, 0, 0, 1 } }); +_ = zgui.knobs.knob_int("i32 Knob", .{ + .v = &v_knob_int, + .v_min = -10, + .v_max = 10, + .size = 250, + .variant = zgui.knobs.KnobVariant{ .wiper_dot = true }, + .flags = zgui.knobs.KnobFlags{ + .drag_horizontal = true, + .no_input = true, + }, +}); +zgui.popStyleColor(.{ .count = 3 }); +``` From 743b86c661abf2f16167a13901e42a82f5de4caf Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Tue, 12 Aug 2025 23:27:56 +0800 Subject: [PATCH 10/13] - Fixed Naming Convention Seems like my function name was a bit different where I used snake case while the other code use camcel case, so I have unified my code to align the coding style of others. --- src/knobs.zig | 10 +++++----- src/zknobs.cpp | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/knobs.zig b/src/knobs.zig index ea221ec..9735081 100644 --- a/src/knobs.zig +++ b/src/knobs.zig @@ -54,21 +54,21 @@ pub fn knob( label: [*:0]const u8, args: KnobTypeGen(f32), ) bool { - return zknobs_knob(label, args.v, args.v_min, args.v_max, args.speed, args.cfmt, args.variant, args.size, args.flags, args.steps, args.angle_min, args.angle_max); + return zknobs_Knob(label, args.v, args.v_min, args.v_max, args.speed, args.cfmt, args.variant, args.size, args.flags, args.steps, args.angle_min, args.angle_max); } /// Remarks: /// - angle_min and angle_max works in radian with the starting position at the right side of the circle /// - steps only affects the visuals for stepped KnobVariant -pub fn knob_int( +pub fn knobInt( label: [*:0]const u8, args: KnobTypeGen(i32), ) bool { - return zknobs_knob_int(label, args.v, args.v_min, args.v_max, args.speed, args.cfmt, args.variant, args.size, args.flags, args.steps, args.angle_min, args.angle_max); + return zknobs_KnobInt(label, args.v, args.v_min, args.v_max, args.speed, args.cfmt, args.variant, args.size, args.flags, args.steps, args.angle_min, args.angle_max); } //---------------------------------------------------------------------------------------------------------------------| -extern fn zknobs_knob( +extern fn zknobs_Knob( label: [*:0]const u8, v: *f32, v_min: f32, @@ -83,7 +83,7 @@ extern fn zknobs_knob( angle_max: f32, ) bool; -extern fn zknobs_knob_int( +extern fn zknobs_KnobInt( label: [*:0]const u8, v: *c_int, v_min: c_int, diff --git a/src/zknobs.cpp b/src/zknobs.cpp index 78b1c5c..c61e288 100644 --- a/src/zknobs.cpp +++ b/src/zknobs.cpp @@ -15,7 +15,7 @@ //-------------------------------------------------------------------------------------------------- extern "C" { - ZGUI_API bool zknobs_knob( + ZGUI_API bool zknobs_Knob( const char *label, float *v, float v_min, @@ -34,7 +34,7 @@ extern "C" ); } - ZGUI_API bool zknobs_knob_int( + ZGUI_API bool zknobs_KnobInt( const char *label, int *v, int v_min, From e32b797810ca45598173fc5f2ac8e54b15d0292d Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Tue, 12 Aug 2025 23:30:04 +0800 Subject: [PATCH 11/13] - Updated README with correct function name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37fb045..531f449 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ zgui.sameLine(.{}); zgui.pushStyleColor4f(.{ .idx = zgui.StyleCol.button_active, .c = .{ 0.6, 0.2, 0.2, 1 } }); zgui.pushStyleColor4f(.{ .idx = zgui.StyleCol.button_hovered, .c = .{ 0.6, 0.4, 0.4, 1 } }); zgui.pushStyleColor4f(.{ .idx = zgui.StyleCol.button, .c = .{ 0.4, 0, 0, 1 } }); -_ = zgui.knobs.knob_int("i32 Knob", .{ +_ = zgui.knobs.knobInt("i32 Knob", .{ .v = &v_knob_int, .v_min = -10, .v_max = 10, From 8493dbba661dd71a3dfac7acd39e3e5b5407d186 Mon Sep 17 00:00:00 2001 From: Chris Heyes <22148308+hazeycode@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:23:21 +0100 Subject: [PATCH 12/13] Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/knobs.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knobs.zig b/src/knobs.zig index 9735081..d33c8a8 100644 --- a/src/knobs.zig +++ b/src/knobs.zig @@ -28,7 +28,7 @@ fn KnobTypeGen(comptime T: type) type { f32 => "%.3f", i32 => "%i", else => { - @panic("Unspported Knob Type"); + @panic("Unsupported Knob Type"); }, }; From bed3c9da4170b83b5ce9dd75b12cdbd55e7d51bc Mon Sep 17 00:00:00 2001 From: Logickin-Lambda Date: Mon, 1 Sep 2025 17:27:56 +0800 Subject: [PATCH 13/13] - Added Version for the knob library Due to the lack of official release, we have concluded to use branch name + commit hash, and I have also set the link used in the version redirecting to that particular commit history for reference. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 531f449..80ef5a0 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Easy to use, hand-crafted API with default arguments, named parameters and Zig s * [Plot API](#plot-api) for advanced data visualizations * [Gizmo API](#gizmo-api) for gizmo * [Node editor API](#node-editor-api) for node based stuff +* [Knobs API](https://github.com/altschuler/imgui-knobs) for knobs ## Versions @@ -21,6 +22,7 @@ Easy to use, hand-crafted API with default arguments, named parameters and Zig s * [ImPlot](https://github.com/epezent/implot) `O.17` * [ImGuizmo](https://github.com/CedricGuillemet/ImGuizmo) `1.89 WIP` * [ImGuiNodeEditor](https://github.com/thedmd/imgui-node-editor/tree/v0.9.3) `O.9.3` +* [imgui-knobs](https://github.com/altschuler/imgui-knobs/commit/8a43bf7b31c4166ec50f3a52c382c2cc66a91516) `main - commit 8a43bf7b31c4166ec50f3a52c382c2cc66a91516` ## Getting started