feat: an open box zoom draws the frame the release will apply
The controller has always tracked its selection; nothing drew it, so a Ctrl or Shift drag framed a range with no feedback until the release. The preview is the area the release will change, not what the pointer swept: Ctrl frames the x range only, so a band across the plot is what it will zoom, where the sliver a straight horizontal drag sweeps is only the gesture. The plot carries the rect a host hands it -- it learns nothing about pointers -- and the renderer draws a low-alpha fill under a light border over everything else inside the axes.
This commit is contained in:
1 parent
1fc97d818f
commit
be1f31a04f
14 files changed
+276
-1
No files matched your search
@@ -81,7 +81,10 @@ run touched a GPU, so read it as an upper bound and a record of the software pat
|
||||
- input parity across hosts: pinch-zoom and pan gestures on Qt6 and GTK4 drive the same
|
||||
controller paths as mouse and wheel (touch parity), with keyboard panning
|
||||
(arrow keys), Escape cancelling a drag or box-zoom, double-click re-fitting the axes,
|
||||
and modifier-masked box zoom (Shift = both axes, Ctrl = x only, Alt = y only);
|
||||
and modifier-masked box zoom (Shift = both axes, Ctrl = x only, Alt = y only), the area being
|
||||
chosen framed as the drag runs -- a low-alpha fill under a light border, widened along the axis
|
||||
the release will *not* zoom, so Ctrl's frame is the band it is about to apply rather than the
|
||||
sliver a straight horizontal drag sweeps;
|
||||
- DPI-consistent sizing: metrics are logical units converted once through
|
||||
`Style::ui_scale`;
|
||||
- an anti-aliased embedded monospaced font, plus an optional runtime TrueType backend
|
||||
|
||||
@@ -344,6 +344,21 @@ u32 PlotArea::pointer_row() const noexcept {
|
||||
return row_in(row_rects_, pointer_.y);
|
||||
}
|
||||
|
||||
void PlotArea::sync_selection_frames() noexcept {
|
||||
const auto frame_for = [](input::Controller& c) -> Rect {
|
||||
return c.drag_mode() == input::DragMode::BoxZoom ? c.selection_preview() : Rect{};
|
||||
};
|
||||
if (figure_ != nullptr) {
|
||||
for (u32 i = 0; i < row_controllers_.size(); ++i) {
|
||||
// An empty rect on the rows that are not choosing: `Rect` has no equality to compare
|
||||
// with, and a store of the same value is what keeps this path free of anything else.
|
||||
figure_->plot(i).set_selection_rect(frame_for(*row_controllers_[i]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
plot_->set_selection_rect(frame_for(controller_));
|
||||
}
|
||||
|
||||
void PlotArea::sync_figure_crosshair() noexcept {
|
||||
if (row_rects_.empty() || row_controllers_.empty()) return;
|
||||
const u32 row_count = static_cast<u32>(row_rects_.size());
|
||||
@@ -534,6 +549,9 @@ void PlotArea::render() noexcept {
|
||||
// `::resize` ratio, not `gtk_widget_get_scale_factor()` -- the latter is the
|
||||
// ceiling under fractional scaling and would over-scale the chrome by up to 60%.
|
||||
renderer_->style().ui_scale = mapping_.factor;
|
||||
// After the scale and before the draw: the preview reads the row's layout, and a layout measured
|
||||
// with the previous frame's scale is not the one being drawn.
|
||||
sync_selection_frames();
|
||||
renderer_->begin_frame(static_cast<u32>(viewport.w), static_cast<u32>(viewport.h), viewport);
|
||||
if (figure_ != nullptr) {
|
||||
renderer_->draw(*figure_);
|
||||
|
||||
@@ -397,6 +397,17 @@ class PlotArea {
|
||||
/// Spreads one row's crosshair over the whole figure (see "Figure mode"). Run once
|
||||
/// per frame, from the render callback, after this frame's row rects are cached.
|
||||
void sync_figure_crosshair() noexcept;
|
||||
/// Hands each row's plot the box-zoom frame its controller is showing, or takes it off
|
||||
/// the ones whose gesture is over. Run once per frame from the render callback, like
|
||||
/// the crosshair's spread and for a related reason: a frame has to go away when a drag
|
||||
/// ends by *any* route -- the release, Escape, a row rebuilt mid-gesture -- and a
|
||||
/// per-event hook would have to be repeated at every one of them, which is how a frame
|
||||
/// ends up stuck on screen after a cancelled zoom.
|
||||
///
|
||||
/// Reads `selection_preview()`, which needs the row's layout, only for a controller
|
||||
/// actually in `BoxZoom`: the cost is paid while a range is being chosen and never
|
||||
/// otherwise.
|
||||
void sync_selection_frames() noexcept;
|
||||
/// `figure_->set_viewport(px)`, one `layout()` into the rect cache, and each row's
|
||||
/// own rect to its controller. The figure-mode half of `refresh_viewport()` and of
|
||||
/// `render()`, which must not drift apart.
|
||||
|
||||
@@ -192,6 +192,16 @@ without knowing what a pointer is. `hovered_cursor()`, `grabbed_cursor()`, `hove
|
||||
`grabbed_region()` and `region_grab(u32)` expose the same answers to a host (a resize cursor, a
|
||||
tooltip, a status line).
|
||||
|
||||
The frame an open box zoom is drawing is published the same way and for the same reason
|
||||
(`Plot::set_selection_rect()`), and the plot still learns nothing about pointers: the host reads the
|
||||
rect off the controller -- `Controller::selection_preview()` -- on every frame of the drag and takes it
|
||||
off the plot when the gesture ends. The preview is deliberately not what the pointer swept. Shift
|
||||
frames both axes and its preview is the swept box; Ctrl frames x only, so its preview is widened to the
|
||||
full height of the axes, and Alt's to the full width. The swept sliver is the *gesture*; the widened
|
||||
rect is what the *release* will apply, which is the one worth showing while it is still only a
|
||||
gesture. Nothing is previewed until the drag has swept every axis the release will act on: an empty
|
||||
rect widened is a band the width of the plot, which would claim a range that does not exist yet.
|
||||
|
||||
## Live follow
|
||||
|
||||
An x axis has a follow state, `XFollow::Off` (the default) or `XFollow::Live`
|
||||
|
||||
@@ -229,6 +229,18 @@ class Controller {
|
||||
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 rectangle a host should *draw* for the open box zoom: the swept area, widened to the full
|
||||
/// axes along whichever axis the modifier's mask will not zoom. Ctrl frames the x range only, so its
|
||||
/// preview is the band that spans the plot -- which is what the release will zoom -- where the area
|
||||
/// a purely horizontal drag sweeps is a sliver a few pixels tall. Shift (and any other combo) frames
|
||||
/// both, and gets the swept box itself.
|
||||
///
|
||||
/// Meaningful while `BoxZoom`, like `selection_rect()`; the zero rect otherwise.
|
||||
///
|
||||
/// Defined in the implementation: widening the preview to the axes needs the plot's layout, and
|
||||
/// `Plot` is only forward-declared here.
|
||||
Rect selection_preview() const noexcept;
|
||||
/// The cursor under the pointer -- a vertical marker or a horizontal ruler, whichever
|
||||
/// line the pointer is nearer -- or `kInvalid` when the pointer is outside the plot area
|
||||
/// or no cursor is within a few pixels.
|
||||
|
||||
@@ -144,6 +144,13 @@ struct PlotLayout {
|
||||
/// clamped against the rect it was measured for, so nothing of the readout can be drawn
|
||||
/// over a label strip or into a figure's neighbouring row.
|
||||
CrosshairLayout crosshair;
|
||||
/// The box-zoom frame, when a modified drag has one open: the area the gesture is framing, in
|
||||
/// viewport pixels, already widened along whichever axis the modifier will *not* zoom (see
|
||||
/// `input::Controller::selection_preview()`, which is where that is decided). Here rather than
|
||||
/// derived by the renderer, for the regions' reason -- the frame draws exactly these numbers -- and
|
||||
/// left as the zero rect whenever no gesture is open, which is what both the fill and the four
|
||||
/// border segments read as "draw nothing".
|
||||
Rect selection;
|
||||
};
|
||||
|
||||
/// What a pointer is on, as of the last pointer event: the cursor marker under it, the
|
||||
@@ -753,6 +760,21 @@ class Plot {
|
||||
/// calls this when the pointer leaves the axes.
|
||||
void clear_crosshair() noexcept { crosshair_.active = false; }
|
||||
|
||||
/// The box-zoom frame the host is showing, in **viewport pixels**, or the zero rect for none.
|
||||
///
|
||||
/// Set by the host from `input::Controller::selection_preview()` while a modified left drag is
|
||||
/// choosing a range, and taken off when it ends. It is the *host's* job and not the plot's because
|
||||
/// the plot has no controller and learns nothing about pointer, button or modifier (see
|
||||
/// `plot::input`): this is the receiving end of that boundary, exactly as `set_hot_handles()` is.
|
||||
/// Drawn as a frame -- a slightly opaque fill under a light border -- over everything else inside
|
||||
/// the axes, so the area being framed stays legible against the traces it covers.
|
||||
void set_selection_rect(Rect rect) noexcept { selection_ = rect; }
|
||||
/// Takes the frame off, leaving the same state as before any drag: the next one starts from nothing.
|
||||
void clear_selection_rect() noexcept { selection_ = Rect{}; }
|
||||
/// The frame as set -- what `layout()` measures into `PlotLayout::selection`, and what a host or a
|
||||
/// test reads back without paying a layout.
|
||||
Rect selection_rect() const noexcept { return selection_; }
|
||||
|
||||
/// Switches the readout panel off -- or back on -- without moving the crosshair: the lines and
|
||||
/// the per-trace markers stay, the panel is not measured and no row is listed. A host that wants
|
||||
/// the pointer's column marked but not a panel over its data sets this once; it is deliberately
|
||||
@@ -969,6 +991,11 @@ class Plot {
|
||||
/// The crosshair's state (see Crosshair): at most one per plot, placed by
|
||||
/// `set_crosshair()` and read back by `crosshair()` and `layout()`.
|
||||
Crosshair crosshair_{};
|
||||
|
||||
/// The host's box-zoom frame (`set_selection_rect()`): viewport pixels, the zero rect when no
|
||||
/// modified drag is choosing a range. State rather than a `PlotLayout` field alone because a host
|
||||
/// sets it on every drag motion and reads it back; `layout()` only carries it through.
|
||||
Rect selection_{};
|
||||
/// The samples `crosshair_value()` reads a span through: `Series::gather()` appends
|
||||
/// `(x, y)` pairs and never clears, so the lookup clears this and refills it with the at
|
||||
/// most two samples a span covers. Sized to hold them from construction -- four floats is
|
||||
|
||||
@@ -654,6 +654,11 @@ class Renderer {
|
||||
/// pointer is on: a readout the legend covered would be the feature failing at its one job.
|
||||
void draw_crosshair_readout(const Plot& plot, const PlotLayout& layout);
|
||||
|
||||
/// The host's box-zoom frame (`PlotLayout::selection`): a fill at `selection_fill` under a border at
|
||||
/// `selection_edge`, or nothing at all when nothing was swept. Drawn last of the interior passes, so
|
||||
/// the area being framed is legible over the traces, the regions and the readout alike.
|
||||
void draw_selection_frame(const PlotLayout& layout);
|
||||
|
||||
/// One marker shape (`Marker`) at a viewport-pixel centre: `Cross` two
|
||||
/// segments, `Circle` an 8-gon, `Square` four. Staged into the line batch in
|
||||
/// pixel space, at `size_px` half-extent, so a marker's size is device pixels
|
||||
|
||||
@@ -55,6 +55,14 @@ struct Style {
|
||||
Color axis_text = Color::hex(0xC8D0DC);
|
||||
Color cursor = Color::hex(0xFFB020);
|
||||
Color measure_fill = Color::hex(0xFFB020, 0.15f);
|
||||
/// The box-zoom selection's frame, drawn while a modified left drag is choosing a range: a light
|
||||
/// neutral near full alpha for the border and the same neutral at a low one for the fill. Neutral
|
||||
/// and not a trace colour, on `invalid_fill`'s argument -- the frame says "this is the area", and a
|
||||
/// tint a trace could have would read as data. The fill's alpha is chosen against the same floor:
|
||||
/// low enough that what is under the frame stays readable, because the point is to show what is
|
||||
/// being selected, not to hide it.
|
||||
Color selection_fill = Color::hex(0xC8D0DC, 0.12f);
|
||||
Color selection_edge = Color::hex(0xC8D0DC, 0.85f);
|
||||
/// The shading over a span the host has declared invalid (`Plot::set_invalid_spans()`): the
|
||||
/// axis furniture's own neutral grey at a low alpha, on the same model as `measure_fill`
|
||||
/// above. Neutral and not a trace colour on purpose -- a band is the *absence* of data, and
|
||||
|
||||
@@ -751,6 +751,31 @@ void Controller::pan(const PointerEvent& e) {
|
||||
}
|
||||
}
|
||||
|
||||
Rect Controller::selection_preview() const noexcept {
|
||||
// Nothing to preview off a box zoom: the mask bits outlive the gesture -- `end_drag()` clears the
|
||||
// selection, not them -- so widening without this guard would hand out a full-band rect for a drag
|
||||
// that finished minutes ago.
|
||||
if (mode_ != DragMode::BoxZoom) return Rect{};
|
||||
// And nothing has been framed until the drag has swept every axis the release will apply: at the
|
||||
// press instant the selection is an empty rect at the pointer, and the *widened* form of that is a
|
||||
// band the width of the plot, which is a claim about a range that does not exist yet. Ctrl's other
|
||||
// axis is deliberately not required: a drag with no vertical movement at all is a band across the
|
||||
// plot, which is exactly what Ctrl is for.
|
||||
if ((box_zoom_x_ && !(selection_.w > 0.0f)) || (box_zoom_y_ && !(selection_.h > 0.0f))) return Rect{};
|
||||
Rect preview = selection_;
|
||||
if (box_zoom_x_ && box_zoom_y_) return preview;
|
||||
const Rect& axes = plot_->layout().axes;
|
||||
if (!box_zoom_x_) {
|
||||
preview.x = axes.x;
|
||||
preview.w = axes.w;
|
||||
}
|
||||
if (!box_zoom_y_) {
|
||||
preview.y = axes.y;
|
||||
preview.h = axes.h;
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
void Controller::box_zoom_update(const PointerEvent& e) {
|
||||
const f32 x = std::min(origin_x_, e.x);
|
||||
const f32 y = std::min(origin_y_, e.y);
|
||||
|
||||
@@ -1101,6 +1101,10 @@ const PlotLayout& Plot::layout() {
|
||||
// its rows are the sample values nearest a data x -- so it wants the transforms the
|
||||
// settled layout pins, exactly as the legend and the regions do.
|
||||
build_crosshair();
|
||||
// The box-zoom frame is not measured, only carried: the host already has it in viewport pixels from
|
||||
// the controller that produced it, and re-deriving it here would be a second rounding of the same
|
||||
// rectangle -- and a second place for it to disagree with what the gesture is doing.
|
||||
layout_.selection = selection_;
|
||||
return layout_;
|
||||
}
|
||||
|
||||
|
||||
@@ -545,6 +545,9 @@ void Renderer::draw_plot(Plot& plot, const PlotLayout& layout) {
|
||||
// panel is drawn over the legend and every other overlay rather than under them, and its
|
||||
// text joins the frame's over flush with the tick labels and titles.
|
||||
draw_crosshair_readout(plot, layout);
|
||||
// The selection frame last of all: it is the gesture still in progress, and nothing inside the axes
|
||||
// should be able to sit on top of the area being chosen.
|
||||
draw_selection_frame(layout);
|
||||
clear_clip();
|
||||
draw_tick_labels(plot, layout);
|
||||
draw_titles(plot, layout);
|
||||
@@ -1960,6 +1963,27 @@ void Renderer::stage_marker(plot::Marker marker, Vec2 centre_px, f32 size_px) {
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::draw_selection_frame(const PlotLayout& layout) {
|
||||
const Rect& frame = layout.selection;
|
||||
// Two guards, not one: a drag swept flat in one direction has a real extent on one axis and none on
|
||||
// the other, and a border on that is still the gesture's own answer -- the fill is what a zero
|
||||
// extent has nothing of.
|
||||
if (!(frame.w > 0.0f) && !(frame.h > 0.0f)) return;
|
||||
if (frame.w > 0.0f && frame.h > 0.0f) fill_pixels(frame, style_.selection_fill);
|
||||
// The border frames the pixels the fill covers and not the measured rect's own fractional edges:
|
||||
// the readout panel's rounding and the readout panel's reason, since a hairline straddling a
|
||||
// fractional edge is a frame bright on two sides and dim on the others.
|
||||
const f32 left = std::ceil(frame.x - 0.5f) + 0.5f;
|
||||
const f32 right = std::ceil(frame.right() - 0.5f) - 0.5f;
|
||||
const f32 top = std::ceil(frame.y - 0.5f) + 0.5f;
|
||||
const f32 bottom = std::ceil(frame.bottom() - 0.5f) - 0.5f;
|
||||
lines_.add_segment({left, top}, {right, top});
|
||||
lines_.add_segment({left, bottom}, {right, bottom});
|
||||
lines_.add_segment({left, top}, {left, bottom});
|
||||
lines_.add_segment({right, top}, {right, bottom});
|
||||
flush_lines(pixel_mapping(), style_.selection_edge, style_.device(1.0f));
|
||||
}
|
||||
|
||||
void Renderer::draw_readout_panel(const Rect& panel, Color border) {
|
||||
if (!(panel.w > 0.0f) || !(panel.h > 0.0f)) return;
|
||||
// The frame's own background at a partial alpha, which dims whatever it covers instead of erasing
|
||||
|
||||
@@ -2403,3 +2403,54 @@ PLOT_TEST(an_unselectable_cursor_is_neither_hovered_nor_grabbed) {
|
||||
PLOT_CHECK_NEAR(p.cursor(readout).x, before, 1e-12);
|
||||
PLOT_CHECK(c.grabbed_cursor() == normal);
|
||||
}
|
||||
|
||||
PLOT_TEST(a_ctrl_box_zoom_previews_the_band_it_will_zoom) {
|
||||
// What a host draws for an open box zoom is not always the area the pointer swept. Ctrl frames the
|
||||
// x range only, so what its release will change is a *band* across the plot -- and a pointer dragged
|
||||
// straight across sweeps a sliver a couple of pixels tall, which would be the frame showing the
|
||||
// gesture instead of its effect. Shift frames both axes and gets the swept box itself; Alt is the
|
||||
// other single-axis mask, and gets the other frame.
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, 800, 400});
|
||||
p.add_x_axis({"t", {0.0, 800.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const plot::Rect axes = p.layout().axes;
|
||||
constexpr plot::u32 kAlt = static_cast<plot::u32>(plot::input::Mod::Alt);
|
||||
plot::input::Controller c(p);
|
||||
c.set_viewport({0, 0, 800, 400});
|
||||
|
||||
c.on_pointer({100, 200, 0, 0, plot::input::Button::Left, kCtrl, true, true, false});
|
||||
// Nothing framed until something was swept: the press alone is an empty rect at the pointer, and a
|
||||
// full-width band derived from it would claim a range that does not exist yet.
|
||||
PLOT_CHECK_NEAR(c.selection_preview().w, 0.0, 1e-6);
|
||||
c.on_pointer({300, 202, 200, 2, plot::input::Button::Left, kCtrl, true, false, false});
|
||||
const plot::Rect ctrl = c.selection_preview();
|
||||
PLOT_CHECK_NEAR(ctrl.x, 100.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(ctrl.w, 200.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(ctrl.y, axes.y, 1e-6);
|
||||
PLOT_CHECK_NEAR(ctrl.h, axes.h, 1e-6);
|
||||
// The gesture's own geometry is untouched: the widening happens in the preview, so the range the
|
||||
// release computes is still the one the pointer described.
|
||||
PLOT_CHECK_NEAR(c.selection_rect().h, 2.0, 1e-6);
|
||||
c.on_pointer({300, 202, 0, 0, plot::input::Button::Left, kCtrl, false, false, true});
|
||||
|
||||
c.on_pointer({100, 50, 0, 0, plot::input::Button::Left, kShift, true, true, false});
|
||||
c.on_pointer({400, 150, 300, 100, plot::input::Button::Left, kShift, true, false, false});
|
||||
const plot::Rect both = c.selection_preview();
|
||||
PLOT_CHECK_NEAR(both.x, 100.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(both.y, 50.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(both.w, 300.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(both.h, 100.0, 1e-6);
|
||||
c.on_pointer({400, 150, 0, 0, plot::input::Button::Left, kShift, false, false, true});
|
||||
|
||||
c.on_pointer({100, 50, 0, 0, plot::input::Button::Left, kAlt, true, true, false});
|
||||
c.on_pointer({110, 150, 10, 100, plot::input::Button::Left, kAlt, true, false, false});
|
||||
const plot::Rect alt = c.selection_preview();
|
||||
PLOT_CHECK_NEAR(alt.x, axes.x, 1e-6);
|
||||
PLOT_CHECK_NEAR(alt.w, axes.w, 1e-6);
|
||||
PLOT_CHECK_NEAR(alt.y, 50.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(alt.h, 100.0, 1e-6);
|
||||
c.on_pointer({110, 150, 0, 0, plot::input::Button::Left, kAlt, false, false, true});
|
||||
// Closed: a frame on a finished gesture would claim the old selection is still open.
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
|
||||
PLOT_CHECK_NEAR(c.selection_preview().w, 0.0, 1e-6);
|
||||
}
|
||||
@@ -2789,3 +2789,22 @@ PLOT_TEST(an_annotation_can_be_moved_by_id) {
|
||||
p.set_annotation(42u, other);
|
||||
PLOT_CHECK_NEAR(p.annotation(id).x, 3.5, 1e-12);
|
||||
}
|
||||
|
||||
PLOT_TEST(the_selection_frame_is_carried_and_taken_off) {
|
||||
// The box-zoom frame is the host's to set -- the plot has no controller and learns nothing about
|
||||
// pointers -- so what it owes is to carry it: out through `layout()` unchanged, because it is
|
||||
// already in viewport pixels and a second derivation would be a second rounding of the same
|
||||
// rectangle, and gone when the host takes it off, because a frame left behind claims the selection
|
||||
// is still open.
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, 800, 400});
|
||||
PLOT_CHECK(p.layout().selection.w == 0.0f && p.layout().selection.h == 0.0f); // nothing swept
|
||||
p.set_selection_rect({10.0f, 20.0f, 30.0f, 40.0f});
|
||||
PLOT_CHECK_NEAR(p.selection_rect().x, 10.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(p.layout().selection.y, 20.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(p.layout().selection.w, 30.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(p.layout().selection.h, 40.0, 1e-6);
|
||||
p.clear_selection_rect();
|
||||
PLOT_CHECK_NEAR(p.selection_rect().w, 0.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(p.layout().selection.h, 0.0, 1e-6);
|
||||
}
|
||||
@@ -7461,3 +7461,61 @@ PLOT_TEST(a_repaired_invalid_span_is_fainter_than_a_missing_one) {
|
||||
missing_ink, m_lo, m_hi, repaired_ink, r_lo, r_hi);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
PLOT_TEST(the_box_zoom_frame_lands_exactly_where_it_says) {
|
||||
// The frame a host sets has to reach the framebuffer and cover exactly the rect it names. Both halves
|
||||
// are read back rather than trusted, and the two renders are compared: what is under the frame is a
|
||||
// trace, the grid and the axis chrome, so the assertions are differential -- a predicate guessing
|
||||
// those colours would be testing the guess.
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
plot::data::ArraySeries s(sine_samples(), 0.0, 1.0);
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
const plot::u32 ax = p.add_x_axis({"t", s.x_bounds(), true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
||||
|
||||
// The *floor* of a pixel rather than its luminance: the fill is the axis furniture's light neutral at
|
||||
// a low alpha, so over a saturated trace it can lower a channel while it raises the floor, and the
|
||||
// floor is what says "something light was laid over this".
|
||||
const auto floor_of = [&](plot::u32 x, plot::u32 y) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - y, px));
|
||||
return static_cast<int>(std::min(px[0], std::min(px[1], px[2])));
|
||||
};
|
||||
// The renderer does not clear -- that is the host's -- and a second draw over the first would double
|
||||
// every trace and change every pixel for reasons that have nothing to do with the frame.
|
||||
const auto draw = [&](bool framed) {
|
||||
if (framed) p.set_selection_rect({200.0f, 100.0f, 200.0f, 120.0f});
|
||||
plot::gl::glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
|
||||
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
};
|
||||
|
||||
draw(false);
|
||||
const int border_before = floor_of(200, 160); // the frame's left border, mid-height
|
||||
const int inside_before = floor_of(300, 160); // the middle of the framed area
|
||||
const int outside_before = floor_of(500, 160); // to the right of it
|
||||
draw(true);
|
||||
const int border_after = floor_of(200, 160);
|
||||
const int inside_after = floor_of(300, 160);
|
||||
const int outside_after = floor_of(500, 160);
|
||||
|
||||
// A frame, not a wash: the border is nearly opaque, the fill is not.
|
||||
PLOT_CHECK(border_after > inside_after);
|
||||
PLOT_CHECK(border_after > border_before + 60);
|
||||
// A fill, not a panel: it lightens what it covers and leaves it readable.
|
||||
PLOT_CHECK(inside_after > inside_before + 10);
|
||||
// Exactly the rect, and not a pixel beyond it: this is the assertion that the frame matches the area
|
||||
// the gesture is choosing rather than merely being somewhere on screen.
|
||||
PLOT_CHECK(outside_after == outside_before);
|
||||
p.clear_selection_rect();
|
||||
}
|
||||
Reference in new issue
Block a user