fix(plot,render,glfw): a hue-neutral crosshair, an em-dash placeholder, one layout per figure event, and a usable readout switch

This commit is contained in:
Martino Ferrari committed 2026-09-28 19:38:30 +02:00
1 parent 10d5129fbf
commit a14f333dbe
10 files changed
+175 -70

No files matched your search

+20 -17
View File
@@ -212,15 +212,16 @@ bool Host::run(const RunConfig& cfg, const FrameFn& frame, Status* st) {
// controller_at() or injected by a scripted run -- is hit-tested against the
// geometry the last frame drew, and the two readers of a row's geometry cannot
// disagree about it.
const std::vector<Rect>& rows = figure_->layout().rows; // the one layout of this frame
for (u32 i = 0; i < row_controllers_.size(); ++i) {
row_controllers_[i]->set_viewport(figure_->layout().rows[i]);
if (i < rows.size()) row_controllers_[i]->set_viewport(rows[i]);
}
// One hover, every row (spec §15): the crosshair the row controller under the pointer
// placed is applied to the whole figure here, after the pointers of this frame have been
// handled and before anything lays the rows out -- so every row's readout lists its own
// signals at the one x the pointer is over. In the run loop rather than in a callback, so a
// scripted run that injects its events straight into a row controller is synced too.
sync_figure_crosshair();
sync_figure_crosshair(rows);
} else {
controller_.set_viewport(viewport); // also Plot::set_viewport, then layout()
}
@@ -430,20 +431,23 @@ input::Controller* Host::row_controller(u32 row) noexcept {
input::Controller& Host::controller_at(f32 x, f32 y) noexcept {
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
const u32 row = row_at(x, y);
if (row >= row_controllers_.size()) return controller_;
// The figure's one layout for this call: both readers below take the row rects it produced, so an
// event lays the figure out once and not once per reader -- this is the hottest path a multi-row
// figure has, and a layout now rebuilds every row's ticks, grid and crosshair geometry.
const std::vector<Rect>& rows = figure_->layout().rows;
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
(void)x; // the rows tile the figure vertically, so y alone picks one -- see row_in()
const u32 row = row_in(rows, y);
last_row_ = row;
input::Controller& controller = *row_controllers_[row];
// The row's own viewport, so the geometry this controller hit-tests is the geometry
// the last frame drew: the same rect `Figure::layout()` gave the row's plot.
controller.set_viewport(figure_->layout().rows[row]);
controller.set_viewport(rows[row]);
return controller;
}
u32 Host::row_at(f32 x, f32 y) const noexcept {
if (figure_ == nullptr || row_controllers_.empty()) return 0u;
const std::vector<Rect>& rows = figure_->layout().rows;
if (rows.empty() || rows.size() != row_controllers_.size()) return 0u;
u32 Host::row_in(const std::vector<Rect>& rows, f32 y) noexcept {
if (rows.empty()) return 0u;
// The row the pixel is in, or -- when it is in none of them, which is the row gap a
// host has asked for with `Figure::set_row_gap()` or the figure's own margins -- the
// row whose rect is nearest. Selection by distance rather than by "the first that
@@ -462,13 +466,12 @@ u32 Host::row_at(f32 x, f32 y) const noexcept {
if (distance == 0.0f) break; // inside this row: nothing can be nearer
}
}
(void)x; // the rows tile the figure vertically, so y alone picks one
return row;
}
void Host::sync_figure_crosshair() noexcept {
const u32 rows = figure_ != nullptr ? figure_->row_count() : 0u;
if (rows == 0u || row_controllers_.empty()) return;
void Host::sync_figure_crosshair(const std::vector<Rect>& rows) noexcept {
if (rows.empty() || row_controllers_.empty()) return;
const u32 row_count = static_cast<u32>(rows.size());
// Which row's crosshair is the hover. With a pointer of the host's own it is the row that pointer
// is over -- the same rule events are routed by. A scripted run injects its events straight into
@@ -477,18 +480,18 @@ void Host::sync_figure_crosshair() noexcept {
// any row holding something else was changed from outside, and that is the one the run touched.
// Nothing differing means every row still holds the last spread -- no new hover -- and the figure
// keeps it.
u32 hovered = rows;
u32 hovered = row_count;
if (have_pointer_) {
hovered = row_at(last_pointer_x_, last_pointer_y_);
hovered = row_in(rows, last_pointer_y_);
} else {
for (u32 i = 0; i < rows; ++i) {
for (u32 i = 0; i < row_count; ++i) {
const Crosshair& candidate = figure_->plot(i).crosshair();
if (candidate.active != spread_active_ || (candidate.active && candidate.x != spread_x_)) {
hovered = i;
break;
}
}
if (hovered >= rows) return;
if (hovered >= row_count) return;
}
// Applied to the whole figure: one x, every row. A row whose controller has *no* crosshair (the
+8 -6
View File
@@ -174,10 +174,11 @@ class Host {
void on_scroll(f64 dx, f64 dy) noexcept;
void on_key(int key, int action, int mods) noexcept;
/// The index of the figure row a framebuffer pixel belongs to, by the rule
/// `controller_at()` routes by: the row the pixel is in, or the nearest row when it is in none
/// (a row gap, or a margin outside the figure). 0 while no figure is set.
u32 row_at(f32 x, f32 y) const noexcept;
/// The index of the row of `rows` a framebuffer y belongs to: the row it is in, or the nearest
/// row when it is in none (a row gap, a margin outside the figure). One rule, shared by the event
/// routing and the crosshair sync -- and taking the rects rather than reading them, so a caller
/// that has just laid the figure out does not make it lay out again for this.
static u32 row_in(const std::vector<Rect>& rows, f32 y) noexcept;
/// Spreads the hover over a whole `Figure` (spec §15).
///
@@ -193,8 +194,9 @@ class Host {
/// whose controller has no crosshair clears the figure's.
///
/// Called once per frame from the run loop, so a scripted run that bypasses the GLFW callbacks
/// gets the same synced figure a real pointer does.
void sync_figure_crosshair() noexcept;
/// gets the same synced figure a real pointer does. `rows` is the figure's row rects, which the
/// run loop has just read -- the one layout per frame this shares with the row viewports.
void sync_figure_crosshair(const std::vector<Rect>& rows) noexcept;
Plot plot_;
input::Controller controller_; ///< forwards its viewport to `plot_`