wip(plot): region layout, hot handles and draggable handles (build restored)
This commit is contained in:
1 parent
7d2349555d
commit
e4d48cc6c3
9 files changed
+915
-37
No files matched your search
@@ -3,6 +3,7 @@
|
||||
#include <plot/core/rect.hpp>
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/input/events.hpp>
|
||||
#include <plot/plot/region.hpp>
|
||||
|
||||
namespace plot {
|
||||
class Plot;
|
||||
@@ -15,11 +16,21 @@ 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()`
|
||||
MoveCursor, ///< a drag is moving the cursor marker it grabbed
|
||||
MoveRegion, ///< a drag is translating a whole region, width preserved
|
||||
ResizeRegionLeft, ///< a drag is moving the bound that was `x0` when it opened
|
||||
ResizeRegionRight, ///< a drag is moving the bound that was `x1` when it opened
|
||||
AxisZoomX, ///< middle drag inside the x label strip; zooms x only
|
||||
AxisZoomY, ///< middle drag inside the y label strip; zooms y only
|
||||
};
|
||||
|
||||
/// The grab zone of a region, named with the primitive it describes
|
||||
/// (`plot/plot/region.hpp`, which this header imports it from): the input layer
|
||||
/// reports it and the renderer colours by it -- through `Plot::hot_handles()`,
|
||||
/// which carries the zone the pointer is on -- so both layers speak of one type
|
||||
/// rather than of two that would have to be kept in step.
|
||||
using plot::RegionGrab;
|
||||
|
||||
/// 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.
|
||||
///
|
||||
@@ -32,16 +43,20 @@ enum class DragMode {
|
||||
/// 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 |
|
||||
/// | left drag on a handle | `MoveCursor` / `MoveRegion` / `ResizeRegion*` | move the handle the press grabbed |
|
||||
/// | left drag elsewhere | `Pan` | translate both ranges |
|
||||
/// | right drag | `Pan` | same, for hosts that reserve left |
|
||||
/// | 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 |
|
||||
///
|
||||
/// The right button pans *and* grabs: both are buttons a host drives drags with, so
|
||||
/// neither one is singled out for handles. Middle keeps its own role on the label
|
||||
/// strips, and a middle drag anywhere else still does nothing.
|
||||
///
|
||||
/// 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
|
||||
@@ -51,6 +66,49 @@ enum class DragMode {
|
||||
/// 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.
|
||||
///
|
||||
/// ## Direct manipulation (spec §5)
|
||||
///
|
||||
/// Cursors and regions are *grabbable*: a press on one enters a drag that moves it,
|
||||
/// with no modifier, and a press anywhere else pans exactly as it always has -- so
|
||||
/// the new gesture is a superset of the old one and every host that drives a drag
|
||||
/// keeps panning. `Ctrl` is no longer needed for a cursor move and does not change
|
||||
/// what a press on one does; it only means the same thing it always did (a press
|
||||
/// away from a handle pans, with or without it).
|
||||
///
|
||||
/// One hit test runs for both, in this order, and that precedence is the whole rule:
|
||||
///
|
||||
/// 1. **Cursor markers first**, at the radius `hovered_cursor()` already uses, the
|
||||
/// nearest one winning. A marker is drawn on top of everything and is hit-tested
|
||||
/// the same way: the shaded band between two measurement cursors is grabbed by
|
||||
/// dragging either of the cursors that bound it, because its edges *are* those
|
||||
/// cursors -- no second path, no second tolerance.
|
||||
/// 2. **Regions next**, resolved nearest-first against the geometry `Plot::layout()`
|
||||
/// measured: a bound inside the grab radius beats the body (so the bound of a
|
||||
/// wide region is grabbable even though the pointer is inside the fill), the
|
||||
/// nearer of the two bounds wins, and with no bound in range the topmost region
|
||||
/// whose interior holds the pointer is the body.
|
||||
/// 3. **Nothing hot**, and the press pans.
|
||||
///
|
||||
/// A region narrower than a few logical pixels is a click rather than an edge grab:
|
||||
/// its two bounds are under the pointer together, so an edge would always win and
|
||||
/// the region could never be moved again once collapsed. Such a region is
|
||||
/// grabbed by its body instead, widened to that same minimum, and it stays movable.
|
||||
/// The *body* drag translates both bounds by one delta -- the width is preserved
|
||||
/// exactly -- while a bound drag moves one end and leaves the other where it was,
|
||||
/// so a drag that crosses one bound over the other swaps them rather than inverting
|
||||
/// the region (`Plot::set_region()` orders on assignment).
|
||||
///
|
||||
/// A region that asks to snap is put on the sample grid once, *on release*, not on
|
||||
/// every motion: a body dragged a fraction of a sample apart would otherwise snap
|
||||
/// both ends back to where they started and never move at all.
|
||||
///
|
||||
/// The hovered and grabbed handles are published to the plot
|
||||
/// (`Plot::set_hot_handles()`) after every pointer event, so the renderer draws them
|
||||
/// highlighted without knowing what a pointer is; `hovered_cursor()`,
|
||||
/// `grabbed_cursor()`, `hovered_region()`, `grabbed_region()` and `region_grab()` are
|
||||
/// the same answer for a host that wants it directly -- a resize cursor, a tooltip,
|
||||
/// a status line.
|
||||
///
|
||||
/// ## Zoom and pan on a live x axis (spec §12)
|
||||
///
|
||||
/// An x axis armed with `XFollow::Live` keeps following through every gesture
|
||||
@@ -122,6 +180,22 @@ class Controller {
|
||||
/// 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;
|
||||
/// The cursor a drag is moving, or `kInvalid` when no cursor drag is open. The two
|
||||
/// are the same answer the plot publishes for the renderer (see HotHandles), for a
|
||||
/// host that wants it directly.
|
||||
u64 grabbed_cursor() const noexcept { return drag_cursor_; }
|
||||
/// The region a drag is moving or resizing, or `kInvalid` when no region drag is
|
||||
/// open.
|
||||
u64 grabbed_region() const noexcept { return drag_region_; }
|
||||
/// The region the pointer is on, or `kInvalid`. `region_grab()` says which zone.
|
||||
u64 hovered_region() const;
|
||||
/// Which zone of `region` the pointer is on: the bound nearest within the grab
|
||||
/// radius (`Left`/`Right`), else `Body` when the pointer is inside the interval --
|
||||
/// widened to the minimum a collapsed region keeps -- and `None` when it is on
|
||||
/// neither, when the pointer is outside the plot area, or when no pointer has been
|
||||
/// seen yet. What a host sets its resize cursor from; an id that names no region
|
||||
/// reports `None`.
|
||||
RegionGrab region_grab(u32 region) const;
|
||||
|
||||
private:
|
||||
void begin_drag(const PointerEvent& e);
|
||||
@@ -132,12 +206,46 @@ class Controller {
|
||||
void box_zoom_update(const PointerEvent& e);
|
||||
void apply_box_zoom();
|
||||
void move_cursor(const PointerEvent& e);
|
||||
void move_region(const PointerEvent& e);
|
||||
void resize_region(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 region and zone a pointer position is on. Both fields are "nothing" when
|
||||
/// the pointer is outside `axes` or no region is hot; `zone` is `Body` for an
|
||||
/// interior hit and never `None` for a region that is named.
|
||||
struct RegionHot {
|
||||
u64 region = kInvalid;
|
||||
RegionGrab zone = RegionGrab::None;
|
||||
};
|
||||
/// One region's zone under a pointer position, with the distance to the bound the
|
||||
/// zone names: 0 for a body, and meaningless for `None`. The one place the region
|
||||
/// hit test lives -- `region_hot()` compares what it returns and `region_grab()`
|
||||
/// reports it -- so the nearest-first rule is written once.
|
||||
struct ZoneHit {
|
||||
RegionGrab zone = RegionGrab::None;
|
||||
f32 distance = 0.0f;
|
||||
};
|
||||
ZoneHit zone_hit(const RegionLayout& region, f32 px, f32 py) const;
|
||||
/// Resolves that hit against the geometry `Plot::layout()` measured -- the same
|
||||
/// rects the renderer paints, so what is drawn and what can be grabbed cannot
|
||||
/// drift apart -- with the precedence the class comment states: nearest grabbable
|
||||
/// bound first, topmost body after it.
|
||||
RegionHot region_hot(f32 px, f32 py) const;
|
||||
/// Opens the drag a press asks for: the cursor hit first, then the region zones.
|
||||
/// False when the press is on no handle at all, i.e. when the caller should pan.
|
||||
/// Records what the drag needs to keep hold of -- the bound an edge resize must
|
||||
/// leave alone, the bounds and the grabbed data x a body move needs to preserve
|
||||
/// the width -- so a changed pointer motion never accumulates error.
|
||||
bool begin_handle_drag(const PointerEvent& e);
|
||||
/// Publishes what the pointer is on to the plot, for the renderer to highlight
|
||||
/// (see HotHandles). Called after every pointer event, so hover and grab are the
|
||||
/// same answer the hit test gave.
|
||||
void publish_hot();
|
||||
|
||||
/// 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
|
||||
@@ -159,6 +267,20 @@ class Controller {
|
||||
f32 origin_x_ = 0.0f;
|
||||
f32 origin_y_ = 0.0f;
|
||||
u64 drag_cursor_ = kInvalid;
|
||||
/// The region drag's state. `drag_region_` is the region and `drag_zone_` the zone
|
||||
/// the press resolved, kept for the whole gesture so the highlight a grab started
|
||||
/// follows the grab rather than the pointer. The three numbers are what keeps a
|
||||
/// motion from accumulating error: an edge resize holds `drag_fixed_x_`, the bound
|
||||
/// it must leave alone, and a body move holds the bounds as they were at the press
|
||||
/// (`drag_x0_`/`drag_x1_`) plus the data x the press grabbed (`drag_grab_x_`), so
|
||||
/// each motion is a fresh function of the pointer rather than a step from the last
|
||||
/// one.
|
||||
u64 drag_region_ = kInvalid;
|
||||
RegionGrab drag_zone_ = RegionGrab::None;
|
||||
f64 drag_fixed_x_ = 0.0;
|
||||
f64 drag_grab_x_ = 0.0;
|
||||
f64 drag_x0_ = 0.0;
|
||||
f64 drag_x1_ = 0.0;
|
||||
bool has_pointer_ = false;
|
||||
f32 pointer_x_ = 0.0f;
|
||||
f32 pointer_y_ = 0.0f;
|
||||
|
||||
Reference in new issue
Block a user