Files
plotter/adapters/imgui/imgui_plot.hpp
T
Martino Ferrari ed3a8ce645 licence: MIT LICENSE and SPDX identifiers in every first-party file
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.
2026-09-28 22:13:40 +02:00

170 lines
8.8 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <optional>
#include <plot/core/rect.hpp>
#include <plot/input/controller.hpp>
#include <plot/math/vec2.hpp>
#include <plot/plot/plot.hpp>
#include <plot/render/renderer.hpp>
namespace plot::imgui {
/// Embeds a `Plot` in a Dear ImGui window as a child region, and drives that plot
/// from ImGui's own input state.
///
/// `draw()` opens a child window of the requested size, translates its screen
/// rectangle into framebuffer pixels, hands that rectangle to
/// `Renderer::begin_frame()` and draws the plot into it: a scissor- and
/// viewport-bounded sub-rectangle of the very framebuffer ImGui is about to draw
/// its chrome into, not an offscreen texture. Nothing is rendered through a
/// texture or an FBO, and ImGui's frame is left undisturbed -- the renderer
/// clears only its own rect and restores the GL state it changed, and ImGui's
/// backend re-establishes its viewport and blend state before its first draw.
///
/// Two pixel spaces meet here, exactly as they do in the GLFW adapter. ImGui
/// lays out and reports in logical *points* -- `io.DisplaySize`, the cursor's
/// screen position, a child's content region -- while this library speaks
/// *framebuffer pixels*. The bridge is `io.DisplayFramebufferScale`, which the
/// platform backend fills in from the window and framebuffer sizes; on an
/// unscaled display it is (1, 1) and nothing changes.
///
/// ## The coordinate assumption
///
/// Points -> pixels through `io.DisplayFramebufferScale` is *the whole
/// framebuffer's* mapping only when both of these hold:
///
/// * the GL viewport the host is drawing its frame with is the whole
/// framebuffer -- origin (0, 0), size `DisplaySize * DisplayFramebufferScale`
/// -- and
/// * the bound draw target is the default framebuffer, not an offscreen one.
///
/// Under that assumption -- the one every ImGui platform backend documents, and
/// what a window that spans its framebuffer gives -- the child's point rectangle
/// is the pixel rectangle the plot is laid out and scissored into, and a gesture
/// lands on the pixel it was aimed at. A host that renders into an FBO, or with
/// a viewport that is not the whole framebuffer (a tiled compositor, a scaled
/// surface, a pass into a texture), breaks the assumption. This widget cannot
/// detect it: it has no view of the host's viewport and no way to ask. Such a
/// host must place the plot itself -- `Renderer::begin_frame()` takes the
/// framebuffer size and the plot's rect directly, so it needs no widget -- or
/// apply its own points -> pixels mapping on top of `plot.set_viewport()`.
///
/// ## What the widget draws over
///
/// The widget renders *immediately*, from inside the child, before ImGui renders
/// its draw data (see the frame order in the demo). It therefore writes the
/// child's pixels before every ImGui command of that frame, and ImGui has no way
/// to be told the child is opaque. The host must not paint over the child's
/// rectangle:
///
/// * the hosting window must not fill its background across the child -- use
/// `ImGui::SetNextWindowBgAlpha(0.0f)` or `ImGuiWindowFlags_NoBackground`
/// (`ImGuiCol_WindowBg`'s default alpha would otherwise dim the plot), and
/// * no other window may overlap the rectangle with opaque content.
///
/// The child's own background is forced transparent by the widget, whatever the
/// host's `ImGuiCol_ChildBg`, so the widget itself never paints over the plot.
///
/// The converse also holds: what ImGui clips away, the widget does not paint. GL
/// draws do not go through ImGui's clipping, so the region is intersected with the
/// clip rect ImGui gives the child (its own rectangle when nothing cuts it off,
/// the visible part of it when an ancestor does) -- a child the hosting window cuts
/// off must not clear and paint over whatever the host drew beside it.
///
/// ## Input
///
/// Pointer, wheel and key state are translated into `plot::input` events and
/// delivered to the plot's `Controller` -- the one the widget owns, reachable
/// through `controller()`. ImGui reports levels (`io.MouseDown`) where the
/// controller wants edges (`PointerEvent::pressed`/`released`), so the widget
/// remembers the previous frame's buttons and synthesises the transitions.
///
/// Events are forwarded while an invisible item covering the child's visible
/// region is hovered -- `IsItemHovered()` rather than `IsWindowHovered()`, because
/// the item is the child's whole rectangle (the child has no padding) and its hover
/// is false while the host is dragging some other widget, so dragging a slider next
/// to the plot cannot pan it. A press that started over the child keeps being
/// delivered until every button is up again, so a drag that leaves the region still
/// ends instead of leaving the plot mid-pan.
///
/// An *invalid* pointer position -- ImGui's `(-FLT_MAX, -FLT_MAX)` sentinel for "the
/// pointer is not on this window", which the platform backends leave behind when the
/// pointer leaves or the window loses focus -- is no position update: the last real
/// position stands, the movement for that frame is zero (a sentinel scaled by the
/// framebuffer scale and differenced against a real position would be a ~1e35 pan),
/// and the button levels still flow, so a drag that leaves the window still ends on
/// its release.
///
/// Wheel input goes to the plot while it is hovered, and the plot's zoom wins
/// over ImGui's window scrolling: `SetItemKeyOwner(ImGuiKey_MouseWheelY)` on that
/// invisible item makes ImGui withhold the wheel from the hosting window. The
/// claim takes effect from the frame after it is made, because ImGui decides
/// window scrolling in its own `NewFrame()` before any widget runs -- so a
/// hosting window with a scroll range can still scroll on the very first wheel of
/// a hover that has only just started. (The child's
/// `ImGuiWindowFlags_NoScrollWithMouse` would not achieve this on its own: ImGui
/// deliberately bubbles the wheel up to the parent when a child refuses it.)
/// ImGui consumes `io.MouseWheel` itself at the end of the frame, so a widget
/// cannot consume it any later than this.
///
/// Not thread-safe: like everything else in the render thread's frame.
class PlotWidget {
public:
PlotWidget() noexcept = default;
PlotWidget(const PlotWidget&) = delete;
PlotWidget& operator=(const PlotWidget&) = delete;
/// Allocates a child region of `size` (ImGui points) identified by `id`, and
/// renders `plot` into the framebuffer pixels it covers with `renderer`, which
/// must already be initialised on the current context.
///
/// Must be called between `ImGui::NewFrame()` and `ImGui::Render()`, from a
/// window; the plot is drawn (GL calls issued) during the call, inside the
/// child, so the host's later ImGui commands never have to restore anything.
///
/// The plot's viewport is the child's visible rectangle, so it fills the child
/// exactly; the widget sets it every frame through the controller, and the plot
/// is re-laid out to match. A degenerate or entirely
/// clipped region draws nothing and forwards nothing, and a partially clipped one
/// draws and accepts gestures only where the host leaves the child visible.
void draw(const char* id, Plot& plot, Vec2 size, Renderer& renderer);
/// The controller the widget feeds, or null before the first `draw()`. It is
/// bound to whichever plot the last `draw()` was given; a `draw()` for a
/// different plot re-binds it and drops any drag in progress. The remembered
/// button levels survive the re-bind -- they are the host's state, not the
/// plot's -- so a host drawing two plots through one widget does not hand the
/// second one a fabricated press.
input::Controller* controller() noexcept { return controller_ ? &*controller_ : nullptr; }
const input::Controller* controller() const noexcept {
return controller_ ? &*controller_ : nullptr;
}
private:
/// Synthesises this frame's `plot::input` events from ImGui's levels.
void forward_input(bool hovered);
/// Drops the remembered pointer position and any capture, keeping the remembered
/// button levels: they are the host's state, and synthesising an edge out of the
/// levels a re-bind started with would fabricate a press.
void reset_pointer() noexcept;
Plot* plot_ = nullptr;
std::optional<input::Controller> controller_;
/// The previous frame's button levels, in ImGui's order (left, right, middle).
bool prev_down_[3] = {false, false, false};
f32 prev_x_ = 0.0f; ///< previous pointer position, in framebuffer pixels
f32 prev_y_ = 0.0f;
bool have_prev_pos_ = false;
/// True while a button pressed over the child is still down: keeps the release
/// flowing even after the pointer has left the region.
bool capturing_ = false;
};
} // namespace plot::imgui