// 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 #include #include 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(plot::input::Mod::Shift)); const u32 shift = static_cast(plot::input::Mod::Shift); const u32 ctrl = static_cast(plot::input::Mod::Ctrl); const u32 alt = static_cast(plot::input::Mod::Alt); const u32 super = static_cast(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::Left)); PLOT_CHECK(button_from(2u) == std::optional(plot::input::Button::Middle)); PLOT_CHECK(button_from(3u) == std::optional(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()); }