#pragma once #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_; } /// 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; Plot plot_; input::Controller controller_; ///< forwards its viewport to `plot_` std::optional renderer_; GLFWwindow* window_ = nullptr; 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