148 lines
7.4 KiB
C++
148 lines
7.4 KiB
C++
#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.
|
|
///
|
|
/// ## 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 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.
|
|
///
|
|
/// 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 content 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 region (a collapsed or clipped child)
|
|
/// draws nothing and forwards nothing.
|
|
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.
|
|
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);
|
|
|
|
/// Forgets the previous frame's pointer state, so a re-bound plot cannot see an
|
|
/// edge that belongs to the old one.
|
|
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
|