fix(input,examples): a linked figure's wheel anchor uses the group's live edge, and every demo verdict reaches its exit status

Review fix pass 1 on the task 7 work; four findings, all closed.

1. The Qt demo's region sat across the middle of the window, which is exactly
   where its own scripted input check pans: after the check's box zoom the press
   opened DragMode::MoveRegion, the x range never moved, and the check printed
   'input check pan FAIL' -- under an exit 0, because the verdict was printed and
   not returned. The region now rides the left sixth (as the GLFW demo's does),
   both checks pass, and the verdict is wired to the exit status. The same audit
   found two more printed-only verdicts -- the GTK demo's scripted-resize layout
   check and its screenshot write (and Qt's) -- now wired too. What stays
   printed-only is the screenshot line of the GLFW host and the ImGui widget,
   where the dump happens inside the adapter's frame loop.

2. A follow-ing link group was invisible to the input layer: a member's own axis
   must be XFollow::Off (link_x() refuses a Live member), so Controller's
   x_zoom_anchor() extrapolated a coordinate from the pointer instead of using
   the live edge and a wheel over a row's axis strip walked the window back by
   19% of its width (behind 0.000000 -> 0.285809 s). The group now publishes its
   own edge to every member (Axis::live_owner + Plot::set_x_live_owner, written
   by Figure::push_link, cleared when the group stops following or the row
   unlinks), and x_zoom_anchor() reads it. Public-header addition, called out.
   Measured after: behind 0.012992 s and the window's right edge bit-identical to
   the edge the group published at the gesture.

3. The multiplot summary printed x_link_count() -- a number of groups -- under
   'rows in it'. It now reports the members and the group count separately, and
   every row line carries its own group id.

4. Host::controller_at() promised to clamp a pixel in a row gap to the nearest
   row and only clamped below the last one, so a gap pixel went to row 0. It now
   selects by distance, and tests/test_glfw_host.cpp pins it with a 40 px gap --
   the first coverage of the host's figure routing, which needs no window.

New cases: tests/test_controller.cpp x2 (the anchor and the hint's lifecycle),
tests/test_glfw_host.cpp x3 (routing, the gap, the single-plot fallback): 229
cases, 0 failed, ASan clean. All five demos exit 0, the three self-checking
modes exit 0, and the amended assertions were confirmed failing against the
pre-fix behaviour.
This commit is contained in:
Martino Ferrari committed 2026-09-28 15:52:08 +02:00
1 parent a70c381a1e
commit daff60e3f0
14 files changed
+441 -55

No files matched your search

+21 -2
View File
@@ -316,6 +316,11 @@ int main(int argc, char** argv) {
const auto keep_focus = [widget] { widget->setFocus(); };
Meter meter;
/// The scripted input check's verdict, read after `exec()`: a FAIL that is only printed
/// is a check no caller can act on -- the exit status is what a headless run reads. Same
/// for the exit frame's screenshot: the headless mode exists to produce that file.
bool inputs_ok = true;
bool shot_ok = true;
// The cursors ride the live window until the user takes over: the first
// ctrl+drag on a cursor, or a pan/zoom that clears the x axis' auto scale,
// stops them where the user put them. The region's bounds ride the same window.
@@ -343,7 +348,12 @@ int main(int argc, char** argv) {
const plot::Range xr = p.x_range(axis_x);
p.set_cursor_x(cursor_a, xr.min + 0.35 * xr.span());
p.set_cursor_x(cursor_b, xr.min + 0.65 * xr.span());
p.set_region(region, xr.min + 0.40 * xr.span(), xr.min + 0.60 * xr.span());
// The left sixth of the window, not its middle: a region is a grabbable handle, so
// one laid across the centre turns the pan this demo's scripted input check makes
// (and the pan a user makes) into a region drag -- the same trap the GLFW demo hit.
// The cursors are not within a grab radius of the centre either, which is what
// keeps the check's press a pan.
p.set_region(region, xr.min + 0.06 * xr.span(), xr.min + 0.18 * xr.span());
}
}
@@ -460,7 +470,9 @@ int main(int argc, char** argv) {
}
if (screenshot != nullptr && screenshot[0] != '\0') {
const bool written = dump_widget_ppm(image, screenshot);
shot_ok = written;
std::printf("[qt_demo] screenshot %s: %s\n", screenshot, written ? "written" : "FAILED");
if (!written) std::fprintf(stderr, "plot_qt_demo: the screenshot could not be written\n");
}
std::printf("[qt_demo] init %s: %s\n", plot::to_string(widget->init_status().code),
widget->init_status().message);
@@ -534,9 +546,15 @@ int main(int argc, char** argv) {
static_cast<double>(check_axes.w), static_cast<double>(check_axes.h));
std::printf("[qt_demo] input check box-zoom %s expected x [%.9f, %.9f], got [%.9f, %.9f]\n",
box_ok ? "PASS" : "FAIL", box_expect_min, box_expect_max, box_got.min, box_got.max);
inputs_ok = box_ok && drag_ok;
std::printf("[qt_demo] input check pan %s expected x [%.9f, %.9f], got [%.9f, %.9f]\n",
drag_ok ? "PASS" : "FAIL", drag_expect_min, drag_expect_min + before_drag.span(),
drag_got.min, drag_got.max);
if (!inputs_ok) {
std::fprintf(stderr,
"plot_qt_demo: the scripted input check failed (see the 'input check' lines"
" above)\n");
}
// The two gestures above left the plot zoomed and panned (that is what makes
// them checks). Put the live framing back before the run summary reads the
@@ -594,5 +612,6 @@ int main(int argc, char** argv) {
// (so the plot's GL resources are released while their context is still alive,
// and before `graph`, which the widget points at), then `graph`, then the
// already-joined `producer`, then `stream` itself.
return app_result;
if (app_result != 0) return app_result;
return inputs_ok && shot_ok ? 0 : 1;
}