The repository had no licence at all, so every source was all-rights-reserved
by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino
Ferrari) and a two-line SPDX header to each of the 113 first-party sources --
48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the
grant travels with each file rather than relying on the root document alone.
Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely
vendored code is untouched: third_party/stb_truetype.h carries no header of
ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py
emit() now writes the two lines into its header comment and the file was
regenerated -- gen_atlas.py --check passes and the regeneration adds only those
comment lines to the previously committed file.
README's provenance section now states MIT and links LICENSE.
No code, test or public API change.
Zeroing an unusable wheel amount turned it into a zero-notch zoom: the factor was
exactly one, the range did not change, but the zoom body still ran through
set_x_range/set_y_range, which clear auto_scale unconditionally. One garbage wheel
therefore disarmed auto-fit on every axis. on_wheel now rejects an unusable amount
before the event can mutate anything, keeping the position fallback.
The tests that should have caught it are strengthened: the wheel case now asserts
auto scale is still armed (it was passing either way), and the invalid-position
case checks a pan measured from the last real position with the drag alive, rather
than a range nothing could have moved.
The widget drew a region derived from the child's content rect alone, so a child
the hosting window cut off cleared and painted over the host's pixels; it also
scaled ImGui's (-FLT_MAX, -FLT_MAX) no-pointer sentinel and differenced it against
the last real position, turning a drag out of the window into a ~1e35 pan, and it
forgot the host's button levels across a re-bind, handing the next plot a press
that never happened.
The region is now the content rect intersected with the child's clip rect (a
no-op when nothing clips it), an invalid position is no position update, and the
button levels survive a re-bind. The controller sanitises non-finite coordinates
and deltas for any host, and the widget's input translation now has a committed
test on the headless EGL context.
Every pixel-to-data conversion in the controller now maps through the
same rectangle Plot::x_transform/y_transform are built from:
- pan() derives the scale from layout().axes.w/.h, so a drag moves the
grabbed data point by exactly the drag distance (the viewport model
stranded it behind the pointer by the label-strip fraction).
- apply_box_zoom() clamps the selection to layout().axes and sets each
range from the transforms (x_transform().to_data_x(l/r), y_transform()
.to_data_y(bottom/top)); the selection's bottom edge is the y minimum.
- end_drag() takes the release event and refreshes the selection from its
coordinates before applying, so a host that reports only press and
release still frames the box instead of degrading it to a click.
- axis_zoom() uses a per-pixel sensitivity (200 px == one decade) rather
than reusing the wheel's per-notch constant, so a pixel drag is no
longer treated as a wheel notch.
Tests now assert the grab-and-drag invariant through the transform (the
old pixel-fraction assertions certified the viewport defect), add a
press+release-only box zoom, and pin the axis-zoom magnitude.