feat(input): host-agnostic events and pan/zoom/cursor controller

This commit is contained in:
Martino Ferrari committed 2026-09-27 22:34:16 +02:00
1 parent 606e40a9f1
commit 5ab854104a
6 files changed
+718

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
#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` with the wheel's sign convention
/// (dragging down zooms in), anchored at the pointer's data coordinate on the
/// axis they zoom.
///
/// 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.
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);
void on_pointer(const PointerEvent& e);
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();
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;
/// Zoom one axis about `anchor` (a data coordinate) by `factor`, recomputing
/// the range so the anchor keeps its screen position. Leaves `auto_scale` false.
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
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <plot/core/types.hpp>
namespace plot::input {
/// Mouse buttons a host can report. The controller never assumes a fixed
/// mapping: right-drag pans as well as left, for hosts that reserve left-drag.
enum class Button { Left, Middle, Right };
/// Keyboard modifier bits for `PointerEvent::mods` / `WheelEvent::mods`.
enum class Mod { None = 0, Shift = 1, Ctrl = 2, Alt = 4, Super = 8 };
/// True when `mods` holds `m`. The single place modifier bits are tested, so a
/// call site never hand-rolls a mask.
constexpr bool has_mod(u32 mods, Mod m) noexcept {
return (mods & static_cast<u32>(m)) != 0u;
}
/// A pointer event in viewport pixels (origin top-left, the same space as
/// `Rect`). `dx`/`dy` are the movement since the previous event; the controller
/// reads them for panning and the absolute `x`/`y` for anchoring and hit tests.
/// `pressed` and `released` are the transitions; `down` is the state after the
/// event. A plain move has all three false/true as appropriate (down only).
struct PointerEvent {
f32 x = 0.0f;
f32 y = 0.0f;
f32 dx = 0.0f;
f32 dy = 0.0f;
Button button = Button::Left;
u32 mods = 0;
bool down = false; ///< the button is held after this event
bool pressed = false; ///< a press transition happened on this event
bool released = false; ///< a release transition happened on this event
};
/// A wheel/scroll event at a pixel position. A positive `dy` (scroll up) zooms
/// in; a positive `dx` scrolls right. Only `dy` drives the controller's zoom.
struct WheelEvent {
f32 x = 0.0f;
f32 y = 0.0f;
f32 dx = 0.0f;
f32 dy = 0.0f;
u32 mods = 0;
};
/// A key event. `key` is an ASCII codepoint for printable keys; hosts that only
/// have scancodes translate them before calling. `down` distinguishes the press
/// from the release -- the controller's actions are press-only.
struct KeyEvent {
int key = 0;
u32 mods = 0;
bool down = false;
};
/// The printable keys the controller reacts to, named so no call site spells a
/// bare character literal. `kKeyZoomIn`/`kKeyZoomOut` zoom about the plot centre,
/// `kKeyFit` re-fits every axis to its traces.
inline constexpr int kKeyZoomIn = '+';
inline constexpr int kKeyZoomOut = '-';
inline constexpr int kKeyFit = 'f';
/// Hosts that report shifted keys uppercase can pass this unchanged.
inline constexpr int kKeyFitUpper = 'F';
} // namespace plot::input