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.
105 lines
4.7 KiB
C++
105 lines
4.7 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
// The GLFW host's event routing in figure mode: which row's controller a pointer pixel
|
|
// reaches.
|
|
//
|
|
// This is the adapter's own logic and not GLFW's -- a figure is several plots stacked in
|
|
// one framebuffer, and the host has to decide which of them a gesture was aimed at -- so it
|
|
// is tested here rather than through a window. Nothing below calls `run()`: the case needs
|
|
// no GL context, no display and no window, only the rects `Figure::layout()` computes and
|
|
// the controllers the host builds from them.
|
|
#include "harness.hpp"
|
|
|
|
#include <glfw/glfw_host.hpp>
|
|
#include <plot/plot/figure.hpp>
|
|
|
|
namespace {
|
|
|
|
/// A figure of `rows` rows over a known rect, with an x and a y axis in every row (so
|
|
/// each row lays out as a plot rather than as an empty rect), handed to a host.
|
|
struct Routed {
|
|
explicit Routed(plot::u32 rows, plot::f32 gap = 0.0f) : figure(rows) {
|
|
figure.set_viewport({0.0f, 0.0f, 300.0f, 600.0f});
|
|
figure.set_row_gap(gap);
|
|
for (plot::u32 row = 0; row < rows; ++row) {
|
|
plot::Plot& p = figure.plot(row);
|
|
p.add_x_axis({"t", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
|
p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
|
}
|
|
host.set_figure(&figure);
|
|
figure.layout();
|
|
}
|
|
|
|
plot::Figure figure;
|
|
plot::glfw::Host host;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
PLOT_TEST(the_host_routes_a_pixel_to_the_row_it_is_in) {
|
|
Routed r(3u);
|
|
const std::vector<plot::Rect> rows = r.figure.layout().rows;
|
|
PLOT_CHECK(rows.size() == 3u);
|
|
for (plot::u32 row = 0; row < 3u; ++row) {
|
|
PLOT_CHECK(r.host.row_controller(row) != nullptr);
|
|
const plot::f32 x = rows[row].x + 0.5f * rows[row].w;
|
|
const plot::f32 y = rows[row].y + 0.5f * rows[row].h;
|
|
PLOT_CHECK(&r.host.controller_at(x, y) == r.host.row_controller(row));
|
|
}
|
|
// Above the first row and below the last one (the figure's own margins) are still the
|
|
// nearest row rather than a dropped event: a drag that leaves the data area must end
|
|
// where it should.
|
|
PLOT_CHECK(&r.host.controller_at(150.0f, 0.0f) == r.host.row_controller(0u));
|
|
PLOT_CHECK(&r.host.controller_at(150.0f, 599.0f) == r.host.row_controller(2u));
|
|
}
|
|
|
|
PLOT_TEST(the_host_routes_a_pixel_in_a_row_gap_to_the_nearest_row) {
|
|
// A gap is what `set_row_gap()` asks for. Rows tile the figure at the default of 0, so
|
|
// this is the only way a pixel can land in none of them -- and the routing must then
|
|
// answer with the row it is *near*, not with row 0, which is what a "first row that
|
|
// matches, else whatever `row` started at" loop would do.
|
|
Routed r(3u, 40.0f);
|
|
const std::vector<plot::Rect> rows = r.figure.layout().rows;
|
|
PLOT_CHECK(rows.size() == 3u);
|
|
for (plot::u32 row = 0; row + 1u < 3u; ++row) {
|
|
const plot::f32 gap = rows[row + 1u].y - rows[row].bottom();
|
|
PLOT_CHECK(gap > 0.0f);
|
|
const plot::f32 middle = rows[row].bottom() + 0.5f * gap;
|
|
// Just past a row's bottom edge the gap pixel is that row's...
|
|
PLOT_CHECK(&r.host.controller_at(150.0f, rows[row].bottom() + 1.0f) ==
|
|
r.host.row_controller(row));
|
|
// ... and just above the next row's top edge it is the next row's.
|
|
PLOT_CHECK(&r.host.controller_at(150.0f, rows[row + 1u].y - 1.0f) ==
|
|
r.host.row_controller(row + 1u));
|
|
// The middle of the gap goes to the nearer of the two rows -- and, whatever it picks,
|
|
// to a row of *this* figure rather than to a default: this is the assertion the
|
|
// "first match, else whatever `row` started at" loop failed, since it answered row 0
|
|
// for every gap pixel.
|
|
plot::input::Controller& in_gap = r.host.controller_at(150.0f, middle);
|
|
PLOT_CHECK(&in_gap == r.host.row_controller(row) || &in_gap == r.host.row_controller(row + 1u));
|
|
PLOT_CHECK(&in_gap != r.host.row_controller(2u) || row + 1u == 2u);
|
|
}
|
|
}
|
|
|
|
PLOT_TEST(the_host_keeps_its_own_controller_while_no_figure_is_set) {
|
|
plot::glfw::Host host;
|
|
PLOT_CHECK(host.figure() == nullptr);
|
|
PLOT_CHECK(host.row_controller(0u) == nullptr);
|
|
// With no figure the routing is the single-plot loop's: one controller for every pixel.
|
|
PLOT_CHECK(&host.controller_at(10.0f, 10.0f) == &host.controller());
|
|
PLOT_CHECK(&host.controller_at(1000.0f, 1000.0f) == &host.controller());
|
|
|
|
plot::Figure figure(2u);
|
|
figure.set_viewport({0.0f, 0.0f, 300.0f, 600.0f});
|
|
host.set_figure(&figure);
|
|
PLOT_CHECK(host.figure() == &figure);
|
|
PLOT_CHECK(host.row_controller(0u) != nullptr);
|
|
PLOT_CHECK(host.row_controller(2u) == nullptr); // a row that does not exist
|
|
// And clearing it goes back to the single-plot answer.
|
|
host.set_figure(nullptr);
|
|
PLOT_CHECK(host.figure() == nullptr);
|
|
PLOT_CHECK(host.row_controller(0u) == nullptr);
|
|
PLOT_CHECK(&host.controller_at(10.0f, 10.0f) == &host.controller());
|
|
}
|