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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user