Refactor: Moved Combo/ListBox functions from imgui.cpp to imgui_widgets.cpp (#2036)

This commit is contained in:
omar 2018-08-30 14:49:28 +02:00
parent 6468a3c0ce
commit f26b8c1e07
2 changed files with 307 additions and 309 deletions

309
imgui.cpp
View File

@ -10732,213 +10732,6 @@ bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags extra_fla
return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", extra_flags); 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. // 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. // 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) bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
@ -11042,108 +10835,6 @@ bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags
return false; 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) bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
{ {
ImGuiWindow* window = GetCurrentWindow(); ImGuiWindow* window = GetCurrentWindow();

View File

@ -892,6 +892,212 @@ void ImGui::Bullet()
// - Combo() // - 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] // WIDGETS: Data Type and Data Formatting Helpers [Internal]
@ -1048,6 +1254,107 @@ void ImGui::Bullet()
// - ListBoxFooter() // - 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 // WIDGETS: Data Plotting