From 6b0bab9e542486609692fcfc8b906043fa211301 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Sat, 6 Jun 2026 21:38:49 -0700 Subject: [PATCH 1/2] v1 parity: RAII scope wrappers for every Begin/End + Push/Pop pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v1 thin tier (imgui_implot_boost) wraps the paired APIs whose End/Pop can't be safely skipped — that is v1's whole value-add. Everything else (items, styling, colormap sampling, coord conversion, axis/legend setup) is already reachable raw via `require implot public`, so it needs no wrapper. This fills the gaps: the only paired APIs v1 was missing. Added (same arity-overload style as the existing with_plot / with_subplots, since gen2 binds a trailing block to the slot after the last explicit arg, so optional args become overloads not defaults): - with_aligned_plots (BeginAlignedPlots / EndAlignedPlots) - with_colormap (PushColormap / PopColormap) - with_style_color (PushStyleColor / PopStyleColor) - with_style_var (PushStyleVar / PopStyleVar — float / int / float2) - with_plot_clip_rect (PushPlotClipRect / PopPlotClipRect) - with_legend_popup (BeginLegendPopup / EndLegendPopup) - with_drag_drop_source_item / _plot / _axis (Begin* / EndDragDropSource) - with_drag_drop_target_plot / _axis / _legend (Begin* / EndDragDropTarget) Push/Pop scopes run the body unconditionally and always Pop; Begin/End scopes run the body iff Begin returned true (the ImPlot contract). tests/integration/test_v1_scopes.das: a headless in-process smoke (v1 has no snapshot hook to query, so it nests every new scope and passes iff it renders crash-free — an unbalanced Push/Pop or a missed End would trip an ImGui assert). Full suite 13/13 green. Co-Authored-By: Claude Opus 4.8 (1M context) --- daslib/implot_boost.das | 133 +++++++++++++++++++++++++++ tests/integration/test_v1_scopes.das | 74 +++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 tests/integration/test_v1_scopes.das diff --git a/daslib/implot_boost.das b/daslib/implot_boost.das index 3ebf1e4..db1ab52 100644 --- a/daslib/implot_boost.das +++ b/daslib/implot_boost.das @@ -52,6 +52,139 @@ def public with_subplots(title : string; rows : int; cols : int; size : float2; } } +// Begin a group of axis-aligned plots, run the body if it began, always End it. +def public with_aligned_plots(group_id : string; blk : block) { + if (BeginAlignedPlots(group_id, true)) { + invoke(blk) + EndAlignedPlots() + } +} +def public with_aligned_plots(group_id : string; vertical : bool; blk : block) { + if (BeginAlignedPlots(group_id, vertical)) { + invoke(blk) + EndAlignedPlots() + } +} + +// ===== Style scopes (Push/Pop — always balanced) ===== + +// Push a colormap for the body; auto-colored items + heatmaps inside sample from it. +def public with_colormap(cmap : ImPlotColormap; blk : block) { + PushColormap(cmap) + invoke(blk) + PopColormap(1) +} + +// Push one ImPlotCol color override (e.g. ImPlotCol.Line) for the body. +def public with_style_color(idx : ImPlotCol; col : float4; blk : block) { + PushStyleColor(idx, col) + invoke(blk) + PopStyleColor(1) +} + +// Push one ImPlotStyleVar override for the body — float / int / float2 forms. +def public with_style_var(idx : ImPlotStyleVar; value : float; blk : block) { + PushStyleVar(idx, value) + invoke(blk) + PopStyleVar(1) +} +def public with_style_var(idx : ImPlotStyleVar; value : int; blk : block) { + PushStyleVar(idx, value) + invoke(blk) + PopStyleVar(1) +} +def public with_style_var(idx : ImPlotStyleVar; value : float2; blk : block) { + PushStyleVar(idx, value) + invoke(blk) + PopStyleVar(1) +} + +// Clip drawing to the plot area (optionally expanded by `expand` px) for the body. +def public with_plot_clip_rect(blk : block) { + PushPlotClipRect(0.0f) + invoke(blk) + PopPlotClipRect() +} +def public with_plot_clip_rect(expand : float; blk : block) { + PushPlotClipRect(expand) + invoke(blk) + PopPlotClipRect() +} + +// ===== Popup & drag-and-drop scopes (body runs iff Begin succeeded) ===== + +// A legend popup (default right mouse button); body emits its contents. +def public with_legend_popup(label_id : string; blk : block) { + if (BeginLegendPopup(label_id, 1)) { + invoke(blk) + EndLegendPopup() + } +} +def public with_legend_popup(label_id : string; mouse_button : int; blk : block) { + if (BeginLegendPopup(label_id, mouse_button)) { + invoke(blk) + EndLegendPopup() + } +} + +// Drag-drop SOURCE on the last item / the whole plot / an axis. +def public with_drag_drop_source_item(label_id : string; blk : block) { + if (BeginDragDropSourceItem(label_id, 0)) { + invoke(blk) + EndDragDropSource() + } +} +def public with_drag_drop_source_item(label_id : string; flags : int; blk : block) { + if (BeginDragDropSourceItem(label_id, flags)) { + invoke(blk) + EndDragDropSource() + } +} +def public with_drag_drop_source_plot(blk : block) { + if (BeginDragDropSourcePlot(0)) { + invoke(blk) + EndDragDropSource() + } +} +def public with_drag_drop_source_plot(flags : int; blk : block) { + if (BeginDragDropSourcePlot(flags)) { + invoke(blk) + EndDragDropSource() + } +} +def public with_drag_drop_source_axis(axis : ImAxis; blk : block) { + if (BeginDragDropSourceAxis(axis, 0)) { + invoke(blk) + EndDragDropSource() + } +} +def public with_drag_drop_source_axis(axis : ImAxis; flags : int; blk : block) { + if (BeginDragDropSourceAxis(axis, flags)) { + invoke(blk) + EndDragDropSource() + } +} + +// Drag-drop TARGET on the whole plot / an axis / the legend. +def public with_drag_drop_target_plot(blk : block) { + if (BeginDragDropTargetPlot()) { + invoke(blk) + EndDragDropTarget() + } +} +def public with_drag_drop_target_axis(axis : ImAxis; blk : block) { + if (BeginDragDropTargetAxis(axis)) { + invoke(blk) + EndDragDropTarget() + } +} +def public with_drag_drop_target_legend(blk : block) { + if (BeginDragDropTargetLegend()) { + invoke(blk) + EndDragDropTarget() + } +} + // ===== Axis setup ===== def public setup_axes(x_label : string; y_label : string) { diff --git a/tests/integration/test_v1_scopes.das b/tests/integration/test_v1_scopes.das new file mode 100644 index 0000000..b4ac8f4 --- /dev/null +++ b/tests/integration/test_v1_scopes.das @@ -0,0 +1,74 @@ +options gen2 + +require imgui +require imgui/imgui_harness +require imgui/imgui_containers_builtin +require imgui/imgui_implot_boost +require implot +require math + +// Headless smoke for the v1 (imgui_implot_boost) RAII scope wrappers. v1 has no +// snapshot hook, so there is nothing to query — instead this exercises every paired +// scope (Begin/End and Push/Pop) for several frames and passes iff it renders +// crash-free: an unbalanced Push/Pop or a missed End would trip an ImGui assert. +// Run: daslang.exe -load_module .../dasImgui -load_module .../dasImguiImplot \ +// tests/integration/test_v1_scopes.das -- --headless --headless-frames 12 + +[export] +def main : int { + harness_init("implot v1 scopes smoke", 900, 700) + var ctx = implot::CreateContext() + let sin_data <- [for (i in range(120)); double(sin(float(i) * 0.05f))] + let cos_data <- [for (i in range(120)); double(cos(float(i) * 0.05f))] + var rc = 1 + var done = false + while (harness_begin_frame()) { + harness_new_frame() + window(W, (text = "v1 scopes", closable = false, flags = ImGuiWindowFlags.None)) { + // Push/Pop style scopes wrap a group of axis-aligned plots (Begin/End scope). + with_style_var(ImPlotStyleVar.LineWeight, 2.0f) { + with_aligned_plots("aligned") { + with_plot("p1", float2(-1.0f, 280.0f), ImPlotFlags.None) { + setup_axes("x", "y") + with_colormap(ImPlotColormap.Viridis) { + plot_line("sin", sin_data) + } + with_style_color(ImPlotCol.Line, float4(1.0f, 0.5f, 0.2f, 1.0f)) { + plot_line("cos", cos_data) + } + with_plot_clip_rect() { + plot_scatter("pts", sin_data) + } + // Conditional scopes: Begin returns false headless (no input), so the + // body is skipped and End is correctly not called — exercises the guard. + with_legend_popup("##leg") {} + with_drag_drop_source_item("src") {} + with_drag_drop_source_plot() {} + with_drag_drop_source_axis(ImAxis.X1) {} + with_drag_drop_target_plot() {} + with_drag_drop_target_axis(ImAxis.X1) {} + with_drag_drop_target_legend() {} + } + with_plot("p2", float2(-1.0f, 280.0f), ImPlotFlags.None) { + setup_axes("x", "y") + plot_bars("bars", cos_data) + } + } + } + } + harness_end_frame() + if (!done && harness_frame_count() >= 6) { + done = true + print("PASS: v1 scope wrappers rendered crash-free for {harness_frame_count()} frames\n") + rc = 0 + request_exit() + } + } + harness_shutdown() + unsafe { + if (ctx != null) { + DestroyContext(ctx) + } + } + return rc +} From 9a233a1f055d42297ffc237c45d1ed32833d7508 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Sat, 6 Jun 2026 21:55:16 -0700 Subject: [PATCH 2/2] address Copilot: drop redundant scalar default args in v1 scope wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The no-optional convenience overloads passed explicit literals (vertical=true, expand=0, mouse_button=1, flags=0) that exactly match the native defaults. The implot binding carries SCALAR C++ defaults (bool/int/float), so the shorter native call is equivalent and drops the magic values — verified by compiling the 1-arg forms. (with_plot/with_subplots stay explicit: their size default is an ImVec2 struct, which the binding does NOT carry, so the shorter form won't compile.) - with_aligned_plots / with_plot_clip_rect / with_legend_popup / with_drag_drop_source_item / _plot / _axis: call the native with no optional arg. Smoke (test_v1_scopes.das) still renders crash-free. Co-Authored-By: Claude Opus 4.8 (1M context) --- daslib/implot_boost.das | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/daslib/implot_boost.das b/daslib/implot_boost.das index db1ab52..5d7455a 100644 --- a/daslib/implot_boost.das +++ b/daslib/implot_boost.das @@ -54,7 +54,7 @@ def public with_subplots(title : string; rows : int; cols : int; size : float2; // Begin a group of axis-aligned plots, run the body if it began, always End it. def public with_aligned_plots(group_id : string; blk : block) { - if (BeginAlignedPlots(group_id, true)) { + if (BeginAlignedPlots(group_id)) { // native default vertical = true invoke(blk) EndAlignedPlots() } @@ -101,7 +101,7 @@ def public with_style_var(idx : ImPlotStyleVar; value : float2; blk : block) { // Clip drawing to the plot area (optionally expanded by `expand` px) for the body. def public with_plot_clip_rect(blk : block) { - PushPlotClipRect(0.0f) + PushPlotClipRect() // native default expand = 0 invoke(blk) PopPlotClipRect() } @@ -115,7 +115,7 @@ def public with_plot_clip_rect(expand : float; blk : block) { // A legend popup (default right mouse button); body emits its contents. def public with_legend_popup(label_id : string; blk : block) { - if (BeginLegendPopup(label_id, 1)) { + if (BeginLegendPopup(label_id)) { // native default mouse button = right invoke(blk) EndLegendPopup() } @@ -129,7 +129,7 @@ def public with_legend_popup(label_id : string; mouse_button : int; blk : block) // Drag-drop SOURCE on the last item / the whole plot / an axis. def public with_drag_drop_source_item(label_id : string; blk : block) { - if (BeginDragDropSourceItem(label_id, 0)) { + if (BeginDragDropSourceItem(label_id)) { // native default flags = 0 invoke(blk) EndDragDropSource() } @@ -141,7 +141,7 @@ def public with_drag_drop_source_item(label_id : string; flags : int; blk : bloc } } def public with_drag_drop_source_plot(blk : block) { - if (BeginDragDropSourcePlot(0)) { + if (BeginDragDropSourcePlot()) { // native default flags = 0 invoke(blk) EndDragDropSource() } @@ -153,7 +153,7 @@ def public with_drag_drop_source_plot(flags : int; blk : block) { } } def public with_drag_drop_source_axis(axis : ImAxis; blk : block) { - if (BeginDragDropSourceAxis(axis, 0)) { + if (BeginDragDropSourceAxis(axis)) { // native default flags = 0 invoke(blk) EndDragDropSource() }