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
+199
-14
@@ -33,6 +33,14 @@ constexpr f32 kCursorHitRadiusLogical = 5.0f;
|
||||
/// A box-zoom selection narrower than this on either side is a click. Logical for
|
||||
/// the same reason: the gesture is the same hand movement on any display.
|
||||
constexpr f32 kMinBoxLogical = 4.0f;
|
||||
/// The narrowest region whose bounds are grabbable, in **logical** pixels. Closer
|
||||
/// together than this, the two bounds are one target the pointer cannot separate:
|
||||
/// an edge would win the hit test every time and the region could never be moved
|
||||
/// again, so a *narrower* region is grabbed by its body -- widened to this same
|
||||
/// minimum around its bounds, which keeps a collapsed region something a pointer can
|
||||
/// still take hold of rather than a line. Logical like the two tolerances above, and
|
||||
/// converted through the same one call.
|
||||
constexpr f32 kMinRegionEdgeGrabLogical = 3.0f;
|
||||
|
||||
/// An axis endpoint in transform space: log10 when the axis is logarithmic, the
|
||||
/// value itself otherwise. Pan and zoom arithmetic runs in this space so a log
|
||||
@@ -96,6 +104,9 @@ void Controller::set_viewport(Rect px) {
|
||||
// transforms read.
|
||||
plot_->set_viewport(px);
|
||||
plot_->layout();
|
||||
// The hover the layout is part of: a handle under the pointer may be somewhere
|
||||
// else now, and the highlight the renderer draws reads what this publishes.
|
||||
publish_hot();
|
||||
}
|
||||
|
||||
void Controller::on_pointer(const PointerEvent& raw) {
|
||||
@@ -107,13 +118,15 @@ void Controller::on_pointer(const PointerEvent& raw) {
|
||||
|
||||
if (e.pressed && e.down) {
|
||||
begin_drag(e);
|
||||
return;
|
||||
}
|
||||
if (e.released) {
|
||||
} else if (e.released) {
|
||||
end_drag(e);
|
||||
return;
|
||||
} else if (e.down && mode_ != DragMode::None) {
|
||||
update_drag(e);
|
||||
}
|
||||
if (e.down && mode_ != DragMode::None) update_drag(e);
|
||||
// Every exit above ends here: a hover that moved, a gesture that opened, and a
|
||||
// gesture that closed all change what is under the pointer, and the publish is
|
||||
// what makes the renderer's highlight (and a host's resize cursor) follow.
|
||||
publish_hot();
|
||||
}
|
||||
|
||||
void Controller::on_wheel(const WheelEvent& raw) {
|
||||
@@ -180,27 +193,112 @@ u64 Controller::hovered_cursor() const {
|
||||
return cursor_at(pointer_x_, pointer_y_);
|
||||
}
|
||||
|
||||
u64 Controller::hovered_region() const {
|
||||
if (!has_pointer_) return kInvalid;
|
||||
return region_hot(pointer_x_, pointer_y_).region;
|
||||
}
|
||||
|
||||
RegionGrab Controller::region_grab(u32 region) const {
|
||||
if (!has_pointer_) return RegionGrab::None;
|
||||
for (const RegionLayout& candidate : std::as_const(*plot_).layout().regions) {
|
||||
if (candidate.region == region) return zone_hit(candidate, pointer_x_, pointer_y_).zone;
|
||||
}
|
||||
return RegionGrab::None; // an id that names no visible region
|
||||
}
|
||||
|
||||
Controller::ZoneHit Controller::zone_hit(const RegionLayout& region, f32 px, f32 py) const {
|
||||
ZoneHit hit;
|
||||
// Off the data area nothing is hot, whether the pointer is over a label strip or
|
||||
// off the plot entirely. A bound that has scrolled just outside `axes` is still
|
||||
// within reach of a pointer inside it -- the slack the cursor hit test has too --
|
||||
// but the strips are not a place a region can be grabbed from.
|
||||
if (!std::as_const(*plot_).layout().axes.contains(px, py)) return hit;
|
||||
|
||||
const f32 left_x = handle_column(region.left_handle);
|
||||
const f32 right_x = handle_column(region.right_handle);
|
||||
const f32 min_w = plot_->style().device(kMinRegionEdgeGrabLogical);
|
||||
// Grabbable bounds: the nearer of the two, when it is within the radius every
|
||||
// other handle in this controller is hit-tested with -- one tolerance, so what
|
||||
// `hovered_cursor()` calls a hit and what a region bound calls a hit agree.
|
||||
if (right_x - left_x >= min_w) {
|
||||
const f32 dl = std::fabs(px - left_x);
|
||||
const f32 dr = std::fabs(px - right_x);
|
||||
const f32 d = std::min(dl, dr);
|
||||
if (d <= plot_->style().device(kCursorHitRadiusLogical)) {
|
||||
hit.zone = dl <= dr ? RegionGrab::Left : RegionGrab::Right;
|
||||
hit.distance = d;
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
// No bound in range: the body, widened by half the minimum on each side so a
|
||||
// region too narrow to have grabbable bounds is still something a pointer can take
|
||||
// hold of rather than a line it cannot hit.
|
||||
const f32 grow = 0.5f * min_w;
|
||||
if (px >= left_x - grow && px <= right_x + grow) hit.zone = RegionGrab::Body;
|
||||
return hit;
|
||||
}
|
||||
|
||||
Controller::RegionHot Controller::region_hot(f32 px, f32 py) const {
|
||||
RegionHot hot;
|
||||
u64 body = kInvalid;
|
||||
f32 nearest = 0.0f;
|
||||
for (const RegionLayout& candidate : std::as_const(*plot_).layout().regions) {
|
||||
const ZoneHit hit = zone_hit(candidate, px, py);
|
||||
if (hit.zone == RegionGrab::None) continue;
|
||||
if (hit.zone == RegionGrab::Body) {
|
||||
// Remembered rather than chosen: a body only wins when no bound *anywhere* is
|
||||
// in range, and the last region holding the pointer is the one on top --
|
||||
// regions are drawn in order, so a later one covers an earlier one's interior.
|
||||
body = candidate.region;
|
||||
continue;
|
||||
}
|
||||
if (hot.region == kInvalid || hit.distance < nearest) {
|
||||
hot.region = candidate.region;
|
||||
hot.zone = hit.zone;
|
||||
nearest = hit.distance;
|
||||
}
|
||||
}
|
||||
if (hot.region != kInvalid) return hot;
|
||||
return body == kInvalid ? RegionHot{} : RegionHot{body, RegionGrab::Body};
|
||||
}
|
||||
|
||||
void Controller::publish_hot() {
|
||||
HotHandles hot;
|
||||
if (has_pointer_) {
|
||||
hot.cursor = cursor_at(pointer_x_, pointer_y_);
|
||||
if (drag_region_ != kInvalid) {
|
||||
// An open region drag keeps its own zone hot for as long as it runs, so the
|
||||
// highlight follows the grab rather than the pointer: a body dragged out from
|
||||
// under the pointer still reads as the region being moved, and a bound keeps
|
||||
// its emphasis after the pointer has left it.
|
||||
hot.region = drag_region_;
|
||||
hot.zone = drag_zone_;
|
||||
} else {
|
||||
const RegionHot region = region_hot(pointer_x_, pointer_y_);
|
||||
hot.region = region.region;
|
||||
hot.zone = region.zone;
|
||||
}
|
||||
}
|
||||
hot.grabbed_cursor = drag_cursor_;
|
||||
plot_->set_hot_handles(hot);
|
||||
}
|
||||
|
||||
// --- drag dispatch --------------------------------------------------------
|
||||
|
||||
void Controller::begin_drag(const PointerEvent& e) {
|
||||
origin_x_ = e.x;
|
||||
origin_y_ = e.y;
|
||||
drag_cursor_ = kInvalid;
|
||||
drag_region_ = kInvalid;
|
||||
drag_zone_ = RegionGrab::None;
|
||||
|
||||
// Shift first, and only on the left: it is a modifier gesture of its own, and a
|
||||
// selection box is what it means even when it starts over a handle.
|
||||
if (e.button == Button::Left && has_mod(e.mods, Mod::Shift)) {
|
||||
mode_ = DragMode::BoxZoom;
|
||||
selection_ = Rect{e.x, e.y, 0.0f, 0.0f};
|
||||
return;
|
||||
}
|
||||
if (e.button == Button::Left && has_mod(e.mods, Mod::Ctrl)) {
|
||||
const u64 hit = cursor_at(e.x, e.y);
|
||||
if (hit != kInvalid) {
|
||||
mode_ = DragMode::MoveCursor;
|
||||
drag_cursor_ = hit;
|
||||
return;
|
||||
}
|
||||
// Ctrl far from any cursor falls through to pan, like an unmodified drag.
|
||||
}
|
||||
if (e.button == Button::Middle) {
|
||||
const PlotLayout& lay = std::as_const(*plot_).layout();
|
||||
if (lay.x_label_area.contains(e.x, e.y)) {
|
||||
@@ -212,14 +310,68 @@ void Controller::begin_drag(const PointerEvent& e) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Everything else -- the left and the right button both, since a host that
|
||||
// reserves the left one still drives its drags with the right -- grabs the handle
|
||||
// under the press before it pans. Ctrl needs no special case: a press on a cursor
|
||||
// does exactly this with or without it, and a ctrl press on nothing still pans.
|
||||
if (begin_handle_drag(e)) return;
|
||||
mode_ = DragMode::Pan; // left and right drag both pan
|
||||
}
|
||||
|
||||
bool Controller::begin_handle_drag(const PointerEvent& e) {
|
||||
// Cursor markers first: they are what a measure's band edges *are* (a band
|
||||
// between two cursors is resized by dragging those cursors), and a marker is drawn
|
||||
// over everything else, so it is the thing a press on it means.
|
||||
const u64 cursor = cursor_at(e.x, e.y);
|
||||
if (cursor != kInvalid) {
|
||||
mode_ = DragMode::MoveCursor;
|
||||
drag_cursor_ = cursor;
|
||||
return true;
|
||||
}
|
||||
|
||||
const RegionHot hot = region_hot(e.x, e.y);
|
||||
if (hot.region == kInvalid) return false;
|
||||
drag_region_ = hot.region;
|
||||
drag_zone_ = hot.zone;
|
||||
const Region& region = plot_->region(static_cast<u32>(hot.region));
|
||||
switch (hot.zone) {
|
||||
case RegionGrab::Left:
|
||||
// The other bound is held for the whole gesture: each motion is then the two
|
||||
// numbers this drag is made of, not a step from the previous one, so a pointer
|
||||
// that moves back and forth cannot accumulate a drift -- and when it crosses
|
||||
// the held bound the store orders them, which swaps the interval instead of
|
||||
// inverting it (see Plot::set_region).
|
||||
mode_ = DragMode::ResizeRegionLeft;
|
||||
drag_fixed_x_ = region.x1;
|
||||
return true;
|
||||
case RegionGrab::Right:
|
||||
mode_ = DragMode::ResizeRegionRight;
|
||||
drag_fixed_x_ = region.x0;
|
||||
return true;
|
||||
case RegionGrab::Body:
|
||||
// A body drag preserves the width exactly by construction: both bounds move by
|
||||
// the same delta from where they were when the gesture opened.
|
||||
mode_ = DragMode::MoveRegion;
|
||||
drag_x0_ = region.x0;
|
||||
drag_x1_ = region.x1;
|
||||
drag_grab_x_ = plot_->x_transform(0u).to_data_x(static_cast<f64>(e.x));
|
||||
return true;
|
||||
case RegionGrab::None:
|
||||
break;
|
||||
}
|
||||
drag_region_ = kInvalid;
|
||||
drag_zone_ = RegionGrab::None;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Controller::update_drag(const PointerEvent& e) {
|
||||
switch (mode_) {
|
||||
case DragMode::Pan: pan(e); break;
|
||||
case DragMode::BoxZoom: box_zoom_update(e); break;
|
||||
case DragMode::MoveCursor: move_cursor(e); break;
|
||||
case DragMode::MoveRegion: move_region(e); break;
|
||||
case DragMode::ResizeRegionLeft:
|
||||
case DragMode::ResizeRegionRight: resize_region(e); break;
|
||||
case DragMode::AxisZoomX:
|
||||
case DragMode::AxisZoomY: axis_zoom(e); break;
|
||||
case DragMode::None: break;
|
||||
@@ -235,9 +387,15 @@ void Controller::end_drag(const PointerEvent& e) {
|
||||
box_zoom_update(e);
|
||||
apply_box_zoom();
|
||||
}
|
||||
// A region's snap is applied exactly once, here, on the gesture's final bounds:
|
||||
// the drag itself stores them verbatim, so they follow the pointer instead of
|
||||
// sticking to the sample grid as it moves (see Plot::snap_region()).
|
||||
if (drag_region_ != kInvalid) plot_->snap_region(static_cast<u32>(drag_region_));
|
||||
mode_ = DragMode::None;
|
||||
selection_ = Rect{};
|
||||
drag_cursor_ = kInvalid;
|
||||
drag_region_ = kInvalid;
|
||||
drag_zone_ = RegionGrab::None;
|
||||
}
|
||||
|
||||
// --- gestures -------------------------------------------------------------
|
||||
@@ -315,6 +473,33 @@ void Controller::move_cursor(const PointerEvent& e) {
|
||||
plot_->set_cursor_x(id, plot_->x_transform(cursor.axis_x).to_data_x(static_cast<f64>(e.x)));
|
||||
}
|
||||
|
||||
void Controller::move_region(const PointerEvent& e) {
|
||||
if (drag_region_ == kInvalid) return;
|
||||
// The pointer's own data coordinate, not the event's delta: the delta a host
|
||||
// reports for one motion is not always the difference between the positions it
|
||||
// reported, and a region's two bounds must move by exactly the same amount or the
|
||||
// width changes. Both come from the bounds the gesture opened with plus this one
|
||||
// offset, so nothing accumulates over a long drag.
|
||||
const f64 grabbed = plot_->x_transform(0u).to_data_x(static_cast<f64>(e.x));
|
||||
const f64 delta = grabbed - drag_grab_x_;
|
||||
plot_->set_region(static_cast<u32>(drag_region_), drag_x0_ + delta, drag_x1_ + delta);
|
||||
}
|
||||
|
||||
void Controller::resize_region(const PointerEvent& e) {
|
||||
if (drag_region_ == kInvalid) return;
|
||||
const f64 x = plot_->x_transform(0u).to_data_x(static_cast<f64>(e.x));
|
||||
// The bound the gesture did not grab stays where it was for the whole drag, and
|
||||
// the grabbed one is wherever the pointer says. When the two cross, the *store*
|
||||
// orders them (`set_region` keeps min and max), so the interval swaps rather than
|
||||
// inverting -- and the pointer keeps hold of its bound: the next motion is still
|
||||
// "the other end plus this pointer", with no state to get stuck in.
|
||||
if (mode_ == DragMode::ResizeRegionLeft) {
|
||||
plot_->set_region(static_cast<u32>(drag_region_), x, drag_fixed_x_);
|
||||
} else {
|
||||
plot_->set_region(static_cast<u32>(drag_region_), drag_fixed_x_, x);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::axis_zoom(const PointerEvent& e) {
|
||||
// A pointer drag reports pixels, not wheel notches, so it uses its own
|
||||
// sensitivity: 200 px is one decade. The gain is a physical distance the hand
|
||||
|
||||
Reference in new issue
Block a user