fix(plot,figure): re-index hot region, logical row floor, refuse live on a linked axis

Three smaller findings at the plot/figure seam:

* `Plot::remove_region()` re-indexed `region_stats_` but left `hot_.region`
  naming the old index, so a frame after a removal highlighted the wrong
  region's bound and an open drag edited a different region. The published
  handle (and its zone) is now cleared or decremented with the cache, and the
  contract documents that a host must not remove a region mid-drag.

* `Figure`'s `kMinRowHeightPx` was applied as a *device*-pixel floor while
  chrome is logical, so at 2x the guarantee was half as strong exactly where
  the chrome is twice as large. It is now a logical constant converted through
  the row's own `ui_scale()`, 1:1 for a non-positive scale as `Style::device()`
  does.

* `Figure::link_x()` refused a member whose axis 0 was `XFollow::Live`, but
  nothing refused `set_x_follow(0u, XFollow::Live)` *after* linking. The group
  then wrote its range into an axis that reinterpreted it against its own live
  edge, read it back as an external change, and tracked one member instead of
  the union. `Plot` now carries a linkage flag (`set_x_link_member()`), set by
  `link_x()` and cleared by `unlink_row()`, and `set_x_follow()` refuses `Live`
  on a linked axis.

Tests: arming `Live` on a linked member leaves it `Off` and unlinking restores
the ability to arm it; the row-height floor is 24 and 48 device pixels at 1x
and 2x respectively.
This commit is contained in:
Martino Ferrari committed 2026-09-28 22:36:37 +02:00
1 parent 2fbd1de7d4
commit 1f5ce0d4a5
5 files changed
+123 -12

No files matched your search

+7
View File
@@ -79,6 +79,13 @@ struct Axis {
/// group stops following or the row leaves it.
bool live_owner = false;
f64 live_owner_newest = 0.0;
/// Set while this plot is a member of a `Figure` x link group (`Figure::link_x()`
/// writes it, `unlink_row()` clears it). The group owns the shared x window *and*
/// its follow state -- `Figure::link_x()` refuses a `Live` member joiner -- so
/// `Plot::set_x_follow(Live)` on this axis is refused after linking too, rather
/// than letting a later call put the member's axis back into a state the group
/// cannot drive (see `Plot::set_x_follow()`).
bool link_member = false;
/// Ticks for the last `refresh()`, ascending, majors and (when asked) minors.
std::vector<Tick> ticks;
+13
View File
@@ -329,6 +329,11 @@ class Plot {
/// newest sample* rather than a pair of numbers. With no live edge -- no trace on
/// the axis, or none of them streaming -- nothing is recomputed and the axis
/// keeps its stored range, `auto_scale` included, exactly as an `Off` axis does.
///
/// On an axis that is a member of a `Figure` x link group, arming `Live` is
/// **refused** (the state is left as it was): the group owns the shared window and
/// its follow state, exactly as `Figure::link_x()` refuses a `Live` member joiner.
/// See `set_x_link_member()`.
void set_x_follow(u32 axis, XFollow f);
/// The follow state of one x axis. `Off` for an axis that does not exist.
XFollow x_follow(u32 axis) const;
@@ -363,6 +368,14 @@ class Plot {
/// axis no owner has claimed, and for an axis that does not exist.
bool x_live_owner(u32 axis, f64& newest) const noexcept;
/// Publishes -- or clears -- that this axis is a member of a `Figure` x link group.
/// `Figure::link_x()` sets it when a row joins and `unlink_row()` clears it when one
/// leaves; it is what makes `set_x_follow(Live)` refused on a linked axis, because
/// the group owns the shared window and the follow state. A no-op for an axis that
/// does not exist. Not for callers outside the figure: a `Plot` that is not in a
/// figure never has a linked axis.
void set_x_link_member(u32 axis, bool linked) noexcept;
/// Data -> pixels for one axis, built from the axis' range, log flag and the
/// current layout. Always invertible: a collapsed range or a collapsed axes
/// rect is widened by math::Transform, so to_data_*(to_screen_*(v)) == v.