diff --git a/daslib/implot_boost_v2.das b/daslib/implot_boost_v2.das index 2c6271d..8b08bac 100644 --- a/daslib/implot_boost_v2.das +++ b/daslib/implot_boost_v2.das @@ -80,8 +80,10 @@ let private LEGEND_LABEL_RETRY_FRAMES = 240 // bounded window to resolve a jus def private capture_legend(var state : PlotScopeState; title : string) { // Read the plot's legend into the snapshot AFTER EndPlot, by title — the rect / shown are - // written during EndPlot's legend render, so GetPlot(title) gives current-frame values. Labels - // come from ImPlot as a fresh string per call, so we cache them (rebuild only when the entry set + // written during EndPlot's legend render, so GetPlot(title) gives current-frame values. + // LegendEntryLabel returns a das-heap-owned copy (it allocateString's into our context — see + // the C++ forwarder, issue #9), so the cached label stays valid even after ImPlot resets its + // internal Legend.Labels buffer next frame. We cache labels (rebuild only when the entry set // changes) rather than re-allocating every frame. Rects / shown are cheap scalars, refreshed // every frame. (A relabel with no count change reads stale — exotic for a steady plot.) let n = LegendEntryCount(title) diff --git a/src/dasIMPLOT.main.cpp b/src/dasIMPLOT.main.cpp index 058dbc0..875cfea 100644 --- a/src/dasIMPLOT.main.cpp +++ b/src/dasIMPLOT.main.cpp @@ -150,10 +150,17 @@ int LegendEntryCount(const char* title) { ImPlotPlot* plot = ImPlot::GetPlot(title); return plot ? plot->Items.GetLegendCount() : 0; } -const char* LegendEntryLabel(const char* title, int index) { +// allocateString (das heap), NOT the raw GetLegendLabel pointer: that points into ImPlot's +// per-frame Legend.Labels buffer, which the next BeginPlot/SetupFinish resets and may realloc — a +// raw return would dangle by the time the snapshot serializes a frame later (empty/garbage on +// macOS, issue #9). Owning the bytes here keeps the das string valid; mirrors the rest of the +// string-returning API (dasImgui's text_range_string / ImGTB_Slice). +char* LegendEntryLabel(const char* title, int index, das::Context* context, das::LineInfoArg* at) { ImPlotPlot* plot = ImPlot::GetPlot(title); - if (!plot || index < 0 || index >= plot->Items.GetLegendCount()) return ""; - return plot->Items.GetLegendLabel(index); + if (!plot || index < 0 || index >= plot->Items.GetLegendCount()) + return context->allocateString(nullptr, 0, at); + const char* label = plot->Items.GetLegendLabel(index); + return context->allocateString(label, strlen(label), at); } bool LegendEntryShown(const char* title, int index) { ImPlotPlot* plot = ImPlot::GetPlot(title); @@ -287,7 +294,7 @@ void Module_dasIMPLOT::initMain () { addExtern(*this, lib, "LegendEntryCount", SideEffects::accessExternal, "das::LegendEntryCount")->args({"title"}); addExtern(*this, lib, "LegendEntryLabel", - SideEffects::accessExternal, "das::LegendEntryLabel")->args({"title", "index"}); + SideEffects::accessExternal, "das::LegendEntryLabel")->args({"title", "index", "context", "lineinfo"}); addExtern(*this, lib, "LegendEntryShown", SideEffects::accessExternal, "das::LegendEntryShown")->args({"title", "index"}); addExtern(*this, lib, "LegendEntryHovered", diff --git a/tests/integration/test_multi_axes.das b/tests/integration/test_multi_axes.das index 0b2cb76..bd2d06a 100644 --- a/tests/integration/test_multi_axes.das +++ b/tests/integration/test_multi_axes.das @@ -37,12 +37,9 @@ def test_multi_axes(t : T?) { t |> success(abs(plot_axis_limit(m, s, "y2_max") - 1050.0lf) < 1.0lf, "Y2 (pressure) max captured ~1050") // The Y2-routed series' legend label resolves (the first-frame label fix). Poll, don't // one-shot: ImPlot can resolve the label a few frames after the item first registers, so a - // read on the axis-convergence snapshot races it. Skipped on macOS — the Y2-routed label - // stays empty there (issue #9, to be debugged on an osx box); the rail's drag checks below - // still run on all platforms. - if (get_platform_name() != "darwin") { - t |> success(wait_for_series_shown(s, "pressure", true) != null, "pressure series present in legend (Y2 label captured)") - } + // read on the axis-convergence snapshot races it. Now runs on all platforms — issue #9 (the + // macOS empty/garbage label) is fixed in the LegendEntryLabel forwarder (returns an owned copy). + t |> success(wait_for_series_shown(s, "pressure", true) != null, "pressure series present in legend (Y2 label captured)") // Drag ONLY the Y2 gutter: pressure's range must move while temp (Y1) and X1 stay put — the // independent-axis pan the tutorial teaches, distinct from a whole-plot body pan (moves all).