fix(glfw): route a drag by the row it began in, and drop stale hover state
`controller_at()` routed every pointer event by the row the pixel was in, ignoring the row a drag started in, so a press on one figure row followed by a motion and release in another delivered the motion and release to a controller whose `mode_ == None`. The owning controller kept `mode_`, `drag_region_`, `drag_cursor_` and `drag_fixed_x_` set for ever: `publish_hot()` kept publishing the grabbed handle, `grabbed_region()` reported a drag that had ended, and a later gesture re-entering the row resumed the stale edit from the old anchor. `feed_pointer_event()` now owns the routing: a press captures the row it landed in and every motion and the release until the button comes up go to that row (`pointer_target()`), cleared on release and when another figure is set. The GLFW callbacks build the event and call it, so a windowless host or a test can drive the routing too. Two neighbours in the same seam: * `sync_figure_crosshair()` called the four-argument overload unconditionally, so a documented vertical-only crosshair (`horizontal == false`) was promoted to a full cross at the stored y == 0, clamped to the axes edge and made permanent; the change detection also keyed on x alone, missing a y-only move to another row at the same x (two rows showing one horizontal line, spec 17). It now consults `crosshair().horizontal` and compares the hovered row's y. * Only the row under the pointer republished its hot handles, so a row the pointer left kept drawing a hovered marker or bound as if it were still under the pointer, and answered `hovered_region()`/`grabbed_cursor()` for it. The run loop clears every other row's handles, keeping the captured row while a drag owns the button, and reads the frame's one layout once instead of recomputing every row five times. Test: a press on row A's region, a motion into row B, a release in row B asserts no controller keeps a drag mode, nothing reports a grabbed handle, and a later press-and-motion that re-enters row A does not resume the old edit.
This commit is contained in:
1 parent
81792dcd05
commit
9aff06acee
3 files changed
+214
-13
No files matched your search
@@ -210,12 +210,13 @@ bool Host::run(const RunConfig& cfg, const FrameFn& frame, Status* st) {
|
||||
// that hands each row its rect (and so its own `Plot` its viewport). The single
|
||||
// controller is untouched -- each row has its own, see controller_at().
|
||||
figure_->set_viewport(viewport);
|
||||
figure_->layout();
|
||||
// Every row's controller gets that row's rect, so a gesture -- routed by
|
||||
// controller_at() or injected by a scripted run -- is hit-tested against the
|
||||
// pointer_target() 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
|
||||
// disagree about it. The rows come off the one recomputing layout of this frame --
|
||||
// calling `layout()` again here, or in `controller_at()`, would lay every row out a
|
||||
// second and a third time for the same frame.
|
||||
const std::vector<Rect>& rows = figure_->layout().rows;
|
||||
for (u32 i = 0; i < row_controllers_.size(); ++i) {
|
||||
if (i < rows.size()) row_controllers_[i]->set_viewport(rows[i]);
|
||||
}
|
||||
@@ -225,6 +226,18 @@ bool Host::run(const RunConfig& cfg, const FrameFn& frame, Status* st) {
|
||||
// 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(rows);
|
||||
// Only the row the pointer's events go to ever republishes its own hot handles, so a row
|
||||
// the pointer left keeps the `HotHandles` it last published and goes on drawing a hovered
|
||||
// marker or region bound as if the pointer were still on it -- and its `hovered_region()`/
|
||||
// `grabbed_cursor()` keep answering for it. Clear every *other* row here, in the run loop,
|
||||
// where that row is known (the captured row while a drag owns the button, so an in-flight
|
||||
// gesture keeps its highlight).
|
||||
if (have_pointer_ && !rows.empty()) {
|
||||
const u32 active = pointer_row(rows);
|
||||
for (u32 i = 0; i < rows.size() && i < row_controllers_.size(); ++i) {
|
||||
if (i != active) figure_->plot(i).set_hot_handles(HotHandles{});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
controller_.set_viewport(viewport); // also Plot::set_viewport, then layout()
|
||||
}
|
||||
@@ -340,7 +353,7 @@ void Host::on_cursor_pos(f64 wx, f64 wy) noexcept {
|
||||
have_pointer_ = true;
|
||||
e.mods = mods_now(window_);
|
||||
e.down = any_button_down(window_);
|
||||
controller_at(e.x, e.y).on_pointer(e);
|
||||
feed_pointer_event(e);
|
||||
}
|
||||
|
||||
void Host::on_mouse_button(int button, int action, int mods) noexcept {
|
||||
@@ -367,7 +380,7 @@ void Host::on_mouse_button(int button, int action, int mods) noexcept {
|
||||
e.pressed = action == GLFW_PRESS;
|
||||
e.released = action == GLFW_RELEASE;
|
||||
e.down = any_button_down(window_);
|
||||
controller_at(e.x, e.y).on_pointer(e);
|
||||
feed_pointer_event(e);
|
||||
}
|
||||
|
||||
void Host::on_scroll(f64 dx, f64 dy) noexcept {
|
||||
@@ -418,8 +431,13 @@ void Host::set_figure(Figure* figure) noexcept {
|
||||
last_row_ = 0u;
|
||||
// A new figure holds no crosshair this host has spread: the comparison in
|
||||
// `sync_figure_crosshair()` starts from "nothing spread", which is the state its rows are in.
|
||||
// Nor does it hold a captured drag row: the controllers are rebuilt below, so the old
|
||||
// capture names a controller that no longer exists.
|
||||
spread_active_ = false;
|
||||
spread_x_ = 0.0;
|
||||
spread_y_ = 0.0;
|
||||
captured_ = false;
|
||||
captured_row_ = 0u;
|
||||
if (figure_ == nullptr) return;
|
||||
const u32 rows = figure_->row_count();
|
||||
row_controllers_.reserve(rows);
|
||||
@@ -449,6 +467,49 @@ input::Controller& Host::controller_at(f32 x, f32 y) noexcept {
|
||||
return controller;
|
||||
}
|
||||
|
||||
void Host::feed_pointer_event(const input::PointerEvent& e) noexcept {
|
||||
if (figure_ == nullptr) {
|
||||
controller_.on_pointer(e);
|
||||
return;
|
||||
}
|
||||
pointer_target(e.x, e.y, e.pressed, e.released).on_pointer(e);
|
||||
}
|
||||
|
||||
input::Controller& Host::pointer_target(f32 x, f32 y, 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 = figure_->layout().rows;
|
||||
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
||||
|
||||
u32 row = 0u;
|
||||
if (captured_) {
|
||||
// A button-down gesture belongs to the row it began in for its whole life: a drag
|
||||
// that crosses into a neighbouring row must not hand its motion -- or its release --
|
||||
// to a controller that never saw the press, which would leave the owner's drag open
|
||||
// for ever (and let a later gesture resume it from the old anchor).
|
||||
row = captured_row_;
|
||||
if (released) captured_ = false;
|
||||
} else {
|
||||
row = row_in(rows, y);
|
||||
if (pressed) {
|
||||
captured_ = true;
|
||||
captured_row_ = row;
|
||||
}
|
||||
}
|
||||
// The captured row is re-read against this frame's rects: the figure can be resized
|
||||
// mid-drag, and the guard keeps a stale index from reaching past the controller list.
|
||||
if (row >= row_controllers_.size()) row = 0u;
|
||||
last_row_ = row;
|
||||
input::Controller& controller = *row_controllers_[row];
|
||||
controller.set_viewport(rows[row]);
|
||||
return controller;
|
||||
}
|
||||
|
||||
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_);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -485,11 +546,18 @@ void Host::sync_figure_crosshair(const std::vector<Rect>& rows) noexcept {
|
||||
// keeps it.
|
||||
u32 hovered = row_count;
|
||||
if (have_pointer_) {
|
||||
hovered = row_in(rows, last_pointer_y_);
|
||||
// The row the pointer's events currently go to -- the captured row while a drag owns
|
||||
// the button, the hovered row otherwise -- so the hover follows the gesture it began.
|
||||
hovered = pointer_row(rows);
|
||||
} else {
|
||||
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_)) {
|
||||
// A row differs when its active state, its x, or the y of *its own* horizontal line
|
||||
// differs from what was spread: a pointer that moves to another row at the same x is
|
||||
// a new hover and must move the one horizontal line, not leave the old row's behind.
|
||||
const bool y_differs = candidate.horizontal && candidate.y != spread_y_;
|
||||
if (candidate.active != spread_active_ ||
|
||||
(candidate.active && (candidate.x != spread_x_ || y_differs))) {
|
||||
hovered = i;
|
||||
break;
|
||||
}
|
||||
@@ -505,8 +573,19 @@ void Host::sync_figure_crosshair(const std::vector<Rect>& rows) noexcept {
|
||||
// figure's, so the rows cannot be left showing a hover the pointer has abandoned.
|
||||
const Plot& row = figure_->plot(hovered);
|
||||
if (row.crosshair().active) {
|
||||
figure_->set_crosshair(row.crosshair().x, hovered, row.crosshair().y, true);
|
||||
if (row.crosshair().horizontal) {
|
||||
// The row carries its own y (its controller placed both coordinates), so the figure's
|
||||
// four-argument overload gives the pointer row the one horizontal line.
|
||||
figure_->set_crosshair(row.crosshair().x, hovered, row.crosshair().y, true);
|
||||
} else {
|
||||
// The row's crosshair is vertical-only -- a caller placed it through the
|
||||
// single-coordinate `Figure::set_crosshair()`, which is documented vertical-only. The
|
||||
// four-argument overload would turn the horizontal line on at the stored (meaningless)
|
||||
// y == 0, clamped to the axes edge and made permanent. Promote only the vertical line.
|
||||
figure_->set_crosshair(row.crosshair().x, true);
|
||||
}
|
||||
spread_x_ = row.crosshair().x;
|
||||
spread_y_ = row.crosshair().y;
|
||||
spread_active_ = true;
|
||||
} else {
|
||||
figure_->clear_crosshair();
|
||||
|
||||
Reference in new issue
Block a user