520 lines
30 KiB
C++
520 lines
30 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#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>
|
|
|
|
#include <gtk4/gtk_input.hpp>
|
|
|
|
// GTK's own type declarations, spelled exactly as GTK spells them
|
|
// (`typedef struct _GtkWidget GtkWidget;` and so on). Forward-declared on
|
|
// purpose, like the GLFW host's `struct GLFWwindow`: this header is
|
|
// self-contained and needs no GTK include directory, so a consumer of the
|
|
// adapter does not inherit GTK's headers -- and, unlike a toolkit whose headers
|
|
// reach `<GL/gl.h>`, either include order works, because these are the same
|
|
// typedefs GTK declares and GTK's headers pull in no GL header at all. Nor does
|
|
// `gtk_input.hpp`, included above for the device-pixel mapping and the drag
|
|
// anchor, name a GDK type: it mirrors the two scroll-unit values and GDK's
|
|
// modifier bits as plain numbers.
|
|
typedef struct _GtkWidget GtkWidget;
|
|
typedef struct _GtkGLArea GtkGLArea;
|
|
typedef struct _GtkGesture GtkGesture;
|
|
typedef struct _GtkGestureClick GtkGestureClick;
|
|
typedef struct _GtkGestureDrag GtkGestureDrag;
|
|
typedef struct _GtkGestureZoom GtkGestureZoom;
|
|
typedef struct _GtkEventControllerScroll GtkEventControllerScroll;
|
|
typedef struct _GtkEventControllerKey GtkEventControllerKey;
|
|
typedef struct _GtkEventControllerMotion GtkEventControllerMotion;
|
|
typedef struct _GdkGLContext GdkGLContext;
|
|
typedef struct _GdkEventSequence GdkEventSequence;
|
|
|
|
namespace plot::gtk4 {
|
|
|
|
/// One frame of host code, run by the render callback after the viewport is set
|
|
/// and before the frame is staged. It is where data is refreshed -- acquiring a
|
|
/// `data::StreamSeries`, re-fitting a live axis, moving cursors -- and `dt` is
|
|
/// the wall time since the previous call, in seconds. Optional; an area with no
|
|
/// callback just re-draws the plot as it stands.
|
|
using FrameFn = std::function<void(Plot&, f64 dt)>;
|
|
|
|
/// Embeds a `Plot` in a GTK4 widget: a `GtkGLArea` whose framebuffer the plot is
|
|
/// rendered into, with GTK's gestures and event controllers translated into
|
|
/// `plot::input` ones.
|
|
///
|
|
/// ## Why a wrapper and not a subclass
|
|
///
|
|
/// The plan calls for a `GtkWidget` subclass, and GTK4 cannot have one here: a
|
|
/// GObject type is registered with `G_DEFINE_TYPE`, which is a C macro that
|
|
/// declares the type's instance and class structs and a `*_get_type()` function,
|
|
/// and none of that is expressible around a C++ class with a vtable. GTK also
|
|
/// refuses to let an application derive from a type with private data
|
|
/// (`GtkGLArea`'s is private) outside C. So this is the factory shape instead:
|
|
/// `create()` builds a `gtk_gl_area_new()` instance, registers the signals and
|
|
/// controllers that drive it, and returns the wrapper that owns them. The
|
|
/// widget is GTK's; the wrapper is the widget's.
|
|
///
|
|
/// ## What the area draws into
|
|
///
|
|
/// `GtkGLArea` creates an FBO of its own and makes it the current draw target
|
|
/// before emitting `::render`, exactly the "currently bound framebuffer"
|
|
/// `Renderer` documents it draws into -- so this area, like the Qt widget,
|
|
/// creates no target of its own and the renderer's scissor-cleared viewport is
|
|
/// the area's whole rectangle. GTK then composites that FBO into the
|
|
/// scene graph as a texture. Nothing here renders to a texture or through a
|
|
/// second pass, and the plot cannot disturb GTK's own drawing by construction:
|
|
/// the only framebuffer the renderer is handed is one GTK created for this
|
|
/// widget, and GTK is the one that composites it. That is a structural argument,
|
|
/// not a tested property.
|
|
///
|
|
/// ## The coordinate convention: device pixels, both sides
|
|
///
|
|
/// Two pixel spaces meet here, exactly as they do in the GLFW, ImGui and Qt
|
|
/// adapters. GTK reports widget allocation and every input coordinate in
|
|
/// **logical** pixels (`gtk_widget_get_width()`, a gesture's `x`/`y`, a drag
|
|
/// offset), while the area's framebuffer -- what `Renderer::begin_frame()` takes
|
|
/// and what every `Rect` in this library speaks -- is in **device** pixels,
|
|
/// the pixels of the GL viewport this area renders into.
|
|
///
|
|
/// This area converts everything into device pixels and keeps it there:
|
|
///
|
|
/// * the viewport handed to the plot is the device size GTK reports in
|
|
/// `::resize`, which is the size of the viewport that signal's default
|
|
/// handler sets -- the exact size of the FBO GTK attaches and the renderer
|
|
/// draws into -- and
|
|
/// * every pointer position, drag offset and wheel position is multiplied by
|
|
/// that same factor before it reaches the controller.
|
|
///
|
|
/// The factor is therefore *derived*, from those two sizes and from nothing else:
|
|
/// `DeviceMapping::from_sizes()` divides the device size by the widget's logical
|
|
/// allocation, and it is the one value both sides read. No scale accessor takes
|
|
/// part: `gtk_widget_get_scale_factor()` is documented to return "the next higher
|
|
/// integer value" under fractional scaling (so a 1.25 or 1.75 display would map
|
|
/// input by 2), and `gdk_surface_get_scale()` describes the surface rather than
|
|
/// the FBO the area was given. The factor is an exact ratio, not an integer.
|
|
///
|
|
/// `device_viewport()` and `to_device()` are the only two places the mapping is
|
|
/// read, so the viewport and the input cannot disagree -- and they must not: a
|
|
/// mismatch renders correctly but pans by the wrong amount and puts a wheel's
|
|
/// anchor in the wrong place, which is invisible on an unscaled display and wrong
|
|
/// on every other one.
|
|
///
|
|
/// The *anchor* a drag's movement is measured from is the one thing kept in
|
|
/// logical pixels: the factor can change between the press and an update (the
|
|
/// window moving to a display with another scale, GTK re-emitting `::resize`), and
|
|
/// a delta is then a logical difference scaled by the factor in force now, never
|
|
/// the difference of two device positions (see `DragAnchor`).
|
|
///
|
|
/// The same factor states the display to the *renderer*: it is written to
|
|
/// `Renderer::style().ui_scale` every frame, which is what makes the chrome the
|
|
/// same physical size at any scale. The style's metrics are logical (see the unit
|
|
/// rule on `render::Style` -- spec §10 reverses the older convention that had them
|
|
/// in device pixels and accepted smaller labels on a high-DPI host), so at scale 2
|
|
/// the label font is 12 logical px drawn at 24 device px: the same size on screen as
|
|
/// at 1x, not a smaller one. This is the same convention the GLFW, Qt and ImGui
|
|
/// adapters use, and none of them has to scale a style metric by hand.
|
|
///
|
|
/// ## Input
|
|
///
|
|
/// GTK decides which gesture a sequence is by recognition, so two gestures share
|
|
/// the work here, and the split is the one the observed signal order forces:
|
|
///
|
|
/// * `GtkGestureDrag` (button 0: any) supplies the *motion*. Its
|
|
/// `drag-update` carries the offset from the press point, which is what a pan,
|
|
/// a box zoom, a cursor move and an axis zoom all measure.
|
|
/// * `GtkGestureClick` (button 0: any) supplies the *edges*: the press and the
|
|
/// release, with the button and the modifier state, exactly as the Qt
|
|
/// widget's `mousePressEvent`/`mouseReleaseEvent` do. It is also what begins
|
|
/// a cursor move: a ctrl+press on a cursor opens `DragMode::MoveCursor` there.
|
|
/// * `GtkGestureZoom` supplies the two-finger pinch: each `scale-changed` is
|
|
/// folded by `ZoomAnchor` (`gtk_input.hpp`) into one `GestureEvent` -- this
|
|
/// frame's zoom about the fingers' centre, and the centre's movement, which is
|
|
/// the two-finger pan a trackpad produces alongside it.
|
|
/// * `GtkEventControllerMotion` supplies hover (the cursor hit test reads the
|
|
/// last position even with no button down). Its positions are forwarded with
|
|
/// `down == false` on purpose: a motion event that claimed a button was down
|
|
/// would drive `update_drag` as well, and every drag would then be applied
|
|
/// twice, once from the drag offset and once from the motion delta.
|
|
/// * `GtkEventControllerScroll` (vertical only) supplies the wheel, in notches
|
|
/// (see `scroll_notches`).
|
|
/// * `GtkEventControllerKey` supplies `+` / `-` / `f`.
|
|
///
|
|
/// GTK emits `drag-begin` *before* the click gesture's `pressed` (both are
|
|
/// emitted while the press event is handled), and a press that never moves still
|
|
/// produces `drag-begin`, `drag-end` and the click's press/release pair. The
|
|
/// adapter therefore opens its gesture from the first press it sees and closes it
|
|
/// from the first release, so the two gestures cannot double-press or
|
|
/// double-release the controller.
|
|
///
|
|
/// ## Threading and repainting
|
|
///
|
|
/// Everything here runs on the thread that owns the GTK main context -- realize,
|
|
/// render, every gesture handler -- which is the thread that owns the GL context.
|
|
/// A host that feeds the plot from a producer thread must acquire its data on
|
|
/// this thread (see the frame callback); the adapter never touches the plot from
|
|
/// anywhere else, and never joins a host's thread.
|
|
///
|
|
/// GTK redraws the area when the widget is exposed, resized or asked to. This
|
|
/// area additionally queues a render after every input event that can change the
|
|
/// picture -- a press, a drag, a wheel or a plot key, but not a hover (the
|
|
/// renderer draws nothing that depends on where the pointer is) -- so a gesture is
|
|
/// 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
|
|
/// read of that FBO (see the demo's `dump_area_ppm`), not of the window: GTK's
|
|
/// chrome lives outside it. The area also reports what happened to the GL context
|
|
/// rather than asserting: `init_status()` is `Ok` once the renderer is up, or the
|
|
/// failure that left the area drawing nothing.
|
|
class PlotArea {
|
|
public:
|
|
/// Creates the `GtkGLArea` that renders `plot`, plus the wrapper that drives
|
|
/// it. `plot` must outlive the widget. The area expands to fill its container;
|
|
/// a host that wants otherwise clears `GtkWidget:hexpand`/`vexpand`.
|
|
///
|
|
/// The returned wrapper is **owned by the widget**: GTK frees it when the
|
|
/// widget is finalized (the GL resources are released earlier, in
|
|
/// `::unrealize`, while the context may still be made current). The caller
|
|
/// therefore never deletes it, and must not keep it past the widget's life.
|
|
static PlotArea* create(Plot& plot) noexcept;
|
|
|
|
PlotArea(const PlotArea&) = delete;
|
|
PlotArea& operator=(const PlotArea&) = delete;
|
|
|
|
/// Installs the per-frame hook (see `FrameFn`); an empty function removes it.
|
|
void set_frame_callback(FrameFn frame) { frame_ = std::move(frame); }
|
|
|
|
/// The widget to put in a window or a box. Valid for the wrapper's whole life.
|
|
GtkWidget* widget() const noexcept { return widget_; }
|
|
|
|
/// 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()`. **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 plot's controller -- the one a gesture on that plot reaches -- or null
|
|
/// while no figure is set or it does not exist. `row` is the **flattened plot index**
|
|
/// (`Figure::plot_count()` of them: the row count when no row holds several cells). 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 plot (cell)
|
|
/// that pixel lands in -- x and y both pick it, as a row may hold several cells side
|
|
/// by side. A pixel inside no cell (a `Figure::set_row_gap()`/`set_col_gap()` gap, a
|
|
/// margin) is the *nearest* cell 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 flattened plot index 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 plot a pointer is over without converting anything. The
|
|
/// same nearest-cell rule (x and y) 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; }
|
|
const Renderer* renderer() const noexcept { return renderer_ ? &*renderer_ : nullptr; }
|
|
|
|
/// What `::realize` made of the context: `Ok`, or the failure that left the
|
|
/// area drawing nothing. A GTK signal handler cannot return a `Status` and
|
|
/// nothing may throw across GTK's callback, so the outcome is recorded here
|
|
/// instead. `ErrorCode::GLFailure` is what a `GtkGLArea` whose context could not
|
|
/// be created reports -- checked with `gtk_gl_area_get_error()` before any GL
|
|
/// entry point is called.
|
|
///
|
|
/// A context that GTK loses *after* realize is not reported here: GTK 4.22 stops
|
|
/// emitting `::render` for an area that carries an error (measured: the raw
|
|
/// signal count freezes on the frame the error is set), so the render callback
|
|
/// -- whose `gtk_gl_area_get_error()` check is the assertion that the context is
|
|
/// usable -- is never entered again. A host sees that as an area that stops
|
|
/// staging frames: `frames_painted()` stops advancing.
|
|
const Status& init_status() const noexcept { return init_status_; }
|
|
|
|
/// Frames actually staged, and the frame time of the last one (ms). The mean is
|
|
/// an exponential moving average seeded by the first frame, of the same
|
|
/// render-only span: the frame callback, `begin_frame`, `draw` and `end_frame`.
|
|
/// It excludes GTK's own compositing of the area, which happens outside this
|
|
/// callback and is not the library's or the adapter's work.
|
|
u64 frames_painted() const noexcept { return frames_; }
|
|
f32 last_frame_ms() const noexcept { return last_frame_ms_; }
|
|
f32 mean_frame_ms() const noexcept { return mean_frame_ms_; }
|
|
|
|
/// The region the last frame was drawn into, in device pixels -- the rect
|
|
/// `begin_frame()` was handed. Zero-sized before the first frame.
|
|
Rect last_device_viewport() const noexcept { return last_viewport_; }
|
|
|
|
/// The area's current rectangle in device pixels: the device size GTK last
|
|
/// reported in `::resize`, which is the size of the viewport that signal's
|
|
/// default handler set. This is the plot's viewport, the size of the framebuffer
|
|
/// GTK attached, and -- a screenshot helper needs it -- the size of a framebuffer
|
|
/// read. Zero-sized until GTK reports a size, which is also when there is
|
|
/// nothing to draw into.
|
|
Rect device_viewport() const noexcept;
|
|
|
|
/// Device pixels per logical pixel: the factor `DeviceMapping::from_sizes()`
|
|
/// derives from the widget's logical allocation and the device size `::resize`
|
|
/// reported, and the one factor both `device_viewport()` and every forwarded
|
|
/// input coordinate is scaled by. `1.0` before the widget is allocated, and
|
|
/// fractional under fractional scaling -- unlike `gtk_widget_get_scale_factor()`,
|
|
/// which returns its ceiling.
|
|
f32 device_scale() const noexcept;
|
|
|
|
private:
|
|
explicit PlotArea(Plot& plot, GtkWidget* widget) noexcept;
|
|
|
|
/// `::realize`: make the context current, check it exists, load the GL entry
|
|
/// points and initialise the renderer. Runs again if GTK realizes the widget
|
|
/// twice (it moved to another surface), which is why it releases the previous
|
|
/// renderer first.
|
|
void realize() noexcept;
|
|
/// `::unrealize`: release the renderer's GL objects while the context can still
|
|
/// be made current -- unconditionally, and by making it current first. GTK clears
|
|
/// the context and the area's error in its own unrealize vfunc, which runs after
|
|
/// this signal's handlers, so this is the last moment at which a `glDelete*` can
|
|
/// name a live context; afterwards the wrapper's destruction would issue those
|
|
/// deletes with none.
|
|
void unrealize() noexcept;
|
|
/// `::render`: stage one frame into the FBO GTK bound.
|
|
void render() noexcept;
|
|
/// `::resize`: hand the new geometry to the plot now, so a gesture arriving
|
|
/// before the next frame maps against the new layout instead of the old one.
|
|
void refresh_viewport() noexcept;
|
|
|
|
/// One pointer event, from whichever gesture saw it first. Owns the
|
|
/// press/release bookkeeping the two gestures share. `time_ms` is the event's
|
|
/// own GDK stamp, in milliseconds, forwarded as the `PointerEvent::time_s` the
|
|
/// double-click detector differences (see `pointer_time_s()`).
|
|
void pointer_press(input::Button button, u32 mods, double x, double y,
|
|
u32 time_ms) noexcept;
|
|
void pointer_release(Vec2 position, u32 time_ms) noexcept;
|
|
/// The gesture event's stamp, in the seconds `PointerEvent::time_s` speaks: GDK
|
|
/// counts milliseconds since an arbitrary epoch, and only differences are read,
|
|
/// so the epoch does not matter -- the unit does.
|
|
static f64 pointer_time_s(u32 time_ms) noexcept { return static_cast<f64>(time_ms) * 1.0e-3; }
|
|
/// Translates the gesture-reported position/delta in *logical* widget pixels
|
|
/// 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 (x, 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 (x and y pick the cell). 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 x, f32 y) noexcept;
|
|
/// The cell of `row_rects_` (the figure's flattened cell rects) a device pixel belongs
|
|
/// to: the cell it is in, or the nearest one when it is in none (a row or column 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 cell it is *near*
|
|
/// instead of to plot 0.
|
|
static u32 row_in(const std::vector<Rect>& rows, f32 x, 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;
|
|
/// Hands each row's plot the box-zoom frame its controller is showing, or takes it off
|
|
/// the ones whose gesture is over. Run once per frame from the render callback, like
|
|
/// the crosshair's spread and for a related reason: a frame has to go away when a drag
|
|
/// ends by *any* route -- the release, Escape, a row rebuilt mid-gesture -- and a
|
|
/// per-event hook would have to be repeated at every one of them, which is how a frame
|
|
/// ends up stuck on screen after a cancelled zoom.
|
|
///
|
|
/// Reads `selection_preview()`, which needs the row's layout, only for a controller
|
|
/// actually in `BoxZoom`: the cost is paid while a range is being chosen and never
|
|
/// otherwise.
|
|
void sync_selection_frames() 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.
|
|
static int on_render(GtkGLArea* area, GdkGLContext* context, void* self) noexcept;
|
|
static void on_resize(GtkGLArea* area, int width, int height, void* self) noexcept;
|
|
|
|
static void on_click_pressed(GtkGestureClick* gesture, int n_press, double x, double y,
|
|
void* self) noexcept;
|
|
static void on_click_released(GtkGestureClick* gesture, int n_press, double x, double y,
|
|
void* self) noexcept;
|
|
static void on_click_stopped(GtkGestureClick* gesture, void* self) noexcept;
|
|
/// The pinch: `begin` anchors the folding, `scale-changed` folds one cumulative
|
|
/// scale into an incremental `GestureEvent`, and `end` closes it. `begin`/`end`
|
|
/// carry the sequence, which a one-active-pinch adapter never needs.
|
|
static void on_zoom_begin(GtkGestureZoom* gesture, GdkEventSequence* sequence,
|
|
void* self) noexcept;
|
|
static void on_zoom_scale_changed(GtkGestureZoom* gesture, double scale,
|
|
void* self) noexcept;
|
|
static void on_zoom_end(GtkGestureZoom* gesture, GdkEventSequence* sequence,
|
|
void* self) noexcept;
|
|
static void on_drag_begin(GtkGestureDrag* gesture, double start_x, double start_y,
|
|
void* self) noexcept;
|
|
static void on_drag_update(GtkGestureDrag* gesture, double offset_x, double offset_y,
|
|
void* self) noexcept;
|
|
static void on_drag_end(GtkGestureDrag* gesture, double offset_x, double offset_y,
|
|
void* self) noexcept;
|
|
static int on_scroll(GtkEventControllerScroll* scroll, double dx, double dy,
|
|
void* self) noexcept;
|
|
static void on_motion(GtkEventControllerMotion* motion, double x, double y,
|
|
void* self) noexcept;
|
|
/// `guint keyval, guint keycode, GdkModifierType state`, all 32-bit values; the
|
|
/// modifier enum is named by its size here because this header does not include
|
|
/// GDK (see the note at the top).
|
|
static int on_key_pressed(GtkEventControllerKey* keys, unsigned int keyval,
|
|
unsigned int keycode, unsigned int state, void* self) noexcept;
|
|
|
|
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 cell rects (one per plot, flattened; the row rects when no row is split)
|
|
/// 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_;
|
|
Status init_status_;
|
|
|
|
/// The button and modifiers of the gesture in progress, and whether one is:
|
|
/// the two gestures share one sequence, so exactly one press is open between a
|
|
/// button-down and its button-up.
|
|
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
|
|
/// cached rather than re-read per event, so a `drag-update` reports the movement
|
|
/// since the previous one, and `have` is whether a drag is recognised.
|
|
DragAnchor drag_;
|
|
/// The pinch gesture's folding state (see `ZoomAnchor`): one per area, alive
|
|
/// between `::begin` and `::end` of the one zoom the gesture recognises.
|
|
ZoomAnchor zoom_;
|
|
/// The last pointer position seen, in device pixels: the wheel's anchor when a
|
|
/// scroll event carries no position of its own.
|
|
Vec2 pointer_{};
|
|
bool have_pointer_ = false;
|
|
/// The device-pixel mapping in force, derived in `::resize` from the widget's
|
|
/// logical allocation and the device size GTK reported. `1.0` and a zero
|
|
/// viewport until then, which is before anything is allocated or drawn.
|
|
DeviceMapping mapping_;
|
|
|
|
u64 frames_ = 0;
|
|
f32 last_frame_ms_ = 0.0f;
|
|
f32 mean_frame_ms_ = 0.0f;
|
|
/// Wall clock at the last render start, or negative before the first one.
|
|
f64 last_render_s_ = -1.0;
|
|
Rect last_viewport_{};
|
|
};
|
|
|
|
} // namespace plot::gtk4
|