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.
244 lines
12 KiB
C++
244 lines
12 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#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>
|
|
|
|
/// GLFW's window type. Forward-declared on purpose: this header is
|
|
/// self-contained and needs no GLFW include directory, so a consumer of the
|
|
/// adapter does not inherit the toolkit's headers (and their `<GL/gl.h>`, whose
|
|
/// macros would rewrite this library's enumerators).
|
|
struct GLFWwindow;
|
|
|
|
namespace plot::glfw {
|
|
|
|
/// What `Host::run()` opens the window with.
|
|
struct RunConfig {
|
|
int width = 1280;
|
|
int height = 720;
|
|
const char* title = "plot";
|
|
bool vsync = true;
|
|
};
|
|
|
|
/// One frame of application code, called by `Host::run()` after the viewport is
|
|
/// set and before the frame is staged, so it is where data is refreshed.
|
|
/// `dt` is the wall time since the previous call, in seconds.
|
|
using FrameFn = std::function<void(plot::Plot&, f64 dt)>;
|
|
|
|
/// The reference host: owns the GLFW window and its OpenGL 3.3 core context, the
|
|
/// GL entry points, the `Renderer`, the `Plot`, the `input::Controller` and the
|
|
/// frame loop, so nothing above it names GLFW or GL.
|
|
///
|
|
/// Per-frame sequence, in this order:
|
|
///
|
|
/// poll events -> controller.set_viewport(framebuffer-pixel rect) ->
|
|
/// frame(plot, dt) -> begin_frame -> draw -> [screenshot] -> end_frame ->
|
|
/// swap buffers
|
|
///
|
|
/// Two pixel spaces meet here and must not be confused. GLFW reports the cursor
|
|
/// and the window in *window* coordinates, while the framebuffer -- what
|
|
/// `begin_frame` takes and what every `Rect` in this library speaks -- can be
|
|
/// larger under a fractional or HiDPI scale, so `glfwGetFramebufferSize()` and
|
|
/// `glfwGetWindowSize()` disagree. The host calls `begin_frame` with the
|
|
/// framebuffer size and scales every pointer coordinate by the ratio
|
|
/// `framebuffer / window`, so a gesture lands on the pixels the plot was laid out
|
|
/// in. The same ratio states the display to the renderer
|
|
/// (`Style::ui_scale`), which is what makes the chrome -- labels, strokes, paddings
|
|
/// -- one physical size at any scale. On an unscaled display the ratio is 1.0 and
|
|
/// nothing changes.
|
|
///
|
|
/// Headless smoke mode, read once per `run()`:
|
|
///
|
|
/// `PLOT_DEMO_EXIT_AFTER_MS=<n>` stop the loop after n ms and return
|
|
/// (default 0: run until the window is closed)
|
|
/// `PLOT_DEMO_SCREENSHOT=<path>` on that final frame, write it with
|
|
/// `Renderer::dump_frame_ppm()` before the swap
|
|
///
|
|
/// That is what lets a CI run a GUI library under `xvfb-run`: no display access
|
|
/// and no screen capture, just a PPM on disk.
|
|
///
|
|
/// A run with a deadline is that automated path, so its window is created hidden
|
|
/// (`GLFW_VISIBLE` false): an automated run must not flash a window on whatever
|
|
/// display it lands on. It also means the loop is not throttled by a display
|
|
/// refresh, so the frame times it reports are the CPU's, not a vblank's. A run
|
|
/// without a deadline shows its window, as an interactive application must.
|
|
///
|
|
/// Not thread-safe, and confined to the thread that runs `run()`.
|
|
class Host {
|
|
public:
|
|
Host() noexcept;
|
|
~Host();
|
|
Host(const Host&) = delete;
|
|
Host& operator=(const Host&) = delete;
|
|
|
|
/// Opens the window, makes a 3.3 core context current, resolves the GL entry
|
|
/// points and initialises the renderer, then runs the loop until the window is
|
|
/// closed or the exit deadline passes. Returns false with `st` filled in when
|
|
/// GLFW, the context or `Renderer::init()` fails; a failed run leaves nothing
|
|
/// open.
|
|
///
|
|
/// On success the window *stays open*, so a caller can stop and join whatever
|
|
/// feeds the plot before the GL resources go away: `close()` (or the
|
|
/// destructor) releases the renderer, the context and the window, in that
|
|
/// order, on this thread. Calling `run()` again reopens the window and keeps
|
|
/// the plot.
|
|
bool run(const RunConfig& cfg, const FrameFn& frame, Status* st = nullptr);
|
|
|
|
/// Releases the renderer and the window. Idempotent, and safe on a host that
|
|
/// never ran.
|
|
void close() noexcept;
|
|
|
|
/// 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's **crosshair** follows the pointer as one crosshair and not one per row: the run
|
|
/// loop reads the hover out of the row under the pointer and applies it to the whole figure
|
|
/// (`sync_figure_crosshair()`), so every row's readout lists its own signals at the one x the
|
|
/// pointer is over. That is what "synced if multiple plots available" means, and the host is the
|
|
/// layer that can do it -- it is the one that knows which row the pointer is in.
|
|
///
|
|
/// 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 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 inside no row -- the row gap a host asked for with `Figure::set_row_gap()`,
|
|
/// or a margin -- is the *nearest* row rather than dropped or defaulted, 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. The event callbacks
|
|
/// route through it, and so can a host that wants to know where a synthetic gesture
|
|
/// would land.
|
|
input::Controller& controller_at(f32 x, f32 y) noexcept;
|
|
|
|
/// The gesture controller the host feeds. Valid for the host's whole life.
|
|
input::Controller& controller() noexcept { return controller_; }
|
|
|
|
/// The renderer, or null while no window is open. Valid from inside the frame
|
|
/// callback and after a successful `run()`; `close()` releases it.
|
|
Renderer* renderer() noexcept { return renderer_ ? &*renderer_ : nullptr; }
|
|
|
|
/// Retitles the open window -- from the frame callback, for a live
|
|
/// samples/second and frame-time readout. A no-op while no window is open.
|
|
void set_title(const char* title) noexcept;
|
|
|
|
/// Framebuffer pixels per window pixel, one ratio per axis, as of the last
|
|
/// event or frame. 1.0 on an unscaled display.
|
|
f32 framebuffer_scale_x() const noexcept { return fb_scale_x_; }
|
|
f32 framebuffer_scale_y() const noexcept { return fb_scale_y_; }
|
|
/// The window's content scale, one per axis, as of the last frame.
|
|
f32 content_scale_x() const noexcept { return content_scale_x_; }
|
|
f32 content_scale_y() const noexcept { return content_scale_y_; }
|
|
|
|
private:
|
|
/// Reads the framebuffer and window sizes and the content scale, refreshing
|
|
/// the window -> framebuffer ratios the pointer translation uses.
|
|
void refresh_sizes() noexcept;
|
|
|
|
/// Callbacks find their host through GLFW's per-window user pointer.
|
|
static Host* from(GLFWwindow* w) noexcept;
|
|
static void cursor_pos_cb(GLFWwindow* w, double x, double y);
|
|
static void mouse_button_cb(GLFWwindow* w, int button, int action, int mods);
|
|
static void scroll_cb(GLFWwindow* w, double dx, double dy);
|
|
static void key_cb(GLFWwindow* w, int key, int scancode, int action, int mods);
|
|
static void framebuffer_size_cb(GLFWwindow* w, int width, int height);
|
|
|
|
void on_cursor_pos(f64 wx, f64 wy) noexcept;
|
|
void on_mouse_button(int button, int action, int mods) noexcept;
|
|
void on_scroll(f64 dx, f64 dy) noexcept;
|
|
void on_key(int key, int action, int mods) noexcept;
|
|
|
|
/// The index of the row of `rows` a framebuffer y belongs to: the row it is in, or the nearest
|
|
/// row when it is in none (a row gap, a margin outside the figure). One rule, shared by the event
|
|
/// routing and the crosshair sync -- and taking the rects rather than reading them, so a caller
|
|
/// that has just laid the figure out does not make it lay out again for this.
|
|
static u32 row_in(const std::vector<Rect>& rows, f32 y) noexcept;
|
|
|
|
/// Spreads the hover over a whole `Figure` (spec §15).
|
|
///
|
|
/// A figure's rows share one x, so the crosshair one row's controller placed is read back here and
|
|
/// applied to every row: the controller turns a pointer position into a data coordinate -- which
|
|
/// is its job, and the same one it does for a single plot -- and the figure spreads that one
|
|
/// coordinate over its rows. Reading it back rather than deriving a second coordinate here is what
|
|
/// keeps the hover the row shows and the hover the figure shows the same number.
|
|
///
|
|
/// The row it reads is the one under the host's pointer, or -- with no pointer of the host's own,
|
|
/// which is how a scripted run injects its events -- the row whose crosshair is not the value this
|
|
/// host spread last (`spread_x_`/`spread_active_`), i.e. the row something outside changed. A row
|
|
/// whose controller has no crosshair clears the figure's.
|
|
///
|
|
/// Called once per frame from the run loop, so a scripted run that bypasses the GLFW callbacks
|
|
/// gets the same synced figure a real pointer does. `rows` is the figure's row rects, which the
|
|
/// run loop has just read -- the one layout per frame this shares with the row viewports.
|
|
void sync_figure_crosshair(const std::vector<Rect>& rows) 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;
|
|
/// The crosshair this host last spread over the figure (see
|
|
/// `sync_figure_crosshair()`): what a row is compared against to find the one a
|
|
/// scripted run touched, since the figure writes one value to every row.
|
|
/// `spread_active_` false means "spread nothing" -- the state before the first
|
|
/// sync, and the one a clear leaves.
|
|
f64 spread_x_ = 0.0;
|
|
bool spread_active_ = false;
|
|
|
|
u32 fb_width_ = 0; ///< framebuffer size of the last refresh_sizes()
|
|
u32 fb_height_ = 0;
|
|
/// Framebuffer pixels per window pixel. Taken from the two sizes GLFW reports
|
|
/// rather than from the content scale, which under a fractional scale does not
|
|
/// round back to the framebuffer size.
|
|
f32 fb_scale_x_ = 1.0f;
|
|
f32 fb_scale_y_ = 1.0f;
|
|
f32 content_scale_x_ = 1.0f;
|
|
f32 content_scale_y_ = 1.0f;
|
|
/// Last pointer position in framebuffer pixels: the deltas the controller pans
|
|
/// with are the movements since the previous event, which GLFW does not report.
|
|
f32 last_pointer_x_ = 0.0f;
|
|
f32 last_pointer_y_ = 0.0f;
|
|
bool have_pointer_ = false;
|
|
};
|
|
|
|
} // namespace plot::glfw
|