Refactor: Moved Combo/ListBox functions from imgui.cpp to imgui_widgets.cpp (#2036)
diff --git a/imgui.cpp b/imgui.cpp
index b89b7d5..3b9251a 100644
--- a/imgui.cpp
+++ b/imgui.cpp
@@ -10732,213 +10732,6 @@
return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", extra_flags);
}
-static float CalcMaxPopupHeightFromItemCount(int items_count)
-{
- ImGuiContext& g = *GImGui;
- if (items_count <= 0)
- return FLT_MAX;
- return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
-}
-
-bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
-{
- // Always consume the SetNextWindowSizeConstraint() call in our early return paths
- ImGuiContext& g = *GImGui;
- ImGuiCond backup_next_window_size_constraint = g.NextWindowData.SizeConstraintCond;
- g.NextWindowData.SizeConstraintCond = 0;
-
- ImGuiWindow* window = GetCurrentWindow();
- if (window->SkipItems)
- return false;
-
- IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
-
- const ImGuiStyle& style = g.Style;
- const ImGuiID id = window->GetID(label);
-
- const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
- const ImVec2 label_size = CalcTextSize(label, NULL, true);
- const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : CalcItemWidth();
- const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f));
- const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
- ItemSize(total_bb, style.FramePadding.y);
- if (!ItemAdd(total_bb, id, &frame_bb))
- return false;
-
- bool hovered, held;
- bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held);
- bool popup_open = IsPopupOpen(id);
-
- const ImRect value_bb(frame_bb.Min, frame_bb.Max - ImVec2(arrow_size, 0.0f));
- const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
- RenderNavHighlight(frame_bb, id);
- if (!(flags & ImGuiComboFlags_NoPreview))
- window->DrawList->AddRectFilled(frame_bb.Min, ImVec2(frame_bb.Max.x - arrow_size, frame_bb.Max.y), frame_col, style.FrameRounding, ImDrawCornerFlags_Left);
- if (!(flags & ImGuiComboFlags_NoArrowButton))
- {
- window->DrawList->AddRectFilled(ImVec2(frame_bb.Max.x - arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button), style.FrameRounding, (w <= arrow_size) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Right);
- RenderArrow(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down);
- }
- RenderFrameBorder(frame_bb.Min, frame_bb.Max, style.FrameRounding);
- if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
- RenderTextClipped(frame_bb.Min + style.FramePadding, value_bb.Max, preview_value, NULL, NULL, ImVec2(0.0f,0.0f));
- if (label_size.x > 0)
- RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
-
- if ((pressed || g.NavActivateId == id) && !popup_open)
- {
- if (window->DC.NavLayerCurrent == 0)
- window->NavLastIds[0] = id;
- OpenPopupEx(id);
- popup_open = true;
- }
-
- if (!popup_open)
- return false;
-
- if (backup_next_window_size_constraint)
- {
- g.NextWindowData.SizeConstraintCond = backup_next_window_size_constraint;
- g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
- }
- else
- {
- if ((flags & ImGuiComboFlags_HeightMask_) == 0)
- flags |= ImGuiComboFlags_HeightRegular;
- IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
- int popup_max_height_in_items = -1;
- if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
- else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
- else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
- SetNextWindowSizeConstraints(ImVec2(w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
- }
-
- char name[16];
- ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth
-
- // Peak into expected window size so we can position it
- if (ImGuiWindow* popup_window = FindWindowByName(name))
- if (popup_window->WasActive)
- {
- ImVec2 size_expected = CalcWindowExpectedSize(popup_window);
- if (flags & ImGuiComboFlags_PopupAlignLeft)
- popup_window->AutoPosLastDirection = ImGuiDir_Left;
- ImRect r_outer = GetWindowAllowedExtentRect(popup_window);
- ImVec2 pos = FindBestWindowPosForPopupEx(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, frame_bb, ImGuiPopupPositionPolicy_ComboBox);
- SetNextWindowPos(pos);
- }
-
- // Horizontally align ourselves with the framed text
- ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings;
- PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(style.FramePadding.x, style.WindowPadding.y));
- bool ret = Begin(name, NULL, window_flags);
- PopStyleVar();
- if (!ret)
- {
- EndPopup();
- IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
- return false;
- }
- return true;
-}
-
-void ImGui::EndCombo()
-{
- EndPopup();
-}
-
-// Getter for the old Combo() API: const char*[]
-static bool Items_ArrayGetter(void* data, int idx, const char** out_text)
-{
- const char* const* items = (const char* const*)data;
- if (out_text)
- *out_text = items[idx];
- return true;
-}
-
-// Getter for the old Combo() API: "item1\0item2\0item3\0"
-static bool Items_SingleStringGetter(void* data, int idx, const char** out_text)
-{
- // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited.
- const char* items_separated_by_zeros = (const char*)data;
- int items_count = 0;
- const char* p = items_separated_by_zeros;
- while (*p)
- {
- if (idx == items_count)
- break;
- p += strlen(p) + 1;
- items_count++;
- }
- if (!*p)
- return false;
- if (out_text)
- *out_text = p;
- return true;
-}
-
-// Old API, prefer using BeginCombo() nowadays if you can.
-bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int popup_max_height_in_items)
-{
- ImGuiContext& g = *GImGui;
-
- // Call the getter to obtain the preview string which is a parameter to BeginCombo()
- const char* preview_value = NULL;
- if (*current_item >= 0 && *current_item < items_count)
- items_getter(data, *current_item, &preview_value);
-
- // The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here.
- if (popup_max_height_in_items != -1 && !g.NextWindowData.SizeConstraintCond)
- SetNextWindowSizeConstraints(ImVec2(0,0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
-
- if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
- return false;
-
- // Display items
- // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed)
- bool value_changed = false;
- for (int i = 0; i < items_count; i++)
- {
- PushID((void*)(intptr_t)i);
- const bool item_selected = (i == *current_item);
- const char* item_text;
- if (!items_getter(data, i, &item_text))
- item_text = "*Unknown item*";
- if (Selectable(item_text, item_selected))
- {
- value_changed = true;
- *current_item = i;
- }
- if (item_selected)
- SetItemDefaultFocus();
- PopID();
- }
-
- EndCombo();
- return value_changed;
-}
-
-// Combo box helper allowing to pass an array of strings.
-bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
-{
- const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items);
- return value_changed;
-}
-
-// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
-bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
-{
- int items_count = 0;
- const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
- while (*p)
- {
- p += strlen(p) + 1;
- items_count++;
- }
- bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items);
- return value_changed;
-}
-
// Tip: pass an empty label (e.g. "##dummy") then you can use the space to draw other text or image.
// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
@@ -11042,108 +10835,6 @@
return false;
}
-// FIXME: Rename to BeginListBox()
-// Helper to calculate the size of a listbox and display a label on the right.
-// Tip: To have a list filling the entire window width, PushItemWidth(-1) and pass an empty label "##empty"
-bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg)
-{
- ImGuiWindow* window = GetCurrentWindow();
- if (window->SkipItems)
- return false;
-
- const ImGuiStyle& style = GetStyle();
- const ImGuiID id = GetID(label);
- const ImVec2 label_size = CalcTextSize(label, NULL, true);
-
- // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
- ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.4f + style.ItemSpacing.y);
- ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y));
- ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
- ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
- window->DC.LastItemRect = bb; // Forward storage for ListBoxFooter.. dodgy.
-
- BeginGroup();
- if (label_size.x > 0)
- RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
-
- BeginChildFrame(id, frame_bb.GetSize());
- return true;
-}
-
-// FIXME: Rename to BeginListBox()
-bool ImGui::ListBoxHeader(const char* label, int items_count, int height_in_items)
-{
- // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
- // We don't add +0.40f if items_count <= height_in_items. It is slightly dodgy, because it means a dynamic list of items will make the widget resize occasionally when it crosses that size.
- // I am expecting that someone will come and complain about this behavior in a remote future, then we can advise on a better solution.
- if (height_in_items < 0)
- height_in_items = ImMin(items_count, 7);
- float height_in_items_f = height_in_items < items_count ? (height_in_items + 0.40f) : (height_in_items + 0.00f);
-
- // We include ItemSpacing.y so that a list sized for the exact number of items doesn't make a scrollbar appears. We could also enforce that by passing a flag to BeginChild().
- ImVec2 size;
- size.x = 0.0f;
- size.y = GetTextLineHeightWithSpacing() * height_in_items_f + GetStyle().ItemSpacing.y;
- return ListBoxHeader(label, size);
-}
-
-// FIXME: Rename to EndListBox()
-void ImGui::ListBoxFooter()
-{
- ImGuiWindow* parent_window = GetCurrentWindow()->ParentWindow;
- const ImRect bb = parent_window->DC.LastItemRect;
- const ImGuiStyle& style = GetStyle();
-
- EndChildFrame();
-
- // Redeclare item size so that it includes the label (we have stored the full size in LastItemRect)
- // We call SameLine() to restore DC.CurrentLine* data
- SameLine();
- parent_window->DC.CursorPos = bb.Min;
- ItemSize(bb, style.FramePadding.y);
- EndGroup();
-}
-
-bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
-{
- const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items);
- return value_changed;
-}
-
-bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items)
-{
- if (!ListBoxHeader(label, items_count, height_in_items))
- return false;
-
- // Assume all items have even height (= 1 line of text). If you need items of different or variable sizes you can create a custom version of ListBox() in your code without using the clipper.
- ImGuiContext& g = *GImGui;
- bool value_changed = false;
- ImGuiListClipper clipper(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to.
- while (clipper.Step())
- for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
- {
- const bool item_selected = (i == *current_item);
- const char* item_text;
- if (!items_getter(data, i, &item_text))
- item_text = "*Unknown item*";
-
- PushID(i);
- if (Selectable(item_text, item_selected))
- {
- *current_item = i;
- value_changed = true;
- }
- if (item_selected)
- SetItemDefaultFocus();
- PopID();
- }
- ListBoxFooter();
- if (value_changed)
- MarkItemEdited(g.CurrentWindow->DC.LastItemId);
-
- return value_changed;
-}
-
bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
{
ImGuiWindow* window = GetCurrentWindow();
diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp
index 07fcc97..76ef649 100644
--- a/imgui_widgets.cpp
+++ b/imgui_widgets.cpp
@@ -892,6 +892,212 @@
// - Combo()
//-------------------------------------------------------------------------
+static float CalcMaxPopupHeightFromItemCount(int items_count)
+{
+ ImGuiContext& g = *GImGui;
+ if (items_count <= 0)
+ return FLT_MAX;
+ return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
+}
+
+bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
+{
+ // Always consume the SetNextWindowSizeConstraint() call in our early return paths
+ ImGuiContext& g = *GImGui;
+ ImGuiCond backup_next_window_size_constraint = g.NextWindowData.SizeConstraintCond;
+ g.NextWindowData.SizeConstraintCond = 0;
+
+ ImGuiWindow* window = GetCurrentWindow();
+ if (window->SkipItems)
+ return false;
+
+ IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
+
+ const ImGuiStyle& style = g.Style;
+ const ImGuiID id = window->GetID(label);
+
+ const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
+ const ImVec2 label_size = CalcTextSize(label, NULL, true);
+ const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : CalcItemWidth();
+ const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f));
+ const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
+ ItemSize(total_bb, style.FramePadding.y);
+ if (!ItemAdd(total_bb, id, &frame_bb))
+ return false;
+
+ bool hovered, held;
+ bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held);
+ bool popup_open = IsPopupOpen(id);
+
+ const ImRect value_bb(frame_bb.Min, frame_bb.Max - ImVec2(arrow_size, 0.0f));
+ const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
+ RenderNavHighlight(frame_bb, id);
+ if (!(flags & ImGuiComboFlags_NoPreview))
+ window->DrawList->AddRectFilled(frame_bb.Min, ImVec2(frame_bb.Max.x - arrow_size, frame_bb.Max.y), frame_col, style.FrameRounding, ImDrawCornerFlags_Left);
+ if (!(flags & ImGuiComboFlags_NoArrowButton))
+ {
+ window->DrawList->AddRectFilled(ImVec2(frame_bb.Max.x - arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button), style.FrameRounding, (w <= arrow_size) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Right);
+ RenderArrow(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down);
+ }
+ RenderFrameBorder(frame_bb.Min, frame_bb.Max, style.FrameRounding);
+ if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
+ RenderTextClipped(frame_bb.Min + style.FramePadding, value_bb.Max, preview_value, NULL, NULL, ImVec2(0.0f,0.0f));
+ if (label_size.x > 0)
+ RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
+
+ if ((pressed || g.NavActivateId == id) && !popup_open)
+ {
+ if (window->DC.NavLayerCurrent == 0)
+ window->NavLastIds[0] = id;
+ OpenPopupEx(id);
+ popup_open = true;
+ }
+
+ if (!popup_open)
+ return false;
+
+ if (backup_next_window_size_constraint)
+ {
+ g.NextWindowData.SizeConstraintCond = backup_next_window_size_constraint;
+ g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
+ }
+ else
+ {
+ if ((flags & ImGuiComboFlags_HeightMask_) == 0)
+ flags |= ImGuiComboFlags_HeightRegular;
+ IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
+ int popup_max_height_in_items = -1;
+ if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
+ else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
+ else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
+ SetNextWindowSizeConstraints(ImVec2(w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
+ }
+
+ char name[16];
+ ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth
+
+ // Peak into expected window size so we can position it
+ if (ImGuiWindow* popup_window = FindWindowByName(name))
+ if (popup_window->WasActive)
+ {
+ ImVec2 size_expected = CalcWindowExpectedSize(popup_window);
+ if (flags & ImGuiComboFlags_PopupAlignLeft)
+ popup_window->AutoPosLastDirection = ImGuiDir_Left;
+ ImRect r_outer = GetWindowAllowedExtentRect(popup_window);
+ ImVec2 pos = FindBestWindowPosForPopupEx(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, frame_bb, ImGuiPopupPositionPolicy_ComboBox);
+ SetNextWindowPos(pos);
+ }
+
+ // Horizontally align ourselves with the framed text
+ ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings;
+ PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(style.FramePadding.x, style.WindowPadding.y));
+ bool ret = Begin(name, NULL, window_flags);
+ PopStyleVar();
+ if (!ret)
+ {
+ EndPopup();
+ IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
+ return false;
+ }
+ return true;
+}
+
+void ImGui::EndCombo()
+{
+ EndPopup();
+}
+
+// Getter for the old Combo() API: const char*[]
+static bool Items_ArrayGetter(void* data, int idx, const char** out_text)
+{
+ const char* const* items = (const char* const*)data;
+ if (out_text)
+ *out_text = items[idx];
+ return true;
+}
+
+// Getter for the old Combo() API: "item1\0item2\0item3\0"
+static bool Items_SingleStringGetter(void* data, int idx, const char** out_text)
+{
+ // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited.
+ const char* items_separated_by_zeros = (const char*)data;
+ int items_count = 0;
+ const char* p = items_separated_by_zeros;
+ while (*p)
+ {
+ if (idx == items_count)
+ break;
+ p += strlen(p) + 1;
+ items_count++;
+ }
+ if (!*p)
+ return false;
+ if (out_text)
+ *out_text = p;
+ return true;
+}
+
+// Old API, prefer using BeginCombo() nowadays if you can.
+bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int popup_max_height_in_items)
+{
+ ImGuiContext& g = *GImGui;
+
+ // Call the getter to obtain the preview string which is a parameter to BeginCombo()
+ const char* preview_value = NULL;
+ if (*current_item >= 0 && *current_item < items_count)
+ items_getter(data, *current_item, &preview_value);
+
+ // The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here.
+ if (popup_max_height_in_items != -1 && !g.NextWindowData.SizeConstraintCond)
+ SetNextWindowSizeConstraints(ImVec2(0,0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
+
+ if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
+ return false;
+
+ // Display items
+ // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed)
+ bool value_changed = false;
+ for (int i = 0; i < items_count; i++)
+ {
+ PushID((void*)(intptr_t)i);
+ const bool item_selected = (i == *current_item);
+ const char* item_text;
+ if (!items_getter(data, i, &item_text))
+ item_text = "*Unknown item*";
+ if (Selectable(item_text, item_selected))
+ {
+ value_changed = true;
+ *current_item = i;
+ }
+ if (item_selected)
+ SetItemDefaultFocus();
+ PopID();
+ }
+
+ EndCombo();
+ return value_changed;
+}
+
+// Combo box helper allowing to pass an array of strings.
+bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
+{
+ const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items);
+ return value_changed;
+}
+
+// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
+bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
+{
+ int items_count = 0;
+ const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
+ while (*p)
+ {
+ p += strlen(p) + 1;
+ items_count++;
+ }
+ bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items);
+ return value_changed;
+}
//-------------------------------------------------------------------------
// WIDGETS: Data Type and Data Formatting Helpers [Internal]
@@ -1048,6 +1254,107 @@
// - ListBoxFooter()
//-------------------------------------------------------------------------
+// FIXME: Rename to BeginListBox()
+// Helper to calculate the size of a listbox and display a label on the right.
+// Tip: To have a list filling the entire window width, PushItemWidth(-1) and pass an empty label "##empty"
+bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg)
+{
+ ImGuiWindow* window = GetCurrentWindow();
+ if (window->SkipItems)
+ return false;
+
+ const ImGuiStyle& style = GetStyle();
+ const ImGuiID id = GetID(label);
+ const ImVec2 label_size = CalcTextSize(label, NULL, true);
+
+ // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
+ ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.4f + style.ItemSpacing.y);
+ ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y));
+ ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
+ ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
+ window->DC.LastItemRect = bb; // Forward storage for ListBoxFooter.. dodgy.
+
+ BeginGroup();
+ if (label_size.x > 0)
+ RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
+
+ BeginChildFrame(id, frame_bb.GetSize());
+ return true;
+}
+
+// FIXME: Rename to BeginListBox()
+bool ImGui::ListBoxHeader(const char* label, int items_count, int height_in_items)
+{
+ // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
+ // We don't add +0.40f if items_count <= height_in_items. It is slightly dodgy, because it means a dynamic list of items will make the widget resize occasionally when it crosses that size.
+ // I am expecting that someone will come and complain about this behavior in a remote future, then we can advise on a better solution.
+ if (height_in_items < 0)
+ height_in_items = ImMin(items_count, 7);
+ float height_in_items_f = height_in_items < items_count ? (height_in_items + 0.40f) : (height_in_items + 0.00f);
+
+ // We include ItemSpacing.y so that a list sized for the exact number of items doesn't make a scrollbar appears. We could also enforce that by passing a flag to BeginChild().
+ ImVec2 size;
+ size.x = 0.0f;
+ size.y = GetTextLineHeightWithSpacing() * height_in_items_f + GetStyle().ItemSpacing.y;
+ return ListBoxHeader(label, size);
+}
+
+// FIXME: Rename to EndListBox()
+void ImGui::ListBoxFooter()
+{
+ ImGuiWindow* parent_window = GetCurrentWindow()->ParentWindow;
+ const ImRect bb = parent_window->DC.LastItemRect;
+ const ImGuiStyle& style = GetStyle();
+
+ EndChildFrame();
+
+ // Redeclare item size so that it includes the label (we have stored the full size in LastItemRect)
+ // We call SameLine() to restore DC.CurrentLine* data
+ SameLine();
+ parent_window->DC.CursorPos = bb.Min;
+ ItemSize(bb, style.FramePadding.y);
+ EndGroup();
+}
+
+bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
+{
+ const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items);
+ return value_changed;
+}
+
+bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items)
+{
+ if (!ListBoxHeader(label, items_count, height_in_items))
+ return false;
+
+ // Assume all items have even height (= 1 line of text). If you need items of different or variable sizes you can create a custom version of ListBox() in your code without using the clipper.
+ ImGuiContext& g = *GImGui;
+ bool value_changed = false;
+ ImGuiListClipper clipper(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to.
+ while (clipper.Step())
+ for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
+ {
+ const bool item_selected = (i == *current_item);
+ const char* item_text;
+ if (!items_getter(data, i, &item_text))
+ item_text = "*Unknown item*";
+
+ PushID(i);
+ if (Selectable(item_text, item_selected))
+ {
+ *current_item = i;
+ value_changed = true;
+ }
+ if (item_selected)
+ SetItemDefaultFocus();
+ PopID();
+ }
+ ListBoxFooter();
+ if (value_changed)
+ MarkItemEdited(g.CurrentWindow->DC.LastItemId);
+
+ return value_changed;
+}
//-------------------------------------------------------------------------
// WIDGETS: Data Plotting