The repository had no licence at all, so every source was all-rights-reserved by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino Ferrari) and a two-line SPDX header to each of the 113 first-party sources -- 48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the grant travels with each file rather than relying on the root document alone. Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely vendored code is untouched: third_party/stb_truetype.h carries no header of ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py emit() now writes the two lines into its header comment and the file was regenerated -- gen_atlas.py --check passes and the regeneration adds only those comment lines to the previously committed file. README's provenance section now states MIT and links LICENSE. No code, test or public API change.
518 lines
21 KiB
C++
518 lines
21 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;
|
|
// 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.
|
|
const std::vector<Rect>& rows = figure_->layout().rows; // the one layout of this frame
|
|
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);
|
|
} 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;
|
|
// 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.
|
|
spread_active_ = false;
|
|
spread_x_ = 0.0;
|
|
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_;
|
|
// The figure's one layout for this call: both readers below take the row rects it produced, so an
|
|
// event lays the figure out once and not once per reader -- this is the hottest path a multi-row
|
|
// figure has, and a layout now rebuilds every row's ticks, grid and crosshair geometry.
|
|
const std::vector<Rect>& rows = figure_->layout().rows;
|
|
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
|
(void)x; // the rows tile the figure vertically, so y alone picks one -- see row_in()
|
|
const u32 row = row_in(rows, 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;
|
|
}
|
|
|
|
u32 Host::row_in(const std::vector<Rect>& rows, f32 y) noexcept {
|
|
if (rows.empty()) return 0u;
|
|
// The row the pixel is in, or -- when it is in none of them, which is the row gap a
|
|
// host has asked for with `Figure::set_row_gap()` or the figure's own margins -- the
|
|
// row whose rect is nearest. Selection by distance rather than by "the first that
|
|
// matches" is what makes a pixel in a gap belong to the row it is *near*: the rows
|
|
// tile the figure at the default gap of 0, so this is unreachable until a host asks
|
|
// for one, and a gesture in a visible gap must not be routed to row 0 by default.
|
|
u32 row = 0u;
|
|
f32 best = std::numeric_limits<f32>::infinity();
|
|
for (u32 i = 0; i < rows.size(); ++i) {
|
|
const f32 top = rows[i].y;
|
|
const f32 bottom = rows[i].y + rows[i].h;
|
|
const f32 distance = y < top ? top - y : (y >= bottom ? y - bottom : 0.0f);
|
|
if (distance < best) {
|
|
best = distance;
|
|
row = i;
|
|
if (distance == 0.0f) break; // inside this row: 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_) {
|
|
hovered = row_in(rows, last_pointer_y_);
|
|
} else {
|
|
for (u32 i = 0; i < row_count; ++i) {
|
|
const Crosshair& candidate = figure_->plot(i).crosshair();
|
|
if (candidate.active != spread_active_ || (candidate.active && candidate.x != spread_x_)) {
|
|
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) {
|
|
figure_->set_crosshair(row.crosshair().x, hovered, row.crosshair().y, true);
|
|
spread_x_ = row.crosshair().x;
|
|
spread_active_ = true;
|
|
} else {
|
|
figure_->clear_crosshair();
|
|
spread_active_ = false;
|
|
}
|
|
}
|
|
|
|
} // namespace plot::glfw
|