The factor both the viewport and every forwarded input coordinate use came from `gtk_widget_get_scale_factor()`, which GTK documents as "the next higher integer value" under fractional scaling. It is now derived by `DeviceMapping::from_sizes()` from the widget's logical allocation and the device size `GtkGLArea::resize` reports -- the viewport that signal's default handler sets with `glViewport(0, 0, w, h)` -- so the viewport, the renderer and input are one value by construction. The public `int scale_factor()` became `f32 device_scale()`. `kSurfacePixelsPerNotch` is now 100.0 and documented as a *calibration* with a stated assumption, not a unit conversion attributed to axis_value120/Qt. `::unrealize` releases the renderer unconditionally, after making the context current: the error it used to check says nothing about whether GL names exist (creation failure is the path that returns before a renderer is built), and skipping the release left the deletes to the wrapper's destructor, with no context. The drag anchor caches its press point and offsets in logical pixels and scales the difference by the factor in force now, so a scale change mid-drag cannot become a phantom pan. The demo stops its producer in `on_teardown`, before destroying the window whose renderer the stream feeds.
581 lines
26 KiB
C++
581 lines
26 KiB
C++
#include <gtk4/gtk_plot_area.hpp>
|
|
|
|
#include <gtk4/gtk_input.hpp>
|
|
|
|
#include <plot/gl/loader.hpp>
|
|
|
|
// GTK after this library's GL surface, the order every adapter here keeps. GTK's
|
|
// headers pull in no `<GL/gl.h>` -- GdkGLContext is opaque and GtkGLArea names no
|
|
// GL type -- so unlike Qt there is no macro to dodge, but the order is the
|
|
// project's rule and it keeps the two from ever having to be thought about.
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <dlfcn.h>
|
|
|
|
// 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
|
|
// than mis-scaling a zoom or dropping a modifier.
|
|
static_assert(static_cast<int>(plot::gtk4::ScrollUnit::Wheel) == GDK_SCROLL_UNIT_WHEEL,
|
|
"ScrollUnit::Wheel must match GDK_SCROLL_UNIT_WHEEL");
|
|
static_assert(static_cast<int>(plot::gtk4::ScrollUnit::Surface) == GDK_SCROLL_UNIT_SURFACE,
|
|
"ScrollUnit::Surface must match GDK_SCROLL_UNIT_SURFACE");
|
|
static_assert((1u << 0) == GDK_SHIFT_MASK, "mods_from's shift bit must match GDK's");
|
|
static_assert((1u << 2) == GDK_CONTROL_MASK, "mods_from's control bit must match GDK's");
|
|
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");
|
|
|
|
namespace plot::gtk4 {
|
|
namespace {
|
|
|
|
/// The entry points the resolver below could not provide, as passed to
|
|
/// `plot::gl::load()`. `load()` may still cover them through its own dlsym
|
|
/// fallback, but only a context-aware resolver is guaranteed to return pointers
|
|
/// valid for the current context, so a fallback is worth reporting. Process-global
|
|
/// and written only from the context's owning thread, which is the only thread
|
|
/// `::realize` runs on.
|
|
constexpr u32 kMaxUnresolved = 16;
|
|
const char* g_unresolved[kMaxUnresolved] = {};
|
|
u32 g_unresolved_count = 0;
|
|
|
|
/// `dlsym()` hands back an object pointer for a symbol that is in fact a
|
|
/// function. POSIX guarantees the two have the same representation, and the bit
|
|
/// copy states that where a `reinterpret_cast` is merely conditionally supported
|
|
/// -- the same respelling the GLFW and Qt adapters use for their own resolvers.
|
|
template <typename Fn>
|
|
Fn symbol_as(void* symbol) noexcept {
|
|
Fn fn = nullptr;
|
|
static_assert(sizeof(fn) == sizeof(symbol), "function and object pointers must match");
|
|
std::memcpy(&fn, &symbol, sizeof(fn));
|
|
return fn;
|
|
}
|
|
|
|
/// The `*GetProcAddress` of the API that owns GTK's current context, as
|
|
/// `plot::gl::ProcLoader`.
|
|
///
|
|
/// GDK4 offers no equivalent: `gdk_gl_context_get_proc_address()` was removed from
|
|
/// GTK4's public API, and GTK resolves its own GL entry points through libepoxy.
|
|
/// The context a `GtkGLArea` gets comes from EGL on both the X11 and the Wayland
|
|
/// backend, so `eglGetProcAddress()` is the context-aware resolver for it -- the
|
|
/// call every EGL client makes. GLX is tried second, for a GDK built on it. Both
|
|
/// are reached with `dlsym(RTLD_DEFAULT, ...)`, which finds them because libgtk
|
|
/// links libEGL and libGLX; neither is called when it is not there, in which case
|
|
/// the name is reported and `load()`'s own fallback gets its chance.
|
|
void* resolve_gl_proc(const char* name) noexcept {
|
|
using GetProcAddress = void* (*)(const char*);
|
|
static const GetProcAddress egl =
|
|
symbol_as<GetProcAddress>(dlsym(RTLD_DEFAULT, "eglGetProcAddress"));
|
|
static const GetProcAddress glx =
|
|
symbol_as<GetProcAddress>(dlsym(RTLD_DEFAULT, "glXGetProcAddressARB"));
|
|
|
|
void* proc = nullptr;
|
|
if (egl != nullptr) proc = egl(name);
|
|
if (proc == nullptr && glx != nullptr) proc = glx(name);
|
|
if (proc == nullptr && g_unresolved_count < kMaxUnresolved) {
|
|
g_unresolved[g_unresolved_count++] = name;
|
|
}
|
|
return proc;
|
|
}
|
|
|
|
/// Monotonic seconds, for the frame timings only.
|
|
f64 now_s() noexcept {
|
|
return std::chrono::duration<f64>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
/// `gtk_event_controller_get_current_event_state()` as the controller's mask.
|
|
u32 gesture_mods(GtkEventController* controller) noexcept {
|
|
return mods_from(static_cast<u32>(gtk_event_controller_get_current_event_state(controller)));
|
|
}
|
|
|
|
/// The position of the event a controller is handling right now, in device pixels
|
|
/// at `mapping`. False when there is no event or it has no position -- a key event
|
|
/// with a scroll controller's anchor is the case that matters.
|
|
bool current_event_position(GtkEventController* controller, const DeviceMapping& mapping,
|
|
Vec2* out) noexcept {
|
|
GdkEvent* event = gtk_event_controller_get_current_event(controller);
|
|
if (event == nullptr) return false;
|
|
double x = 0.0;
|
|
double y = 0.0;
|
|
if (!gdk_event_get_position(event, &x, &y)) return false;
|
|
*out = mapping.to_device(x, y);
|
|
return true;
|
|
}
|
|
} // namespace
|
|
|
|
PlotArea* PlotArea::create(Plot& plot) noexcept {
|
|
GtkWidget* widget = gtk_gl_area_new();
|
|
|
|
// The context is asked for at construction: GTK reads these when it creates the
|
|
// context at realize, and `gtk_gl_area_set_required_version` is documented to
|
|
// have no effect afterwards. 3.3 core is the baseline the library's loader
|
|
// resolves; the plot is 2D, so a depth buffer and a stencil buffer would only
|
|
// cost memory (GTK's own default is none, stated explicitly so a GTK that
|
|
// changed it could not silently add one).
|
|
gtk_gl_area_set_required_version(GTK_GL_AREA(widget), 3, 3);
|
|
gtk_gl_area_set_has_depth_buffer(GTK_GL_AREA(widget), FALSE);
|
|
gtk_gl_area_set_has_stencil_buffer(GTK_GL_AREA(widget), FALSE);
|
|
|
|
// A widget that cannot take focus never receives a key event, and the
|
|
// controller's `+`, `-` and `f` are key events.
|
|
gtk_widget_set_focusable(widget, TRUE);
|
|
gtk_widget_set_hexpand(widget, TRUE);
|
|
gtk_widget_set_vexpand(widget, TRUE);
|
|
|
|
auto* self = new PlotArea(plot, widget);
|
|
// The wrapper dies with the widget. `::unrealize` has already released the GL
|
|
// objects by then, while the context could still be made current.
|
|
g_object_set_data_full(G_OBJECT(widget), "plot-gtk4-area", self,
|
|
[](gpointer data) { delete static_cast<PlotArea*>(data); });
|
|
|
|
g_signal_connect(widget, "realize", G_CALLBACK(&PlotArea::on_realize), self);
|
|
g_signal_connect(widget, "unrealize", G_CALLBACK(&PlotArea::on_unrealize), self);
|
|
g_signal_connect(widget, "render", G_CALLBACK(&PlotArea::on_render), self);
|
|
g_signal_connect(widget, "resize", G_CALLBACK(&PlotArea::on_resize), self);
|
|
|
|
// One click gesture and one drag gesture, both listening to every button: the
|
|
// plot pans on left *and* right, box-zooms on shift+left, moves a cursor on
|
|
// ctrl+left and zooms an axis on middle. GTK cannot be asked for that set
|
|
// directly, so the two gestures split it (see the class comment) and the button
|
|
// and modifiers decide which gesture the controller runs.
|
|
GtkGesture* click = gtk_gesture_click_new();
|
|
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), 0); // 0: any button
|
|
g_signal_connect(click, "pressed", G_CALLBACK(&PlotArea::on_click_pressed), self);
|
|
g_signal_connect(click, "released", G_CALLBACK(&PlotArea::on_click_released), self);
|
|
g_signal_connect(click, "stopped", G_CALLBACK(&PlotArea::on_click_stopped), self);
|
|
gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));
|
|
|
|
GtkGesture* drag = gtk_gesture_drag_new();
|
|
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), 0);
|
|
g_signal_connect(drag, "drag-begin", G_CALLBACK(&PlotArea::on_drag_begin), self);
|
|
g_signal_connect(drag, "drag-update", G_CALLBACK(&PlotArea::on_drag_update), self);
|
|
g_signal_connect(drag, "drag-end", G_CALLBACK(&PlotArea::on_drag_end), self);
|
|
gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(drag));
|
|
|
|
// 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
|
|
// smooth device in surface pixels; both are converted before they reach the
|
|
// controller (see scroll_notches).
|
|
GtkEventController* scroll =
|
|
gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_VERTICAL);
|
|
g_signal_connect(scroll, "scroll", G_CALLBACK(&PlotArea::on_scroll), self);
|
|
gtk_widget_add_controller(widget, scroll);
|
|
|
|
// Hover: the controller's cursor hit test reads the last pointer position even
|
|
// while no button is down.
|
|
GtkEventController* motion = gtk_event_controller_motion_new();
|
|
g_signal_connect(motion, "motion", G_CALLBACK(&PlotArea::on_motion), self);
|
|
gtk_widget_add_controller(widget, motion);
|
|
|
|
GtkEventController* key = gtk_event_controller_key_new();
|
|
g_signal_connect(key, "key-pressed", G_CALLBACK(&PlotArea::on_key_pressed), self);
|
|
gtk_widget_add_controller(widget, key);
|
|
|
|
return self;
|
|
}
|
|
|
|
PlotArea::PlotArea(Plot& plot, GtkWidget* widget) noexcept
|
|
: plot_(&plot), controller_(plot), widget_(widget) {}
|
|
|
|
Rect PlotArea::device_viewport() const noexcept { return mapping_.viewport; }
|
|
|
|
f32 PlotArea::device_scale() const noexcept { return mapping_.factor; }
|
|
|
|
Vec2 PlotArea::to_device(double x, double y) const noexcept {
|
|
// The mapping `device_viewport()` is the viewport of, applied to a position and
|
|
// to the offsets derived from two of them: one mapping on both sides is what
|
|
// makes a gesture land on the data under the cursor.
|
|
return mapping_.to_device(x, y);
|
|
}
|
|
|
|
void PlotArea::refresh_viewport() noexcept {
|
|
// The controller owns no viewport: this hands it to the plot (which re-lays the
|
|
// 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.
|
|
controller_.set_viewport(device_viewport());
|
|
}
|
|
|
|
// --- the GL signals -------------------------------------------------------
|
|
|
|
void PlotArea::realize() noexcept {
|
|
// A second realize means GTK created a new context for a widget that moved to
|
|
// another surface: the names the renderer holds belonged to the old one, and
|
|
// nothing here may outlive it. Released before anything of ours exists on the
|
|
// new context, so the deletes it issues can only name objects that are not
|
|
// there.
|
|
renderer_.reset();
|
|
|
|
gtk_gl_area_make_current(GTK_GL_AREA(widget_));
|
|
GError* error = gtk_gl_area_get_error(GTK_GL_AREA(widget_));
|
|
if (error != nullptr) {
|
|
// The context GTK was asked for does not exist: no GL entry point may be
|
|
// called, and the area draws nothing. Reported, not asserted -- a GTK signal
|
|
// handler has nowhere to return a failure to.
|
|
init_status_ = Status{ErrorCode::GLFailure,
|
|
"GtkGLArea could not create a GL 3.3 context; see the GTK warning above"};
|
|
std::fprintf(stderr, "[plot::gtk4] GLFailure: %s (%s)\n", init_status_.message,
|
|
error->message != nullptr ? error->message : "no detail");
|
|
return;
|
|
}
|
|
|
|
g_unresolved_count = 0;
|
|
const bool loaded = plot::gl::load(&resolve_gl_proc);
|
|
// Printed before the failure branch: the names collected here are what explains
|
|
// a failure, so a load that failed has to report them too.
|
|
if (g_unresolved_count > 0) {
|
|
std::fprintf(stderr,
|
|
"[plot::gtk4] eglGetProcAddress() did not resolve %u required entry point(s);"
|
|
" plot::gl::load() %s for:",
|
|
g_unresolved_count, loaded ? "used its dlsym fallback" : "failed");
|
|
for (u32 i = 0; i < g_unresolved_count; ++i) std::fprintf(stderr, " %s", g_unresolved[i]);
|
|
std::fprintf(stderr, "\n");
|
|
}
|
|
if (!loaded) {
|
|
init_status_ = Status{ErrorCode::Unsupported,
|
|
"plot::gl::load() failed: the current context is missing a required"
|
|
" OpenGL 3.3 entry point"};
|
|
std::fprintf(stderr, "[plot::gtk4] %s\n", init_status_.message);
|
|
return;
|
|
}
|
|
|
|
renderer_.emplace();
|
|
Status status;
|
|
if (!renderer_->init(&status)) {
|
|
init_status_ = status.code != ErrorCode::Ok ? status
|
|
: Status{ErrorCode::GLFailure,
|
|
"Renderer::init() failed"};
|
|
// The renderer stays engaged: its destructor releases whatever the partial
|
|
// init built, and it runs in unrealize() with the context current.
|
|
std::fprintf(stderr, "[plot::gtk4] Renderer::init(): %s: %s\n", to_string(init_status_.code),
|
|
init_status_.message);
|
|
return;
|
|
}
|
|
init_status_ = Status{};
|
|
refresh_viewport();
|
|
}
|
|
|
|
void PlotArea::unrealize() noexcept {
|
|
// Made current first: the renderer's RAII handles issue `glDelete*` from their
|
|
// destructors, and GTK clears the context in its own unrealize vfunc, which runs
|
|
// after this signal's handlers -- so this is the last moment at which those
|
|
// deletes can name a live context.
|
|
gtk_gl_area_make_current(GTK_GL_AREA(widget_));
|
|
// Released unconditionally. The area's error says nothing about whether the
|
|
// renderer holds names to delete: it is set when context *creation* failed, and
|
|
// that is the path `realize()` returns from before it builds a renderer, so a
|
|
// renderer present here has GL objects behind it. Leaving it for the wrapper's
|
|
// destructor -- which runs at widget finalisation, after GTK has freed the
|
|
// context -- would issue those deletes with nothing current, which is exactly
|
|
// what the old error check was trying to avoid. `make_current()` and `reset()`
|
|
// are no-ops on a widget that never got a context and on an empty renderer.
|
|
renderer_.reset();
|
|
press_open_ = false;
|
|
drag_.have = false;
|
|
}
|
|
|
|
void PlotArea::render() noexcept {
|
|
// GTK guarantees the context is current and the area's FBO is bound when
|
|
// `::render` runs; making it current is a no-op in that case, and the error
|
|
// check is the assertion that it holds rather than an assumption. Both are
|
|
// `gtk_gl_area_make_current()`'s documented behaviour, and the check costs one
|
|
// call per frame -- the alternative is drawing through entry points that
|
|
// belonged to a context GTK has already thrown away.
|
|
gtk_gl_area_make_current(GTK_GL_AREA(widget_));
|
|
if (gtk_gl_area_get_error(GTK_GL_AREA(widget_)) != nullptr) return;
|
|
if (!renderer_ || !renderer_->ready()) return;
|
|
|
|
const Rect viewport = device_viewport();
|
|
if (viewport.w < 1.0f || viewport.h < 1.0f) return; // nothing to draw into
|
|
|
|
// The controller owns no viewport: this hands it to the plot (which re-lays the
|
|
// chrome out with it) and refreshes the layout, so every frame is laid out for
|
|
// the geometry of *this* frame. A resize changes the layout; it never scales a
|
|
// stale one.
|
|
controller_.set_viewport(viewport);
|
|
|
|
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);
|
|
|
|
renderer_->begin_frame(static_cast<u32>(viewport.w), static_cast<u32>(viewport.h), viewport);
|
|
renderer_->draw(*plot_);
|
|
renderer_->end_frame();
|
|
|
|
const f32 frame_ms = static_cast<f32>((now_s() - started) * 1.0e3);
|
|
last_frame_ms_ = frame_ms;
|
|
// Seeded by the first frame rather than started at zero, so the mean is the mean
|
|
// of the frames that ran from the first one.
|
|
mean_frame_ms_ = frames_ == 0 ? frame_ms : mean_frame_ms_ + 0.1f * (frame_ms - mean_frame_ms_);
|
|
last_viewport_ = viewport;
|
|
++frames_;
|
|
}
|
|
|
|
void PlotArea::on_realize(GtkGLArea*, void* self) noexcept {
|
|
static_cast<PlotArea*>(self)->realize();
|
|
}
|
|
|
|
void PlotArea::on_unrealize(GtkGLArea*, void* self) noexcept {
|
|
static_cast<PlotArea*>(self)->unrealize();
|
|
}
|
|
|
|
int PlotArea::on_render(GtkGLArea*, GdkGLContext*, void* self) noexcept {
|
|
static_cast<PlotArea*>(self)->render();
|
|
// FALSE on purpose, not "I did not draw": `GtkGLArea::render`'s return value is
|
|
// accumulated with `g_signal_accumulator_true_handled`, so the first handler to
|
|
// return TRUE *stops the emission* -- and a host that connects its own handler
|
|
// (a screenshot read, a frame counter) would never be called. GTK itself does
|
|
// not read the value (it composites the FBO the area rendered into either way),
|
|
// and a GtkGLArea subclass overriding the vfunc is the only place TRUE means
|
|
// anything.
|
|
return FALSE;
|
|
}
|
|
|
|
void PlotArea::on_resize(GtkGLArea*, int width, int height, void* self) noexcept {
|
|
auto* area = static_cast<PlotArea*>(self);
|
|
// GTK's two sizes, and the one place the mapping is derived from them. `width`
|
|
// and `height` are the device pixels of the viewport this signal's default
|
|
// handler is about to set -- `glViewport(0, 0, width, height)` in GTK's own
|
|
// `gtk_gl_area_resize()` -- and the widget's width and height, which GTK has
|
|
// already updated when it emits this, are the logical pixels that same rectangle
|
|
// is reported in. Their ratio is the factor for the viewport *and* for every
|
|
// forwarded coordinate; refreshing here means a gesture arriving between this
|
|
// resize and the next frame maps against the new geometry rather than the old.
|
|
area->mapping_ = DeviceMapping::from_sizes(
|
|
Vec2{static_cast<f32>(gtk_widget_get_width(area->widget_)),
|
|
static_cast<f32>(gtk_widget_get_height(area->widget_))},
|
|
Vec2{static_cast<f32>(width), static_cast<f32>(height)});
|
|
area->refresh_viewport();
|
|
}
|
|
|
|
// --- pointer input --------------------------------------------------------
|
|
|
|
void PlotArea::pointer_press(input::Button button, u32 mods, double x, double y) noexcept {
|
|
if (press_open_) return; // one press per button-down sequence: see drag_begin
|
|
press_open_ = true;
|
|
press_button_ = button;
|
|
press_mods_ = mods;
|
|
|
|
const Vec2 p = to_device(x, y);
|
|
pointer_ = p;
|
|
have_pointer_ = true;
|
|
|
|
input::PointerEvent e;
|
|
e.x = p.x;
|
|
e.y = p.y;
|
|
e.dx = 0.0f;
|
|
e.dy = 0.0f;
|
|
e.button = button;
|
|
e.mods = mods;
|
|
e.down = true;
|
|
e.pressed = true;
|
|
controller_.on_pointer(e);
|
|
gtk_gl_area_queue_render(GTK_GL_AREA(widget_));
|
|
}
|
|
|
|
void PlotArea::pointer_release(Vec2 position) noexcept {
|
|
if (!press_open_) return;
|
|
press_open_ = false;
|
|
|
|
input::PointerEvent e;
|
|
e.x = position.x;
|
|
e.y = position.y;
|
|
e.dx = 0.0f;
|
|
e.dy = 0.0f;
|
|
e.button = press_button_;
|
|
e.mods = press_mods_;
|
|
e.down = false;
|
|
e.released = true;
|
|
controller_.on_pointer(e);
|
|
gtk_gl_area_queue_render(GTK_GL_AREA(widget_));
|
|
}
|
|
|
|
void PlotArea::on_click_pressed(GtkGestureClick* gesture, int, double x, double y,
|
|
void* self) noexcept {
|
|
auto* area = static_cast<PlotArea*>(self);
|
|
const std::optional<input::Button> button =
|
|
button_from(gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture)));
|
|
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);
|
|
}
|
|
|
|
void PlotArea::on_click_released(GtkGestureClick* gesture, int, double x, double y,
|
|
void* self) noexcept {
|
|
(void)gesture;
|
|
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));
|
|
}
|
|
|
|
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
|
|
// the first motion of every drag. That stop must not end the gesture: the drag
|
|
// is still running, and its updates keep driving the plot. Only a stop with no
|
|
// 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_);
|
|
}
|
|
|
|
void PlotArea::on_drag_begin(GtkGestureDrag* gesture, double start_x, double start_y,
|
|
void* self) noexcept {
|
|
auto* area = static_cast<PlotArea*>(self);
|
|
area->drag_.begin(static_cast<f32>(start_x), static_cast<f32>(start_y));
|
|
area->pointer_ = area->drag_.position(area->mapping_);
|
|
area->have_pointer_ = true;
|
|
|
|
// GTK emits `drag-begin` before the click gesture's `pressed` (both while the
|
|
// press event is handled), so this is normally the second press of the sequence
|
|
// and `pointer_press` drops it. It is not always: if the click gesture never
|
|
// pressed -- a host that grabs the sequence, a GTK that reorders its gestures --
|
|
// the press is opened here instead, so a drag cannot be lost for want of an
|
|
// edge. The button and modifiers come from the drag gesture for that case.
|
|
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);
|
|
}
|
|
}
|
|
|
|
void PlotArea::on_drag_update(GtkGestureDrag* gesture, double offset_x, double offset_y,
|
|
void* self) noexcept {
|
|
(void)gesture;
|
|
auto* area = static_cast<PlotArea*>(self);
|
|
// A drag with no press open is a drag of a button the adapter does not forward
|
|
// (see button_from): its motion must not reach the controller as a held-drag
|
|
// move, which is the one event the controller acts on from its button state.
|
|
if (!area->press_open_) return;
|
|
// The anchor stays logical (see `DragAnchor`): the gesture reports the offset
|
|
// from the press point in logical pixels, and this is a logical difference
|
|
// scaled by the factor in force *now*, so a factor change between two samples
|
|
// cannot turn into a phantom movement of its own.
|
|
const DragAnchor::Sample sample =
|
|
area->drag_.update(offset_x, offset_y, area->mapping_);
|
|
area->pointer_ = sample.position;
|
|
area->have_pointer_ = true;
|
|
|
|
input::PointerEvent e;
|
|
e.x = sample.position.x;
|
|
e.y = sample.position.y;
|
|
e.dx = sample.delta.x;
|
|
e.dy = sample.delta.y;
|
|
e.button = area->press_button_;
|
|
e.mods = area->press_mods_;
|
|
e.down = true;
|
|
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
|
|
// the offset `drag-end` itself reports -- which may be past the last update's --
|
|
// 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);
|
|
}
|
|
|
|
int PlotArea::on_scroll(GtkEventControllerScroll* scroll, double dx, double dy,
|
|
void* self) noexcept {
|
|
auto* area = static_cast<PlotArea*>(self);
|
|
const ScrollUnit unit = gtk_event_controller_scroll_get_unit(scroll) == GDK_SCROLL_UNIT_SURFACE
|
|
? ScrollUnit::Surface
|
|
: ScrollUnit::Wheel;
|
|
const f64 notches = scroll_notches(dy, unit);
|
|
if (notches == 0.0) {
|
|
// Not a zoom, so not this widget's event: returning FALSE lets an enclosing
|
|
// scrolled window scroll instead. A zero-notch zoom is not harmless -- it
|
|
// changes no range but still runs the range setters, which clear auto scale --
|
|
// so it must not reach the controller either.
|
|
return FALSE;
|
|
}
|
|
|
|
Vec2 at{};
|
|
if (!current_event_position(GTK_EVENT_CONTROLLER(scroll), area->mapping_, &at)) {
|
|
if (!area->have_pointer_) return FALSE; // no anchor to zoom about
|
|
at = area->pointer_; // the last position a pointer event reported
|
|
}
|
|
area->pointer_ = at;
|
|
area->have_pointer_ = true;
|
|
|
|
input::WheelEvent e;
|
|
e.x = at.x;
|
|
e.y = at.y;
|
|
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);
|
|
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
|
return TRUE; // consumed: the plot zoomed
|
|
}
|
|
|
|
void PlotArea::on_motion(GtkEventControllerMotion* motion, double x, double y, void* self) noexcept {
|
|
auto* area = static_cast<PlotArea*>(self);
|
|
const Vec2 p = area->to_device(x, y);
|
|
area->pointer_ = p;
|
|
area->have_pointer_ = true;
|
|
|
|
// Hover only, with `down` false whatever the buttons are doing: a motion event
|
|
// that claimed a button was held would be fed to `update_drag` as well, and a
|
|
// drag would then be applied twice -- once from the drag gesture's offset and
|
|
// once from this position's difference.
|
|
(void)motion;
|
|
input::PointerEvent e;
|
|
e.x = p.x;
|
|
e.y = p.y;
|
|
e.dx = 0.0f;
|
|
e.dy = 0.0f;
|
|
e.down = false;
|
|
area->controller_.on_pointer(e);
|
|
}
|
|
|
|
// --- keyboard -------------------------------------------------------------
|
|
|
|
int PlotArea::on_key_pressed(GtkEventControllerKey* keys, unsigned int keyval,
|
|
unsigned int keycode, unsigned int state, void* self) noexcept {
|
|
(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
|
|
}
|
|
|
|
input::KeyEvent e;
|
|
e.key = key;
|
|
e.mods = mods_from(state);
|
|
// 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);
|
|
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
|
return TRUE; // consumed: the plot acted on it
|
|
}
|
|
|
|
} // namespace plot::gtk4
|