168 lines
8.5 KiB
C++
168 lines
8.5 KiB
C++
#pragma once
|
|
|
|
#include <plot/core/rect.hpp>
|
|
#include <plot/core/types.hpp>
|
|
#include <plot/input/events.hpp>
|
|
|
|
namespace plot {
|
|
class Plot;
|
|
}
|
|
|
|
namespace plot::input {
|
|
|
|
/// What a drag in progress is doing.
|
|
enum class DragMode {
|
|
None,
|
|
Pan, ///< translate every range (left or right drag)
|
|
BoxZoom, ///< a shift+left selection is open; the ranges change on release
|
|
MoveCursor, ///< ctrl+left drag is moving `hovered_cursor()`
|
|
AxisZoomX, ///< middle drag inside the x label strip; zooms x only
|
|
AxisZoomY, ///< middle drag inside the y label strip; zooms y only
|
|
};
|
|
|
|
/// Turns host events into plot gestures, knowing nothing about Qt, GTK, GLFW or
|
|
/// ImGui -- Tasks 8-11 translate their native events into the value types above.
|
|
///
|
|
/// It is pure CPU and needs no GL context and no font: everything it reads comes
|
|
/// from `Plot`'s layout, ranges and transforms. The controller owns no viewport
|
|
/// of its own: set_viewport() forwards to `Plot::set_viewport` (and refreshes the
|
|
/// layout) so the two can never disagree, and every gesture runs through
|
|
/// `Plot`'s range setters, which clear `auto_scale` in one place.
|
|
///
|
|
/// Gesture table (grab-and-drag: the data follows the cursor):
|
|
/// | gesture | mode | effect |
|
|
/// |---|---|---|
|
|
/// | left drag | `Pan` | translate both ranges |
|
|
/// | right drag | `Pan` | same, for hosts that reserve left |
|
|
/// | shift + left drag | `BoxZoom` | both ranges from the selection rect |
|
|
/// | ctrl + left drag near a cursor | `MoveCursor` | move that cursor |
|
|
/// | middle drag on the x label strip | `AxisZoomX` | x-only zoom |
|
|
/// | middle drag on the y label strip | `AxisZoomY` | y-only zoom |
|
|
/// | wheel (shift: x only, ctrl: y only) | -- | zoom about the pointer |
|
|
/// | `+` / `-` | -- | zoom about the plot centre |
|
|
/// | `f` | -- | auto-fit every axis |
|
|
///
|
|
/// Both axis-zoom drags read the vertical `dy` in pointer pixels with the
|
|
/// wheel's sign convention (dragging down zooms in), anchored at the pointer's
|
|
/// data coordinate on the axis they zoom. The drag has its own per-pixel
|
|
/// sensitivity -- a pixel is not a wheel notch -- of one decade per 200 **logical**
|
|
/// px, so a 200 px drag scales the span by 10 whatever the display's scale.
|
|
///
|
|
/// A drag that changes no range is harmless: a box-zoom selection under 4 px on
|
|
/// either side is a click, and leaves the ranges alone.
|
|
///
|
|
/// ## Zoom and pan on a live x axis (spec §12)
|
|
///
|
|
/// An x axis armed with `XFollow::Live` keeps following through every gesture
|
|
/// below. Nothing here reads or writes the follow state, and the ranges these
|
|
/// gestures set are read by `Plot::set_x_range` as a span and an offset behind the
|
|
/// live edge rather than as absolute positions, so both halves of the requirement
|
|
/// fall out of the paths that already existed:
|
|
///
|
|
/// - the wheel *multiplies* the span, so notches compound and the zoom is
|
|
/// scale-free. Its anchor is the data coordinate under the pointer while the
|
|
/// pointer is inside `axes`; outside it there is no such coordinate, and on a
|
|
/// live axis the live edge is used instead (see `x_zoom_anchor()`), which leaves
|
|
/// the window ending where it ended. Either way the offset is adjusted to keep
|
|
/// the anchor still, and following stays armed;
|
|
/// - a pan shifts the offset by the drag's data delta and leaves the span alone, so
|
|
/// a user can walk back through the retained window and stops at the live edge
|
|
/// rather than past it.
|
|
///
|
|
/// An `Off` axis -- every axis by default -- behaves exactly as it always has.
|
|
///
|
|
/// ## Tolerances are logical, the coordinates they are compared against are not
|
|
///
|
|
/// The plot geometry the controller hit-tests in is device pixels (see
|
|
/// `PlotLayout`), and each adapter converts its pointer coordinates into the same
|
|
/// space before forwarding them. A tolerance written as a device constant would
|
|
/// therefore change its *physical* size with the display: a 5 px grab radius is
|
|
/// half the hand movement on a 2x panel, so the same cursor becomes harder to
|
|
/// catch there. Both tolerances -- the cursor grab radius and the box-zoom click
|
|
/// threshold -- are therefore logical constants, converted through
|
|
/// `Style::device()` at the scale of the plot the controller already holds: that is
|
|
/// the single place the logical -> device rule lives, and the plot is the one that
|
|
/// knows what its device pixel is worth, so the controller keeps no copy of the scale
|
|
/// for a host to keep in step.
|
|
///
|
|
/// The middle-drag axis zoom's per-pixel gain is logical for the same reason, by a
|
|
/// different route: it is stated in *logical* pixels (200 of them is one decade),
|
|
/// and the drag delta arrives in device pixels like every other coordinate here, so
|
|
/// `axis_zoom()` divides it by `Plot::ui_scale()` before applying the gain. Without
|
|
/// that the same physical drag would zoom twice as much at 2x. The wheel needs no
|
|
/// such conversion: a notch is already device-independent, and the wheel path is
|
|
/// left alone.
|
|
class Controller {
|
|
public:
|
|
explicit Controller(Plot& plot) noexcept : plot_(&plot) {}
|
|
|
|
/// Sets the plot's viewport (the controller keeps none of its own) and
|
|
/// refreshes the layout, so gesture geometry and the host agree.
|
|
void set_viewport(Rect px);
|
|
|
|
/// Events are sanitised before they are acted on: a non-finite coordinate or
|
|
/// delta -- a host's "no pointer" sentinel (ImGui reports `-FLT_MAX`), a NaN
|
|
/// from a degenerate mapping, an infinity from a lost device -- becomes the last
|
|
/// known position and no movement. Garbage must not reach the ranges: a gesture
|
|
/// that sets a range also clears auto scale, so a range driven to 1e35 is one the
|
|
/// user can neither pan nor zoom back. The buttons in the same event are still
|
|
/// acted on, so a drag whose release arrives with a bad position still ends.
|
|
void on_pointer(const PointerEvent& e);
|
|
/// Same, and a wheel amount that is not a number is *refused* rather than
|
|
/// zeroed: a zero-notch zoom changes no range but still goes through the range
|
|
/// setters, which clear auto scale -- so a sanitised amount would quietly disarm
|
|
/// auto-fit. An unusable amount leaves the plot exactly as it was; an unusable
|
|
/// *position* only loses its anchor, falling back to the last known one.
|
|
void on_wheel(const WheelEvent& e);
|
|
void on_key(const KeyEvent& e);
|
|
|
|
DragMode drag_mode() const noexcept { return mode_; }
|
|
/// The current box-zoom selection in pixels; meaningful while BoxZoom.
|
|
Rect selection_rect() const noexcept { return selection_; }
|
|
/// The cursor under the pointer, or `kInvalid` when the pointer is outside the
|
|
/// plot area or no cursor is within a few pixels.
|
|
u64 hovered_cursor() const;
|
|
|
|
private:
|
|
void begin_drag(const PointerEvent& e);
|
|
void update_drag(const PointerEvent& e);
|
|
void end_drag(const PointerEvent& e);
|
|
|
|
void pan(const PointerEvent& e);
|
|
void box_zoom_update(const PointerEvent& e);
|
|
void apply_box_zoom();
|
|
void move_cursor(const PointerEvent& e);
|
|
void axis_zoom(const PointerEvent& e);
|
|
|
|
/// The nearest visible cursor within the hit radius of (px, py), or kInvalid.
|
|
/// kInvalid when (px, py) is outside the plot axes.
|
|
u64 cursor_at(f32 px, f32 py) const;
|
|
|
|
/// The data x a zoom of `axis` holds fixed, for a gesture at (px, py). The
|
|
/// coordinate under the pointer while the pointer is inside `axes`, which is what
|
|
/// makes a zoom feel anchored to the data being pointed at; outside `axes` there
|
|
/// is no such coordinate, and a live axis (`XFollow::Live`) uses its live edge
|
|
/// instead, so a zoom made from the label strip or off the plot zooms the window
|
|
/// about the newest sample and leaves it ending where it ended.
|
|
f64 x_zoom_anchor(u32 axis, f32 px, f32 py) const;
|
|
|
|
/// Zoom one axis about `anchor` (a data coordinate) by `factor`, recomputing
|
|
/// the range so the anchor keeps its screen position. Leaves `auto_scale` false;
|
|
/// under `XFollow::Live` the range is read by `Plot::set_x_range` as a span and an
|
|
/// offset behind the live edge, which is what keeps following armed.
|
|
void zoom_x_axis(u32 axis, f64 anchor, f64 factor);
|
|
void zoom_y_axis(u32 axis, f64 anchor, f64 factor);
|
|
|
|
Plot* plot_ = nullptr;
|
|
DragMode mode_ = DragMode::None;
|
|
Rect selection_{};
|
|
f32 origin_x_ = 0.0f;
|
|
f32 origin_y_ = 0.0f;
|
|
u64 drag_cursor_ = kInvalid;
|
|
bool has_pointer_ = false;
|
|
f32 pointer_x_ = 0.0f;
|
|
f32 pointer_y_ = 0.0f;
|
|
};
|
|
|
|
} // namespace plot::input
|