636 lines
27 KiB
C++
636 lines
27 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#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>
|
|
#include <limits>
|
|
|
|
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;
|
|
// The cached rows described a figure's geometry; the next run() re-reads them from
|
|
// scratch, and until then `controller_at()` answers the single-plot controller
|
|
// rather than rects from a window that no longer exists.
|
|
row_rects_.clear();
|
|
// 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);
|
|
// The frame's one layout of the figure, into the cache every event handler
|
|
// between now and the next frame reads (R7): the rows below, and the routing of
|
|
// whatever the next `glfwPollEvents()` batch delivers, all take *these* rects
|
|
// rather than each paying for another layout.
|
|
refresh_row_rects();
|
|
const std::vector<Rect>& rows = row_rects_;
|
|
for (u32 i = 0; i < row_controllers_.size(); ++i) {
|
|
if (i < rows.size()) row_controllers_[i]->set_viewport(rows[i]);
|
|
}
|
|
// One hover, every row (spec §15): the crosshair the row controller under the pointer
|
|
// placed is applied to the whole figure here, after the pointers of this frame have been
|
|
// handled and before anything lays the rows out -- so every row's readout lists its own
|
|
// signals at the one x the pointer is over. In the run loop rather than in a callback, so a
|
|
// scripted run that injects its events straight into a row controller is synced too.
|
|
sync_figure_crosshair(rows);
|
|
// Only the row the pointer's events go to ever republishes its own hot handles, so a row
|
|
// the pointer left keeps the `HotHandles` it last published and goes on drawing a hovered
|
|
// marker or region bound as if the pointer were still on it -- and its `hovered_region()`/
|
|
// `grabbed_cursor()` keep answering for it. Clear every *other* row here, in the run loop,
|
|
// where that row is known (the captured row while a drag owns the button, so an in-flight
|
|
// gesture keeps its highlight).
|
|
if (have_pointer_ && !rows.empty()) {
|
|
const u32 active = pointer_row(rows);
|
|
for (u32 i = 0; i < rows.size() && i < row_controllers_.size(); ++i) {
|
|
if (i != active) figure_->plot(i).set_hot_handles(HotHandles{});
|
|
}
|
|
}
|
|
} 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_);
|
|
e.time_s = glfwGetTime(); // GLFW's monotonic clock; only differences are read
|
|
feed_pointer_event(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_);
|
|
e.time_s = glfwGetTime(); // the double-click detector reads differences of these
|
|
feed_pointer_event(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;
|
|
// Arrows pan (10% of the span per press, repeats included) and Escape cancels
|
|
// the active drag: translated, exactly like the ASCII set above, and the
|
|
// controller decides what each key does. Escape no longer closes the window --
|
|
// the window manager's close button and Alt+F4 are the way to quit, and a key
|
|
// the controller has a gesture for must reach it.
|
|
case GLFW_KEY_ESCAPE: mapped = input::kKeyEscape; break;
|
|
case GLFW_KEY_LEFT: mapped = input::kKeyLeft; break;
|
|
case GLFW_KEY_RIGHT: mapped = input::kKeyRight; break;
|
|
case GLFW_KEY_UP: mapped = input::kKeyUp; break;
|
|
case GLFW_KEY_DOWN: mapped = input::kKeyDown; break;
|
|
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;
|
|
// A new figure holds no crosshair this host has spread: the comparison in
|
|
// `sync_figure_crosshair()` starts from "nothing spread", which is the state its rows are in.
|
|
// Nor does it hold a captured drag row: the controllers are rebuilt below, so the old
|
|
// capture names a controller that no longer exists.
|
|
spread_active_ = false;
|
|
spread_x_ = 0.0;
|
|
spread_y_ = 0.0;
|
|
captured_ = false;
|
|
captured_row_ = 0u;
|
|
captured_button_ = input::Button::Left;
|
|
if (figure_ == nullptr) {
|
|
row_rects_.clear(); // no figure, no rows: the handlers fall back to `controller_`
|
|
return;
|
|
}
|
|
// One controller per **plot** (a row of several cells has one for each), indexed by the
|
|
// flattened plot index the figure uses.
|
|
const u32 plots = figure_->plot_count();
|
|
row_controllers_.reserve(plots);
|
|
for (u32 i = 0; i < plots; ++i) {
|
|
row_controllers_.push_back(std::make_unique<input::Controller>(figure_->plot(i)));
|
|
}
|
|
// The new figure's rects go into the cache now, not at the next frame: an event
|
|
// arriving between set_figure() and the first frame -- a scripted run, the headless
|
|
// tests -- must be routed against the figure it was just handed, and the next
|
|
// `refresh_row_rects()` re-reads them once per frame as usual.
|
|
refresh_row_rects();
|
|
}
|
|
|
|
void Host::refresh_row_rects() noexcept {
|
|
// The one `Figure::layout()` per call: every event handler reads `row_rects_`
|
|
// instead of paying this again, which is the whole point of the cache (R7). The rects
|
|
// are the figure's **cells** (one per plot, flattened), which for a figure with one
|
|
// cell per row are exactly its row rects.
|
|
row_rects_ = figure_ != nullptr ? figure_->layout().cells : std::vector<Rect>{};
|
|
}
|
|
|
|
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_;
|
|
// The cached rects `refresh_row_rects()` filled -- the same ones the last frame
|
|
// drew -- and not a fresh `Figure::layout()`: an event pays no layout, which is
|
|
// what made a burst of pointer moves on a multi-row figure re-lay every row out
|
|
// per move (R7). Between a resize and the loop's next refresh the cache is one
|
|
// frame stale, which is the documented worst case.
|
|
const std::vector<Rect>& rows = row_rects_;
|
|
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
|
const u32 row = row_in(rows, x, y);
|
|
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;
|
|
}
|
|
|
|
void Host::feed_pointer_event(const input::PointerEvent& e) noexcept {
|
|
if (figure_ == nullptr) {
|
|
controller_.on_pointer(e);
|
|
return;
|
|
}
|
|
pointer_target(e.x, e.y, e.button, e.pressed, e.released).on_pointer(e);
|
|
}
|
|
|
|
input::Controller& Host::pointer_target(f32 x, f32 y, input::Button button, bool pressed,
|
|
bool released) noexcept {
|
|
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
|
|
const std::vector<Rect>& rows = row_rects_;
|
|
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
|
|
|
u32 row = 0u;
|
|
if (captured_) {
|
|
// A button-down gesture belongs to the row it began in for its whole life: a drag
|
|
// that crosses into a neighbouring row must not hand its motion -- or its release --
|
|
// to a controller that never saw the press, which would leave the owner's drag open
|
|
// for ever (and let a later gesture resume it from the old anchor).
|
|
row = captured_row_;
|
|
// Only the owning button's release ends the capture (R5): a second button's
|
|
// release -- pressed meanwhile as a chord -- must not strand the first button's
|
|
// drag, and its press must not steal the capture to the row it landed in.
|
|
if (released && button == captured_button_) captured_ = false;
|
|
} else {
|
|
row = row_in(rows, x, y);
|
|
if (pressed) {
|
|
captured_ = true;
|
|
captured_row_ = row;
|
|
captured_button_ = button;
|
|
}
|
|
}
|
|
// The captured row is re-read against this frame's rects: the figure can be resized
|
|
// mid-drag, and the guard keeps a stale index from reaching past the controller list.
|
|
if (row >= row_controllers_.size()) row = 0u;
|
|
last_row_ = row;
|
|
input::Controller& controller = *row_controllers_[row];
|
|
controller.set_viewport(rows[row]);
|
|
return controller;
|
|
}
|
|
|
|
u32 Host::pointer_row(const std::vector<Rect>& rows) noexcept {
|
|
if (captured_ && captured_row_ < rows.size()) return captured_row_;
|
|
return row_in(rows, last_pointer_x_, last_pointer_y_);
|
|
}
|
|
|
|
u32 Host::row_in(const std::vector<Rect>& rows, f32 x, f32 y) noexcept {
|
|
if (rows.empty()) return 0u;
|
|
// The cell the pixel is in, or -- when it is in none of them, which is the row gap or
|
|
// column gap a host has asked for with `Figure::set_row_gap()`/`set_col_gap()` or the
|
|
// figure's own margins -- the cell whose rect is nearest (squared distance from the
|
|
// pixel to the rect). Selection by distance rather than by "the first that matches" is
|
|
// what makes a pixel in a gap belong to the cell it is *near*, and a gesture in a
|
|
// visible gap must not be routed to plot 0 by default. For a figure with one cell per
|
|
// row every cell spans the figure's width, so this reduces to the nearest row by y.
|
|
u32 row = 0u;
|
|
double best = std::numeric_limits<double>::infinity();
|
|
for (u32 i = 0; i < rows.size(); ++i) {
|
|
const Rect& r = rows[i];
|
|
// In double: in f32 the dy^2 term can be swallowed by a large dx^2 (pointer far outside).
|
|
const double dx = x < r.x ? double(r.x) - x : (x >= r.right() ? double(x) - r.right() : 0.0);
|
|
const double dy = y < r.y ? double(r.y) - y : (y >= r.bottom() ? double(y) - r.bottom() : 0.0);
|
|
const double distance = dx * dx + dy * dy;
|
|
if (distance < best) {
|
|
best = distance;
|
|
row = i;
|
|
if (distance == 0.0) break; // inside this cell: nothing can be nearer
|
|
}
|
|
}
|
|
return row;
|
|
}
|
|
|
|
void Host::sync_figure_crosshair(const std::vector<Rect>& rows) noexcept {
|
|
if (rows.empty() || row_controllers_.empty()) return;
|
|
const u32 row_count = static_cast<u32>(rows.size());
|
|
|
|
// Which row's crosshair is the hover. With a pointer of the host's own it is the row that pointer
|
|
// is over -- the same rule events are routed by. A scripted run injects its events straight into
|
|
// a row controller and the host never sees a pointer, so there the hover is the row whose
|
|
// crosshair is *not* the one this host spread last: the figure writes one value to every row, so
|
|
// any row holding something else was changed from outside, and that is the one the run touched.
|
|
// Nothing differing means every row still holds the last spread -- no new hover -- and the figure
|
|
// keeps it.
|
|
u32 hovered = row_count;
|
|
if (have_pointer_) {
|
|
// The row the pointer's events currently go to -- the captured row while a drag owns
|
|
// the button, the hovered row otherwise -- so the hover follows the gesture it began.
|
|
hovered = pointer_row(rows);
|
|
} else {
|
|
for (u32 i = 0; i < row_count; ++i) {
|
|
const Crosshair& candidate = figure_->plot(i).crosshair();
|
|
// A row differs when its active state, its x, or the y of *its own* horizontal line
|
|
// differs from what was spread: a pointer that moves to another row at the same x is
|
|
// a new hover and must move the one horizontal line, not leave the old row's behind.
|
|
const bool y_differs = candidate.horizontal && candidate.y != spread_y_;
|
|
if (candidate.active != spread_active_ ||
|
|
(candidate.active && (candidate.x != spread_x_ || y_differs))) {
|
|
hovered = i;
|
|
break;
|
|
}
|
|
}
|
|
if (hovered >= row_count) return;
|
|
}
|
|
|
|
// Applied to the whole figure: one x, every row; and the pointer row's **own** y, only to that
|
|
// row -- so the row under the pointer gets the one horizontal line, and no row ever shows
|
|
// another row's y (spec §17). The y is the row's own data coordinate, placed by its controller
|
|
// from the pointer's pixel row, and it is never spread to a sibling. A row whose controller has
|
|
// *no* crosshair (the pointer left its axes, which is the only thing that clears one) clears the
|
|
// figure's, so the rows cannot be left showing a hover the pointer has abandoned.
|
|
const Plot& row = figure_->plot(hovered);
|
|
if (row.crosshair().active) {
|
|
if (row.crosshair().horizontal) {
|
|
// The row carries its own y (its controller placed both coordinates), so the figure's
|
|
// four-argument overload gives the pointer row the one horizontal line.
|
|
figure_->set_crosshair(row.crosshair().x, hovered, row.crosshair().y, true);
|
|
} else {
|
|
// The row's crosshair is vertical-only -- a caller placed it through the
|
|
// single-coordinate `Figure::set_crosshair()`, which is documented vertical-only. The
|
|
// four-argument overload would turn the horizontal line on at the stored (meaningless)
|
|
// y == 0, clamped to the axes edge and made permanent. Promote only the vertical line.
|
|
figure_->set_crosshair(row.crosshair().x, true);
|
|
}
|
|
spread_x_ = row.crosshair().x;
|
|
spread_y_ = row.crosshair().y;
|
|
spread_active_ = true;
|
|
} else {
|
|
figure_->clear_crosshair();
|
|
spread_active_ = false;
|
|
}
|
|
}
|
|
|
|
} // namespace plot::glfw
|