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.
320 lines
17 KiB
C++
320 lines
17 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
// The GTK adapter's input translation, driven directly.
|
|
//
|
|
// Four pieces of it are this project's logic and not a toolkit's, and none of them
|
|
// shows up in a screenshot: the device-pixel mapping GTK's two sizes imply, the
|
|
// pixel space a drag anchor is kept in, the wheel's unit and sign, and the
|
|
// modifier mask. A headless X server delivers discrete wheel notches through XTEST
|
|
// (and only at an integer scale), so the surface branch of `scroll_notches()` and
|
|
// this session's fractional scales have 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::DeviceMapping;
|
|
using plot::gtk4::DragAnchor;
|
|
using plot::gtk4::ScrollUnit;
|
|
using plot::gtk4::ZoomAnchor;
|
|
using plot::gtk4::button_from;
|
|
using plot::gtk4::key_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, and GDK defines no notch size for them
|
|
// ("surface pixels to scroll directly on screen" is its whole description), so the
|
|
// divisor is this adapter's *calibration* and not a conversion -- see
|
|
// kSurfacePixelsPerNotch, and note that it is uncalibrated against real hardware.
|
|
// What a test can hold is the reasoning rather than the number: the constant is
|
|
// what one notch is, the scaling is linear and unquantised (passing a pixel delta
|
|
// through unscaled would make a flick of a few hundred pixels tens of notches, a
|
|
// zoom no gesture can undo as fast as it happens), and the value is chosen so that
|
|
// a deliberate two-finger gesture -- of the order of 100 surface pixels -- is one
|
|
// detent rather than several.
|
|
PLOT_TEST(gtk_scroll_surface_deltas_are_scaled_by_the_calibration) {
|
|
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(3000.0, ScrollUnit::Surface), -30.0);
|
|
// The calibration's own assumption, as an invariant that survives a re-tune: a
|
|
// deliberate gesture of 100 px is at most one detent, never several.
|
|
PLOT_CHECK(kSurfacePixelsPerNotch >= 100.0);
|
|
PLOT_CHECK(scroll_notches(-100.0, ScrollUnit::Surface) <= 1.0 + 1e-9);
|
|
// The sign is the wheel's, and a smooth device's tail is not quantised away: the
|
|
// scale is a pure division, so a 0.1-pixel event stays a 0.001-notch zoom
|
|
// (0.015% of the span). 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, ScrollUnit::Surface), -1.0);
|
|
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);
|
|
// The unit decides which path runs: a delta GDK reported in wheel units is one
|
|
// unit per detent however large it is, and is never divided by the calibration.
|
|
PLOT_CHECK_NEAR(scroll_notches(-kSurfacePixelsPerNotch, ScrollUnit::Wheel),
|
|
kSurfacePixelsPerNotch);
|
|
}
|
|
|
|
// The device-pixel mapping is derived from GTK's two sizes -- the widget's logical
|
|
// allocation and the device size `GtkGLArea::resize` reports for the viewport its
|
|
// default handler sets -- and from no scale accessor. The defect this pins:
|
|
// `gtk_widget_get_scale_factor()` is documented to return "the next higher integer
|
|
// value" under fractional scaling, so an 800-logical-pixel widget whose viewport
|
|
// is 1000 device pixels (a 1.25 display) had its input multiplied by 2: a pointer
|
|
// at logical x=100 was forwarded as 200, 75 device pixels past where the FBO puts
|
|
// it, and `device_viewport()` claimed 1600 of them. The ratio of the two sizes is
|
|
// the space the renderer draws into, so it is the one input must use.
|
|
PLOT_TEST(gtk_device_mapping_is_the_ratio_of_the_two_sizes) {
|
|
const DeviceMapping quarter = DeviceMapping::from_sizes({800.0f, 600.0f}, {1000.0f, 750.0f});
|
|
PLOT_CHECK_NEAR(quarter.factor, 1.25, 1e-6);
|
|
PLOT_CHECK_NEAR(quarter.viewport.x, 0.0);
|
|
PLOT_CHECK_NEAR(quarter.viewport.y, 0.0);
|
|
PLOT_CHECK_NEAR(quarter.viewport.w, 1000.0);
|
|
PLOT_CHECK_NEAR(quarter.viewport.h, 750.0);
|
|
PLOT_CHECK_NEAR(quarter.to_device(100.0, 200.0).x, 125.0, 1e-4);
|
|
PLOT_CHECK_NEAR(quarter.to_device(100.0, 200.0).y, 250.0, 1e-4);
|
|
|
|
const DeviceMapping three_quarters =
|
|
DeviceMapping::from_sizes({800.0f, 600.0f}, {1400.0f, 1050.0f});
|
|
PLOT_CHECK_NEAR(three_quarters.factor, 1.75, 1e-6);
|
|
PLOT_CHECK_NEAR(three_quarters.viewport.w, 1400.0);
|
|
PLOT_CHECK_NEAR(three_quarters.viewport.h, 1050.0);
|
|
PLOT_CHECK_NEAR(three_quarters.to_device(400.0, 0.0).x, 700.0, 1e-4);
|
|
}
|
|
|
|
// At the integer scales the headless evidence runs at, the derived factor is the
|
|
// integer one -- 1600/800 is exactly 2, 800/800 exactly 1 -- so the mapping the
|
|
// previous code built from `gtk_widget_get_scale_factor()` is unchanged there, and
|
|
// only a fractional scale can tell the two derivations apart. A widget with no
|
|
// allocation has no ratio to take: it maps 1:1 and reports the (zero) size GTK
|
|
// gave, which is the state before anything is drawn or any event can arrive.
|
|
PLOT_TEST(gtk_device_mapping_is_unchanged_at_the_integer_scales) {
|
|
const DeviceMapping at_1 = DeviceMapping::from_sizes({800.0f, 600.0f}, {800.0f, 600.0f});
|
|
PLOT_CHECK(at_1.factor == 1.0f);
|
|
PLOT_CHECK_NEAR(at_1.to_device(120.0, 40.0).x, 120.0, 1e-6);
|
|
PLOT_CHECK_NEAR(at_1.to_device(120.0, 40.0).y, 40.0, 1e-6);
|
|
|
|
const DeviceMapping at_2 = DeviceMapping::from_sizes({800.0f, 600.0f}, {1600.0f, 1200.0f});
|
|
PLOT_CHECK(at_2.factor == 2.0f);
|
|
PLOT_CHECK_NEAR(at_2.viewport.w, 1600.0);
|
|
PLOT_CHECK_NEAR(at_2.to_device(120.0, 40.0).x, 240.0, 1e-6);
|
|
|
|
const DeviceMapping unallocated = DeviceMapping::from_sizes({0.0f, 0.0f}, {0.0f, 0.0f});
|
|
PLOT_CHECK(unallocated.factor == 1.0f);
|
|
PLOT_CHECK_NEAR(unallocated.viewport.w, 0.0);
|
|
PLOT_CHECK_NEAR(unallocated.viewport.h, 0.0);
|
|
}
|
|
|
|
// The drag anchor stays *logical* while the factor is re-derived per update: GTK
|
|
// re-emits `::resize` when the widget's scale-factor property changes, so a drag
|
|
// can span a scale change. Cached in device pixels, the same gesture reported
|
|
// `20 * 1.25 - 10 * 1.0 = 15` device pixels of movement where the pointer moved
|
|
// 12.5, and a position of `100 * 1.0 + 20 * 1.25 = 125` where the FBO puts it at
|
|
// 150. `Controller::pan` applies such a delta as a fraction of the range span and
|
|
// clears auto scale, so the phantom 2.5 pixels are a real pan of the plot and a
|
|
// live axis silently disarmed -- the same defect the Qt adapter's `PointerAnchor`
|
|
// pins for `QDpi` changes.
|
|
PLOT_TEST(gtk_drag_anchor_stays_logical_across_a_scale_change) {
|
|
const DeviceMapping at_1 = DeviceMapping::from_sizes({1000.0f, 800.0f}, {1000.0f, 800.0f});
|
|
const DeviceMapping at_125 = DeviceMapping::from_sizes({1000.0f, 800.0f}, {1250.0f, 1000.0f});
|
|
PLOT_CHECK(at_1.factor == 1.0f);
|
|
PLOT_CHECK_NEAR(at_125.factor, 1.25, 1e-6);
|
|
|
|
DragAnchor anchor;
|
|
anchor.begin(100.0f, 100.0f);
|
|
PLOT_CHECK(anchor.have);
|
|
// The press point itself is reported in device pixels at the factor in force.
|
|
PLOT_CHECK_NEAR(anchor.position(at_1).x, 100.0, 1e-6);
|
|
PLOT_CHECK_NEAR(anchor.position(at_1).y, 100.0, 1e-6);
|
|
|
|
// First sample, before the move: the pointer moved 10 logical pixels.
|
|
const DragAnchor::Sample first = anchor.update(10.0, 0.0, at_1);
|
|
PLOT_CHECK_NEAR(first.delta.x, 10.0, 1e-6);
|
|
PLOT_CHECK_NEAR(first.position.x, 110.0, 1e-6);
|
|
|
|
// The window is now on the 1.25 display: the same gesture has moved 10 further
|
|
// logical pixels, so the movement is 10 * 1.25 = 12.5 device pixels and the
|
|
// position is (100 + 20) * 1.25 = 150, whatever the press was scaled by.
|
|
const DragAnchor::Sample second = anchor.update(20.0, 0.0, at_125);
|
|
PLOT_CHECK_NEAR(second.delta.x, 12.5, 1e-6);
|
|
PLOT_CHECK_NEAR(second.delta.y, 0.0, 1e-6);
|
|
PLOT_CHECK_NEAR(second.position.x, 150.0, 1e-6);
|
|
|
|
// The release is the press point plus the offset `drag-end` reports, at the
|
|
// factor in force then -- not the last sample's position, and not an anchor
|
|
// cached in device pixels. It may be past the last update's offset, which is why
|
|
// `end()` takes its own.
|
|
PLOT_CHECK_NEAR(anchor.end(25.0, 0.0, at_125).x, 156.25, 1e-6);
|
|
PLOT_CHECK(!anchor.have);
|
|
}
|
|
|
|
// 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());
|
|
}
|
|
|
|
// --- the pinch --------------------------------------------------------------
|
|
|
|
// `GtkGestureZoom` reports a cumulative scale since the gesture began, while the
|
|
// controller's `GestureEvent` wants this frame's zoom factor; `ZoomAnchor` is the
|
|
// fold, and it is the same fold the Qt adapter's `PinchTracker` does -- a
|
|
// cumulative 1.0 -> 1.25 -> 1.5 spread is a 1.25 frame then a 1.2 frame, so the
|
|
// zoom the plot applies is the zoom the fingers made.
|
|
PLOT_TEST(gtk_zoom_anchor_folds_the_cumulative_scale_into_increments) {
|
|
// A 2x widget: the focal point and its movement are reported in *device* pixels,
|
|
// like every coordinate the controller sees.
|
|
const DeviceMapping at_2 = DeviceMapping::from_sizes({400.0f, 300.0f}, {800.0f, 600.0f});
|
|
plot::gtk4::ZoomAnchor zoom;
|
|
|
|
zoom.begin(1.0f, {100.0f, 75.0f});
|
|
PLOT_CHECK(zoom.active);
|
|
// The begin itself folds to "nothing happened yet", whenever the first
|
|
// scale-changed arrives with the scale it began at.
|
|
const plot::input::GestureEvent started = zoom.update(1.0f, {100.0f, 75.0f}, at_2);
|
|
PLOT_CHECK_NEAR(started.scale, 1.0, 1e-6);
|
|
PLOT_CHECK_NEAR(started.dx, 0.0, 1e-6);
|
|
PLOT_CHECK_NEAR(started.dy, 0.0, 1e-6);
|
|
|
|
// Fingers spread to 1.25 about a steady centre: one 1.25 zoom frame, anchored
|
|
// at the centre in device pixels (100 * 2, 75 * 2).
|
|
const plot::input::GestureEvent spread = zoom.update(1.25f, {100.0f, 75.0f}, at_2);
|
|
PLOT_CHECK_NEAR(spread.scale, 1.25, 1e-6);
|
|
PLOT_CHECK_NEAR(spread.x, 200.0, 1e-6);
|
|
PLOT_CHECK_NEAR(spread.y, 150.0, 1e-6);
|
|
PLOT_CHECK_NEAR(spread.dx, 0.0, 1e-6);
|
|
PLOT_CHECK_NEAR(spread.dy, 0.0, 1e-6);
|
|
|
|
// Spread on to 1.5 while the fingers drift 10 logical pixels right: the
|
|
// increment is 1.2 and the pan is 20 device pixels.
|
|
const plot::input::GestureEvent drifted = zoom.update(1.5f, {110.0f, 75.0f}, at_2);
|
|
PLOT_CHECK_NEAR(drifted.scale, 1.2, 1e-6);
|
|
PLOT_CHECK_NEAR(drifted.dx, 20.0, 1e-6);
|
|
PLOT_CHECK_NEAR(drifted.dy, 0.0, 1e-6);
|
|
}
|
|
|
|
// The focal point is cached in logical widget pixels, so a factor change between
|
|
// two updates (the window dragged to another display mid-pinch, GTK re-emitting
|
|
// `::resize`) cannot read the ratio change as a focal jump; and an update outside
|
|
// a gesture -- before any begin, or a straggler after the end -- zooms and moves
|
|
// nothing rather than zooming from a made-up anchor.
|
|
PLOT_TEST(gtk_zoom_anchor_stays_logical_and_ignores_stragglers) {
|
|
const DeviceMapping at_1 = DeviceMapping::from_sizes({400.0f, 300.0f}, {400.0f, 300.0f});
|
|
const DeviceMapping at_125 = DeviceMapping::from_sizes({400.0f, 300.0f}, {500.0f, 375.0f});
|
|
plot::gtk4::ZoomAnchor zoom;
|
|
|
|
// The orphan update, before any begin.
|
|
const plot::input::GestureEvent orphan = zoom.update(2.0f, {10.0f, 10.0f}, at_1);
|
|
PLOT_CHECK_NEAR(orphan.scale, 1.0, 1e-9);
|
|
PLOT_CHECK_NEAR(orphan.dx, 0.0, 1e-9);
|
|
PLOT_CHECK_NEAR(orphan.dy, 0.0, 1e-9);
|
|
|
|
zoom.begin(1.0f, {100.0f, 100.0f});
|
|
// Four logical pixels of drift across a 1.0 -> 1.25 factor change: 4 * 1.25 =
|
|
// 5 device pixels, and the anchor for the next frame is the new logical point.
|
|
const plot::input::GestureEvent rescaled = zoom.update(1.0f, {104.0f, 100.0f}, at_125);
|
|
PLOT_CHECK_NEAR(rescaled.scale, 1.0, 1e-6);
|
|
PLOT_CHECK_NEAR(rescaled.dx, 5.0, 1e-6);
|
|
PLOT_CHECK_NEAR(rescaled.x, 104.0 * 1.25, 1e-6);
|
|
|
|
// The gesture ends; a straggler scale-changed does nothing.
|
|
zoom.end();
|
|
PLOT_CHECK(!zoom.active);
|
|
const plot::input::GestureEvent straggler = zoom.update(2.0f, {120.0f, 100.0f}, at_125);
|
|
PLOT_CHECK_NEAR(straggler.scale, 1.0, 1e-9);
|
|
PLOT_CHECK_NEAR(straggler.dx, 0.0, 1e-9);
|
|
}
|
|
|
|
// --- the keyboard -----------------------------------------------------------
|
|
|
|
// The whole key translation, now that it names arrows and Escape too. The
|
|
// function keys are matched on the keyval, which for them is layout- and
|
|
// level-independent; the printable keys on the Unicode the keyval decodes to,
|
|
// which is what makes `+` and shift+`=` (and the keypad's plus) one key to the
|
|
// controller. Anything else is `0` -- the caller hands the event back to GTK.
|
|
PLOT_TEST(gtk_keys_map_by_keyval_first_and_unicode_second) {
|
|
PLOT_CHECK(key_from(0xFF1Bu, 0u) == plot::input::kKeyEscape); // GDK_KEY_Escape
|
|
PLOT_CHECK(key_from(0xFF51u, 0u) == plot::input::kKeyLeft); // GDK_KEY_Left
|
|
PLOT_CHECK(key_from(0xFF53u, 0u) == plot::input::kKeyRight); // GDK_KEY_Right
|
|
PLOT_CHECK(key_from(0xFF52u, 0u) == plot::input::kKeyUp);
|
|
PLOT_CHECK(key_from(0xFF54u, 0u) == plot::input::kKeyDown);
|
|
|
|
// Printable keys, by code point -- the keyval is irrelevant to this branch.
|
|
PLOT_CHECK(key_from(0x002Bu, static_cast<u32>('+')) == plot::input::kKeyZoomIn);
|
|
PLOT_CHECK(key_from(0x003Du, static_cast<u32>('=')) == plot::input::kKeyZoomIn);
|
|
PLOT_CHECK(key_from(0x002Du, static_cast<u32>('-')) == plot::input::kKeyZoomOut);
|
|
PLOT_CHECK(key_from(0x005Fu, static_cast<u32>('_')) == plot::input::kKeyZoomOut);
|
|
PLOT_CHECK(key_from(0x0066u, static_cast<u32>('f')) == plot::input::kKeyFit);
|
|
PLOT_CHECK(key_from(0x0046u, static_cast<u32>('F')) == plot::input::kKeyFit);
|
|
|
|
// The keyval wins when both could match: the order is the seam's contract, not
|
|
// an accident of GDK's tables.
|
|
PLOT_CHECK(key_from(0xFF51u, static_cast<u32>('x')) == plot::input::kKeyLeft);
|
|
|
|
// And nothing a plot has no gesture for maps to a key: an unmapped keyval with
|
|
// an unmapped code point is refused, and so is a bare zero.
|
|
PLOT_CHECK(key_from(0x0078u, static_cast<u32>('x')) == 0);
|
|
PLOT_CHECK(key_from(0u, 0u) == 0);
|
|
}
|