feat(plot)!: harden pipeline, XY series, dash/markers, touch, packaging
Breaking: Series::gather emits x relative to x0() and takes a GatherMode; consumers reconstruct absolute x in f64 before mapping (series.hpp documents the contract). Bugs, each pinned by a fail-before regression test: - render: f32 staging quantised epoch-time x into 128 px columns; every data-space draw now stages to_space(x) - to_space(origin) with the origin folded into the offset uniform in f64 (epoch/zero-origin twins rasterise identically) - ticks: major test was ULP-fragile and blanked epoch axes; majors are an integer-index test and are never truncated by the minor cap - gather: LOD envelope emitted buckets straddling [begin,end); ceil/floor bucket edges + raw lead/trail spans - live follow: log x axes held a linear span and zoomed every frame; span and offset now live in axis space (plot + both figure link-group paths) - input: a zero-notch wheel disarmed auto_scale on every axis; dy==0 pans x, factor == 1 / inf refused in wheel and drag zoom - render: the single text flush painted legend names over the crosshair readout panel; two batches, under flushed before the panel - data: NaN dropped from min/max unless it seeded the fold; non-finite samples are now excluded from every aggregate and from cached bounds Robustness: viewport normalisation (no clamp UB), non-finite range refusal, pyramid level cap at 57 (no shift/div UB through public gather), rounded ring capacity documented + pre-sized, GL blend func and UNPACK_ALIGNMENT save/restore, Renderer::release_gl() teardown, u64 buffer epochs, annotation captions clamped, button-aware GLFW row capture, cached figure row rects (no per-event layout), ruler-pair measurement refused, span-nonfinite guards in axis_ends. Features: XY series (sorted x, binary-search neighbours/snap, LOD over sample index); StreamSeries::freeze(); ArraySeries::push(); per-trace dash and markers (scatter at width 0) with auto or literal stride; GatherMode Envelope/Nearest/Mean via bucket_sum/sumsq; Style::line_aa_px; per-glyph text colour (readout values in trace colour); wheel-dx x pan; arrow-key pan; Escape cancels a drag and restores captured ranges; modifier-masked box zoom (Shift both, Ctrl x, Alt y); double-click reset via PointerEvent::time_s; ImGui key repeat unified; pinch gestures on Qt and GTK; pixelDelta wheel path and DevicePixelRatioChange handling; GNUInstallDirs, SOVERSION, SameMinorVersion, generated plot/version.hpp, relocatable plot.pc, licence install, FILES_MATCHING, GitHub Actions CI (debug/coverage/asan). Suite 283 -> 350 cases, 0 failed; coverage 93.9% (3730/3971) over 24 files, floor 80/90 held; ASAN preset suite green; all four demos run headless (the GTK scripted-resize check needs a window manager and fails identically on the parent commit); readme_main.cpp committed and compiles with the README command; pkg-config and find_package consumption verified from a relocated install prefix.
This commit is contained in:
1 parent
f132a5ec92
commit
38ef459897
77 files changed
+5905
-688
No files matched your search
@@ -229,4 +229,115 @@ inline f64 scroll_notches(f64 dy, ScrollUnit unit) noexcept {
|
||||
return -dy;
|
||||
}
|
||||
|
||||
/// The keyvals this adapter maps, as GDK's own numbers, mirrored here so the
|
||||
/// mapping below -- and a test that drives it -- needs no GTK include directory.
|
||||
/// `gtk_plot_area.cpp` static-asserts each against GDK's own macro.
|
||||
///
|
||||
/// These are the *function* keyvals (the `0xFF00` page), fixed by X11's keysym
|
||||
/// convention GDK inherits; a keypad arrow (a keypad key with Num Lock off) is a
|
||||
/// different keyval and deliberately unmapped, exactly as the GLFW adapter maps
|
||||
/// the main-row arrows and not `GLFW_KEY_KP_4`.
|
||||
constexpr u32 kGdkKeyEscape = 0xFF1Bu;
|
||||
constexpr u32 kGdkKeyLeft = 0xFF51u;
|
||||
constexpr u32 kGdkKeyUp = 0xFF52u;
|
||||
constexpr u32 kGdkKeyRight = 0xFF53u;
|
||||
constexpr u32 kGdkKeyDown = 0xFF54u;
|
||||
|
||||
/// One key-press translation, by keyval first and Unicode fallback second.
|
||||
///
|
||||
/// GTK names a key by its keyval, which is layout- and level-dependent: `+` is
|
||||
/// usually shift+`=` and arrives as a different keyval, but both decode to the
|
||||
/// same Unicode code point, which is why the printable keys go through
|
||||
/// `gdk_keyval_to_unicode()`. The function keys decode to no code point at all,
|
||||
/// so they are matched on the keyval itself, before that fallback. `0` means "no
|
||||
/// plot key": the caller returns the event to GTK unhandled.
|
||||
///
|
||||
/// The keypad's plus and minus decode to the same code points as the main row's,
|
||||
/// so they come along for free -- the same set the GLFW adapter maps with
|
||||
/// `GLFW_KEY_KP_ADD`/`GLFW_KEY_KP_SUBTRACT`.
|
||||
inline int key_from(u32 keyval, u32 unicode) noexcept {
|
||||
switch (keyval) {
|
||||
case kGdkKeyEscape: return input::kKeyEscape;
|
||||
case kGdkKeyLeft: return input::kKeyLeft;
|
||||
case kGdkKeyRight: return input::kKeyRight;
|
||||
case kGdkKeyUp: return input::kKeyUp;
|
||||
case kGdkKeyDown: return input::kKeyDown;
|
||||
default: break;
|
||||
}
|
||||
switch (unicode) {
|
||||
case static_cast<u32>(input::kKeyZoomIn):
|
||||
case static_cast<u32>('='): return input::kKeyZoomIn;
|
||||
case static_cast<u32>(input::kKeyZoomOut):
|
||||
case static_cast<u32>('_'): return input::kKeyZoomOut;
|
||||
case static_cast<u32>(input::kKeyFit):
|
||||
case static_cast<u32>(input::kKeyFitUpper): return input::kKeyFit;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// The state one `GtkGestureZoom` is folded into `input::GestureEvent`s from.
|
||||
///
|
||||
/// `GtkGestureZoom` reports a **cumulative** scale (`gtk_gesture_zoom_get_scale_delta()`
|
||||
/// and the `scale-changed` argument are the zoom since the gesture began, 1.0 at
|
||||
/// the start) and knows the focal point only as the bounding box of the pointer
|
||||
/// the gesture holds (`gtk_gesture_bounding_box_center()`), also cumulative in the
|
||||
/// sense that it is where the fingers are *now*. The controller wants the
|
||||
/// increments -- `GestureEvent::scale` is this frame's zoom factor about the focal
|
||||
/// point, 1.0 = no change, and `(dx, dy)` the focal point's movement since the
|
||||
/// previous gesture event -- so the area owns one of these and folds each
|
||||
/// `scale-changed` exactly like the Qt adapter folds its pinch (`PinchTracker`).
|
||||
///
|
||||
/// The focal point is cached in **logical** widget pixels, like `DragAnchor` above,
|
||||
/// for the same reason: the mapping's factor is re-derived on every `::resize`,
|
||||
/// and a cache in device pixels would compare two points taken at different
|
||||
/// factors. The difference of two logical points, scaled by the factor in force
|
||||
/// now, is the movement the fingers actually made.
|
||||
///
|
||||
/// A plain value with no toolkit dependency, like every other type here: the area
|
||||
/// owns one, and the tests drive it directly.
|
||||
struct ZoomAnchor {
|
||||
bool active = false; ///< true between the gesture's begin and its end
|
||||
f32 last_scale = 1.0f; ///< the cumulative scale the previous update folded
|
||||
f32 x = 0.0f; ///< last focal point, logical widget pixels
|
||||
f32 y = 0.0f;
|
||||
|
||||
/// Starts a gesture at its first cumulative `scale` and logical focal point.
|
||||
/// Records only: a zoom that just began has zoomed and moved nothing yet.
|
||||
void begin(f32 scale, Vec2 logical_center) noexcept {
|
||||
active = true;
|
||||
last_scale = scale;
|
||||
x = logical_center.x;
|
||||
y = logical_center.y;
|
||||
}
|
||||
|
||||
/// Folds one `scale-changed` into an incremental `GestureEvent`, in device
|
||||
/// pixels at `mapping`: the zoom about the focal point, and the focal point's
|
||||
/// movement since the previous update. A no-op before `begin()`, so a
|
||||
/// `scale-changed` that arrives without a begin -- a host that missed the start
|
||||
/// -- zooms nothing rather than zooming from a made-up anchor.
|
||||
input::GestureEvent update(f32 scale, Vec2 logical_center,
|
||||
const DeviceMapping& mapping) noexcept {
|
||||
input::GestureEvent g;
|
||||
if (!active) return g;
|
||||
// This frame's zoom: the cumulative ratio over the one last folded. The first
|
||||
// update after begin() divides the scale by itself -- 1.0, no zoom, as it
|
||||
// should be when the fingers have not moved apart yet.
|
||||
g.scale = last_scale != 0.0f ? scale / last_scale : 1.0f;
|
||||
const Vec2 device = mapping.to_device(logical_center.x, logical_center.y);
|
||||
g.x = device.x;
|
||||
g.y = device.y;
|
||||
const Vec2 moved{logical_center.x - x, logical_center.y - y};
|
||||
const Vec2 device_moved = mapping.to_device(moved.x, moved.y);
|
||||
g.dx = device_moved.x;
|
||||
g.dy = device_moved.y;
|
||||
last_scale = scale;
|
||||
x = logical_center.x;
|
||||
y = logical_center.y;
|
||||
return g;
|
||||
}
|
||||
|
||||
/// Ends the gesture: a straggler update after this zooms nothing.
|
||||
void end() noexcept { active = false; }
|
||||
};
|
||||
|
||||
} // namespace plot::gtk4
|
||||
@@ -31,6 +31,11 @@ static_assert((1u << 2) == GDK_CONTROL_MASK, "mods_from's control bit must match
|
||||
static_assert((1u << 3) == GDK_ALT_MASK, "mods_from's alt bit must match GDK's");
|
||||
static_assert((1u << 26) == GDK_SUPER_MASK, "mods_from's super bit must match GDK's");
|
||||
static_assert((1u << 28) == GDK_META_MASK, "mods_from's meta bit must match GDK's");
|
||||
static_assert(plot::gtk4::kGdkKeyEscape == GDK_KEY_Escape, "the mirrored Escape keyval must match GDK's");
|
||||
static_assert(plot::gtk4::kGdkKeyLeft == GDK_KEY_Left, "the mirrored Left keyval must match GDK's");
|
||||
static_assert(plot::gtk4::kGdkKeyUp == GDK_KEY_Up, "the mirrored Up keyval must match GDK's");
|
||||
static_assert(plot::gtk4::kGdkKeyRight == GDK_KEY_Right, "the mirrored Right keyval must match GDK's");
|
||||
static_assert(plot::gtk4::kGdkKeyDown == GDK_KEY_Down, "the mirrored Down keyval must match GDK's");
|
||||
|
||||
namespace plot::gtk4 {
|
||||
namespace {
|
||||
@@ -158,6 +163,18 @@ PlotArea* PlotArea::create(Plot& plot) noexcept {
|
||||
g_signal_connect(drag, "drag-end", G_CALLBACK(&PlotArea::on_drag_end), self);
|
||||
gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(drag));
|
||||
|
||||
// The two-finger pinch: `scale-changed` carries the cumulative zoom since the
|
||||
// gesture began, and the gesture's bounding-box centre is the focal point -- the
|
||||
// pair `ZoomAnchor` folds into the incremental `GestureEvent` the controller
|
||||
// takes (zoom about the focal point, focal movement as the pan). No button:
|
||||
// unlike the click/drag pair this gesture is touch-only by nature, and a mouse
|
||||
// never produces one.
|
||||
GtkGesture* zoom = gtk_gesture_zoom_new();
|
||||
g_signal_connect(zoom, "begin", G_CALLBACK(&PlotArea::on_zoom_begin), self);
|
||||
g_signal_connect(zoom, "scale-changed", G_CALLBACK(&PlotArea::on_zoom_scale_changed), self);
|
||||
g_signal_connect(zoom, "end", G_CALLBACK(&PlotArea::on_zoom_end), self);
|
||||
gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(zoom));
|
||||
|
||||
// Vertical only, as the plan asks: a purely horizontal scroll is not a zoom, and
|
||||
// because this controller never sees one it cannot be mistaken for one. GTK
|
||||
// reports the unit per event, so a discrete wheel arrives in notches and a
|
||||
@@ -279,6 +296,7 @@ void PlotArea::unrealize() noexcept {
|
||||
renderer_.reset();
|
||||
press_open_ = false;
|
||||
drag_.have = false;
|
||||
zoom_.end();
|
||||
}
|
||||
|
||||
void PlotArea::render() noexcept {
|
||||
@@ -364,7 +382,8 @@ void PlotArea::on_resize(GtkGLArea*, int width, int height, void* self) noexcept
|
||||
|
||||
// --- pointer input --------------------------------------------------------
|
||||
|
||||
void PlotArea::pointer_press(input::Button button, u32 mods, double x, double y) 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
|
||||
press_open_ = true;
|
||||
press_button_ = button;
|
||||
@@ -383,11 +402,12 @@ void PlotArea::pointer_press(input::Button button, u32 mods, double x, double y)
|
||||
e.mods = mods;
|
||||
e.down = true;
|
||||
e.pressed = true;
|
||||
e.time_s = pointer_time_s(time_ms); // the double-click detector differences these
|
||||
controller_.on_pointer(e);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(widget_));
|
||||
}
|
||||
|
||||
void PlotArea::pointer_release(Vec2 position) noexcept {
|
||||
void PlotArea::pointer_release(Vec2 position, u32 time_ms) noexcept {
|
||||
if (!press_open_) return;
|
||||
press_open_ = false;
|
||||
|
||||
@@ -400,6 +420,7 @@ void PlotArea::pointer_release(Vec2 position) noexcept {
|
||||
e.mods = press_mods_;
|
||||
e.down = false;
|
||||
e.released = true;
|
||||
e.time_s = pointer_time_s(time_ms);
|
||||
controller_.on_pointer(e);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(widget_));
|
||||
}
|
||||
@@ -412,7 +433,8 @@ void PlotArea::on_click_pressed(GtkGestureClick* gesture, int, double x, double
|
||||
if (!button) return; // a button the controller has no gesture for
|
||||
// The modifiers come off the *event*, not the gesture: GTK reports the state as
|
||||
// it was before the press, which is what the controller's gesture table reads.
|
||||
area->pointer_press(*button, gesture_mods(GTK_EVENT_CONTROLLER(gesture)), x, y);
|
||||
area->pointer_press(*button, gesture_mods(GTK_EVENT_CONTROLLER(gesture)), x, y,
|
||||
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)));
|
||||
}
|
||||
|
||||
void PlotArea::on_click_released(GtkGestureClick* gesture, int, double x, double y,
|
||||
@@ -421,11 +443,11 @@ void PlotArea::on_click_released(GtkGestureClick* gesture, int, double x, double
|
||||
auto* area = static_cast<PlotArea*>(self);
|
||||
// The first release closes the gesture; a drag's own `drag-end` may already have
|
||||
// done it (GTK emits both, `drag-end` first), and this is then a no-op.
|
||||
area->pointer_release(area->to_device(x, y));
|
||||
area->pointer_release(area->to_device(x, y),
|
||||
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)));
|
||||
}
|
||||
|
||||
void PlotArea::on_click_stopped(GtkGestureClick* gesture, void* self) noexcept {
|
||||
(void)gesture;
|
||||
auto* area = static_cast<PlotArea*>(self);
|
||||
// A sequence can be cancelled rather than released -- GTK stops the click
|
||||
// gesture as soon as the drag gesture claims the same sequence, which happens on
|
||||
@@ -434,7 +456,11 @@ void PlotArea::on_click_stopped(GtkGestureClick* gesture, void* self) noexcept {
|
||||
// drag in progress (an exotic cancellation by another widget or a device) closes
|
||||
// the press, which it has to, or the controller would stay in whatever mode the
|
||||
// press began.
|
||||
if (area->press_open_ && !area->drag_.have) area->pointer_release(area->pointer_);
|
||||
if (area->press_open_ && !area->drag_.have) {
|
||||
area->pointer_release(
|
||||
area->pointer_,
|
||||
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)));
|
||||
}
|
||||
}
|
||||
|
||||
void PlotArea::on_drag_begin(GtkGestureDrag* gesture, double start_x, double start_y,
|
||||
@@ -453,7 +479,8 @@ void PlotArea::on_drag_begin(GtkGestureDrag* gesture, double start_x, double sta
|
||||
const std::optional<input::Button> button =
|
||||
button_from(gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture)));
|
||||
if (button) {
|
||||
area->pointer_press(*button, gesture_mods(GTK_EVENT_CONTROLLER(gesture)), start_x, start_y);
|
||||
area->pointer_press(*button, gesture_mods(GTK_EVENT_CONTROLLER(gesture)), start_x, start_y,
|
||||
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,13 +509,14 @@ void PlotArea::on_drag_update(GtkGestureDrag* gesture, double offset_x, double o
|
||||
e.button = area->press_button_;
|
||||
e.mods = area->press_mods_;
|
||||
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);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
||||
}
|
||||
|
||||
void PlotArea::on_drag_end(GtkGestureDrag* gesture, double offset_x, double offset_y,
|
||||
void* self) noexcept {
|
||||
(void)gesture;
|
||||
auto* area = static_cast<PlotArea*>(self);
|
||||
// The release carries the authoritative end of the gesture: the controller reads
|
||||
// its position to finish a box zoom's selection, so this is the press point plus
|
||||
@@ -496,7 +524,46 @@ void PlotArea::on_drag_end(GtkGestureDrag* gesture, double offset_x, double offs
|
||||
// and not the last update's position.
|
||||
const Vec2 position = area->drag_.end(offset_x, offset_y, area->mapping_);
|
||||
area->pointer_ = position;
|
||||
area->pointer_release(position);
|
||||
area->pointer_release(
|
||||
position, gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)));
|
||||
}
|
||||
|
||||
// --- the pinch ------------------------------------------------------------
|
||||
|
||||
void PlotArea::on_zoom_begin(GtkGestureZoom* gesture, GdkEventSequence*, void* self) noexcept {
|
||||
auto* area = static_cast<PlotArea*>(self);
|
||||
// The focal point is the centre of the bounding box around the pointers the
|
||||
// gesture holds, in logical widget pixels -- the same space every other
|
||||
// gesture coordinate here arrives in, and what `to_device()` maps.
|
||||
double cx = 0.0;
|
||||
double cy = 0.0;
|
||||
gtk_gesture_get_bounding_box_center(GTK_GESTURE(gesture), &cx, &cy);
|
||||
area->zoom_.begin(static_cast<f32>(gtk_gesture_zoom_get_scale_delta(gesture)),
|
||||
Vec2{static_cast<f32>(cx), static_cast<f32>(cy)});
|
||||
}
|
||||
|
||||
void PlotArea::on_zoom_scale_changed(GtkGestureZoom* gesture, double scale,
|
||||
void* self) noexcept {
|
||||
auto* area = static_cast<PlotArea*>(self);
|
||||
double cx = 0.0;
|
||||
double cy = 0.0;
|
||||
gtk_gesture_get_bounding_box_center(GTK_GESTURE(gesture), &cx, &cy);
|
||||
// The `scale` argument and `gtk_gesture_zoom_get_scale_delta()` are the same
|
||||
// cumulative number -- the zoom since `::begin` -- so the folded event is this
|
||||
// *frame's* zoom factor about the fingers' centre, plus the centre's movement,
|
||||
// which is the two-finger pan. An update without a begin folds to a no-op (see
|
||||
// `ZoomAnchor::update`), so a gesture whose start the host never saw zooms
|
||||
// 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);
|
||||
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
||||
}
|
||||
|
||||
void PlotArea::on_zoom_end(GtkGestureZoom*, GdkEventSequence*, void* self) noexcept {
|
||||
// No trailing event: the last `scale-changed` carried the gesture's final zoom,
|
||||
// and `end` only closes the folding so a straggler update zooms nothing.
|
||||
static_cast<PlotArea*>(self)->zoom_.end();
|
||||
}
|
||||
|
||||
int PlotArea::on_scroll(GtkEventControllerScroll* scroll, double dx, double dy,
|
||||
@@ -550,6 +617,8 @@ void PlotArea::on_motion(GtkEventControllerMotion* motion, double x, double y, v
|
||||
e.dx = 0.0f;
|
||||
e.dy = 0.0f;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -560,20 +629,14 @@ int PlotArea::on_key_pressed(GtkEventControllerKey* keys, unsigned int keyval,
|
||||
(void)keycode;
|
||||
(void)keys;
|
||||
auto* area = static_cast<PlotArea*>(self);
|
||||
// The keys are named by their *symbol*: GTK reports a layout's keyval, and the
|
||||
// keypad's plus and minus are the same symbol as the main row's. `'='` is mapped
|
||||
// with `'+'` so both spellings of "zoom in" work (on most layouts `+` *is*
|
||||
// shift+`=`), the pair the GLFW and Qt adapters map.
|
||||
int key = 0;
|
||||
switch (gdk_keyval_to_unicode(keyval)) {
|
||||
case static_cast<guint32>(input::kKeyZoomIn):
|
||||
case static_cast<guint32>('='): key = input::kKeyZoomIn; break;
|
||||
case static_cast<guint32>(input::kKeyZoomOut):
|
||||
case static_cast<guint32>('_'): key = input::kKeyZoomOut; break;
|
||||
case static_cast<guint32>(input::kKeyFit):
|
||||
case static_cast<guint32>(input::kKeyFitUpper): key = input::kKeyFit; break;
|
||||
default: return FALSE; // not a plot key: GTK and the window keep it
|
||||
}
|
||||
// The whole translation lives in gtk_input.hpp (see `key_from`): the function
|
||||
// keys -- arrows, Escape -- are matched on the *keyval*, which is layout- and
|
||||
// level-independent for them, and the printable ones on the Unicode they decode
|
||||
// to, which is what makes `+` and shift+`=` (usually the same physical key) both
|
||||
// zoom in, and the keypad's plus and minus come along for free. `0` is "no plot
|
||||
// key": GTK and the window keep it.
|
||||
const int key = key_from(static_cast<u32>(keyval), static_cast<u32>(gdk_keyval_to_unicode(keyval)));
|
||||
if (key == 0) return FALSE;
|
||||
|
||||
input::KeyEvent e;
|
||||
e.key = key;
|
||||
|
||||
@@ -32,10 +32,12 @@ 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 {
|
||||
|
||||
@@ -134,6 +136,10 @@ using FrameFn = std::function<void(Plot&, f64 dt)>;
|
||||
/// 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
|
||||
@@ -271,9 +277,16 @@ class PlotArea {
|
||||
void refresh_viewport() noexcept;
|
||||
|
||||
/// One pointer event, from whichever gesture saw it first. Owns the
|
||||
/// press/release bookkeeping the two gestures share.
|
||||
void pointer_press(input::Button button, u32 mods, double x, double y) noexcept;
|
||||
void pointer_release(Vec2 position) noexcept;
|
||||
/// 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;
|
||||
@@ -289,6 +302,15 @@ class PlotArea {
|
||||
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,
|
||||
@@ -324,6 +346,9 @@ class PlotArea {
|
||||
/// 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_{};
|
||||
|
||||
Reference in new issue
Block a user