277 lines
12 KiB
C++
277 lines
12 KiB
C++
#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
|