Skip to content

Commit

Permalink
Fully functional legend scrolling
Browse files Browse the repository at this point in the history
  • Loading branch information
epezent committed Aug 20, 2023
1 parent 18c7243 commit 625b04b
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 56 deletions.
6 changes: 4 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ The list below represents a combination of high-priority work, nice-to-have feat

## Legend

- `ImPlotLegendFlags_Scroll`
- improve legend icons (e.g. adopt markers, gradients, etc)
- make legend frame use ButtonBehavior (maybe impossible)
- generalize legend rendering for plots and subplots
- add draggable scroll bar if users need it

## Tools / Misc.

Expand Down Expand Up @@ -98,3 +98,5 @@ Ideally every `PlotX` function should use our faster rendering pipeline when it
- add `PlotBarGroups` wrapper that makes rendering groups of bars easier, with stacked bar support
- `PlotBars` restore outlines
- add hover/active color for plot axes
- make legend frame use ButtonBehavior
- `ImPlotLegendFlags_Scroll` (default behavior)
183 changes: 143 additions & 40 deletions implot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,28 @@ ImVec2 CalcLegendSize(ImPlotItemGroup& items, const ImVec2& pad, const ImVec2& s
return legend_size;
}

bool ClampLegendRect(ImRect& legend_rect, const ImRect& outer_rect, const ImVec2& pad) {
bool clamped = false;
ImRect outer_rect_pad(outer_rect.Min + pad, outer_rect.Max - pad);
if (legend_rect.Min.x < outer_rect_pad.Min.x) {
legend_rect.Min.x = outer_rect_pad.Min.x;
clamped = true;
}
if (legend_rect.Min.y < outer_rect_pad.Min.y) {
legend_rect.Min.y = outer_rect_pad.Min.y;
clamped = true;
}
if (legend_rect.Max.x > outer_rect_pad.Max.x) {
legend_rect.Max.x = outer_rect_pad.Max.x;
clamped = true;
}
if (legend_rect.Max.y > outer_rect_pad.Max.y) {
legend_rect.Max.y = outer_rect_pad.Max.y;
clamped = true;
}
return clamped;
}

int LegendSortingComp(const void* _a, const void* _b) {
ImPlotItemGroup* items = GImPlot->SortItems;
const int a = *(const int*)_a;
Expand Down Expand Up @@ -2278,19 +2300,19 @@ void SetupAxesLimits(double x_min, double x_max, double y_min, double y_max, ImP

void SetupLegend(ImPlotLocation location, ImPlotLegendFlags flags) {
ImPlotContext& gp = *GImPlot;
IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr && !gp.CurrentPlot->SetupLocked,
"Setup needs to be called after BeginPlot and before any setup locking functions (e.g. PlotX)!");
IM_ASSERT_USER_ERROR(gp.CurrentItems != nullptr,
"SetupLegend() needs to be called within an itemized context!");
ImPlotLegend& legend = gp.CurrentItems->Legend;
// check and set location
if (location != legend.PreviousLocation)
legend.Location = location;
legend.PreviousLocation = location;
// check and set flags
if (flags != legend.PreviousFlags)
legend.Flags = flags;
legend.PreviousFlags = flags;
IM_ASSERT_USER_ERROR((gp.CurrentPlot != nullptr && !gp.CurrentPlot->SetupLocked) || (gp.CurrentSubplot != nullptr && gp.CurrentPlot == nullptr),
"Setup needs to be called after BeginPlot or BeginSubplots and before any setup locking functions (e.g. PlotX)!");
if (gp.CurrentItems) {
ImPlotLegend& legend = gp.CurrentItems->Legend;
// check and set location
if (location != legend.PreviousLocation)
legend.Location = location;
legend.PreviousLocation = location;
// check and set flags
if (flags != legend.PreviousFlags)
legend.Flags = flags;
legend.PreviousFlags = flags;
}
}

void SetupMouseText(ImPlotLocation location, ImPlotMouseTextFlags flags) {
Expand Down Expand Up @@ -3023,6 +3045,7 @@ void EndPlot() {
plot.Items.GetItemByIndex(i)->LegendHovered = false;
// render legend
if (!ImHasFlag(plot.Flags, ImPlotFlags_NoLegend) && plot.Items.GetLegendCount() > 0) {

ImPlotLegend& legend = plot.Items.Legend;
const bool legend_out = ImHasFlag(legend.Flags, ImPlotLegendFlags_Outside);
const bool legend_horz = ImHasFlag(legend.Flags, ImPlotLegendFlags_Horizontal);
Expand All @@ -3032,24 +3055,57 @@ void EndPlot() {
legend.Location,
legend_out ? gp.Style.PlotPadding : gp.Style.LegendPadding);
legend.Rect = ImRect(legend_pos, legend_pos + legend_size);
// test hover
legend.Hovered = ImGui::IsWindowHovered() && legend.Rect.Contains(IO.MousePos);
legend.RectClamped = legend.Rect;
const bool legend_scrollable = ClampLegendRect(legend.RectClamped,
legend_out ? plot.FrameRect : plot.PlotRect,
legend_out ? gp.Style.PlotPadding : gp.Style.LegendPadding
);
const ImGuiButtonFlags legend_button_flags = ImGuiButtonFlags_AllowOverlap
| ImGuiButtonFlags_PressedOnClick
| ImGuiButtonFlags_PressedOnDoubleClick
| ImGuiButtonFlags_MouseButtonLeft
| ImGuiButtonFlags_MouseButtonRight
| ImGuiButtonFlags_MouseButtonMiddle
| ImGuiButtonFlags_FlattenChildren;
ImGui::KeepAliveID(plot.Items.ID);
ImGui::ButtonBehavior(legend.RectClamped, plot.Items.ID, &legend.Hovered, &legend.Held, legend_button_flags);
legend.Hovered = legend.Hovered || (ImGui::IsWindowHovered() && legend.Rect.Contains(IO.MousePos));

if (legend_scrollable) {
if (legend.Hovered) {
ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, plot.Items.ID);
if (IO.MouseWheel != 0.0f) {
ImVec2 max_step = legend.Rect.GetSize() * 0.67f;
float font_size = ImGui::GetCurrentWindow()->CalcFontSize();
float scroll_step = ImFloor(ImMin(2 * font_size, max_step.x));
legend.Scroll.x += scroll_step * IO.MouseWheel;
legend.Scroll.y += scroll_step * IO.MouseWheel;
}
}
const ImVec2 min_scroll_offset = legend.RectClamped.GetSize() - legend.Rect.GetSize();
legend.Scroll.x = ImClamp(legend.Scroll.x, min_scroll_offset.x, 0.0f);
legend.Scroll.y = ImClamp(legend.Scroll.y, min_scroll_offset.y, 0.0f);
const ImVec2 scroll_offset = legend_horz ? ImVec2(legend.Scroll.x, 0) : ImVec2(0, legend.Scroll.y);
ImVec2 legend_offset = legend.RectClamped.Min - legend.Rect.Min + scroll_offset;
legend.Rect.Min += legend_offset;
legend.Rect.Max += legend_offset;
} else {
legend.Scroll = ImVec2(0,0);
}

if (legend_out)
ImGui::PushClipRect(plot.FrameRect.Min, plot.FrameRect.Max, true);
else
PushPlotClipRect();
ImU32 col_bg = GetStyleColorU32(ImPlotCol_LegendBg);
ImU32 col_bd = GetStyleColorU32(ImPlotCol_LegendBorder);
DrawList.AddRectFilled(legend.Rect.Min, legend.Rect.Max, col_bg);
DrawList.AddRect(legend.Rect.Min, legend.Rect.Max, col_bd);
const ImU32 col_bg = GetStyleColorU32(ImPlotCol_LegendBg);
const ImU32 col_bd = GetStyleColorU32(ImPlotCol_LegendBorder);
ImGui::PushClipRect(legend.RectClamped.Min, legend.RectClamped.Max, true);
DrawList.AddRectFilled(legend.RectClamped.Min, legend.RectClamped.Max, col_bg);
bool legend_contextable = ShowLegendEntries(plot.Items, legend.Rect, legend.Hovered, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz, DrawList)
&& !ImHasFlag(legend.Flags, ImPlotLegendFlags_NoMenus);
&& !ImHasFlag(legend.Flags, ImPlotLegendFlags_NoMenus);
DrawList.AddRect(legend.RectClamped.Min, legend.RectClamped.Max, col_bd);
ImGui::PopClipRect();

// main ctx menu
if (gp.OpenContextThisFrame && legend_contextable && !ImHasFlag(plot.Flags, ImPlotFlags_NoMenus))
ImGui::OpenPopup("##LegendContext");
ImGui::PopClipRect();
ImGui::OpenPopup("##LegendContext");

if (ImGui::BeginPopup("##LegendContext")) {
ImGui::Text("Legend"); ImGui::Separator();
if (ShowLegendContextMenu(legend, !ImHasFlag(plot.Flags, ImPlotFlags_NoLegend)))
Expand Down Expand Up @@ -3497,6 +3553,7 @@ void EndSubplots() {
ImPlotContext& gp = *GImPlot;
IM_ASSERT_USER_ERROR(gp.CurrentSubplot != nullptr, "Mismatched BeginSubplots()/EndSubplots()!");
ImPlotSubplot& subplot = *gp.CurrentSubplot;
const ImGuiIO& IO = ImGui::GetIO();
// set alignments
for (int r = 0; r < subplot.Rows; ++r)
subplot.RowAlignmentData[r].End();
Expand All @@ -3515,24 +3572,60 @@ void EndSubplots() {
const bool share_items = ImHasFlag(subplot.Flags, ImPlotSubplotFlags_ShareItems);
ImDrawList& DrawList = *ImGui::GetWindowDrawList();
if (share_items && !ImHasFlag(subplot.Flags, ImPlotSubplotFlags_NoLegend) && subplot.Items.GetLegendCount() > 0) {
const bool legend_horz = ImHasFlag(subplot.Items.Legend.Flags, ImPlotLegendFlags_Horizontal);
ImPlotLegend& legend = subplot.Items.Legend;
const bool legend_horz = ImHasFlag(legend.Flags, ImPlotLegendFlags_Horizontal);
const ImVec2 legend_size = CalcLegendSize(subplot.Items, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz);
const ImVec2 legend_pos = GetLocationPos(subplot.FrameRect, legend_size, subplot.Items.Legend.Location, gp.Style.PlotPadding);
subplot.Items.Legend.Rect = ImRect(legend_pos, legend_pos + legend_size);
subplot.Items.Legend.Hovered = subplot.FrameHovered && subplot.Items.Legend.Rect.Contains(ImGui::GetIO().MousePos);
ImGui::PushClipRect(subplot.FrameRect.Min, subplot.FrameRect.Max, true);
ImU32 col_bg = GetStyleColorU32(ImPlotCol_LegendBg);
ImU32 col_bd = GetStyleColorU32(ImPlotCol_LegendBorder);
DrawList.AddRectFilled(subplot.Items.Legend.Rect.Min, subplot.Items.Legend.Rect.Max, col_bg);
DrawList.AddRect(subplot.Items.Legend.Rect.Min, subplot.Items.Legend.Rect.Max, col_bd);
bool legend_contextable = ShowLegendEntries(subplot.Items, subplot.Items.Legend.Rect, subplot.Items.Legend.Hovered, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz, DrawList)
&& !ImHasFlag(subplot.Items.Legend.Flags, ImPlotLegendFlags_NoMenus);
const ImVec2 legend_pos = GetLocationPos(subplot.FrameRect, legend_size, legend.Location, gp.Style.PlotPadding);
legend.Rect = ImRect(legend_pos, legend_pos + legend_size);
legend.RectClamped = legend.Rect;
const bool legend_scrollable = ClampLegendRect(legend.RectClamped,subplot.FrameRect, gp.Style.PlotPadding);
const ImGuiButtonFlags legend_button_flags = ImGuiButtonFlags_AllowOverlap
| ImGuiButtonFlags_PressedOnClick
| ImGuiButtonFlags_PressedOnDoubleClick
| ImGuiButtonFlags_MouseButtonLeft
| ImGuiButtonFlags_MouseButtonRight
| ImGuiButtonFlags_MouseButtonMiddle
| ImGuiButtonFlags_FlattenChildren;
ImGui::KeepAliveID(subplot.Items.ID);
ImGui::ButtonBehavior(legend.RectClamped, subplot.Items.ID, &legend.Hovered, &legend.Held, legend_button_flags);
legend.Hovered = legend.Hovered || (subplot.FrameHovered && legend.Rect.Contains(ImGui::GetIO().MousePos));

if (legend_scrollable) {
if (legend.Hovered) {
ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, subplot.Items.ID);
if (IO.MouseWheel != 0.0f) {
ImVec2 max_step = legend.Rect.GetSize() * 0.67f;
float font_size = ImGui::GetCurrentWindow()->CalcFontSize();
float scroll_step = ImFloor(ImMin(2 * font_size, max_step.x));
legend.Scroll.x += scroll_step * IO.MouseWheel;
legend.Scroll.y += scroll_step * IO.MouseWheel;
}
}
const ImVec2 min_scroll_offset = legend.RectClamped.GetSize() - legend.Rect.GetSize();
legend.Scroll.x = ImClamp(legend.Scroll.x, min_scroll_offset.x, 0.0f);
legend.Scroll.y = ImClamp(legend.Scroll.y, min_scroll_offset.y, 0.0f);
const ImVec2 scroll_offset = legend_horz ? ImVec2(legend.Scroll.x, 0) : ImVec2(0, legend.Scroll.y);
ImVec2 legend_offset = legend.RectClamped.Min - legend.Rect.Min + scroll_offset;
legend.Rect.Min += legend_offset;
legend.Rect.Max += legend_offset;
} else {
legend.Scroll = ImVec2(0,0);
}

const ImU32 col_bg = GetStyleColorU32(ImPlotCol_LegendBg);
const ImU32 col_bd = GetStyleColorU32(ImPlotCol_LegendBorder);
ImGui::PushClipRect(legend.RectClamped.Min, legend.RectClamped.Max, true);
DrawList.AddRectFilled(legend.RectClamped.Min, legend.RectClamped.Max, col_bg);
bool legend_contextable = ShowLegendEntries(subplot.Items, legend.Rect, legend.Hovered, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz, DrawList)
&& !ImHasFlag(legend.Flags, ImPlotLegendFlags_NoMenus);
DrawList.AddRect(legend.RectClamped.Min, legend.RectClamped.Max, col_bd);
ImGui::PopClipRect();

if (legend_contextable && !ImHasFlag(subplot.Flags, ImPlotSubplotFlags_NoMenus) && ImGui::GetIO().MouseReleased[gp.InputMap.Menu])
ImGui::OpenPopup("##LegendContext");
ImGui::PopClipRect();
if (ImGui::BeginPopup("##LegendContext")) {
ImGui::Text("Legend"); ImGui::Separator();
if (ShowLegendContextMenu(subplot.Items.Legend, !ImHasFlag(subplot.Flags, ImPlotFlags_NoLegend)))
if (ShowLegendContextMenu(legend, !ImHasFlag(subplot.Flags, ImPlotFlags_NoLegend)))
ImFlipFlag(subplot.Flags, ImPlotFlags_NoLegend);
ImGui::EndPopup();
}
Expand Down Expand Up @@ -4180,7 +4273,7 @@ bool BeginDragDropTargetAxis(ImAxis axis) {
bool BeginDragDropTargetLegend() {
SetupLock();
ImPlotItemGroup& items = *GImPlot->CurrentItems;
ImRect rect = items.Legend.Rect;
ImRect rect = items.Legend.RectClamped;
return ImGui::BeginDragDropTargetCustom(rect, items.ID);
}

Expand Down Expand Up @@ -5119,6 +5212,7 @@ void ShowMetricsWindow(bool* p_popen) {
static bool show_frame_rects = false;
static bool show_subplot_frame_rects = false;
static bool show_subplot_grid_rects = false;
static bool show_legend_rects = false;

ImDrawList& fg = *ImGui::GetForegroundDrawList();

Expand All @@ -5143,6 +5237,7 @@ void ShowMetricsWindow(bool* p_popen) {
ImGui::Checkbox("Show Axis Rects", &show_axis_rects);
ImGui::Checkbox("Show Subplot Frame Rects", &show_subplot_frame_rects);
ImGui::Checkbox("Show Subplot Grid Rects", &show_subplot_grid_rects);
ImGui::Checkbox("Show Legend Rects", &show_legend_rects);
ImGui::TreePop();
}
const int n_plots = gp.Plots.GetBufSize();
Expand All @@ -5164,13 +5259,21 @@ void ShowMetricsWindow(bool* p_popen) {
fg.AddRect(plot->Axes[i].HoverRect.Min, plot->Axes[i].HoverRect.Max, IM_COL32(0,255,0,255));
}
}
if (show_legend_rects && plot->Items.GetLegendCount() > 0) {
fg.AddRect(plot->Items.Legend.Rect.Min, plot->Items.Legend.Rect.Max, IM_COL32(255,192,0,255));
fg.AddRect(plot->Items.Legend.RectClamped.Min, plot->Items.Legend.RectClamped.Max, IM_COL32(255,128,0,255));
}
}
for (int p = 0; p < n_subplots; ++p) {
ImPlotSubplot* subplot = gp.Subplots.GetByIndex(p);
if (show_subplot_frame_rects)
fg.AddRect(subplot->FrameRect.Min, subplot->FrameRect.Max, IM_COL32(255,0,0,255));
if (show_subplot_grid_rects)
fg.AddRect(subplot->GridRect.Min, subplot->GridRect.Max, IM_COL32(0,0,255,255));
if (show_legend_rects && subplot->Items.GetLegendCount() > 0) {
fg.AddRect(subplot->Items.Legend.Rect.Min, subplot->Items.Legend.Rect.Max, IM_COL32(255,192,0,255));
fg.AddRect(subplot->Items.Legend.RectClamped.Min, subplot->Items.Legend.RectClamped.Max, IM_COL32(255,128,0,255));
}
}
if (ImGui::TreeNode("Plots","Plots (%d)", n_plots)) {
for (int p = 0; p < n_plots; ++p) {
Expand Down
2 changes: 1 addition & 1 deletion implot.h
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ IMPLOT_API void SetupAxes(const char* x_label, const char* y_label, ImPlotAxisFl
// Sets the primary X and Y axes range limits. If ImPlotCond_Always is used, the axes limits will be locked (shorthand for two calls to SetupAxisLimits).
IMPLOT_API void SetupAxesLimits(double x_min, double x_max, double y_min, double y_max, ImPlotCond cond = ImPlotCond_Once);

// Sets up the plot legend.
// Sets up the plot legend. This can also be called immediately after BeginSubplots when using ImPlotSubplotFlags_ShareItems.
IMPLOT_API void SetupLegend(ImPlotLocation location, ImPlotLegendFlags flags=0);
// Set the location of the current plot's mouse position text (default = South|East).
IMPLOT_API void SetupMouseText(ImPlotLocation location, ImPlotMouseTextFlags flags=0);
Expand Down
Loading

0 comments on commit 625b04b

Please sign in to comment.