// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include #include #include #include #include #include #include #include #include #include #include /// 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 ``, 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; /// 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=` stop the loop after n ms and return /// (default 0: run until the window is closed) /// `PLOT_DEMO_SCREENSHOT=` 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 plot's controller -- the one a gesture on that plot reaches -- or null /// while no figure is set or it does not exist. `row` is the **flattened plot index** /// (`Figure::plot_count()` of them: the row count when no row holds several cells). 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 /// plot (cell) that pixel lands in otherwise -- x and y both pick it, as a row may hold /// several cells side by side. The cell rects come from the cache /// `refresh_row_rects()` fills -- the same ones the renderer draws -- so a gesture /// lands on the row it was aimed at and no event lays the figure out; a pixel /// inside no cell -- the row or column gap a host asked for with /// `Figure::set_row_gap()`/`set_col_gap()`, or a margin -- is the *nearest* cell 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. It is the pure /// query -- what a gesture *would* land on -- and it does not move the capture /// below; the GLFW callbacks and `feed_pointer_event()` are the routing. input::Controller& controller_at(f32 x, f32 y) noexcept; /// Feeds one pointer event, stated in framebuffer pixels, through the host's own /// routing and into the controller it belongs to. The GLFW callbacks fill the /// position and the button transitions out of GLFW and call exactly this, so a host /// or a test with no window can drive the routing directly. /// /// The routing is the row the event lands in, with one exception that is the whole /// point of it: a press **captures** the row it landed in, and every motion and the /// release until *that button* comes up go to the captured row. A drag that crosses /// into a neighbouring figure row is therefore still the drag it began as -- the /// owning controller sees the motion and the release that end it -- rather than /// being handed to a controller whose `mode_ == None`, which would leave the owner's /// drag open for ever and let a later gesture resume it from the old anchor. /// /// Single-gesture policy: a figure routes one button-down gesture at a time. The /// first button to press claims the capture; a second button pressed meanwhile -- /// a chord, or a trackpad that reports a second finger as a button -- neither /// steals the capture to another row nor ends it, and only the owning button's /// release ends it. Whatever the second button's events do to the controllers is /// the controllers' business; the host only guarantees they all arrive at the row /// the first press claimed. void feed_pointer_event(const input::PointerEvent& e) noexcept; /// Re-reads the figure's row rects into the cache every event handler below /// reads -- the one `Figure::layout()` the routing costs between two frames. The /// run loop calls this once per frame, right after the figure's viewport is set, /// so the rects the handlers answer from are the rects that frame draws; the /// handlers themselves never lay the figure out. /// /// A driver that injects events without running the loop -- a scripted run, the /// headless tests -- calls this after re-configuring the figure (a new viewport, /// a new row gap, `set_figure()`): it is the scripted equivalent of the frame the /// run loop would have staged. Until it runs, the handlers answer from the last /// refreshed rects, which is also what happens across a window resize: GLFW /// delivers the resize and any pointer events queued behind it in one batch, the /// events before the resize see the rows the previous frame drew, and the loop's /// next `refresh_row_rects()` brings the cache onto the new geometry -- rows are /// at most one frame stale, never wrong by a second layout. void refresh_row_rects() 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 cell of `rows` (the figure's flattened cell rects) a framebuffer pixel /// belongs to: the cell it is in, or the nearest cell when it is in none (a row or column 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& rows, f32 x, f32 y) noexcept; /// The controller a pointer event at (x, y) belongs to, applying the button capture: /// a press claims the row it landed in (`captured_row_` + `captured_button_`) and /// every motion and the release until *that button* comes up are routed to it; /// `controller_at()` is the pure query that never captures. Also hands the target /// its row's viewport, so the hit-test geometry is the last frame's. input::Controller& pointer_target(f32 x, f32 y, input::Button button, bool pressed, bool released) noexcept; /// The plot (flattened index) the pointer's events currently go to: the captured one /// while a drag owns the button, the one under the pointer otherwise. The run loop uses /// it to clear every *other* plot's hot handles, so one the pointer left cannot keep /// drawing a hover or a grab it last published. u32 pointer_row(const std::vector& rows) 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& rows) noexcept; Plot plot_; input::Controller controller_; ///< forwards its viewport to `plot_` std::optional 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> 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. `spread_y_` is the horizontal line's own /// value, compared as well so a y-only change -- the same x in another row -- /// is not mistaken for "nothing changed". f64 spread_x_ = 0.0; f64 spread_y_ = 0.0; bool spread_active_ = false; /// The row a button-down drag has captured, and the button that owns the capture: /// the row its press landed in owns that button's motions and its release until /// the button comes up. `captured_` is false between gestures, and cleared when /// another figure is set. The button is recorded because only the owning button's /// release ends the capture (R5): a second button pressed while the drag runs is /// routed to the same row and must neither steal the capture nor end it, or its /// release would strand the first button's drag. bool captured_ = false; u32 captured_row_ = 0u; input::Button captured_button_ = input::Button::Left; /// The figure's cell rects (one per plot, flattened; the row rects when no row is split) as of the last `refresh_row_rects()` -- the cache the /// event routing reads instead of laying the figure out per event (R7). Filled by /// `set_figure()` and once per frame by the run loop; empty while no figure is /// set, which is the state `controller_at()`/`pointer_target()` fall back to the /// single-plot controller on. std::vector row_rects_; 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