adapters: route pointer events by cell rect

This commit is contained in:
Martino Ferrari committed 2026-11-03 17:04:16 +01:00
1 parent 7d8c453232
commit e5b7faadec
6 files changed
+145 -93

No files matched your search

+25 -21
View File
@@ -455,9 +455,11 @@ void Host::set_figure(Figure* figure) noexcept {
row_rects_.clear(); // no figure, no rows: the handlers fall back to `controller_`
return;
}
const u32 rows = figure_->row_count();
row_controllers_.reserve(rows);
for (u32 i = 0; i < rows; ++i) {
// One controller per **plot** (a row of several cells has one for each), indexed by the
// flattened plot index the figure uses.
const u32 plots = figure_->plot_count();
row_controllers_.reserve(plots);
for (u32 i = 0; i < plots; ++i) {
row_controllers_.push_back(std::make_unique<input::Controller>(figure_->plot(i)));
}
// The new figure's rects go into the cache now, not at the next frame: an event
@@ -469,8 +471,10 @@ void Host::set_figure(Figure* figure) noexcept {
void Host::refresh_row_rects() noexcept {
// The one `Figure::layout()` per call: every event handler reads `row_rects_`
// instead of paying this again, which is the whole point of the cache (R7).
row_rects_ = figure_ != nullptr ? figure_->layout().rows : std::vector<Rect>{};
// instead of paying this again, which is the whole point of the cache (R7). The rects
// are the figure's **cells** (one per plot, flattened), which for a figure with one
// cell per row are exactly its row rects.
row_rects_ = figure_ != nullptr ? figure_->layout().cells : std::vector<Rect>{};
}
input::Controller* Host::row_controller(u32 row) noexcept {
@@ -486,8 +490,7 @@ input::Controller& Host::controller_at(f32 x, f32 y) noexcept {
// frame stale, which is the documented worst case.
const std::vector<Rect>& rows = row_rects_;
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);
const u32 row = row_in(rows, x, 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
@@ -506,7 +509,6 @@ void Host::feed_pointer_event(const input::PointerEvent& e) noexcept {
input::Controller& Host::pointer_target(f32 x, f32 y, input::Button button, bool pressed,
bool released) noexcept {
(void)x; // the rows tile the figure vertically, so y alone picks one -- see row_in()
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
const std::vector<Rect>& rows = row_rects_;
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
@@ -523,7 +525,7 @@ input::Controller& Host::pointer_target(f32 x, f32 y, input::Button button, bool
// drag, and its press must not steal the capture to the row it landed in.
if (released && button == captured_button_) captured_ = false;
} else {
row = row_in(rows, y);
row = row_in(rows, x, y);
if (pressed) {
captured_ = true;
captured_row_ = row;
@@ -541,27 +543,29 @@ input::Controller& Host::pointer_target(f32 x, f32 y, input::Button button, bool
u32 Host::pointer_row(const std::vector<Rect>& rows) noexcept {
if (captured_ && captured_row_ < rows.size()) return captured_row_;
return row_in(rows, last_pointer_y_);
return row_in(rows, last_pointer_x_, last_pointer_y_);
}
u32 Host::row_in(const std::vector<Rect>& rows, f32 y) noexcept {
u32 Host::row_in(const std::vector<Rect>& rows, f32 x, 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
// matches" is what makes a pixel in a gap belong to the row it is *near*: the rows
// tile the figure at the default gap of 0, so this is unreachable until a host asks
// for one, and a gesture in a visible gap must not be routed to row 0 by default.
// The cell the pixel is in, or -- when it is in none of them, which is the row gap or
// column gap a host has asked for with `Figure::set_row_gap()`/`set_col_gap()` or the
// figure's own margins -- the cell whose rect is nearest (squared distance from the
// pixel to the rect). Selection by distance rather than by "the first that matches" is
// what makes a pixel in a gap belong to the cell it is *near*, and a gesture in a
// visible gap must not be routed to plot 0 by default. For a figure with one cell per
// row every cell spans the figure's width, so this reduces to the nearest row by y.
u32 row = 0u;
f32 best = std::numeric_limits<f32>::infinity();
for (u32 i = 0; i < rows.size(); ++i) {
const f32 top = rows[i].y;
const f32 bottom = rows[i].y + rows[i].h;
const f32 distance = y < top ? top - y : (y >= bottom ? y - bottom : 0.0f);
const Rect& r = rows[i];
const f32 dx = x < r.x ? r.x - x : (x >= r.right() ? x - r.right() : 0.0f);
const f32 dy = y < r.y ? r.y - y : (y >= r.bottom() ? y - r.bottom() : 0.0f);
const f32 distance = dx * dx + dy * dy;
if (distance < best) {
best = distance;
row = i;
if (distance == 0.0f) break; // inside this row: nothing can be nearer
if (distance == 0.0f) break; // inside this cell: nothing can be nearer
}
}
return row;
+19 -15
View File
@@ -121,18 +121,21 @@ class Host {
/// `plot_` is then untouched by any of this.
void set_figure(Figure* figure) noexcept;
Figure* figure() noexcept { return figure_; }
/// One figure row's controller -- the one a gesture on that row reaches -- or
/// null while no figure is set or the row does not exist. A host reads a drag
/// One figure plot's controller -- the one a gesture on that plot reaches -- or null
/// while no figure is set or it does not exist. `row` is the **flattened plot index**
/// (`Figure::plot_count()` of them: the row count when no row holds several cells). A host reads a drag
/// mode or a hover out of it (`controller()` is the single-plot loop's, and is
/// not the one figure gestures go to).
input::Controller* row_controller(u32 row) noexcept;
/// The controller an event at framebuffer pixel (x, y) belongs to: `controller_` --
/// the single-plot loop -- while no figure is set, and the controller of the figure
/// row that pixel lands in otherwise. The row rects come from the cache
/// plot (cell) that pixel lands in otherwise -- x and y both pick it, as a row may hold
/// several cells side by side. The cell rects come from the cache
/// `refresh_row_rects()` fills -- the same ones the renderer draws -- so a gesture
/// lands on the row it was aimed at and no event lays the figure out; a pixel
/// inside no row -- the row gap a host asked for with `Figure::set_row_gap()`,
/// or a margin -- is the *nearest* row rather than dropped or defaulted, so a drag
/// inside no cell -- the row or column gap a host asked for with
/// `Figure::set_row_gap()`/`set_col_gap()`, or a margin -- is the *nearest* cell rather than
/// dropped or defaulted, so a drag
/// that leaves the data area still ends where it should.
///
/// It also hands that row its own viewport, which is what keeps the controller's
@@ -217,11 +220,12 @@ class Host {
void on_scroll(f64 dx, f64 dy) noexcept;
void on_key(int key, int action, int mods) 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;
/// The index of the cell of `rows` (the figure's flattened cell rects) a framebuffer pixel
/// belongs to: the cell it is in, or the nearest cell when it is in none (a row or column 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 x, f32 y) noexcept;
/// The controller a pointer event at (x, y) belongs to, applying the button capture:
/// a press claims the row it landed in (`captured_row_` + `captured_button_`) and
@@ -231,10 +235,10 @@ class Host {
input::Controller& pointer_target(f32 x, f32 y, input::Button button, bool pressed,
bool released) noexcept;
/// The row the pointer's events currently go to: the captured row while a drag owns
/// the button, the row under the pointer otherwise. The run loop uses it to clear
/// every *other* row's hot handles, so a row the pointer left cannot keep drawing a
/// hover or a grab it last published.
/// The plot (flattened index) the pointer's events currently go to: the captured one
/// while a drag owns the button, the one under the pointer otherwise. The run loop uses
/// it to clear every *other* plot's hot handles, so one the pointer left cannot keep
/// drawing a hover or a grab it last published.
u32 pointer_row(const std::vector<Rect>& rows) noexcept;
/// Spreads the hover over a whole `Figure` (spec §15).
@@ -292,7 +296,7 @@ class Host {
u32 captured_row_ = 0u;
input::Button captured_button_ = input::Button::Left;
/// The figure's row rects as of the last `refresh_row_rects()` -- the cache the
/// The figure's cell rects (one per plot, flattened; the row rects when no row is split) as of the last `refresh_row_rects()` -- the cache the
/// event routing reads instead of laying the figure out per event (R7). Filled by
/// `set_figure()` and once per frame by the run loop; empty while no figure is
/// set, which is the state `controller_at()`/`pointer_target()` fall back to the