Fixing minor issues in multifigure plots
This commit is contained in:
1 parent
38ef459897
commit
98cfc5805b
4 files changed
+394
-14
No files matched your search
+238
-11
@@ -18,6 +18,8 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
// The mirrored constants in gtk_input.hpp are pinned to GDK's here: a GDK that
|
||||
// renumbered the scroll units or moved a modifier bit fails to compile rather
|
||||
@@ -217,9 +219,187 @@ void PlotArea::refresh_viewport() noexcept {
|
||||
// chrome out with it) and refreshes the layout. `last_device_viewport()` is
|
||||
// deliberately not assigned here -- it records the region a *frame* was drawn
|
||||
// into, which is `render()`'s to set.
|
||||
if (figure_ != nullptr) {
|
||||
lay_figure_out(device_viewport());
|
||||
return;
|
||||
}
|
||||
controller_.set_viewport(device_viewport());
|
||||
}
|
||||
|
||||
// --- figure mode ----------------------------------------------------------
|
||||
|
||||
void PlotArea::set_figure(Figure* figure) noexcept {
|
||||
figure_ = figure;
|
||||
row_controllers_.clear();
|
||||
last_row_ = 0;
|
||||
// A new figure holds no crosshair this area has spread, and no captured press row:
|
||||
// the controllers are rebuilt below, so an open press would name one that is gone.
|
||||
spread_active_ = false;
|
||||
spread_x_ = 0.0;
|
||||
spread_y_ = 0.0;
|
||||
press_open_ = false;
|
||||
press_row_ = 0;
|
||||
drag_.have = false;
|
||||
zoom_.end();
|
||||
if (figure_ == nullptr) {
|
||||
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) {
|
||||
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
|
||||
// arriving between `set_figure()` and the first render -- which is every event a
|
||||
// headless probe injects -- must be routed against the figure it was just handed.
|
||||
lay_figure_out(device_viewport());
|
||||
}
|
||||
|
||||
void PlotArea::lay_figure_out(Rect px) noexcept {
|
||||
figure_->set_viewport(px);
|
||||
refresh_row_rects();
|
||||
for (u32 i = 0; i < row_controllers_.size() && i < row_rects_.size(); ++i) {
|
||||
// Each row's own rect, which is the viewport `Figure::layout()` just gave that
|
||||
// row's `Plot`: the controller and the plot must agree on it or a gesture lands on
|
||||
// the wrong pixels. Set here rather than by the controller, which owns no viewport.
|
||||
row_controllers_[i]->set_viewport(row_rects_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
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>{};
|
||||
}
|
||||
|
||||
input::Controller* PlotArea::row_controller(u32 row) noexcept {
|
||||
return row < row_controllers_.size() ? row_controllers_[row].get() : nullptr;
|
||||
}
|
||||
|
||||
input::Controller& PlotArea::controller_at(f32 x, f32 y) noexcept {
|
||||
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
|
||||
// The cached rects `refresh_row_rects()` filled -- the same ones the last frame drew
|
||||
// -- 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);
|
||||
input::Controller& controller = *row_controllers_[row];
|
||||
controller.set_viewport(row_rects_[row]);
|
||||
return controller;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
input::Controller& PlotArea::target_controller(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
|
||||
// landed in, however far the pointer has travelled since. Handing a `drag-update` or
|
||||
// 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);
|
||||
// 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;
|
||||
last_row_ = row;
|
||||
input::Controller& controller = *row_controllers_[row];
|
||||
controller.set_viewport(row_rects_[row]);
|
||||
return controller;
|
||||
}
|
||||
|
||||
u32 PlotArea::row_in(const std::vector<Rect>& rows, 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.
|
||||
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);
|
||||
if (distance < best) {
|
||||
best = distance;
|
||||
row = i;
|
||||
if (distance == 0.0f) break; // inside this row: nothing can be nearer
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
u32 PlotArea::pointer_row() const noexcept {
|
||||
if (row_rects_.empty()) return 0;
|
||||
// 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);
|
||||
}
|
||||
|
||||
void PlotArea::sync_figure_crosshair() noexcept {
|
||||
if (row_rects_.empty() || row_controllers_.empty()) return;
|
||||
const u32 row_count = static_cast<u32>(row_rects_.size());
|
||||
|
||||
// Which row's crosshair is the hover. With a pointer of this area's own it is the row
|
||||
// that pointer's events go to -- the same rule the routing uses, so the hover follows
|
||||
// the gesture it began. A headless probe injects its events straight into a row
|
||||
// controller and the area never sees a pointer, so there the hover is the row whose
|
||||
// crosshair is *not* the one last spread: the figure writes one value to every row,
|
||||
// so a row holding anything else was changed from outside. Nothing differing means no
|
||||
// new hover, and the figure keeps the one it has.
|
||||
u32 hovered = row_count;
|
||||
if (have_pointer_) {
|
||||
hovered = pointer_row();
|
||||
} else {
|
||||
for (u32 i = 0; i < row_count; ++i) {
|
||||
const Crosshair& candidate = figure_->plot(i).crosshair();
|
||||
// A row differs when its active state, its x, or the y of *its own* horizontal
|
||||
// line differs from what was spread: a pointer moving to another row at the same
|
||||
// x is a new hover and must move the one horizontal line, not leave the old one.
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (hovered >= row_count) return;
|
||||
}
|
||||
|
||||
// One x to every row, and the hovered row's **own** y only to that row (spec §17):
|
||||
// the rows of a figure share x and not y -- row 0 may be volts and row 1 degrees --
|
||||
// so a horizontal line spread across the figure would mean a different value in each.
|
||||
// A row whose controller has *no* crosshair (the pointer left its axes, the only
|
||||
// thing that clears one) clears the 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) {
|
||||
if (row.crosshair().horizontal) {
|
||||
figure_->set_crosshair(row.crosshair().x, hovered, row.crosshair().y, true);
|
||||
} else {
|
||||
// Vertical-only: a caller placed it through the single-coordinate
|
||||
// `Figure::set_crosshair()`. The four-argument overload would turn the horizontal
|
||||
// line on at the stored (meaningless) y == 0, clamped to the axes edge and made
|
||||
// permanent, so only the vertical line is promoted.
|
||||
figure_->set_crosshair(row.crosshair().x, true);
|
||||
}
|
||||
spread_x_ = row.crosshair().x;
|
||||
spread_y_ = row.crosshair().y;
|
||||
spread_active_ = true;
|
||||
} else {
|
||||
figure_->clear_crosshair();
|
||||
spread_active_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- the GL signals -------------------------------------------------------
|
||||
|
||||
void PlotArea::realize() noexcept {
|
||||
@@ -317,12 +497,36 @@ void PlotArea::render() noexcept {
|
||||
// chrome out with it) and refreshes the layout, so every frame is laid out for
|
||||
// the geometry of *this* frame. A resize changes the layout; it never scales a
|
||||
// stale one.
|
||||
controller_.set_viewport(viewport);
|
||||
if (figure_ != nullptr) {
|
||||
// The figure's one layout for this frame: it hands every row its rect (and so
|
||||
// every row's `Plot` its viewport) and fills the rect cache the routing of
|
||||
// whatever GTK delivers before the next frame answers from.
|
||||
lay_figure_out(viewport);
|
||||
// One hover, every row: the crosshair the row under the pointer placed is applied
|
||||
// to the whole figure here, after this frame's events have been handled and before
|
||||
// anything lays the rows out again. Here rather than in a handler, so a headless
|
||||
// probe that injects straight into a row controller is synced too.
|
||||
sync_figure_crosshair();
|
||||
// Only the row the pointer's events go to republishes its own hot handles, so a row
|
||||
// the pointer has left would keep the `HotHandles` it last published and go on
|
||||
// drawing a hovered marker or cursor grab as if the pointer were still on it. Clear
|
||||
// every *other* row here, where that row is known.
|
||||
if (have_pointer_ && !row_rects_.empty()) {
|
||||
const u32 active = pointer_row();
|
||||
for (u32 i = 0; i < row_rects_.size() && i < row_controllers_.size(); ++i) {
|
||||
if (i != active) figure_->plot(i).set_hot_handles(HotHandles{});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
controller_.set_viewport(viewport);
|
||||
}
|
||||
|
||||
const f64 started = now_s();
|
||||
const f64 dt = last_render_s_ >= 0.0 ? started - last_render_s_ : 0.0;
|
||||
last_render_s_ = started;
|
||||
if (frame_) frame_(*plot_, dt);
|
||||
// A figure's rows take the pointer, but the frame callback is still given a `Plot&`:
|
||||
// row 0 is the one it gets, so the callback argument and `figure()->plot(0)` agree.
|
||||
if (frame_) frame_(figure_ != nullptr ? figure_->plot(0u) : *plot_, dt);
|
||||
|
||||
// The mapping's factor is the display's scale, so the renderer draws its logical
|
||||
// style metrics at the same ratio the viewport and every forwarded coordinate are
|
||||
@@ -331,7 +535,11 @@ void PlotArea::render() noexcept {
|
||||
// ceiling under fractional scaling and would over-scale the chrome by up to 60%.
|
||||
renderer_->style().ui_scale = mapping_.factor;
|
||||
renderer_->begin_frame(static_cast<u32>(viewport.w), static_cast<u32>(viewport.h), viewport);
|
||||
renderer_->draw(*plot_);
|
||||
if (figure_ != nullptr) {
|
||||
renderer_->draw(*figure_);
|
||||
} else {
|
||||
renderer_->draw(*plot_);
|
||||
}
|
||||
renderer_->end_frame();
|
||||
|
||||
const f32 frame_ms = static_cast<f32>((now_s() - started) * 1.0e3);
|
||||
@@ -385,11 +593,16 @@ void PlotArea::on_resize(GtkGLArea*, int width, int height, void* self) noexcept
|
||||
void PlotArea::pointer_press(input::Button button, u32 mods, double x, double y,
|
||||
u32 time_ms) noexcept {
|
||||
if (press_open_) return; // one press per button-down sequence: see drag_begin
|
||||
|
||||
const Vec2 p = to_device(x, 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_open_ = true;
|
||||
press_button_ = button;
|
||||
press_mods_ = mods;
|
||||
|
||||
const Vec2 p = to_device(x, y);
|
||||
pointer_ = p;
|
||||
have_pointer_ = true;
|
||||
|
||||
@@ -403,12 +616,16 @@ 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
|
||||
controller_.on_pointer(e);
|
||||
target_controller(p.y).on_pointer(e);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(widget_));
|
||||
}
|
||||
|
||||
void PlotArea::pointer_release(Vec2 position, u32 time_ms) noexcept {
|
||||
if (!press_open_) return;
|
||||
// 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);
|
||||
press_open_ = false;
|
||||
|
||||
input::PointerEvent e;
|
||||
@@ -421,7 +638,7 @@ void PlotArea::pointer_release(Vec2 position, u32 time_ms) noexcept {
|
||||
e.down = false;
|
||||
e.released = true;
|
||||
e.time_s = pointer_time_s(time_ms);
|
||||
controller_.on_pointer(e);
|
||||
target.on_pointer(e);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(widget_));
|
||||
}
|
||||
|
||||
@@ -511,7 +728,9 @@ void PlotArea::on_drag_update(GtkGestureDrag* gesture, double offset_x, double o
|
||||
e.down = true;
|
||||
e.time_s = area->pointer_time_s(
|
||||
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)));
|
||||
area->controller_.on_pointer(e);
|
||||
// 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);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
||||
}
|
||||
|
||||
@@ -556,7 +775,9 @@ void PlotArea::on_zoom_scale_changed(GtkGestureZoom* gesture, double scale,
|
||||
// nothing rather than zooming from a made-up anchor.
|
||||
const input::GestureEvent g = area->zoom_.update(
|
||||
static_cast<f32>(scale), Vec2{static_cast<f32>(cx), static_cast<f32>(cy)}, area->mapping_);
|
||||
area->controller_.on_gesture(g);
|
||||
// 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);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
||||
}
|
||||
|
||||
@@ -595,7 +816,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->controller_.on_wheel(e);
|
||||
area->target_controller(at.y).on_wheel(e);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
||||
return TRUE; // consumed: the plot zoomed
|
||||
}
|
||||
@@ -619,7 +840,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->controller_.on_pointer(e);
|
||||
area->target_controller(p.y).on_pointer(e);
|
||||
}
|
||||
|
||||
// --- keyboard -------------------------------------------------------------
|
||||
@@ -644,7 +865,13 @@ int PlotArea::on_key_pressed(GtkEventControllerKey* keys, unsigned int keyval,
|
||||
// GTK reports a held key as repeated presses and sends no release the controller
|
||||
// would act on anyway (its actions are press-only), so the press is the event.
|
||||
e.down = true;
|
||||
area->controller_.on_key(e);
|
||||
// A key has no position of its own, so it goes to the row the last event was routed
|
||||
// to -- the row the user is looking at.
|
||||
input::Controller& controller =
|
||||
(area->figure_ == nullptr || area->last_row_ >= area->row_controllers_.size())
|
||||
? area->controller_
|
||||
: *area->row_controllers_[area->last_row_];
|
||||
controller.on_key(e);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
||||
return TRUE; // consumed: the plot acted on it
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user