433 lines
21 KiB
C++
433 lines
21 KiB
C++
#include "harness.hpp"
|
|
|
|
#include <plot/input/controller.hpp>
|
|
#include <plot/plot/plot.hpp>
|
|
#include <plot/data/array_series.hpp>
|
|
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
using plot::f32;
|
|
using plot::f64;
|
|
using plot::u32;
|
|
|
|
constexpr u32 kShift = static_cast<u32>(plot::input::Mod::Shift);
|
|
constexpr u32 kCtrl = static_cast<u32>(plot::input::Mod::Ctrl);
|
|
constexpr u32 kNoAxis = ~u32{0};
|
|
|
|
/// A 800x400 plot with one x axis spanning [0, 100] and the controller attached.
|
|
struct Fixture {
|
|
plot::Plot plot;
|
|
u32 ax = kNoAxis;
|
|
u32 ay = kNoAxis;
|
|
plot::input::Controller controller{plot};
|
|
|
|
Fixture() {
|
|
plot.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
ax = plot.add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
|
controller.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
}
|
|
};
|
|
|
|
f32 screen_x_of(const plot::Plot& p, u32 axis, f64 data) {
|
|
return static_cast<f32>(p.x_transform(axis).to_screen_x(data));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
PLOT_TEST(wheel_zoom_keeps_data_under_cursor_fixed) {
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, true, plot::AxisPlace::Bottom, false});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
const f32 cursor_x = 200.0f;
|
|
const f64 before = p.x_transform(ax).to_data_x(cursor_x);
|
|
plot::input::WheelEvent w{cursor_x, 100.0f, 0.0f, 1.0f, 0};
|
|
c.on_wheel(w);
|
|
const f64 after = p.x_transform(ax).to_data_x(cursor_x);
|
|
PLOT_CHECK_NEAR(before, after, 1e-6);
|
|
PLOT_CHECK(plot::Range{p.x_range(ax)}.span() < 100.0); // zoomed in
|
|
}
|
|
|
|
PLOT_TEST(left_drag_pans_without_changing_span) {
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, true, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
const double span_before = p.x_range(ax).span();
|
|
const f64 grabbed = p.x_transform(ax).to_data_x(100.0f); // data under the press point
|
|
c.on_pointer({100, 100, 0, 0, plot::input::Button::Left, 0, true, true, false});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::Pan);
|
|
c.on_pointer({180, 130, 80, 30, plot::input::Button::Left, 0, true, false, false});
|
|
c.on_pointer({180, 130, 0, 0, plot::input::Button::Left, 0, false, false, true});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).span(), span_before, 1e-9); // span unchanged
|
|
// Assert the INVARIANT, not a pixel fraction. Grab-and-drag means the data
|
|
// grabbed at the press point is under the pointer afterwards. Pixels map to
|
|
// data through layout().axes, NOT the whole viewport: the viewport model makes
|
|
// dragged data lag the cursor by the label-strip fraction, a defect no
|
|
// pixel-fraction assertion can see because it certifies its own arithmetic.
|
|
PLOT_CHECK_NEAR(p.x_transform(ax).to_data_x(180.0f), grabbed, 1e-9);
|
|
PLOT_CHECK(p.y_range(ay).max > 1.0); // moving down raises y range
|
|
PLOT_CHECK(!p.x_axis(ax).auto_scale); // a pan is a user choice, not a fit
|
|
}
|
|
|
|
PLOT_TEST(shift_drag_box_zooms_to_selection) {
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 800.0}, true, plot::AxisPlace::Bottom, false});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
const u32 e_shift = 1u << 0; // Mod::Shift
|
|
const f64 want_lo = p.x_transform(ax).to_data_x(100.0f);
|
|
const f64 want_hi = p.x_transform(ax).to_data_x(300.0f);
|
|
c.on_pointer({100, 50, 0, 0, plot::input::Button::Left, e_shift, true, true, false});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::BoxZoom);
|
|
c.on_pointer({300, 150, 200, 100, plot::input::Button::Left, e_shift, true, false, false});
|
|
PLOT_CHECK_NEAR(c.selection_rect().x, 100.0, 1e-6);
|
|
PLOT_CHECK_NEAR(c.selection_rect().w, 200.0, 1e-6);
|
|
c.on_pointer({300, 150, 0, 0, plot::input::Button::Left, e_shift, false, false, true});
|
|
// The box frames exactly the data it encloses: the ranges come from the axes
|
|
// transform, and the selection is clamped to layout().axes, not the viewport.
|
|
PLOT_CHECK_NEAR(p.x_range(ax).min, want_lo, 1e-6);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, want_hi, 1e-6);
|
|
}
|
|
|
|
PLOT_TEST(box_zoom_uses_release_position_without_a_move) {
|
|
// A host that reports only a press and a release -- no motion event between
|
|
// them -- MUST still frame the box the release describes: the release position
|
|
// is authoritative for the end of the gesture.
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 800.0}, true, plot::AxisPlace::Bottom, false});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
const f64 want_lo = p.x_transform(ax).to_data_x(100.0f);
|
|
const f64 want_hi = p.x_transform(ax).to_data_x(300.0f);
|
|
c.on_pointer({100, 50, 0, 0, plot::input::Button::Left, kShift, true, true, false});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::BoxZoom);
|
|
// No move: selection_ is still 0x0 until the release supplies the far corner.
|
|
c.on_pointer({300, 150, 0, 0, plot::input::Button::Left, kShift, false, false, true});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).min, want_lo, 1e-6);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, want_hi, 1e-6);
|
|
PLOT_CHECK(!p.x_axis(ax).auto_scale); // a box zoom is a user choice, not a fit
|
|
}
|
|
|
|
PLOT_TEST(auto_scale_disabled_by_user_interaction) {
|
|
// A wheel zoom or pan MUST clear auto_scale on the affected axis so the range
|
|
// the user chose is not snapped back by the next autoscale.
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, true, plot::AxisPlace::Bottom, false});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
c.on_wheel({400.0f, 200.0f, 0.0f, 1.0f, 0});
|
|
PLOT_CHECK(!p.x_axis(ax).auto_scale);
|
|
}
|
|
|
|
// --- resolutions of the brief's ambiguity, pinned ------------------------
|
|
|
|
PLOT_TEST(box_zoom_below_threshold_is_a_click) {
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 800.0}, false, plot::AxisPlace::Bottom, false});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
c.on_pointer({100, 50, 0, 0, plot::input::Button::Left, kShift, true, true, false});
|
|
c.on_pointer({102, 52, 2, 2, plot::input::Button::Left, kShift, true, false, false});
|
|
c.on_pointer({102, 52, 0, 0, plot::input::Button::Left, kShift, false, false, true});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).min, 0.0, 1e-9);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, 800.0, 1e-9);
|
|
}
|
|
|
|
PLOT_TEST(shift_wheel_zooms_x_only) {
|
|
Fixture f;
|
|
f.ay = f.plot.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
|
f.controller.on_wheel({400.0f, 200.0f, 0.0f, 1.0f, kShift});
|
|
PLOT_CHECK(f.plot.x_range(f.ax).span() < 100.0);
|
|
PLOT_CHECK_NEAR(f.plot.y_range(f.ay).span(), 2.0, 1e-9);
|
|
}
|
|
|
|
PLOT_TEST(ctrl_wheel_zooms_y_only) {
|
|
Fixture f;
|
|
f.ay = f.plot.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
|
f.controller.on_wheel({400.0f, 200.0f, 0.0f, 1.0f, kCtrl});
|
|
PLOT_CHECK_NEAR(f.plot.x_range(f.ax).span(), 100.0, 1e-9);
|
|
PLOT_CHECK(f.plot.y_range(f.ay).span() < 2.0);
|
|
}
|
|
|
|
PLOT_TEST(hovered_cursor_only_inside_plot_area) {
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 cid = p.add_cursor(50.0, plot::Color::hex(0xFF0000));
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
const f32 sx = screen_x_of(p, ax, 50.0);
|
|
c.on_pointer({sx, 200, 0, 0, plot::input::Button::Left, 0, false, false, false});
|
|
PLOT_CHECK(c.hovered_cursor() == cid);
|
|
// The left label strip is outside the plot area.
|
|
c.on_pointer({10, 200, 0, 0, plot::input::Button::Left, 0, false, false, false});
|
|
PLOT_CHECK(c.hovered_cursor() == plot::kInvalid);
|
|
c.on_pointer({sx, 200, 0, 0, plot::input::Button::Left, 0, false, false, false});
|
|
PLOT_CHECK(c.hovered_cursor() == cid);
|
|
}
|
|
|
|
PLOT_TEST(ctrl_drag_moves_the_nearest_cursor) {
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 cid = p.add_cursor(20.0, plot::Color::hex(0xFF0000));
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
const f32 sx = screen_x_of(p, ax, 20.0);
|
|
c.on_pointer({sx, 200, 0, 0, plot::input::Button::Left, kCtrl, true, true, false});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::MoveCursor);
|
|
c.on_pointer({sx + 100, 200, 100, 0, plot::input::Button::Left, kCtrl, true, false, false});
|
|
c.on_pointer({sx + 100, 200, 0, 0, plot::input::Button::Left, kCtrl, false, false, true});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
|
|
PLOT_CHECK(p.cursor(cid).x > 20.0);
|
|
PLOT_CHECK_NEAR(p.cursor(cid).x, p.x_transform(ax).to_data_x(sx + 100), 1e-9);
|
|
}
|
|
|
|
PLOT_TEST(middle_drag_on_x_strip_zooms_x_only) {
|
|
Fixture f;
|
|
f.ay = f.plot.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
|
const plot::PlotLayout& l = f.plot.layout();
|
|
const f32 strip_x = l.axes.x + 0.5f * l.axes.w;
|
|
const f32 strip_y = l.x_label_area.y + 0.5f * l.x_label_area.h;
|
|
f.controller.on_pointer({strip_x, strip_y, 0, 0, plot::input::Button::Middle, 0, true, true, false});
|
|
PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::AxisZoomX);
|
|
// Same sign convention as the wheel: a positive dy zooms in.
|
|
f.controller.on_pointer({strip_x, strip_y + 20, 0, 20, plot::input::Button::Middle, 0, true, false, false});
|
|
f.controller.on_pointer({strip_x, strip_y + 20, 0, 0, plot::input::Button::Middle, 0, false, false, true});
|
|
// 20 px of drag is a tenth of a decade: the span is 10^(-20/200) of its
|
|
// former self. A pixel is not a wheel notch, so this pins the magnitude.
|
|
PLOT_CHECK_NEAR(f.plot.x_range(f.ax).span(), 100.0 * std::pow(10.0, -20.0 / 200.0), 1e-6);
|
|
PLOT_CHECK_NEAR(f.plot.y_range(f.ay).span(), 2.0, 1e-9);
|
|
PLOT_CHECK(!f.plot.x_axis(f.ax).auto_scale);
|
|
PLOT_CHECK(f.plot.y_axis(f.ay).auto_scale); // the untouched y axis keeps auto scale
|
|
}
|
|
|
|
PLOT_TEST(middle_drag_on_y_strip_zooms_y_only) {
|
|
Fixture f;
|
|
f.ay = f.plot.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
|
const plot::PlotLayout& l = f.plot.layout();
|
|
const f32 strip_x = l.y_label_area.x + 0.5f * l.y_label_area.w;
|
|
const f32 strip_y = l.axes.y + 0.5f * l.axes.h;
|
|
f.controller.on_pointer({strip_x, strip_y, 0, 0, plot::input::Button::Middle, 0, true, true, false});
|
|
PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::AxisZoomY);
|
|
f.controller.on_pointer({strip_x, strip_y + 20, 0, 20, plot::input::Button::Middle, 0, true, false, false});
|
|
f.controller.on_pointer({strip_x, strip_y + 20, 0, 0, plot::input::Button::Middle, 0, false, false, true});
|
|
PLOT_CHECK_NEAR(f.plot.x_range(f.ax).span(), 100.0, 1e-9);
|
|
PLOT_CHECK_NEAR(f.plot.y_range(f.ay).span(), 2.0 * std::pow(10.0, -20.0 / 200.0), 1e-6);
|
|
}
|
|
|
|
PLOT_TEST(key_plus_zooms_in_about_the_centre) {
|
|
Fixture f;
|
|
const f64 mid_before = 0.5 * (f.plot.x_range(f.ax).min + f.plot.x_range(f.ax).max);
|
|
f.controller.on_key({plot::input::kKeyZoomIn, 0, true});
|
|
PLOT_CHECK(f.plot.x_range(f.ax).span() < 100.0);
|
|
PLOT_CHECK_NEAR(0.5 * (f.plot.x_range(f.ax).min + f.plot.x_range(f.ax).max), mid_before, 1e-9);
|
|
const f64 span_after = f.plot.x_range(f.ax).span();
|
|
f.controller.on_key({plot::input::kKeyZoomIn, 0, false}); // key-up is ignored
|
|
PLOT_CHECK_NEAR(f.plot.x_range(f.ax).span(), span_after, 1e-12);
|
|
}
|
|
|
|
PLOT_TEST(key_minus_zooms_out) {
|
|
Fixture f;
|
|
f.controller.on_key({plot::input::kKeyZoomOut, 0, true});
|
|
PLOT_CHECK(f.plot.x_range(f.ax).span() > 100.0);
|
|
}
|
|
|
|
/// A host can hand the controller a coordinate that is not a number: ImGui reports
|
|
/// (-FLT_MAX, -FLT_MAX) for "no mouse position", and any host can produce a NaN out
|
|
/// of a degenerate mapping of its own. The pan arithmetic runs through the axis
|
|
/// transforms, so such a value would leave a range that is not a range -- and
|
|
/// because a gesture that sets a range also clears auto scale, panning and zooming
|
|
/// could not bring it back.
|
|
PLOT_TEST(non_finite_pointer_does_not_move_a_range) {
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, true, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
const plot::Range xr = p.x_range(ax);
|
|
const plot::Range yr = p.y_range(ay);
|
|
|
|
c.on_pointer({100, 100, 0, 0, plot::input::Button::Left, 0, true, true, false});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::Pan);
|
|
|
|
const float inf = std::numeric_limits<float>::infinity();
|
|
const float nan = std::numeric_limits<float>::quiet_NaN();
|
|
const float sentinel = -std::numeric_limits<float>::max(); // ImGui's "no pointer"
|
|
|
|
// The pointer-left-the-window sentinel arrives at the unrepresentable end of the
|
|
// axis, so scaling and differencing it is a 1e35 pan. It must read as no
|
|
// movement at all.
|
|
c.on_pointer({sentinel, sentinel, inf, -inf, plot::input::Button::Left, 0, true, false, false});
|
|
PLOT_CHECK(std::isfinite(p.x_range(ax).min) && std::isfinite(p.x_range(ax).max));
|
|
PLOT_CHECK(std::isfinite(p.y_range(ay).min) && std::isfinite(p.y_range(ay).max));
|
|
PLOT_CHECK_NEAR(p.x_range(ax).min, xr.min, 1e-9);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, xr.max, 1e-9);
|
|
PLOT_CHECK_NEAR(p.y_range(ay).min, yr.min, 1e-9);
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::Pan); // still the same drag
|
|
|
|
// A usable position with an unusable delta is no movement either, and a later
|
|
// usable position is not differenced against the sentinel.
|
|
c.on_pointer({180, 130, inf, nan, plot::input::Button::Left, 0, true, false, false});
|
|
PLOT_CHECK_NEAR(p.x_range(ax).min, xr.min, 1e-9);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, xr.max, 1e-9);
|
|
PLOT_CHECK_NEAR(p.y_range(ay).min, yr.min, 1e-9);
|
|
c.on_pointer({220, 160, 40, 30, plot::input::Button::Left, 0, true, false, false});
|
|
PLOT_CHECK(std::isfinite(p.x_range(ax).min)); // a real move still pans
|
|
PLOT_CHECK(p.x_range(ax).min < xr.min);
|
|
|
|
// The buttons in a sanitised event still act: the release ends the drag.
|
|
c.on_pointer({220, 160, 0, 0, plot::input::Button::Left, 0, false, false, true});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
|
|
}
|
|
|
|
PLOT_TEST(non_finite_wheel_does_not_break_a_range) {
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, true, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
|
|
const float inf = std::numeric_limits<float>::infinity();
|
|
const float nan = std::numeric_limits<float>::quiet_NaN();
|
|
|
|
// A zoom is anchored at the data coordinate under the pointer, so a position that
|
|
// is not a number must not become the anchor: the axis would be re-centred on
|
|
// NaN, which no gesture could undo. The fallback is a real coordinate and the
|
|
// zoom still happens. (A *finite* sentinel -- ImGui's `-FLT_MAX` for "no pointer"
|
|
// -- is not the controller's to interpret: finite coordinates are legal input.
|
|
// The adapter translates that one; see tests/test_imgui_widget.cpp.)
|
|
const double span_before = p.x_range(ax).span();
|
|
c.on_wheel({nan, nan, 0.0f, 1.0f, 0});
|
|
PLOT_CHECK(std::isfinite(p.x_range(ax).min) && std::isfinite(p.x_range(ax).max));
|
|
PLOT_CHECK(std::isfinite(p.y_range(ay).min) && std::isfinite(p.y_range(ay).max));
|
|
PLOT_CHECK(std::fabs(p.x_range(ax).min) < 1e6 && std::fabs(p.x_range(ax).max) < 1e6);
|
|
PLOT_CHECK(p.x_range(ax).span() < span_before);
|
|
|
|
// A non-finite amount is no zoom at all -- and, just as important, it must not
|
|
// touch the axes. A *zeroed* amount would be a zero-notch zoom (a factor of
|
|
// exactly one): the range would survive, but the range setters it runs through
|
|
// clear auto scale, so one garbage wheel would quietly disarm auto-fit.
|
|
p.set_x_auto_scale(ax, true);
|
|
p.set_y_auto_scale(ay, true);
|
|
const plot::Range xr = p.x_range(ax);
|
|
const plot::Range yr = p.y_range(ay);
|
|
c.on_wheel({400.0f, 200.0f, 0.0f, nan, 0});
|
|
c.on_wheel({400.0f, 200.0f, 0.0f, inf, 0});
|
|
PLOT_CHECK_NEAR(p.x_range(ax).min, xr.min, 1e-12);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, xr.max, 1e-12);
|
|
PLOT_CHECK_NEAR(p.y_range(ay).min, yr.min, 1e-12);
|
|
PLOT_CHECK(p.x_axis(ax).auto_scale);
|
|
PLOT_CHECK(p.y_axis(ay).auto_scale);
|
|
}
|
|
|
|
PLOT_TEST(key_f_auto_fits_traces) {
|
|
plot::Plot p;
|
|
p.set_viewport({0, 0, 800, 400});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
|
std::vector<f32> ys(51);
|
|
for (u64 i = 0; i < ys.size(); ++i) ys[i] = static_cast<f32>(i) * 0.1f; // 0.0 .. 5.0
|
|
plot::data::ArraySeries s(std::move(ys), 0.0, 1.0); // x = 0 .. 50
|
|
p.add_trace({&s, plot::Color::hex(0x4FA3FF), 1.5f, 0, 0});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800, 400});
|
|
c.on_key({plot::input::kKeyFit, 0, true});
|
|
PLOT_CHECK(p.x_range(ax).min < 0.0);
|
|
PLOT_CHECK(p.x_range(ax).max < 100.0); // fitted to the series' 0..50
|
|
PLOT_CHECK(p.y_range(ay).min < 0.0);
|
|
PLOT_CHECK(p.y_range(ay).max > 1.0); // fitted to the series' 0..5
|
|
PLOT_CHECK(p.x_axis(ax).auto_scale); // a fit re-arms auto scale
|
|
PLOT_CHECK(p.y_axis(ay).auto_scale);
|
|
}
|
|
|
|
// --- tolerances are logical, coordinates are device pixels -------------------
|
|
|
|
/// The grab radius is a distance the user aims with, so it must be the same
|
|
/// *physical* distance at any DPI: the controller compares device-pixel coordinates
|
|
/// against `logical * Plot::ui_scale()`. The press positions below are the same
|
|
/// logical distance (0.9 and 1.1 of the 5 px radius) from the cursor at both scales,
|
|
/// so the same press grabs -- and the same press just outside does not.
|
|
PLOT_TEST(grab_radius_scales_with_dpi) {
|
|
for (const f32 scale : {1.0f, 2.0f}) {
|
|
plot::Plot p;
|
|
p.set_ui_scale(scale);
|
|
p.set_viewport({0, 0, 800.0f * scale, 400.0f * scale});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 cid = p.add_cursor(50.0, plot::Color::hex(0xFF0000));
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800.0f * scale, 400.0f * scale});
|
|
PLOT_CHECK_NEAR(p.ui_scale(), scale, 1e-6);
|
|
|
|
const f32 sx = screen_x_of(p, ax, 50.0);
|
|
const f32 inside = 0.9f * 5.0f * scale; // logical: within the grab radius
|
|
const f32 outside = 1.1f * 5.0f * scale; // logical: just past it
|
|
c.on_pointer({sx + inside, 200.0f * scale, 0, 0, plot::input::Button::Left, 0, false, false,
|
|
false});
|
|
PLOT_CHECK(c.hovered_cursor() == cid);
|
|
c.on_pointer({sx + outside, 200.0f * scale, 0, 0, plot::input::Button::Left, 0, false, false,
|
|
false});
|
|
PLOT_CHECK(c.hovered_cursor() == plot::kInvalid);
|
|
// ...and a ctrl-drag at that same logical distance opens a cursor move, which is
|
|
// the gesture the radius actually gates.
|
|
c.on_pointer({sx + inside, 200.0f * scale, 0, 0, plot::input::Button::Left, kCtrl, true, true,
|
|
false});
|
|
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::MoveCursor);
|
|
}
|
|
}
|
|
|
|
/// The box-zoom click threshold is a hand movement too: a selection the user
|
|
/// perceives as a few pixels is a click at any DPI, so both dimensions are compared
|
|
/// against `logical * Plot::ui_scale()`.
|
|
PLOT_TEST(box_zoom_click_threshold_scales_with_dpi) {
|
|
for (const f32 scale : {1.0f, 2.0f}) {
|
|
plot::Plot p;
|
|
p.set_ui_scale(scale);
|
|
p.set_viewport({0, 0, 800.0f * scale, 400.0f * scale});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 800.0}, true, plot::AxisPlace::Bottom, false});
|
|
plot::input::Controller c(p);
|
|
c.set_viewport({0, 0, 800.0f * scale, 400.0f * scale});
|
|
const plot::Range before = p.x_range(ax);
|
|
|
|
// 3 logical px on each side: a click, at both scales.
|
|
const f32 x0 = 100.0f * scale;
|
|
const f32 y0 = 50.0f * scale;
|
|
const f32 small = 3.0f * scale;
|
|
c.on_pointer({x0, y0, 0, 0, plot::input::Button::Left, kShift, true, true, false});
|
|
c.on_pointer({x0 + small, y0 + small, small, small, plot::input::Button::Left, kShift, true,
|
|
false, false});
|
|
c.on_pointer({x0 + small, y0 + small, 0, 0, plot::input::Button::Left, kShift, false, false,
|
|
true});
|
|
PLOT_CHECK_NEAR(p.x_range(ax).min, before.min, 1e-9);
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, before.max, 1e-9);
|
|
|
|
// 5 logical px on each side: a box, at both scales.
|
|
const f32 large = 5.0f * scale;
|
|
c.on_pointer({x0, y0, 0, 0, plot::input::Button::Left, kShift, true, true, false});
|
|
c.on_pointer({x0 + large, y0 + large, large, large, plot::input::Button::Left, kShift, true,
|
|
false, false});
|
|
c.on_pointer({x0 + large, y0 + large, 0, 0, plot::input::Button::Left, kShift, false, false,
|
|
true});
|
|
PLOT_CHECK(p.x_range(ax).min > before.min);
|
|
PLOT_CHECK(p.x_range(ax).max < before.max);
|
|
}
|
|
}
|