diff --git a/include/imguix/widgets.hpp b/include/imguix/widgets.hpp index e9fb003..b0a8722 100644 --- a/include/imguix/widgets.hpp +++ b/include/imguix/widgets.hpp @@ -49,6 +49,7 @@ # include # include # include +# include #endif #endif // IMGUIX_WIDGETS_HPP_INCLUDED diff --git a/include/imguix/widgets/plot/plot_overlay.hpp b/include/imguix/widgets/plot/plot_overlay.hpp new file mode 100644 index 0000000..e3fa5a9 --- /dev/null +++ b/include/imguix/widgets/plot/plot_overlay.hpp @@ -0,0 +1,46 @@ +#pragma once +#ifndef _IMGUIX_WIDGETS_PLOT_PLOT_OVERLAY_HPP_INCLUDED +#define _IMGUIX_WIDGETS_PLOT_PLOT_OVERLAY_HPP_INCLUDED + +/// \file plot_overlay.hpp +/// \brief Place ordinary ImGui widgets in the screen-space overlay of an ImPlot canvas. + +#include + +#include +#include + +namespace ImGuiX::Widgets { + + /// \brief Geometry exposed to a plot overlay callback. + struct PlotOverlayContext { + ImVec2 plot_pos{}; ///< Top-left screen position of the plot canvas. + ImVec2 plot_size{}; ///< Screen-space size of the plot canvas. + ImVec2 content_pos{}; ///< Screen position after overlay offset and padding. + }; + + /// \brief Placement and clipping policy for a plot overlay. + struct PlotOverlayConfig { + ImVec2 offset{}; ///< Screen-space offset from the plot top-left corner. + ImVec2 padding{-1.0f, -1.0f}; ///< Negative components use ItemInnerSpacing. + bool clip_to_plot = true; ///< Clip widget draw commands to the plot canvas. + }; + + using PlotOverlayCallback = std::function; + + /// \brief Draw ordinary ImGui widgets over the current ImPlot canvas. + /// \param config Screen-space placement and clipping policy. + /// \param callback Widgets to submit while the overlay cursor is active. + /// \pre Called between a successful ImPlot::BeginPlot() and ImPlot::EndPlot(). + /// \post Parent ImGui cursor and layout state are unchanged by the overlay. + /// \note Popup windows created by the callback are not clipped to the plot, as they + /// are separate ImGui windows. + void PlotOverlay(const PlotOverlayConfig& config, const PlotOverlayCallback& callback); + +} // namespace ImGuiX::Widgets + +#ifdef IMGUIX_HEADER_ONLY +#include "plot_overlay.ipp" +#endif + +#endif // _IMGUIX_WIDGETS_PLOT_PLOT_OVERLAY_HPP_INCLUDED diff --git a/include/imguix/widgets/plot/plot_overlay.ipp b/include/imguix/widgets/plot/plot_overlay.ipp new file mode 100644 index 0000000..278b552 --- /dev/null +++ b/include/imguix/widgets/plot/plot_overlay.ipp @@ -0,0 +1,50 @@ +#include +#include + +#include + +namespace ImGuiX::Widgets { + + IMGUIX_IMPL_INLINE void PlotOverlay(const PlotOverlayConfig& config, + const PlotOverlayCallback& callback) { + if (!callback) { + return; + } + + ImPlotPlot* current_plot = ImPlot::GetCurrentPlot(); + IM_ASSERT(current_plot != nullptr && + "PlotOverlay() must be called between BeginPlot() and EndPlot()"); + if (current_plot == nullptr) { + return; + } + + ImGuiWindow* window = ImGui::GetCurrentWindow(); + ImGuiContext& context = *GImGui; + const ImGuiWindowTempData saved_window_data = window->DC; + const ImGuiLastItemData saved_last_item = context.LastItemData; + + const ImVec2 plot_pos = ImPlot::GetPlotPos(); + const ImVec2 plot_size = ImPlot::GetPlotSize(); + const ImVec2 style_padding = ImGui::GetStyle().ItemInnerSpacing; + const ImVec2 padding(config.padding.x >= 0.0f ? config.padding.x : style_padding.x, + config.padding.y >= 0.0f ? config.padding.y : style_padding.y); + const ImVec2 content_pos(plot_pos.x + config.offset.x + padding.x, + plot_pos.y + config.offset.y + padding.y); + + if (config.clip_to_plot) { + ImGui::PushClipRect(plot_pos, + ImVec2(plot_pos.x + plot_size.x, plot_pos.y + plot_size.y), true); + } + ImGui::SetCursorScreenPos(content_pos); + callback(PlotOverlayContext{plot_pos, plot_size, content_pos}); + if (config.clip_to_plot) { + ImGui::PopClipRect(); + } + + // Overlay widgets must not consume parent layout space or make a later + // SameLine()/ItemSize() depend on the last widget drawn over the plot. + window->DC = saved_window_data; + context.LastItemData = saved_last_item; + } + +} // namespace ImGuiX::Widgets diff --git a/tests/test_plot_overlay_header_only.cpp b/tests/test_plot_overlay_header_only.cpp new file mode 100644 index 0000000..7d34be2 --- /dev/null +++ b/tests/test_plot_overlay_header_only.cpp @@ -0,0 +1,154 @@ +#define IMGUIX_HEADER_ONLY + +#include + +#include +#include + +#include +#include + +namespace { + + constexpr float kEpsilon = 0.01f; + + bool nearly_equal(const ImVec2& left, const ImVec2& right) { + return std::fabs(left.x - right.x) <= kEpsilon && std::fabs(left.y - right.y) <= kEpsilon; + } + + bool clip_matches(const ImVec4& clip, const ImVec2& min, const ImVec2& max) { + return std::fabs(clip.x - min.x) <= kEpsilon && std::fabs(clip.y - min.y) <= kEpsilon && + std::fabs(clip.z - max.x) <= kEpsilon && std::fabs(clip.w - max.y) <= kEpsilon; + } + +} // namespace + +int main() { + ImGui::CreateContext(); + ImPlot::CreateContext(); + + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(640.0f, 480.0f); + io.DeltaTime = 1.0f / 60.0f; + unsigned char* pixels = nullptr; + int atlas_width = 0; + int atlas_height = 0; + io.Fonts->GetTexDataAsRGBA32(&pixels, &atlas_width, &atlas_height); + + ImVec2 first_plot_pos{}; + ImVec2 first_plot_size{}; + ImVec2 first_button_min{}; + ImVec2 first_button_max{}; + ImVec2 cursor_before_overlay{}; + ImVec2 cursor_after_overlay{}; + bool first_callback_called = false; + bool second_callback_called = false; + bool popup_visible = false; + + const auto render_frame = [&](const ImVec2& mouse_pos, const bool mouse_down) { + io.AddMousePosEvent(mouse_pos.x, mouse_pos.y); + io.AddMouseButtonEvent(ImGuiMouseButton_Left, mouse_down); + ImGui::NewFrame(); + ImGui::SetNextWindowPos(ImVec2(20.0f, 20.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(560.0f, 380.0f), ImGuiCond_Always); + ImGui::Begin("##plot_overlay_test", nullptr, ImGuiWindowFlags_NoDecoration); + + ImGui::SetCursorPos(ImVec2(28.0f, 32.0f)); + if (ImPlot::BeginPlot("##overlay_first_plot", ImVec2(230.0f, 180.0f), + ImPlotFlags_NoLegend)) { + first_plot_pos = ImPlot::GetPlotPos(); + first_plot_size = ImPlot::GetPlotSize(); + ImPlot::SetupAxis(ImAxis_X1, nullptr); + ImPlot::SetupAxis(ImAxis_Y1, nullptr); + const double x_values[] = {0.0, 1.0, 2.0}; + const double y_values[] = {1.0, 2.0, 1.5}; + ImPlot::PlotLine("series", x_values, y_values, 3); + + cursor_before_overlay = ImGui::GetCursorScreenPos(); + ImGuiX::Widgets::PlotOverlayConfig config; + config.padding = ImVec2(6.0f, 6.0f); + ImGuiX::Widgets::PlotOverlay(config, [&](const auto& overlay) { + first_callback_called = true; + if (!nearly_equal(overlay.plot_pos, first_plot_pos) || + !nearly_equal(overlay.plot_size, first_plot_size)) { + first_callback_called = false; + } + if (ImGui::Button("##overlay_button", ImVec2(54.0f, 0.0f))) { + ImGui::OpenPopup("##overlay_popup"); + } + first_button_min = ImGui::GetItemRectMin(); + first_button_max = ImGui::GetItemRectMax(); + ImGui::SameLine(); + ImGui::SetNextItemWidth(62.0f); + if (ImGui::BeginCombo("##overlay_combo", "H1")) { + ImGui::Selectable("H1", true); + ImGui::EndCombo(); + } + if (ImGui::BeginPopup("##overlay_popup")) { + popup_visible = true; + ImGui::TextUnformatted("popup"); + ImGui::EndPopup(); + } + }); + cursor_after_overlay = ImGui::GetCursorScreenPos(); + ImPlot::EndPlot(); + } + + ImGui::SameLine(0.0f, 18.0f); + if (ImPlot::BeginPlot("##overlay_second_plot", ImVec2(230.0f, 180.0f), + ImPlotFlags_NoLegend)) { + ImGuiX::Widgets::PlotOverlayConfig config; + config.padding = ImVec2(4.0f, 4.0f); + ImGuiX::Widgets::PlotOverlay(config, [&](const auto&) { + second_callback_called = true; + ImGui::TextUnformatted("second"); + }); + ImPlot::EndPlot(); + } + + ImGui::End(); + ImGui::Render(); + }; + + render_frame(ImVec2(-100.0f, -100.0f), false); + if (!first_callback_called || !second_callback_called || + !nearly_equal(cursor_before_overlay, cursor_after_overlay)) { + std::cerr << "plot overlay did not preserve callback/layout state\n"; + ImPlot::DestroyContext(); + ImGui::DestroyContext(); + return 1; + } + + bool found_plot_clip = false; + for (const ImDrawList* draw_list : ImGui::GetDrawData()->CmdLists) { + for (const ImDrawCmd& command : draw_list->CmdBuffer) { + if (command.ElemCount > 0 && + clip_matches(command.ClipRect, first_plot_pos, + ImVec2(first_plot_pos.x + first_plot_size.x, + first_plot_pos.y + first_plot_size.y))) { + found_plot_clip = true; + } + } + } + if (!found_plot_clip) { + std::cerr << "plot overlay draw commands were not clipped to the plot\n"; + ImPlot::DestroyContext(); + ImGui::DestroyContext(); + return 1; + } + + const ImVec2 button_center((first_button_min.x + first_button_max.x) * 0.5f, + (first_button_min.y + first_button_max.y) * 0.5f); + render_frame(button_center, true); + render_frame(button_center, false); + if (!popup_visible) { + std::cerr << "plot overlay button did not open its popup\n"; + ImPlot::DestroyContext(); + ImGui::DestroyContext(); + return 1; + } + + ImPlot::DestroyContext(); + ImGui::DestroyContext(); + return 0; +}