feat(imgui): embeddable plot widget and demo
This commit is contained in:
1 parent
fb2c2f5e4d
commit
15931a2405
9 files changed
+1072
-38
No files matched your search
@@ -0,0 +1,147 @@
|
||||
#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
|
||||
Reference in new issue
Block a user