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.
446 lines
17 KiB
C++
446 lines
17 KiB
C++
#include <glfw/glfw_host.hpp>
|
|
|
|
// Before GLFW's header, so it does not include <GL/gl.h>: that header's macros
|
|
// (GL_COLOR_BUFFER_BIT and friends) would rewrite `plot::gl`'s enumerators of the
|
|
// same name. The adapter needs GLFW for the window, the context and the events
|
|
// only -- every GL entry point comes from plot::gl::load().
|
|
#define GLFW_INCLUDE_NONE
|
|
#include <GLFW/glfw3.h>
|
|
|
|
#include <plot/gl/loader.hpp>
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
namespace plot::glfw {
|
|
namespace {
|
|
|
|
/// GLFW_MOD_* and `plot::input::Mod` are the same bits, by design: the
|
|
/// controller documents `Mod` as the host-translatable mask. A key event's mask
|
|
/// therefore passes through, and only a pointer or wheel event, whose callbacks
|
|
/// carry no mask, has to ask the window for the current key state.
|
|
static_assert(static_cast<u32>(GLFW_MOD_SHIFT) == static_cast<u32>(input::Mod::Shift));
|
|
static_assert(static_cast<u32>(GLFW_MOD_CONTROL) == static_cast<u32>(input::Mod::Ctrl));
|
|
static_assert(static_cast<u32>(GLFW_MOD_ALT) == static_cast<u32>(input::Mod::Alt));
|
|
static_assert(static_cast<u32>(GLFW_MOD_SUPER) == static_cast<u32>(input::Mod::Super));
|
|
|
|
u32 mods_now(GLFWwindow* w) noexcept {
|
|
u32 m = 0;
|
|
if (glfwGetKey(w, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ||
|
|
glfwGetKey(w, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS) {
|
|
m |= GLFW_MOD_SHIFT;
|
|
}
|
|
if (glfwGetKey(w, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS ||
|
|
glfwGetKey(w, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS) {
|
|
m |= GLFW_MOD_CONTROL;
|
|
}
|
|
if (glfwGetKey(w, GLFW_KEY_LEFT_ALT) == GLFW_PRESS ||
|
|
glfwGetKey(w, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS) {
|
|
m |= GLFW_MOD_ALT;
|
|
}
|
|
if (glfwGetKey(w, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS ||
|
|
glfwGetKey(w, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS) {
|
|
m |= GLFW_MOD_SUPER;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
bool any_button_down(GLFWwindow* w) noexcept {
|
|
return glfwGetMouseButton(w, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS ||
|
|
glfwGetMouseButton(w, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS ||
|
|
glfwGetMouseButton(w, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS;
|
|
}
|
|
|
|
/// `glfwGetProcAddress` returns GLFW's `void (*)(void)`, `plot::gl::ProcLoader`
|
|
/// returns `void*`: a function pointer respelled as an object pointer. POSIX
|
|
/// guarantees the two have the same representation, and the bit copy states that
|
|
/// where a `reinterpret_cast` would be merely conditionally supported.
|
|
void* resolve_gl(const char* name) noexcept {
|
|
GLFWglproc p = glfwGetProcAddress(name);
|
|
void* out = nullptr;
|
|
static_assert(sizeof(out) == sizeof(p), "function and object pointers must match");
|
|
std::memcpy(&out, &p, sizeof(out));
|
|
return out;
|
|
}
|
|
|
|
bool fail(Status* st, ErrorCode code, const char* message) noexcept {
|
|
if (st != nullptr) {
|
|
st->code = code;
|
|
st->message = message;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// `PLOT_DEMO_EXIT_AFTER_MS` as seconds: 0.0 when unset, unparsable or not
|
|
/// positive, which is "run until the window is closed".
|
|
f64 exit_after_seconds() noexcept {
|
|
const char* v = std::getenv("PLOT_DEMO_EXIT_AFTER_MS");
|
|
if (v == nullptr || v[0] == '\0') return 0.0;
|
|
char* end = nullptr;
|
|
const f64 ms = std::strtod(v, &end);
|
|
if (end == v || !(ms > 0.0)) return 0.0;
|
|
return ms * 1.0e-3;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Host::Host() noexcept : controller_(plot_) {}
|
|
|
|
Host::~Host() { close(); }
|
|
|
|
void Host::close() noexcept {
|
|
// The renderer's RAII handles call glDelete* from their destructors, so the
|
|
// context has to be current -- and alive -- when they run.
|
|
if (renderer_) {
|
|
if (window_ != nullptr) glfwMakeContextCurrent(window_);
|
|
renderer_.reset();
|
|
}
|
|
if (window_ != nullptr) {
|
|
glfwDestroyWindow(window_);
|
|
window_ = nullptr;
|
|
}
|
|
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().
|
|
fb_width_ = 0;
|
|
fb_height_ = 0;
|
|
fb_scale_x_ = 1.0f;
|
|
fb_scale_y_ = 1.0f;
|
|
}
|
|
|
|
bool Host::run(const RunConfig& cfg, const FrameFn& frame, Status* st) {
|
|
if (st != nullptr) *st = Status{};
|
|
close(); // a second run() starts from a clean window
|
|
|
|
if (glfwInit() != GLFW_TRUE) {
|
|
return fail(st, ErrorCode::Unsupported, "glfwInit() failed");
|
|
}
|
|
|
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
|
// Required on macOS 10.7+, ignored elsewhere.
|
|
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
|
|
// A run with a deadline is the automated path, so it stays off-screen: a
|
|
// bounded CI run must never flash a window on a real display.
|
|
const f64 exit_after_s = exit_after_seconds();
|
|
if (exit_after_s > 0.0) glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
|
|
|
GLFWwindow* w = glfwCreateWindow(cfg.width > 0 ? cfg.width : 1,
|
|
cfg.height > 0 ? cfg.height : 1,
|
|
cfg.title != nullptr ? cfg.title : "plot", nullptr, nullptr);
|
|
if (w == nullptr) {
|
|
const char* desc = nullptr;
|
|
(void)glfwGetError(&desc);
|
|
std::fprintf(stderr, "plot::glfw: glfwCreateWindow() failed: %s\n",
|
|
desc != nullptr ? desc : "no error description");
|
|
close();
|
|
return fail(st, ErrorCode::Unsupported,
|
|
"glfwCreateWindow() failed: no OpenGL 3.3 core context (see stderr)");
|
|
}
|
|
|
|
window_ = w;
|
|
glfwSetWindowUserPointer(w, this);
|
|
glfwMakeContextCurrent(w);
|
|
glfwSwapInterval(cfg.vsync ? 1 : 0);
|
|
glfwSetCursorPosCallback(w, &Host::cursor_pos_cb);
|
|
glfwSetMouseButtonCallback(w, &Host::mouse_button_cb);
|
|
glfwSetScrollCallback(w, &Host::scroll_cb);
|
|
glfwSetKeyCallback(w, &Host::key_cb);
|
|
glfwSetFramebufferSizeCallback(w, &Host::framebuffer_size_cb);
|
|
refresh_sizes();
|
|
|
|
if (!plot::gl::load(&resolve_gl)) {
|
|
close();
|
|
return fail(st, ErrorCode::Unsupported,
|
|
"plot::gl::load() failed: the context is missing a required OpenGL 3.3 entry point");
|
|
}
|
|
|
|
renderer_.emplace();
|
|
if (!renderer_->init(st)) {
|
|
close();
|
|
if (st != nullptr && st->code == ErrorCode::Ok) {
|
|
*st = Status{ErrorCode::GLFailure, "Renderer::init() failed"};
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const char* const screenshot = std::getenv("PLOT_DEMO_SCREENSHOT");
|
|
const f64 start = glfwGetTime();
|
|
f64 previous = start;
|
|
|
|
while (glfwWindowShouldClose(window_) == GLFW_FALSE) {
|
|
glfwPollEvents();
|
|
refresh_sizes();
|
|
|
|
if (fb_width_ == 0 || fb_height_ == 0) {
|
|
// Minimised, or a size the compositor reports as zero: nothing to draw
|
|
// into, and nothing to hand `begin_frame` -- a stale larger size would be
|
|
// a lie about the framebuffer. Idle on events rather than spin, and still
|
|
// honour the exit deadline here: a bounded run that never gets a drawable
|
|
// must end, not idle past the deadline it was given. There is no frame to
|
|
// dump in this state, so the screenshot is skipped.
|
|
if (exit_after_s > 0.0 && glfwGetTime() - start >= exit_after_s) {
|
|
std::fprintf(stderr,
|
|
"[glfw] exit deadline reached with no drawable (framebuffer 0x0);"
|
|
" nothing to dump\n");
|
|
break;
|
|
}
|
|
glfwWaitEventsTimeout(0.05);
|
|
continue;
|
|
}
|
|
|
|
// The plot's region is the whole window here, which is what the controller
|
|
// 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_)};
|
|
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;
|
|
// 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);
|
|
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);
|
|
std::printf("[glfw] screenshot %s: %s\n", screenshot,
|
|
dumped ? "written" : shot.message);
|
|
std::fflush(stdout);
|
|
}
|
|
renderer_->end_frame();
|
|
glfwSwapBuffers(window_);
|
|
|
|
if (exiting) break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Host::set_title(const char* title) noexcept {
|
|
if (window_ != nullptr) glfwSetWindowTitle(window_, title != nullptr ? title : "");
|
|
}
|
|
|
|
void Host::refresh_sizes() noexcept {
|
|
if (window_ == nullptr) return;
|
|
int fw = 0;
|
|
int fh = 0;
|
|
int ww = 0;
|
|
int wh = 0;
|
|
glfwGetFramebufferSize(window_, &fw, &fh);
|
|
glfwGetWindowSize(window_, &ww, &wh);
|
|
float sx = 1.0f;
|
|
float sy = 1.0f;
|
|
glfwGetWindowContentScale(window_, &sx, &sy);
|
|
content_scale_x_ = sx;
|
|
content_scale_y_ = sy;
|
|
// Assigned unconditionally, zero included: an iconified window really does
|
|
// report a 0x0 framebuffer (Windows, and several Linux window managers), and
|
|
// the loop's no-drawable guard reads exactly this. Only ever writing a
|
|
// positive size would leave the guard unreachable and keep handing
|
|
// `begin_frame` a stale size for a framebuffer that is no longer that big.
|
|
fb_width_ = fw > 0 ? static_cast<u32>(fw) : 0u;
|
|
fb_height_ = fh > 0 ? static_cast<u32>(fh) : 0u;
|
|
if (ww > 0 && fw > 0) fb_scale_x_ = static_cast<f32>(fw) / static_cast<f32>(ww);
|
|
if (wh > 0 && fh > 0) fb_scale_y_ = static_cast<f32>(fh) / static_cast<f32>(wh);
|
|
// The renderer draws its logical style metrics at this same ratio, so the chrome
|
|
// is the same physical size at any scale: one value states the display, and a
|
|
// 12 px label is 24 device px on the 2x panel both these sizes describe. Read here
|
|
// rather than in the frame loop because this is the one place the two sizes are,
|
|
// and re-read whenever the window moves to a display with another scale.
|
|
if (renderer_) renderer_->style().ui_scale = fb_scale_x_;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ events
|
|
|
|
Host* Host::from(GLFWwindow* w) noexcept {
|
|
return w != nullptr ? static_cast<Host*>(glfwGetWindowUserPointer(w)) : nullptr;
|
|
}
|
|
|
|
void Host::cursor_pos_cb(GLFWwindow* w, double x, double y) {
|
|
if (Host* h = from(w)) h->on_cursor_pos(x, y);
|
|
}
|
|
|
|
void Host::mouse_button_cb(GLFWwindow* w, int button, int action, int mods) {
|
|
if (Host* h = from(w)) h->on_mouse_button(button, action, mods);
|
|
}
|
|
|
|
void Host::scroll_cb(GLFWwindow* w, double dx, double dy) {
|
|
if (Host* h = from(w)) h->on_scroll(dx, dy);
|
|
}
|
|
|
|
void Host::key_cb(GLFWwindow* w, int key, int scancode, int action, int mods) {
|
|
(void)scancode;
|
|
if (Host* h = from(w)) h->on_key(key, action, mods);
|
|
}
|
|
|
|
void Host::framebuffer_size_cb(GLFWwindow* w, int width, int height) {
|
|
(void)width;
|
|
(void)height;
|
|
if (Host* h = from(w)) h->refresh_sizes();
|
|
}
|
|
|
|
void Host::on_cursor_pos(f64 wx, f64 wy) noexcept {
|
|
// Window coordinates -> framebuffer pixels: the same translation the viewport
|
|
// and every other Rect here use, so a gesture lands where it was aimed.
|
|
const f32 x = static_cast<f32>(wx) * fb_scale_x_;
|
|
const f32 y = static_cast<f32>(wy) * fb_scale_y_;
|
|
input::PointerEvent e;
|
|
e.x = x;
|
|
e.y = y;
|
|
if (have_pointer_) {
|
|
e.dx = x - last_pointer_x_;
|
|
e.dy = y - last_pointer_y_;
|
|
}
|
|
last_pointer_x_ = x;
|
|
last_pointer_y_ = y;
|
|
have_pointer_ = true;
|
|
e.mods = mods_now(window_);
|
|
e.down = any_button_down(window_);
|
|
controller_at(e.x, e.y).on_pointer(e);
|
|
}
|
|
|
|
void Host::on_mouse_button(int button, int action, int mods) noexcept {
|
|
input::Button b;
|
|
switch (button) {
|
|
case GLFW_MOUSE_BUTTON_LEFT: b = input::Button::Left; break;
|
|
case GLFW_MOUSE_BUTTON_MIDDLE: b = input::Button::Middle; break;
|
|
case GLFW_MOUSE_BUTTON_RIGHT: b = input::Button::Right; break;
|
|
default: return; // the controller has no gesture for the auxiliary buttons
|
|
}
|
|
double wx = 0.0;
|
|
double wy = 0.0;
|
|
glfwGetCursorPos(window_, &wx, &wy);
|
|
input::PointerEvent e;
|
|
e.x = static_cast<f32>(wx) * fb_scale_x_;
|
|
e.y = static_cast<f32>(wy) * fb_scale_y_;
|
|
// The press/release is also a pointer sample: the next move's delta starts
|
|
// here, or a drag's first motion would include the jump to the click.
|
|
last_pointer_x_ = e.x;
|
|
last_pointer_y_ = e.y;
|
|
have_pointer_ = true;
|
|
e.button = b;
|
|
e.mods = static_cast<u32>(mods);
|
|
e.pressed = action == GLFW_PRESS;
|
|
e.released = action == GLFW_RELEASE;
|
|
e.down = any_button_down(window_);
|
|
controller_at(e.x, e.y).on_pointer(e);
|
|
}
|
|
|
|
void Host::on_scroll(f64 dx, f64 dy) noexcept {
|
|
double wx = 0.0;
|
|
double wy = 0.0;
|
|
glfwGetCursorPos(window_, &wx, &wy);
|
|
input::WheelEvent e;
|
|
e.x = static_cast<f32>(wx) * fb_scale_x_;
|
|
e.y = static_cast<f32>(wy) * fb_scale_y_;
|
|
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_at(e.x, e.y).on_wheel(e);
|
|
}
|
|
|
|
void Host::on_key(int key, int action, int mods) noexcept {
|
|
int mapped = 0;
|
|
switch (key) {
|
|
// The controller zooms on '+', which on most layouts is shift+'='; the '='
|
|
// key is mapped to it as well, so both spellings zoom in.
|
|
case GLFW_KEY_EQUAL:
|
|
case GLFW_KEY_KP_ADD: mapped = input::kKeyZoomIn; break;
|
|
case GLFW_KEY_MINUS:
|
|
case GLFW_KEY_KP_SUBTRACT: mapped = input::kKeyZoomOut; break;
|
|
case GLFW_KEY_F: mapped = input::kKeyFit; break;
|
|
case GLFW_KEY_ESCAPE:
|
|
if (action == GLFW_PRESS) glfwSetWindowShouldClose(window_, GLFW_TRUE);
|
|
return;
|
|
default: return;
|
|
}
|
|
input::KeyEvent e;
|
|
e.key = mapped;
|
|
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);
|
|
}
|
|
|
|
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
|