#include "harness.hpp" #include #include #include #include #include #include namespace { using plot::f32; using plot::f64; using plot::u32; constexpr u32 kShift = static_cast(plot::input::Mod::Shift); constexpr u32 kCtrl = static_cast(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(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(); 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 // 80 px right-drag on an 800 px plot showing [0, 100] reveals 10 units of // earlier data: the data follows the cursor. (The brief printed +10.0 here.) PLOT_CHECK_NEAR(p.x_range(ax).min, -10.0, 1e-6); PLOT_CHECK(p.y_range(ay).max > 1.0); // moving down raises y range } 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 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}); PLOT_CHECK_NEAR(p.x_range(ax).min, 100.0, 1e-6); PLOT_CHECK_NEAR(p.x_range(ax).max, 300.0, 1e-6); } 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}); 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_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(f.plot.y_range(f.ay).span() < 2.0); } 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); } 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 ys(51); for (u64 i = 0; i < ys.size(); ++i) ys[i] = static_cast(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); }