#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. 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(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; 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