#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` 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 px, so /// a 200 px drag scales the span by 10. /// /// 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. /// /// ## 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 multiplied by `Plot::ui_scale()`, /// which is also why the controller reads the scale off the plot it already holds /// instead of keeping a copy a host would have to keep in step. /// /// The middle-drag axis zoom's per-pixel gain is deliberately *not* in that group: /// it is a data-navigation sensitivity rather than a size (the wheel and `+`/`-` /// are scale-free already, being counted in notches), and spec ยง12 reworks the /// zoom and pan gestures that own it. 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; /// 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