feat(examples): a three-row synced Figure demo, on a GLFW host that draws one
The multi-plot example the phase exists for: three plots stacked in one Figure, sharing one x link with following armed, each row carrying its own named signals and its own legend, driven by six StreamSeries at 500 kS/s. Two cursors and a region with an exact statistics readout reach the direct manipulation of spec §5 in the same frame, and PLOT_DEMO_SYNC=1 is a self-checking mode that exits non-zero unless all three rows hold one range and the shared right edge keeps tracking the newest sample through a zoom, a pan on another row and more arriving data. The GLFW host gains the figure mode the demo needs: set_figure() makes the run loop draw the Figure instead of its own plot and route each pointer, wheel and key event to the row it lands in, through one Controller per row (row_controller()). All of it is opt-in; the single-plot path is untouched.
This commit is contained in:
1 parent
770da4c2b2
commit
942f88c568
9 files changed
+917
-39
No files matched your search
@@ -102,6 +102,10 @@ void Host::close() noexcept {
|
||||
}
|
||||
glfwMakeContextCurrent(nullptr);
|
||||
glfwTerminate();
|
||||
// The figure and its row controllers are deliberately *not* dropped here: they are the
|
||||
// caller's state, created by set_figure(), and a second run() reuses them (the run loop
|
||||
// hands each row its rect every frame). Dropping them would silently stop routing
|
||||
// gestures to the rows on the second run.
|
||||
have_pointer_ = false;
|
||||
// A closed host reports no size: a second run() must not see the previous
|
||||
// window's framebuffer size before its first refresh_sizes().
|
||||
@@ -197,17 +201,39 @@ bool Host::run(const RunConfig& cfg, const FrameFn& frame, Status* st) {
|
||||
// and `begin_frame` must agree on: one rect, in framebuffer pixels, set
|
||||
// before the frame callback runs so it sees the layout it draws into.
|
||||
const Rect viewport{0.0f, 0.0f, static_cast<f32>(fb_width_), static_cast<f32>(fb_height_)};
|
||||
controller_.set_viewport(viewport); // also Plot::set_viewport, then layout()
|
||||
if (figure_ != nullptr) {
|
||||
// Figure mode: the rows tile the whole framebuffer, and it is `Figure::layout()`
|
||||
// that hands each row its rect (and so its own `Plot` its viewport). The single
|
||||
// controller is untouched -- each row has its own, see controller_at().
|
||||
figure_->set_viewport(viewport);
|
||||
figure_->layout();
|
||||
// Every row's controller gets that row's rect, so a gesture -- routed by
|
||||
// controller_at() or injected by a scripted run -- is hit-tested against the
|
||||
// geometry the last frame drew, and the two readers of a row's geometry cannot
|
||||
// disagree about it.
|
||||
for (u32 i = 0; i < row_controllers_.size(); ++i) {
|
||||
row_controllers_[i]->set_viewport(figure_->layout().rows[i]);
|
||||
}
|
||||
} else {
|
||||
controller_.set_viewport(viewport); // also Plot::set_viewport, then layout()
|
||||
}
|
||||
|
||||
const f64 now = glfwGetTime();
|
||||
const f64 dt = now - previous;
|
||||
previous = now;
|
||||
if (frame) frame(plot_, dt);
|
||||
// A figure's rows take the pointer, but the frame callback still has to be given a
|
||||
// `Plot&`: row 0 is the one it gets, so the callback argument and `figure()->plot(0)`
|
||||
// are the same plot.
|
||||
if (frame) frame(figure_ != nullptr ? figure_->plot(0u) : plot_, dt);
|
||||
|
||||
const bool exiting = exit_after_s > 0.0 && (now - start) >= exit_after_s;
|
||||
|
||||
renderer_->begin_frame(fb_width_, fb_height_, viewport);
|
||||
renderer_->draw(plot_);
|
||||
if (figure_ != nullptr) {
|
||||
renderer_->draw(*figure_);
|
||||
} else {
|
||||
renderer_->draw(plot_);
|
||||
}
|
||||
if (exiting && screenshot != nullptr && screenshot[0] != '\0') {
|
||||
Status shot;
|
||||
const bool dumped = renderer_->dump_frame_ppm(screenshot, &shot);
|
||||
@@ -303,7 +329,7 @@ void Host::on_cursor_pos(f64 wx, f64 wy) noexcept {
|
||||
have_pointer_ = true;
|
||||
e.mods = mods_now(window_);
|
||||
e.down = any_button_down(window_);
|
||||
controller_.on_pointer(e);
|
||||
controller_at(e.x, e.y).on_pointer(e);
|
||||
}
|
||||
|
||||
void Host::on_mouse_button(int button, int action, int mods) noexcept {
|
||||
@@ -330,7 +356,7 @@ void Host::on_mouse_button(int button, int action, int mods) noexcept {
|
||||
e.pressed = action == GLFW_PRESS;
|
||||
e.released = action == GLFW_RELEASE;
|
||||
e.down = any_button_down(window_);
|
||||
controller_.on_pointer(e);
|
||||
controller_at(e.x, e.y).on_pointer(e);
|
||||
}
|
||||
|
||||
void Host::on_scroll(f64 dx, f64 dy) noexcept {
|
||||
@@ -343,7 +369,7 @@ void Host::on_scroll(f64 dx, f64 dy) noexcept {
|
||||
e.dx = static_cast<f32>(dx);
|
||||
e.dy = static_cast<f32>(dy); // GLFW's positive y (scroll up) is the zoom-in sign
|
||||
e.mods = mods_now(window_);
|
||||
controller_.on_wheel(e);
|
||||
controller_at(e.x, e.y).on_wheel(e);
|
||||
}
|
||||
|
||||
void Host::on_key(int key, int action, int mods) noexcept {
|
||||
@@ -367,7 +393,53 @@ void Host::on_key(int key, int action, int mods) noexcept {
|
||||
// GLFW reports auto-repeat as its own action; a held key keeps acting, exactly
|
||||
// as holding the key on a keyboard does.
|
||||
e.down = action != GLFW_RELEASE;
|
||||
controller_.on_key(e);
|
||||
// A key has no position of its own: it goes to the row the pointer was last over,
|
||||
// which is the row the user is looking at.
|
||||
input::Controller& controller =
|
||||
(figure_ == nullptr || last_row_ >= row_controllers_.size()) ? controller_
|
||||
: *row_controllers_[last_row_];
|
||||
controller.on_key(e);
|
||||
}
|
||||
|
||||
void Host::set_figure(Figure* figure) noexcept {
|
||||
figure_ = figure;
|
||||
row_controllers_.clear();
|
||||
last_row_ = 0u;
|
||||
if (figure_ == nullptr) return;
|
||||
const u32 rows = figure_->row_count();
|
||||
row_controllers_.reserve(rows);
|
||||
for (u32 i = 0; i < rows; ++i) {
|
||||
row_controllers_.push_back(std::make_unique<input::Controller>(figure_->plot(i)));
|
||||
}
|
||||
}
|
||||
|
||||
input::Controller* Host::row_controller(u32 row) noexcept {
|
||||
return row < row_controllers_.size() ? row_controllers_[row].get() : nullptr;
|
||||
}
|
||||
|
||||
input::Controller& Host::controller_at(f32 x, f32 y) noexcept {
|
||||
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
|
||||
const FigureLayout& layout = figure_->layout();
|
||||
const std::vector<Rect>& rows = layout.rows;
|
||||
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
||||
u32 row = 0u;
|
||||
if (y >= rows.back().y + rows.back().h) {
|
||||
row = static_cast<u32>(rows.size() - 1u); // below every row: the nearest one is the last
|
||||
} else {
|
||||
for (u32 i = 0; i < rows.size(); ++i) {
|
||||
if (y >= rows[i].y && y < rows[i].y + rows[i].h) {
|
||||
row = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
(void)x; // the rows tile the figure vertically, so y alone picks one
|
||||
last_row_ = row;
|
||||
input::Controller& controller = *row_controllers_[row];
|
||||
// The row's own viewport, so the geometry this controller hit-tests is the geometry
|
||||
// the last frame drew: the same rect `Figure::layout()` gave the row's plot.
|
||||
controller.set_viewport(rows[row]);
|
||||
return controller;
|
||||
}
|
||||
|
||||
} // namespace plot::glfw
|
||||
@@ -0,0 +1,13 @@
|
||||
e.mods = static_cast<u32>(mods);
|
||||
// GLFW reports auto-repeat as its own action; a held key keeps acting, exactly
|
||||
// as holding the key on a keyboard does.
|
||||
e.down = action != GLFW_RELEASE;
|
||||
// A key has no position of its own: it goes to the row the pointer was last over,
|
||||
// which is the row the user is looking at.
|
||||
input::Controller& controller =
|
||||
(figure_ == nullptr || last_row_ >= row_controllers_.size()) ? controller_
|
||||
: *row_controllers_[last_row_];
|
||||
controller.on_key(e);
|
||||
}
|
||||
|
||||
} // namespace plot::glfw
|
||||
@@ -1,12 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <plot/core/rect.hpp>
|
||||
#include <plot/core/status.hpp>
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/input/controller.hpp>
|
||||
#include <plot/plot/figure.hpp>
|
||||
#include <plot/plot/plot.hpp>
|
||||
#include <plot/render/renderer.hpp>
|
||||
|
||||
@@ -96,6 +99,25 @@ class Host {
|
||||
|
||||
/// The plot the frame callback populates. Valid for the host's whole life.
|
||||
Plot& plot() noexcept { return plot_; }
|
||||
/// Draw a whole `Figure` instead of the host's own plot: the multi-plot shape,
|
||||
/// several rows sharing one x axis, which one `Plot` cannot express. While a
|
||||
/// figure is set the run loop lays it out over the framebuffer, routes each
|
||||
/// pointer, wheel and key event to the row the event lands in (one `Controller`
|
||||
/// per row, created here, each bound to that row's own `Plot`), and draws it with
|
||||
/// `Renderer::draw(Figure&)`. The frame callback is handed the figure's **row 0**
|
||||
/// as its `Plot&`, so `plot()` and the frame argument agree.
|
||||
///
|
||||
/// The figure is borrowed and must outlive the run. Passing null restores the
|
||||
/// single-plot loop, which is what every other caller uses; the host's own
|
||||
/// `plot_` is then untouched by any of this.
|
||||
void set_figure(Figure* figure) noexcept;
|
||||
Figure* figure() noexcept { return figure_; }
|
||||
/// One figure row's controller -- the one a gesture on that row reaches -- or
|
||||
/// null while no figure is set or the row does not exist. A host reads a drag
|
||||
/// mode or a hover out of it (`controller()` is the single-plot loop's, and is
|
||||
/// not the one figure gestures go to).
|
||||
input::Controller* row_controller(u32 row) noexcept;
|
||||
|
||||
/// The gesture controller the host feeds. Valid for the host's whole life.
|
||||
input::Controller& controller() noexcept { return controller_; }
|
||||
|
||||
@@ -133,10 +155,33 @@ class Host {
|
||||
void on_scroll(f64 dx, f64 dy) noexcept;
|
||||
void on_key(int key, int action, int mods) noexcept;
|
||||
|
||||
/// The controller an event at framebuffer pixel (x, y) belongs to: `controller_`
|
||||
/// -- the single-plot loop -- while no figure is set, and the controller of the
|
||||
/// figure row that pixel lands in otherwise. The row rects come from
|
||||
/// `Figure::layout()`, the same ones the renderer draws, so a gesture lands on the
|
||||
/// row it was aimed at; a pixel outside every row (the row gap, or a chrome strip
|
||||
/// below the last row) is clamped to the nearest row rather than dropped, so a
|
||||
/// drag that leaves the data area still ends where it should.
|
||||
///
|
||||
/// It also hands that row its own viewport, which is what keeps the controller's
|
||||
/// hit-test geometry in step with the layout between two frames.
|
||||
input::Controller& controller_at(f32 x, f32 y) noexcept;
|
||||
|
||||
Plot plot_;
|
||||
input::Controller controller_; ///< forwards its viewport to `plot_`
|
||||
std::optional<Renderer> renderer_;
|
||||
GLFWwindow* window_ = nullptr;
|
||||
/// The figure the host draws instead of `plot_`, and one controller per row for
|
||||
/// it -- one per `Plot`, because a `Controller` is bound to a plot at construction
|
||||
/// and this is what makes a gesture on any row reach that row. Both are empty
|
||||
/// until `set_figure()` is called, and the run loop's single-plot path reads
|
||||
/// neither.
|
||||
Figure* figure_ = nullptr;
|
||||
std::vector<std::unique_ptr<input::Controller>> row_controllers_;
|
||||
/// The row the last event landed in, which is where a key event goes: a key has
|
||||
/// no position of its own, and the row the pointer is over is the one the user is
|
||||
/// looking at.
|
||||
u32 last_row_ = 0u;
|
||||
|
||||
u32 fb_width_ = 0; ///< framebuffer size of the last refresh_sizes()
|
||||
u32 fb_height_ = 0;
|
||||
|
||||
Reference in new issue
Block a user