feat(gtk4): GtkGLArea-based plot area and demo

This commit is contained in:
Martino Ferrari committed 2026-09-28 02:09:31 +02:00
1 parent 3c0e7763ab
commit 36cd3421f3
10 files changed
+1748 -2

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
// The GTK adapter's input translation, driven directly.
//
// Two pieces of it are this project's logic and not a toolkit's, and neither shows
// up in a screenshot: the wheel's unit and sign conversion, and the modifier mask.
// A headless X server delivers discrete wheel notches through XTEST, so the
// surface-pixel branch of `scroll_notches()` has no event to arrive on -- and the
// mask matters because GDK's state carries the mouse-button bits, which would
// otherwise ride into the controller's events.
//
// `gtk_input.hpp` names no GDK type (it mirrors the two scroll-unit values), so
// this compiles without GTK and without PLOT_BUILD_GTK4.
#include "harness.hpp"
#include <gtk4/gtk_input.hpp>
#include <cmath>
#include <optional>
using plot::f64;
using plot::u32;
using plot::gtk4::ScrollUnit;
using plot::gtk4::button_from;
using plot::gtk4::kSurfacePixelsPerNotch;
using plot::gtk4::mods_from;
using plot::gtk4::scroll_notches;
// GDK's positive `dy` is a scroll *south* (the wheel pushed away from the user) and
// a `ScrollUnit::Wheel` delta is already one unit per detent, so a notch up --
// `dy == -1.0`, what an X11 button-4 press arrives as -- has to leave as `+1.0`.
// Getting the sign wrong would invert every zoom, which looks like a working plot
// with an upside-down wheel.
PLOT_TEST(gtk_scroll_wheel_notches_are_already_notches_and_the_sign_flips) {
PLOT_CHECK_NEAR(scroll_notches(-1.0, ScrollUnit::Wheel), 1.0);
PLOT_CHECK_NEAR(scroll_notches(1.0, ScrollUnit::Wheel), -1.0);
PLOT_CHECK_NEAR(scroll_notches(-3.0, ScrollUnit::Wheel), 3.0);
PLOT_CHECK_NEAR(scroll_notches(0.0, ScrollUnit::Wheel), 0.0);
}
// A smooth device reports surface pixels. Passing them through unscaled is the
// defect this pins: a 60-pixel flick is half a notch with the conversion and 60
// notches -- a span multiplied by ~1e-4 -- without it, for a gesture the user
// cannot undo faster than it happens.
PLOT_TEST(gtk_scroll_surface_deltas_become_notches) {
PLOT_CHECK_NEAR(scroll_notches(-kSurfacePixelsPerNotch, ScrollUnit::Surface), 1.0);
PLOT_CHECK_NEAR(scroll_notches(-2.0 * kSurfacePixelsPerNotch, ScrollUnit::Surface), 2.0);
PLOT_CHECK_NEAR(scroll_notches(-60.0, ScrollUnit::Surface), 0.5);
PLOT_CHECK_NEAR(scroll_notches(3000.0, ScrollUnit::Surface), -25.0);
// A smooth device's tail is not quantised away: the conversion is a pure scale,
// so a 0.12-pixel event stays a 0.001-notch zoom (0.015% of the span), exactly as
// the Qt adapter's `angleDelta() / 120` does for a fractional angle. What the
// caller can refuse is an *exactly* zero amount: no zoom, so the event is not this
// widget's (see the adapter's `on_scroll`).
PLOT_CHECK_NEAR(scroll_notches(-kSurfacePixelsPerNotch / 1000.0, ScrollUnit::Surface), 0.001,
1e-12);
PLOT_CHECK_NEAR(scroll_notches(0.0, ScrollUnit::Surface), 0.0);
}
// An amount the controller must refuse is not silently turned into a number here:
// `Controller::on_wheel` rejects a non-finite delta as a strict no-op, and that
// decision belongs to it alone.
PLOT_TEST(gtk_scroll_passes_a_non_finite_delta_through) {
PLOT_CHECK(std::isnan(scroll_notches(std::nan(""), ScrollUnit::Wheel)));
PLOT_CHECK(std::isnan(scroll_notches(std::nan(""), ScrollUnit::Surface)));
}
// GDK reports the button bits inside the same state word as the modifiers -- a
// release arrives with `GDK_BUTTON1_MASK` (1 << 8) set, a right-drag with `1 << 10`
// -- so the translation is a mask. Without it every drag's events would carry bits
// no other adapter's do.
PLOT_TEST(gtk_mods_ignore_the_mouse_button_bits) {
PLOT_CHECK(mods_from(0x100u) == 0u); // GDK_BUTTON1_MASK
PLOT_CHECK(mods_from(0x400u) == 0u); // GDK_BUTTON3_MASK
PLOT_CHECK(mods_from(0x501u) == static_cast<u32>(plot::input::Mod::Shift));
const u32 shift = static_cast<u32>(plot::input::Mod::Shift);
const u32 ctrl = static_cast<u32>(plot::input::Mod::Ctrl);
const u32 alt = static_cast<u32>(plot::input::Mod::Alt);
const u32 super = static_cast<u32>(plot::input::Mod::Super);
PLOT_CHECK(mods_from(1u) == shift); // GDK_SHIFT_MASK
PLOT_CHECK(mods_from(4u) == ctrl); // GDK_CONTROL_MASK
PLOT_CHECK(mods_from(8u) == alt); // GDK_ALT_MASK
PLOT_CHECK(mods_from(1u << 26) == super); // GDK_SUPER_MASK
PLOT_CHECK(mods_from(1u << 28) == super); // GDK_META_MASK, Command on macOS
PLOT_CHECK(mods_from(1u | 4u | 8u | (1u << 26)) == (shift | ctrl | alt | super));
// GDK_LOCK_MASK (CapsLock) is not a modifier the controller names.
PLOT_CHECK(mods_from(2u) == 0u);
}
// The gestures listen to "any button", so the numbers that reach the mapping are
// 1, 2, 3 and whatever the device reports. Only the three the controller has a
// gesture for are accepted: an unhandled button is dropped rather than defaulting
// to a left-drag pan.
PLOT_TEST(gtk_buttons_map_to_the_controllers_three_or_to_nothing) {
PLOT_CHECK(button_from(1u) == std::optional<plot::input::Button>(plot::input::Button::Left));
PLOT_CHECK(button_from(2u) == std::optional<plot::input::Button>(plot::input::Button::Middle));
PLOT_CHECK(button_from(3u) == std::optional<plot::input::Button>(plot::input::Button::Right));
PLOT_CHECK(!button_from(0u).has_value());
PLOT_CHECK(!button_from(8u).has_value()); // a navigation button
PLOT_CHECK(!button_from(9u).has_value());
}