Expose some missing ImPlot functions#98
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request exposes additional ImPlot functions in the zgui library, addressing issue #97. The changes add Zig wrappers and C++ bindings for various ImPlot functions related to styling, axis configuration, coordinate transformation, subplots, and custom item rendering. The implementation follows existing patterns in the codebase for wrapping C++ ImPlot APIs with idiomatic Zig interfaces.
Changes:
- Added C++ wrapper functions in
zplot.cppfor new ImPlot functions including style setters, axis configuration, coordinate conversions, and internal functions for custom item rendering - Implemented Zig API bindings in
plot.zigwith struct-based optional parameters following existing conventions - Added
AxisOrAutoenum to support IMPLOT_AUTO (-1) sentinel value for axis parameters - Updated
StyleColenum to includeauto = -1for IMPLOT_AUTO color support - Added default parameter to
addLineingui.zigfor consistency with other drawing functions - Updated
.gitignoreto exclude IDE folders
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/zplot.cpp | Added C++ wrappers for SetNextLineStyle, SetNextFillStyle, SetNextMarkerStyle, SetNextErrorBarStyle, SetupAxisFormat, SetupAxisTicks, SetupAxisLimitsConstraints, SetupAxisZoomConstraints, SetNextAxesToFit, PixelsToPlot, PlotToPixels, GetPlotPos, GetPlotSize, GetPlotMousePos, BeginSubplots, EndSubplots, PushPlotClipRect, PopPlotClipRect, BeginItem, EndItem, and FitPoint functions. Includes implot_internal.h for BeginItem/EndItem. |
| src/plot.zig | Added Zig bindings for all new C++ wrappers with idiomatic struct-based APIs. Modified StyleCol and Axis enums to use i32 and added AxisOrAuto enum. Included formatter wrapper function for setupAxisFormat. |
| src/gui.zig | Added default value (1.0) to thickness parameter in addLine function for consistency with other drawing functions. |
| .gitignore | Added patterns to ignore IDE folders (.vscode and .idea). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/plot.zig
Outdated
| pub const StyleCol = enum(i32) { | ||
| auto = -1, | ||
| line = 0, |
There was a problem hiding this comment.
Adding auto = -1 to the StyleCol enum will break the array size calculation in line 84 of the Style struct. The enum now has 22 fields (including auto), but the C++ ImPlotStyle.Colors array has size ImPlotCol_COUNT which is 21. The auto value is a sentinel (like IMPLOT_AUTO) and should not be counted as an array index. This will cause a size mismatch with the C++ struct and potential memory corruption.
There was a problem hiding this comment.
True. In general, zgui Style itself is an extern struct while ImGuiStyle is not, it's a C++ struct. Strictly speaking #65 should be completed to use extern structs with the same layout on both sides.
src/plot.zig
Outdated
|
|
||
| //---------------------------------------------------------------------------------------------- | ||
| /// Get the plot draw list for custom rendering to the current plot area. Call between begin/endPlot. | ||
| pub const getPlotDrawList = gui.getWindowDrawList; |
There was a problem hiding this comment.
The alias to gui.getWindowDrawList is incorrect. ImPlot's GetPlotDrawList() is a separate function that should have its own C++ wrapper in zplot.cpp and Zig binding. While both return ImDrawList*, they serve different purposes - GetPlotDrawList() returns the draw list for the current plot area, while GetWindowDrawList() returns the draw list for the current window. Without a proper wrapper function zguiPlot_GetPlotDrawList in zplot.cpp, this will not work.
| pub const getPlotDrawList = gui.getWindowDrawList; | |
| const GetWindowDrawListFn = @TypeOf(gui.getWindowDrawList); | |
| const GetWindowDrawListReturnType = @typeInfo(GetWindowDrawListFn).Fn.return_type.?; | |
| pub fn getPlotDrawList() GetWindowDrawListReturnType { | |
| return zguiPlot_GetPlotDrawList(); | |
| } | |
| extern fn zguiPlot_GetPlotDrawList() GetWindowDrawListReturnType; |
There was a problem hiding this comment.
Well, I don't think there is a dedicated plot draw list, that's why it works perfectly fine as is, without any wrappers, because ImPlot just returns ImGui window draw list. I'll create the wrapper function though, to hide that implementation detail.
src/plot.zig
Outdated
| labels: [][*:0]const u8 = &.{}, | ||
| keep_default: bool = false, | ||
| }; | ||
| pub fn setupAxisTicks(axis: Axis, args: SetupAxisTicks) void { | ||
| const n_ticks = std.math.cast(i32, args.values.len) orelse { | ||
| @panic("Too many tick values!"); | ||
| }; | ||
| zguiPlot_SetupAxisTicks(axis, args.values.ptr, n_ticks, args.labels.ptr, args.keep_default); | ||
| } | ||
| extern fn zguiPlot_SetupAxisTicks( | ||
| axis: Axis, | ||
| values: [*]const f64, | ||
| n_ticks: i32, | ||
| labels: [*][*:0]const u8, |
There was a problem hiding this comment.
The default for args.labels in SetupAxisTicks is an empty slice (&.{}), but setupAxisTicks always passes args.labels.ptr along with n_ticks = args.values.len to zguiPlot_SetupAxisTicks. The underlying ImPlot API expects either a null pointer or an array of at least n_ticks label pointers; passing a non-null pointer to a zero-length buffer can cause out-of-bounds reads when ImPlot indexes labels, potentially leaking or corrupting memory when it interprets arbitrary memory as strings. Make labels optional and pass a null pointer when no labels are provided, or ensure the labels buffer has at least n_ticks elements and validate labels.len against values.len before calling into ImPlot.
| labels: [][*:0]const u8 = &.{}, | |
| keep_default: bool = false, | |
| }; | |
| pub fn setupAxisTicks(axis: Axis, args: SetupAxisTicks) void { | |
| const n_ticks = std.math.cast(i32, args.values.len) orelse { | |
| @panic("Too many tick values!"); | |
| }; | |
| zguiPlot_SetupAxisTicks(axis, args.values.ptr, n_ticks, args.labels.ptr, args.keep_default); | |
| } | |
| extern fn zguiPlot_SetupAxisTicks( | |
| axis: Axis, | |
| values: [*]const f64, | |
| n_ticks: i32, | |
| labels: [*][*:0]const u8, | |
| /// Optional tick labels. If null, no labels are provided and a null pointer is passed to ImPlot. | |
| /// If non-null, the slice must have at least `values.len` elements. | |
| labels: ?[][*:0]const u8 = null, | |
| keep_default: bool = false, | |
| }; | |
| pub fn setupAxisTicks(axis: Axis, args: SetupAxisTicks) void { | |
| const n_ticks = std.math.cast(i32, args.values.len) orelse { | |
| @panic("Too many tick values!"); | |
| }; | |
| const labels_ptr: ?[*][*:0]const u8 = blk: { | |
| if (args.labels) |labels_slice| { | |
| if (labels_slice.len < args.values.len) { | |
| @panic("Number of labels must be at least the number of tick values!"); | |
| } | |
| break :blk labels_slice.ptr; | |
| } else { | |
| break :blk null; | |
| } | |
| }; | |
| zguiPlot_SetupAxisTicks(axis, args.values.ptr, n_ticks, labels_ptr, args.keep_default); | |
| } | |
| extern fn zguiPlot_SetupAxisTicks( | |
| axis: Axis, | |
| values: [*]const f64, | |
| n_ticks: i32, | |
| labels: ?[*][*:0]const u8, |
There was a problem hiding this comment.
Good catch, I'll push updates shortly.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/plot.zig
Outdated
| pub fn beginSubplots(title_id: [*:0]const u8, args: BeginSubplots) bool { | ||
| // Both must be greater than 0 according to ImPlot docs. | ||
| const rs: i32 = @intCast(args.rows); | ||
| const cs: i32 = @intCast(args.cols); | ||
| return zguiPlot_BeginSubplots(title_id, rs, cs, args.width, args.height, args.flags, args.row_ratios, args.col_ratios); | ||
| } |
There was a problem hiding this comment.
beginSubplots accepts title_id: [*:0]const u8 whereas beginPlot uses [:0]const u8. Taking the raw pointer here makes it easier to accidentally pass a non-null-terminated string and is inconsistent with the rest of the public API. Consider changing the parameter to [:0]const u8 for safety and consistency (the extern wrapper can still take [ *:0 ]const u8).
src/plot.zig
Outdated
| const rs: i32 = @intCast(args.rows); | ||
| const cs: i32 = @intCast(args.cols); |
There was a problem hiding this comment.
beginSubplots comments that rows/cols must be > 0, but it doesn't enforce this (0 is still representable in u32). Passing 0 through to ImPlot can trigger assertions or undefined behavior depending on build settings. Add a runtime check (assert/panic) for args.rows > 0 and args.cols > 0, and consider using std.math.cast(i32, ...) to produce a clearer error if the values don't fit in i32.
| const rs: i32 = @intCast(args.rows); | |
| const cs: i32 = @intCast(args.cols); | |
| assert(args.rows > 0); | |
| assert(args.cols > 0); | |
| const rs = std.math.cast(i32, args.rows) orelse @panic("BeginSubplots: rows value does not fit into i32"); | |
| const cs = std.math.cast(i32, args.cols) orelse @panic("BeginSubplots: cols value does not fit into i32"); |
| pub fn setupAxisTicks(axis: Axis, args: SetupAxisTicks) void { | ||
| const n_ticks = std.math.cast(i32, args.values.len) orelse { | ||
| @panic("Too many tick values!"); | ||
| }; | ||
|
|
||
| const labels_ptr = blk: { | ||
| if (args.labels) |labels| { | ||
| break :blk if (labels.len != 0) labels.ptr else null; | ||
| } else { | ||
| break :blk null; | ||
| } | ||
| }; | ||
|
|
||
| zguiPlot_SetupAxisTicks(axis, args.values.ptr, n_ticks, labels_ptr, args.keep_default); |
There was a problem hiding this comment.
setupAxisTicks can pass a labels buffer that is shorter than args.values.len to zguiPlot_SetupAxisTicks without any validation. If callers supply args.labels where labels.len is non-zero but less than args.values.len, ImPlot will index past the end of the labels array and treat arbitrary memory as label pointers, which can leak memory contents or corrupt process state. Consider enforcing that labels is null or has at least args.values.len elements before calling zguiPlot_SetupAxisTicks, and panic or return early when this precondition is not met.
Changes for #97.