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.
280 lines
12 KiB
C++
280 lines
12 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#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 -- if the host leaves them visible. ImGui hands a child
|
|
// the clip rect of an ancestor that cuts it off (and the child's own rect when
|
|
// nothing does, which makes the intersection below a no-op); the widget's GL
|
|
// draws do not go through ImGui's clipping, so a child the hosting window cuts
|
|
// off would otherwise clear and paint over whatever the host drew outside it.
|
|
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
|
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
|
const ImVec2 clip_min = ImGui::GetWindowDrawList()->GetClipRectMin();
|
|
const ImVec2 clip_max = ImGui::GetWindowDrawList()->GetClipRectMax();
|
|
const f32 x0 = origin.x > clip_min.x ? origin.x : clip_min.x;
|
|
const f32 y0 = origin.y > clip_min.y ? origin.y : clip_min.y;
|
|
const f32 x1 = origin.x + avail.x < clip_max.x ? origin.x + avail.x : clip_max.x;
|
|
const f32 y1 = origin.y + avail.y < clip_max.y ? origin.y + avail.y : clip_max.y;
|
|
if (x1 <= x0 || y1 <= y0) {
|
|
// Nothing of the child is visible: no region to draw into, no rect to aim a
|
|
// gesture at, and InvisibleButton wants a non-zero size.
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleColor();
|
|
return;
|
|
}
|
|
const ImVec2 visible{x1 - x0, y1 - y0};
|
|
|
|
// An invisible item covering the *visible* 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::SetCursorScreenPos(ImVec2(x0, y0));
|
|
ImGui::InvisibleButton("surface", visible,
|
|
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{x0 * scale.x, y0 * scale.y, visible.x * scale.x, visible.y * scale.y};
|
|
|
|
if (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);
|
|
|
|
// The backend's framebuffer scale is the display's scale, so the renderer draws
|
|
// its logical style metrics at it: the same assertion above pins it positive,
|
|
// and the chrome is then the same physical size whatever surface the host
|
|
// renders into.
|
|
renderer.style().ui_scale = scale.x;
|
|
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;
|
|
|
|
// ImGui reports "the pointer is not on this window" as a sentinel position:
|
|
// imgui_impl_glfw's cursor-enter callback leaves (-FLT_MAX, -FLT_MAX) when the
|
|
// pointer leaves, and ClearInputMouse() does the same on a lost focus. Scaling
|
|
// that and differencing it against the last real position would hand the
|
|
// controller a ~1e35 "movement" for that frame -- and with a press captured over
|
|
// the plot, that movement is a pan, so the range would leave the representable
|
|
// world (and only `f` would bring it back, since a gesture that sets a range also
|
|
// clears auto scale). An invalid position is therefore no position *update*: the
|
|
// last real one stands, the movement is zero, and the button levels below still
|
|
// flow, so the release that ends a drag is never lost.
|
|
const bool pos_valid = ImGui::IsMousePosValid(&io.MousePos);
|
|
const f32 px = pos_valid ? io.MousePos.x * scale.x : prev_x_;
|
|
const f32 py = pos_valid ? io.MousePos.y * scale.y : prev_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 ((!pos_valid && !have_prev_pos_) || (!hovered && !capturing_)) {
|
|
// Either no position has ever been reported (nothing to aim at, no movement
|
|
// to forward), or the pointer is nowhere near the plot. Remember the levels
|
|
// either way, so re-entering the region does not synthesise an edge out of the
|
|
// state it left with -- and so a re-bound plot, which keeps its state too,
|
|
// cannot fabricate one either.
|
|
prev_down_[0] = down[0];
|
|
prev_down_[1] = down[1];
|
|
prev_down_[2] = down[2];
|
|
if (pos_valid) {
|
|
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. An invalid position contributes none of it.
|
|
const f32 dx = (pos_valid && have_prev_pos_) ? px - prev_x_ : 0.0f;
|
|
const f32 dy = (pos_valid && 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];
|
|
if (pos_valid) {
|
|
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 {
|
|
// The remembered button levels deliberately survive a re-bind, because they are
|
|
// the host's input state and not the plot's: clearing them would make the next
|
|
// frame synthesise a press out of a button that is already down, and a host
|
|
// drawing two plots through one widget re-binds twice a frame. Only the
|
|
// position memory and the capture are dropped, so the new plot's first event
|
|
// carries no movement carried over from the old one.
|
|
prev_x_ = 0.0f;
|
|
prev_y_ = 0.0f;
|
|
have_prev_pos_ = false;
|
|
capturing_ = false;
|
|
}
|
|
|
|
} // namespace plot::imgui
|