#include #include 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(input::Mod::Shift); if (io.KeyCtrl) mods |= static_cast(input::Mod::Ctrl); if (io.KeyAlt) mods |= static_cast(input::Mod::Alt); if (io.KeySuper) mods |= static_cast(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); const u32 fb_w = static_cast(fb_w_f + 0.5f); const u32 fb_h = static_cast(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