#pragma once #include #include #include 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