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

+34 -29
View File
@@ -245,9 +245,11 @@ void PlotArea::set_figure(Figure* figure) noexcept {
row_rects_.clear(); // no figure, no rows: the routing falls 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 geometry goes into the cache now, not at the next frame: an event
@@ -269,8 +271,10 @@ void PlotArea::lay_figure_out(Rect px) noexcept {
void PlotArea::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.
row_rects_ = figure_ != nullptr ? figure_->layout().rows : std::vector<Rect>{};
// instead of paying this again, which is the whole point of the cache. 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* PlotArea::row_controller(u32 row) noexcept {
@@ -283,8 +287,7 @@ input::Controller& PlotArea::controller_at(f32 x, f32 y) noexcept {
// -- and not a fresh `Figure::layout()`: an event pays no layout, or a burst of
// pointer motion over a multi-row figure would re-lay every row out per move.
if (row_rects_.empty() || row_rects_.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(row_rects_, y);
const u32 row = row_in(row_rects_, x, y);
input::Controller& controller = *row_controllers_[row];
controller.set_viewport(row_rects_[row]);
return controller;
@@ -292,11 +295,11 @@ input::Controller& PlotArea::controller_at(f32 x, f32 y) noexcept {
u32 PlotArea::row_at(double x, double y) const noexcept {
if (figure_ == nullptr || row_rects_.empty()) return 0;
(void)x;
return row_in(row_rects_, to_device(x, y).y);
const Vec2 p = to_device(x, y);
return row_in(row_rects_, p.x, p.y);
}
input::Controller& PlotArea::target_controller(f32 y) noexcept {
input::Controller& PlotArea::target_controller(f32 x, f32 y) noexcept {
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
if (row_rects_.empty() || row_rects_.size() != row_controllers_.size()) return controller_;
// The capture: while a press is open the gesture belongs to the row that press
@@ -304,7 +307,7 @@ input::Controller& PlotArea::target_controller(f32 y) noexcept {
// the release to the row the pointer is over *now* would leave the first row's drag
// open for ever -- its controller never sees the release that ends it -- and let a
// later gesture resume it from the stale anchor.
u32 row = press_open_ ? press_row_ : row_in(row_rects_, y);
u32 row = press_open_ ? press_row_ : row_in(row_rects_, x, y);
// Re-read against this frame's rects: the widget can be resized mid-drag, and the
// guard keeps a stale index from reaching past the controller list.
if (row >= row_controllers_.size()) row = 0;
@@ -314,23 +317,25 @@ input::Controller& PlotArea::target_controller(f32 y) noexcept {
return controller;
}
u32 PlotArea::row_in(const std::vector<Rect>& rows, f32 y) noexcept {
u32 PlotArea::row_in(const std::vector<Rect>& rows, f32 x, f32 y) noexcept {
if (rows.empty()) return 0;
// The row the pixel is in, or -- when it is in none of them, which is the row gap a
// host asked for with `Figure::set_row_gap()` or the figure's own margins -- the row
// whose rect is nearest. By distance rather than by "the first that matches": the
// rows tile the figure at the default gap of 0, so this is unreachable until a host
// asks for a gap, and a gesture in a visible gap must not default to row 0.
// The cell the pixel is in, or -- when it is in none of them, which is the row or
// column gap a host 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). By distance rather than by "the first that matches": a gesture
// in a visible gap must not default to plot 0. 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 = 0;
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;
@@ -341,7 +346,7 @@ u32 PlotArea::pointer_row() const noexcept {
// The captured row while a press owns the gesture, so an in-flight drag keeps its
// highlight even after the pointer has left the row it began in.
if (press_open_ && press_row_ < row_rects_.size()) return press_row_;
return row_in(row_rects_, pointer_.y);
return row_in(row_rects_, pointer_.x, pointer_.y);
}
void PlotArea::sync_selection_frames() noexcept {
@@ -616,7 +621,7 @@ void PlotArea::pointer_press(input::Button button, u32 mods, double x, double y,
// The figure row this gesture belongs to for its whole life, claimed from the press's
// own position *before* the event is dispatched (see `target_controller()`). Harmless
// and unread while no figure is set.
press_row_ = figure_ != nullptr ? row_in(row_rects_, p.y) : 0;
press_row_ = figure_ != nullptr ? row_in(row_rects_, p.x, p.y) : 0;
press_open_ = true;
press_button_ = button;
press_mods_ = mods;
@@ -634,7 +639,7 @@ void PlotArea::pointer_press(input::Button button, u32 mods, double x, double y,
e.down = true;
e.pressed = true;
e.time_s = pointer_time_s(time_ms); // the double-click detector differences these
target_controller(p.y).on_pointer(e);
target_controller(p.x, p.y).on_pointer(e);
gtk_gl_area_queue_render(GTK_GL_AREA(widget_));
}
@@ -643,7 +648,7 @@ void PlotArea::pointer_release(Vec2 position, u32 time_ms) noexcept {
// Resolved while the press is still open, so the release reaches the row the press
// claimed rather than the row the pointer ended over -- the controller that owns the
// drag is the only one that can close it.
input::Controller& target = target_controller(position.y);
input::Controller& target = target_controller(position.x, position.y);
press_open_ = false;
input::PointerEvent e;
@@ -748,7 +753,7 @@ void PlotArea::on_drag_update(GtkGestureDrag* gesture, double offset_x, double o
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)));
// The press's row, not the row the pointer has reached: `press_open_` is true here,
// so a drag that crosses a row boundary stays with the controller that began it.
area->target_controller(sample.position.y).on_pointer(e);
area->target_controller(sample.position.x, sample.position.y).on_pointer(e);
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
}
@@ -795,7 +800,7 @@ void PlotArea::on_zoom_scale_changed(GtkGestureZoom* gesture, double scale,
static_cast<f32>(scale), Vec2{static_cast<f32>(cx), static_cast<f32>(cy)}, area->mapping_);
// The folded event carries the focal point, which is the position this pinch belongs
// to; while a press is open the capture overrides it, as it does for a drag.
area->target_controller(g.y).on_gesture(g);
area->target_controller(g.x, g.y).on_gesture(g);
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
}
@@ -834,7 +839,7 @@ int PlotArea::on_scroll(GtkEventControllerScroll* scroll, double dx, double dy,
e.dx = static_cast<f32>(scroll_notches(dx, unit));
e.dy = static_cast<f32>(notches);
e.mods = gesture_mods(GTK_EVENT_CONTROLLER(scroll));
area->target_controller(at.y).on_wheel(e);
area->target_controller(at.x, at.y).on_wheel(e);
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
return TRUE; // consumed: the plot zoomed
}
@@ -858,7 +863,7 @@ void PlotArea::on_motion(GtkEventControllerMotion* motion, double x, double y, v
e.down = false;
e.time_s = PlotArea::pointer_time_s(
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(motion)));
area->target_controller(p.y).on_pointer(e);
area->target_controller(p.x, p.y).on_pointer(e);
}
// --- keyboard -------------------------------------------------------------