diff --git a/doc/source/_static/tutorials/query_and_hover.mp4 b/doc/source/_static/tutorials/query_and_hover.mp4 new file mode 100644 index 0000000..878f5da Binary files /dev/null and b/doc/source/_static/tutorials/query_and_hover.mp4 differ diff --git a/doc/source/tutorials/query_and_hover.rst b/doc/source/tutorials/query_and_hover.rst index 9c1d86d..0930e1f 100644 --- a/doc/source/tutorials/query_and_hover.rst +++ b/doc/source/tutorials/query_and_hover.rst @@ -22,7 +22,7 @@ annotation uses. let mp = GetPlotMousePos(ImAxis.X1, ImAxis.Y1) // data coords g_cursor_x[0] = mp.x plot_inf_lines("cursor", g_cursor_x) // vertical crosshair - plot_text("({mp.x}, {mp.y})", mp.x, mp.y, float2(10.0f, 10.0f)) + plot_text("({mp.x:.1f}, {mp.y:.2f})", mp.x, mp.y, float2(10.0f, 10.0f)) } } @@ -32,6 +32,17 @@ Source: ``examples/tutorial/query_and_hover.das``. :language: das :linenos: +Walkthrough +=========== + +.. video:: query_and_hover.mp4 + +The recording glides the cursor across the plot with real synthetic input: the +vertical crosshair and the ``(x, y)`` label track it every frame, and ImPlot's +corner mouse readout updates alongside. It self-verifies that ``hovered`` flips true +and ``GetPlotMousePos`` resolves into the band the cursor was aimed at — left, then +right — so a dead hover or a frozen readout fails at teardown. + Live query ========== @@ -47,7 +58,9 @@ Annotating the cursor here a one-element array reused each frame for the crosshair (kept as a global to avoid a per-frame allocation). ``plot_text(text, x, y, pix_offset)`` anchors a label at a data point, offset by screen pixels so it sits beside the cursor rather than -under it. +under it. The label uses ``fmt`` precision specifiers in the interpolation — +``{mp.x:.1f}`` / ``{mp.y:.2f}`` — so it reads ``(74.0, -0.19)`` instead of full +double precision. Testing the hover ================= diff --git a/examples/tutorial/query_and_hover.das b/examples/tutorial/query_and_hover.das index 5c1c7ba..7d0dc9a 100644 --- a/examples/tutorial/query_and_hover.das +++ b/examples/tutorial/query_and_hover.das @@ -6,6 +6,7 @@ require imgui/imgui_widgets_builtin require imgui/imgui_implot_boost_v2 require implot require math +require strings // ============================================================================= // TUTORIAL: query_and_hover — read the cursor's plot position live and annotate it. @@ -58,7 +59,7 @@ def update() { g_cursor_x[0] = mp.x next_line_style(float4(1.00f, 1.00f, 1.00f, 0.50f), 1.0f) plot_inf_lines("cursor", g_cursor_x) - plot_text("({mp.x}, {mp.y})", mp.x, mp.y, float2(10.0f, 10.0f)) + plot_text("({mp.x:.1f}, {mp.y:.2f})", mp.x, mp.y, float2(10.0f, 10.0f)) } } } diff --git a/tests/integration/record_query_and_hover.das b/tests/integration/record_query_and_hover.das new file mode 100644 index 0000000..f54c9e0 --- /dev/null +++ b/tests/integration/record_query_and_hover.das @@ -0,0 +1,84 @@ +options gen2 +options indenting = 4 +options no_unused_block_arguments = false +options no_unused_function_arguments = false + +require imgui/imgui_implot_app public +require imgui/imgui_implot_playwright public +require implot +require daslib/json public +require daslib/json_boost public + +//! Driver: record query_and_hover.apng - a plot that reads the cursor LIVE. While the +//! pointer is over the plot the example draws a vertical crosshair at the cursor x and a +//! (x, y) label in DATA coords (GetPlotMousePos). The interaction this tutorial teaches +//! is the HOVER: move the cursor and the readout follows, every frame. Driven with a REAL +//! synthetic cursor glide, voiced and self-verifying - each beat waits for `hovered` to +//! flip true, then asserts GetPlotMousePos resolved into the band the cursor was aimed at +//! (and that it moved between beats), so a dead hover or a frozen readout aborts at +//! teardown. The headless regression is test_query_and_hover.das. + +let CHART = "PLOT_WIN/CHART" + +// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed work +// (glide + verify) so the next say_begin can't fire early and overlap the voice. +def hold_remainder(dwell : uint; t0 : int64) { + let elapsed = uint(get_time_usec(t0) / 1000) + if (dwell > elapsed) { + sleep(dwell - elapsed) + } +} + +[export] +def main { + with_implot_recording_app("examples/tutorial/query_and_hover.das", "query_and_hover.apng", 45) $(app) { + let s = implot_open(app, CHART) + var snap = wait_for_widget(app, CHART, 15.0f) + if (snap == null) { + panic("{CHART} never rendered - wrong app running?") + } + // Data area geometry (GetPlotPos / GetPlotSize) - maps screen fractions to data x. + let p = plot_payload(snap, s) + let px = p?["pos"]?["x"] ?? 0.0f + let py = p?["pos"]?["y"] ?? 0.0f + let pw = p?["plot_size"]?["x"] ?? 0.0f + let ph = p?["plot_size"]?["y"] ?? 0.0f + + // ---- Beat 1: the live readout ---- + move_to(app, (px + pw * 0.5f, py + ph * 0.5f), 700) + var hc = wait_for_hovered(s, true) + record_check(app, "the plot reports hovered while the cursor is over it", hc != null) + say(app, "hover to read the plot", CHART, + [voice = "This plot reads the cursor live. While you hover over it, it draws a crosshair at the cursor and labels its position in data coordinates."]) + + // ---- Beat 2: the readout follows the cursor (glide to the left third) ---- + let d_l = say_begin(app, "the readout follows the cursor", CHART, + [voice = "Move the cursor and the readout follows. Here, near the left, the label shows the x and y under the pointer, updated every frame."]) + let t_l = ref_time_ticks() + move_to(app, (px + pw * 0.22f, py + ph * 0.42f), 900) + wait_for_mouse_idle(app) + var hl = wait_for_hovered(s, true) + let mx_l = mouse_x(hl, s) + record_check(app, "readout tracks the cursor near the left (x in [8,40], got {mx_l})", + mx_l >= 8.0lf && mx_l <= 40.0lf) + hold_remainder(d_l, t_l) + + // ---- Beat 3: read any point (glide across to the right third) ---- + let d_r = say_begin(app, "read any point", CHART, + [voice = "Glide across to the right and the crosshair and the label track right along with it. You can read off the value at any point on the plot."]) + let t_r = ref_time_ticks() + move_to(app, (px + pw * 0.74f, py + ph * 0.58f), 1100) + wait_for_mouse_idle(app) + var hr = wait_for_hovered(s, true) + let mx_r = mouse_x(hr, s) + record_check(app, "readout tracks the cursor near the right (x in [60,92], got {mx_r})", + mx_r >= 60.0lf && mx_r <= 92.0lf) + record_check(app, "the readout moved right with the cursor (left {mx_l} < right {mx_r})", + mx_r > mx_l + 20.0lf) + hold_remainder(d_r, t_r) + } +} + +def mouse_x(var snap : JsonValue?; s : PlotSession) : double { + return plot_mouse_pos(snap, s)._0 +}