feat(imgui): embeddable plot widget and demo

This commit is contained in:
Martino Ferrari committed 2026-09-28 00:38:31 +02:00
1 parent fb2c2f5e4d
commit 15931a2405
9 files changed
+1072 -38

No files matched your search

+237
View File
@@ -0,0 +1,237 @@
#include <imgui/imgui_plot.hpp>
#include <imgui.h>
namespace plot::imgui {
namespace {
/// ImGui's button indices are left, right, middle; the controller's enum is the
/// same three, spelled out.
input::Button button_from_index(int index) noexcept {
switch (index) {
case 1:
return input::Button::Right;
case 2:
return input::Button::Middle;
default:
return input::Button::Left;
}
}
/// ImGui's modifier booleans as the controller's translatable mask. The bit
/// values are `input::Mod`'s by definition: it is documented as the mask a host
/// translates into, and the GLFW adapter's static_asserts pin the same layout.
u32 mods_from(const ImGuiIO& io) noexcept {
u32 mods = 0;
if (io.KeyShift) mods |= static_cast<u32>(input::Mod::Shift);
if (io.KeyCtrl) mods |= static_cast<u32>(input::Mod::Ctrl);
if (io.KeyAlt) mods |= static_cast<u32>(input::Mod::Alt);
if (io.KeySuper) mods |= static_cast<u32>(input::Mod::Super);
return mods;
}
} // namespace
void PlotWidget::draw(const char* id, Plot& plot, Vec2 size, Renderer& renderer) {
ImGuiIO& io = ImGui::GetIO();
if (!controller_ || plot_ != &plot) {
// The controller holds a Plot& (and the drag state that goes with it), so a
// different plot is a different controller; re-binding drops any drag rather
// than replaying its edges against the new plot.
controller_.emplace(plot);
plot_ = &plot;
reset_pointer();
}
// The whole translation below rests on the backend having filled in a sane
// scale; a zero one would collapse the region and silently draw nothing.
IM_ASSERT(io.DisplayFramebufferScale.x > 0.0f && io.DisplayFramebufferScale.y > 0.0f);
// The child is a plot surface, not a scrollable panel: no scrollbar, whatever
// the item inside it measures. The wheel belongs to the plot while it is
// hovered, and is taken away from window scrolling by the key owner below.
const ImGuiWindowFlags child_flags = ImGuiWindowFlags_NoScrollbar;
// The child is the plot's surface: it must paint nothing of its own over it.
// ChildBg is transparent in every stock theme, but a host may have pushed a
// panel colour; forcing it here means the widget can never cover its own plot.
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
const bool shown =
ImGui::BeginChild(id, ImVec2(size.x, size.y), ImGuiChildFlags_None, child_flags);
if (!shown) {
// Clipped away entirely (or collapsed): there is no region to draw into, and
// no rect to aim a gesture at. EndChild is still owed.
ImGui::EndChild();
ImGui::PopStyleColor();
return;
}
// A borderless child has no padding and no border, so the content region is
// the child's rectangle itself: the pixels the plot draws are exactly the ones
// the host laid out for it.
const ImVec2 origin = ImGui::GetCursorScreenPos();
const ImVec2 avail = ImGui::GetContentRegionAvail();
if (avail.x <= 0.0f || avail.y <= 0.0f) {
// A child with no room (a degenerate layout): there is no region to draw
// into and no rect to aim a gesture at, and InvisibleButton wants a size.
ImGui::EndChild();
ImGui::PopStyleColor();
return;
}
// An invisible item covering the region is what makes it *the* hovered thing:
// `IsItemHovered()` is false while a drag elsewhere in the host is active, so
// the plot does not pan when the user drags a slider next to it, and
// `SetItemKeyOwner()` -- ImGui's documented way for a widget to take the wheel
// away from window scrolling -- needs an item to own it. Both buttons are
// enabled: the controller's left and right drags are the plot's own, and ImGui
// only needs to know that the pointer is interacting with this region.
ImGui::InvisibleButton("surface", ImVec2(avail.x, avail.y),
ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight |
ImGuiButtonFlags_MouseButtonMiddle);
const bool hovered = ImGui::IsItemHovered();
ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY);
const ImVec2 scale = io.DisplayFramebufferScale;
const f32 fb_w_f = io.DisplaySize.x * scale.x;
const f32 fb_h_f = io.DisplaySize.y * scale.y;
// Points -> framebuffer pixels: the assumption documented on the class. State
// it in the arithmetic so a reader sees where the mapping enters.
const Rect region{origin.x * scale.x, origin.y * scale.y, avail.x * scale.x, avail.y * scale.y};
if (region.w > 0.0f && region.h > 0.0f && fb_w_f >= 1.0f && fb_h_f >= 1.0f) {
// One viewport for the gesture geometry and the frame: the controller
// forwards it to the plot (and re-lays it out), and `begin_frame` gets the
// same rect and the whole framebuffer it is a sub-rectangle of.
controller_->set_viewport(region);
forward_input(hovered);
const u32 fb_w = static_cast<u32>(fb_w_f + 0.5f);
const u32 fb_h = static_cast<u32>(fb_h_f + 0.5f);
renderer.begin_frame(fb_w, fb_h, region);
renderer.draw(plot);
renderer.end_frame();
// `end_frame` leaves the GL state neutral and the viewport at the plot's
// rect; ImGui's backend sets its own viewport, blend function and so on
// before its first draw, so the chrome renders correctly after this.
}
ImGui::EndChild();
ImGui::PopStyleColor();
}
void PlotWidget::forward_input(bool hovered) {
ImGuiIO& io = ImGui::GetIO();
const ImVec2 scale = io.DisplayFramebufferScale;
const f32 px = io.MousePos.x * scale.x;
const f32 py = io.MousePos.y * scale.y;
const bool down[3] = {io.MouseDown[0], io.MouseDown[1], io.MouseDown[2]};
const bool any_down = down[0] || down[1] || down[2];
const u32 mods = mods_from(io);
if (!hovered && !capturing_) {
// Nowhere near the plot: remember the levels anyway, so re-entering the
// region does not synthesise an edge out of the state it left with.
prev_down_[0] = down[0];
prev_down_[1] = down[1];
prev_down_[2] = down[2];
prev_x_ = px;
prev_y_ = py;
have_prev_pos_ = true;
return;
}
// The movement since the previous frame is what a pan consumes; it rides the
// first event of this frame and nothing else, so one frame's motion can never
// be counted twice.
const f32 dx = have_prev_pos_ ? px - prev_x_ : 0.0f;
const f32 dy = have_prev_pos_ ? py - prev_y_ : 0.0f;
bool delta_pending = true;
const auto deliver = [&](input::Button button, bool pressed, bool released) {
input::PointerEvent e;
e.x = px;
e.y = py;
if (delta_pending) {
e.dx = dx;
e.dy = dy;
delta_pending = false;
}
e.button = button;
e.mods = mods;
e.down = any_down;
e.pressed = pressed;
e.released = released;
controller_->on_pointer(e);
};
// ImGui reports levels; the controller wants edges. One event per button whose
// level changed, which is the press or the release the host's callback would
// have carried.
for (int i = 0; i < 3; ++i) {
if (down[i] == prev_down_[i]) continue;
deliver(button_from_index(i), down[i], !down[i]);
}
if (delta_pending) {
// No transition this frame: one plain move sample, so a pan in progress
// advances by exactly the movement since the last one and
// Controller::hovered_cursor() sees where the pointer is.
deliver(input::Button::Left, false, false);
}
if (hovered && io.MouseWheel != 0.0f) {
input::WheelEvent e;
e.x = px;
e.y = py;
e.dx = io.MouseWheelH;
e.dy = io.MouseWheel; // ImGui's positive y (scroll up) is the zoom-in sign
e.mods = mods;
controller_->on_wheel(e);
}
// Keyboard gestures, on the same hover gate as the pointer: a key pressed
// while the plot is under the pointer is aimed at the plot. `+` on most
// layouts is shift+`=`, and either spelling zooms in, as in the GLFW adapter.
if (hovered && !io.WantTextInput) {
const auto key = [&](int code) {
input::KeyEvent e;
e.key = code;
e.mods = mods;
e.down = true; // ImGui already edge-detected the press
controller_->on_key(e);
};
if (ImGui::IsKeyPressed(ImGuiKey_Equal, false) ||
ImGui::IsKeyPressed(ImGuiKey_KeypadAdd, false)) {
key(input::kKeyZoomIn);
}
if (ImGui::IsKeyPressed(ImGuiKey_Minus, false) ||
ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract, false)) {
key(input::kKeyZoomOut);
}
if (ImGui::IsKeyPressed(ImGuiKey_F, false)) key(input::kKeyFit);
}
prev_down_[0] = down[0];
prev_down_[1] = down[1];
prev_down_[2] = down[2];
prev_x_ = px;
prev_y_ = py;
have_prev_pos_ = true;
// Capture: a press delivered while the pointer was over the child keeps the
// release flowing even once the pointer has left it.
capturing_ = any_down && (capturing_ || hovered);
}
void PlotWidget::reset_pointer() noexcept {
prev_down_[0] = false;
prev_down_[1] = false;
prev_down_[2] = false;
prev_x_ = 0.0f;
prev_y_ = 0.0f;
have_prev_pos_ = false;
capturing_ = false;
}
} // namespace plot::imgui