#pragma once #include #include #include #include #include #include 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 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