Fixing minor issues in multifigure plots
This commit is contained in:
1 parent
38ef459897
commit
98cfc5805b
4 files changed
+392
-12
No files matched your search
@@ -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.
|
||||
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);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <plot/core/rect.hpp>
|
||||
#include <plot/core/status.hpp>
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/input/controller.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
#include <plot/plot/figure.hpp>
|
||||
#include <plot/plot/plot.hpp>
|
||||
#include <plot/render/renderer.hpp>
|
||||
|
||||
@@ -171,6 +174,42 @@ using FrameFn = std::function<void(Plot&, f64 dt)>;
|
||||
/// visible without waiting for the host's next frame, and the frame rate stays the
|
||||
/// host's: the demo pumps `g_timeout_add(16, gtk_gl_area_queue_render)`.
|
||||
///
|
||||
/// ## Figure mode
|
||||
///
|
||||
/// `set_figure()` makes the area draw a whole `Figure` -- several rows sharing one
|
||||
/// x axis -- instead of its own `Plot`, exactly as `glfw::Host::set_figure()` does
|
||||
/// for the reference host, and for the same reason: the multi-row shape is one
|
||||
/// `Figure`, not N widgets, because the x link groups, the shared y gutter and the
|
||||
/// one shared x axis strip all live inside it and cannot be reassembled from
|
||||
/// separate areas. The GTK plumbing above is unchanged by it; what changes is where
|
||||
/// an event goes and what the renderer is handed.
|
||||
///
|
||||
/// A `Controller` is bound to a plot at construction, so figure mode holds **one
|
||||
/// controller per row**, created by `set_figure()`, and routes each event to the row
|
||||
/// it lands in (`controller_at()`). The row rects come from a cache
|
||||
/// (`refresh_row_rects()`) that the render callback fills once per frame, so a burst
|
||||
/// of pointer motion costs no layouts; between a resize and the next frame the cache
|
||||
/// is one frame stale, which is the documented worst case.
|
||||
///
|
||||
/// A gesture belongs to the row its **press** landed in for its whole life: the
|
||||
/// press claims the row, and every `drag-update`, pinch, wheel and the release that
|
||||
/// follow while that press is open go there, however far the pointer travels. A drag
|
||||
/// that crosses into the row below is therefore still the drag it began as, instead
|
||||
/// of being handed to a controller that never saw the press -- which would leave the
|
||||
/// first row's drag open for ever and let a later gesture resume it from the stale
|
||||
/// anchor. The single open press `press_open_` already enforces (see the input
|
||||
/// section) is that capture; this adapter needs no separate button bookkeeping the
|
||||
/// way the GLFW host does, because GTK's two gestures share one sequence.
|
||||
///
|
||||
/// The **crosshair** is one crosshair over the figure and not one per row: the row
|
||||
/// under the pointer places it, and `sync_figure_crosshair()` reads it back and
|
||||
/// applies it to every row, so each row's readout lists its own traces at the one x
|
||||
/// the pointer is over. Reading it back rather than deriving a second coordinate
|
||||
/// here is what keeps the row's hover and the figure's the same number.
|
||||
///
|
||||
/// A key event has no position, so it goes to the row the last event was routed to,
|
||||
/// which is the row the user is looking at.
|
||||
///
|
||||
/// ## Headless use
|
||||
///
|
||||
/// A `GtkGLArea` renders into its own FBO, so a screenshot of the *plot* is a
|
||||
@@ -202,10 +241,56 @@ class PlotArea {
|
||||
/// The plot this area renders. Valid for the widget's whole life.
|
||||
Plot& plot() noexcept { return *plot_; }
|
||||
const Plot& plot() const noexcept { return *plot_; }
|
||||
/// The controller this area feeds, bound to `plot()`.
|
||||
/// The controller this area feeds, bound to `plot()`. **Not** the one a figure's
|
||||
/// gestures reach: see `row_controller()`.
|
||||
input::Controller& controller() noexcept { return controller_; }
|
||||
const input::Controller& controller() const noexcept { return controller_; }
|
||||
|
||||
/// Draws `figure` instead of this area's own plot, with one `input::Controller` per
|
||||
/// row and every event routed to the row it lands in (see "Figure mode" above).
|
||||
/// The figure is borrowed and must outlive the widget.
|
||||
///
|
||||
/// The frame callback is still handed a `Plot&`, and in figure mode it is **row
|
||||
/// 0**, so the callback's argument and `figure()->plot(0)` are the same plot --
|
||||
/// matching `glfw::Host`. A host that needs the figure itself captures it.
|
||||
///
|
||||
/// Passing null restores the single-plot path; `plot()` is untouched throughout,
|
||||
/// so the area goes back to drawing exactly what it drew before.
|
||||
void set_figure(Figure* figure) noexcept;
|
||||
Figure* figure() noexcept { return figure_; }
|
||||
const Figure* figure() const 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 mode or a
|
||||
/// hover out of it.
|
||||
input::Controller* row_controller(u32 row) noexcept;
|
||||
|
||||
/// The controller an event at device pixel `(x, y)` belongs to: `controller()`
|
||||
/// while no figure is set, and otherwise the controller of the figure row that
|
||||
/// pixel lands in -- a pixel inside no row (a `Figure::set_row_gap()` gap, a
|
||||
/// margin) is the *nearest* row rather than dropped, 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 hit-test geometry in step with the layout between
|
||||
/// two frames.
|
||||
///
|
||||
/// The pure query: it reports what a gesture *would* land on and never claims the
|
||||
/// press capture, so a host may call it freely.
|
||||
input::Controller& controller_at(f32 x, f32 y) noexcept;
|
||||
|
||||
/// The figure row a point in the widget's **logical** coordinates lands in -- the
|
||||
/// coordinates every GTK event controller reports, so a host's own drop target or
|
||||
/// context menu can name the row a pointer is over without converting anything. The
|
||||
/// same nearest-row rule as `controller_at()`, and 0 while no figure is set.
|
||||
u32 row_at(double x, double y) const noexcept;
|
||||
|
||||
/// Re-reads the figure's row rects into the cache the event routing answers from --
|
||||
/// the one `Figure::layout()` the routing costs between two frames. The render
|
||||
/// callback runs it once per frame, right after the figure's viewport is set, so
|
||||
/// the rects the handlers use are the rects that frame draws. A driver that injects
|
||||
/// events without rendering (a headless probe) calls it after re-configuring the
|
||||
/// figure; a no-op with no figure set.
|
||||
void refresh_row_rects() noexcept;
|
||||
|
||||
/// The renderer, or null until a successful `::realize`. Valid from the first
|
||||
/// render onwards, and after the widget's context exists.
|
||||
Renderer* renderer() noexcept { return renderer_ ? &*renderer_ : nullptr; }
|
||||
@@ -291,6 +376,32 @@ class PlotArea {
|
||||
/// into device pixels. The one place that mapping exists.
|
||||
Vec2 to_device(double x, double y) const noexcept;
|
||||
|
||||
/// The controller one event of this area's goes to, given the event's device y.
|
||||
/// `controller_` while no figure is set; otherwise the row the gesture *belongs*
|
||||
/// to: the row the open press claimed while a press is open, and the row under `y`
|
||||
/// between gestures. That is the capture `controller_at()` deliberately omits, and
|
||||
/// it is why this -- not `controller_at()` -- is what every handler below calls.
|
||||
///
|
||||
/// Hands the chosen row its own viewport, so the geometry it hit-tests is the
|
||||
/// geometry the last frame drew.
|
||||
input::Controller& target_controller(f32 y) noexcept;
|
||||
/// The row of `row_rects_` a device y belongs to: the row it is in, or the nearest
|
||||
/// one when it is in none (a row gap, a margin outside the figure). Selection by
|
||||
/// distance rather than "the first that matches" is what makes a pixel in a visible
|
||||
/// gap belong to the row it is *near* instead of to row 0.
|
||||
static u32 row_in(const std::vector<Rect>& rows, f32 y) noexcept;
|
||||
/// The row this area's events currently go to: the press's row while one is open,
|
||||
/// so an in-flight gesture keeps its highlight, and the row under the last pointer
|
||||
/// position otherwise. 0 with no rows.
|
||||
u32 pointer_row() const noexcept;
|
||||
/// Spreads one row's crosshair over the whole figure (see "Figure mode"). Run once
|
||||
/// per frame, from the render callback, after this frame's row rects are cached.
|
||||
void sync_figure_crosshair() noexcept;
|
||||
/// `figure_->set_viewport(px)`, one `layout()` into the rect cache, and each row's
|
||||
/// own rect to its controller. The figure-mode half of `refresh_viewport()` and of
|
||||
/// `render()`, which must not drift apart.
|
||||
void lay_figure_out(Rect px) noexcept;
|
||||
|
||||
static void on_realize(GtkGLArea* area, void* self) noexcept;
|
||||
static void on_unrealize(GtkGLArea* area, void* self) noexcept;
|
||||
/// `gboolean` is `int`; a GtkGLArea render handler ignores the context.
|
||||
@@ -329,6 +440,26 @@ class PlotArea {
|
||||
|
||||
Plot* plot_ = nullptr; ///< declared first: the controller is built over it
|
||||
input::Controller controller_;
|
||||
/// The figure this area draws instead of `plot_`, and one controller per row for it
|
||||
/// -- one per `Plot`, because a `Controller` is bound to a plot at construction and
|
||||
/// this is what makes a gesture on any row reach that row. Both are empty until
|
||||
/// `set_figure()` is called, and the single-plot path reads neither.
|
||||
Figure* figure_ = nullptr;
|
||||
std::vector<std::unique_ptr<input::Controller>> row_controllers_;
|
||||
/// The figure's row rects as of the last `refresh_row_rects()`: the cache the event
|
||||
/// routing reads instead of laying the figure out per event. Empty while no figure
|
||||
/// is set, which is the state the routing falls back to `controller_` on.
|
||||
std::vector<Rect> row_rects_;
|
||||
/// The row the last event was routed to, which is where a key event goes: a key has
|
||||
/// no position of its own.
|
||||
u32 last_row_ = 0;
|
||||
/// The crosshair last spread over the figure (see `sync_figure_crosshair()`): what a
|
||||
/// row is compared against to find the one something outside changed, since the
|
||||
/// figure writes one value to every row. `spread_active_` false means "spread
|
||||
/// nothing" -- the state before the first sync, and the one a clear leaves.
|
||||
f64 spread_x_ = 0.0;
|
||||
f64 spread_y_ = 0.0;
|
||||
bool spread_active_ = false;
|
||||
GtkWidget* widget_ = nullptr;
|
||||
std::optional<Renderer> renderer_;
|
||||
FrameFn frame_;
|
||||
@@ -340,6 +471,10 @@ class PlotArea {
|
||||
bool press_open_ = false;
|
||||
input::Button press_button_ = input::Button::Left;
|
||||
u32 press_mods_ = 0;
|
||||
/// The figure row the open press landed in -- the gesture's capture (see
|
||||
/// `target_controller()`). Meaningless while `press_open_` is false and while no
|
||||
/// figure is set.
|
||||
u32 press_row_ = 0;
|
||||
/// The drag gesture's anchor: its press point and last offset, in *logical*
|
||||
/// pixels. Logical on purpose: a factor change between two samples of a drag
|
||||
/// cannot then turn into a phantom movement (see `DragAnchor`). The offset is
|
||||
|
||||
@@ -126,6 +126,18 @@ class Figure {
|
||||
void set_row_gap(f32 device_px) noexcept { row_gap_ = device_px > 0.0f ? device_px : 0.0f; }
|
||||
f32 row_gap() const noexcept { return row_gap_; }
|
||||
|
||||
/// A floor under the figure's common left gutter, in device pixels. The gutter
|
||||
/// `layout()` imposes is the maximum over the rows' *measured* needs, so it follows
|
||||
/// the tick labels: a `TickFormat::Si` axis whose range moves from `1.5` to `150` to
|
||||
/// `1.5k` changes the width of its labels, and with it `axes.x` and `axes.w` of every
|
||||
/// row. A caller that needs a layout which does not depend on content reserves the
|
||||
/// widest label its formatter can produce once, through this, and the figure takes the
|
||||
/// larger of the two. Negative values are clamped to 0 (the default: measure only).
|
||||
void set_min_y_gutter(f32 device_px) noexcept {
|
||||
min_y_gutter_ = device_px > 0.0f ? device_px : 0.0f;
|
||||
}
|
||||
f32 min_y_gutter() const noexcept { return min_y_gutter_; }
|
||||
|
||||
/// Whether the shared x axis is the figure's: every row reserves no x label strip
|
||||
/// and one shared axis is drawn under the last row (the default), or every row
|
||||
/// draws its own labels and the figure draws nothing extra.
|
||||
@@ -143,7 +155,8 @@ class Figure {
|
||||
/// It is also the one place the rows' left chrome can be made to agree (spec §16):
|
||||
/// each row is laid out once to learn the gutter its own labels and title column
|
||||
/// need, the figure takes the maximum and imposes it on every row through
|
||||
/// `Plot::set_min_y_gutter()` before laying the rows out once more -- so every row's
|
||||
/// `Plot::set_min_y_gutter()` -- never below `set_min_y_gutter()`'s own floor -- before
|
||||
/// laying the rows out once more, so every row's
|
||||
/// `axes.x` and `axes.w` are equal and a vertical feature lands in the same pixel
|
||||
/// column in each. Two passes per row per call, not a loop: the gutter is a maximum
|
||||
/// over rows and a row's own strip width does not depend on it.
|
||||
@@ -312,6 +325,7 @@ class Figure {
|
||||
std::vector<XLink> links_;
|
||||
FigureLayout layout_{};
|
||||
f32 row_gap_ = 0.0f;
|
||||
f32 min_y_gutter_ = 0.0f;
|
||||
bool labels_bottom_only_ = true;
|
||||
};
|
||||
|
||||
|
||||
+5
-1
@@ -467,7 +467,11 @@ const FigureLayout& Figure::layout() {
|
||||
// per row settles the answer exactly. Measuring with a floor from the previous frame
|
||||
// would be the only reason to iterate, and clearing it before the measurement pass is
|
||||
// what removes it.
|
||||
f32 gutter = 0.0f;
|
||||
//
|
||||
// `set_min_y_gutter()` seeds the maximum rather than being imposed on a row before the
|
||||
// measurement, so a reserved floor cannot widen what a row reports and then be widened
|
||||
// by its own reservation on the next frame.
|
||||
f32 gutter = min_y_gutter_;
|
||||
for (u32 row = 0; row < rows; ++row) {
|
||||
plots_[row].set_min_y_gutter(0.0f);
|
||||
const PlotLayout& row_layout = plots_[row].layout();
|
||||
|
||||
Reference in new issue
Block a user