diff --git a/imgui.cpp b/imgui.cpp index 0af2142..5d6e5df 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -694,7 +694,7 @@ static void CloseInactivePopups(ImGuiWindow* ref_window); static void ClosePopupToLevel(int remaining); static ImGuiWindow* GetFrontMostModalRootWindow(); -static ImVec2 FindBestPopupWindowPos(const ImVec2& base_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& rect_to_avoid); +static ImVec2 FindBestWindowPosForPopup(const ImVec2& base_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& rect_to_avoid); static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void* user_data); static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); @@ -4508,14 +4508,14 @@ { ImGuiContext& g = *GImGui; char window_name[16]; - ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip%02d", g.TooltipOverrideCount); + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", g.TooltipOverrideCount); if (override_previous_tooltip) if (ImGuiWindow* window = FindWindowByName(window_name)) if (window->Active) { // Hide previous tooltips. We can't easily "reset" the content of a window so we create a new one. window->HiddenFrames = 1; - ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip%02d", ++g.TooltipOverrideCount); + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", ++g.TooltipOverrideCount); } ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoNavFocus; Begin(window_name, NULL, flags | extra_flags); @@ -4558,7 +4558,7 @@ int current_stack_size = g.CurrentPopupStack.Size; ImVec2 mouse_pos = g.IO.MousePos; ImVec2 popup_pos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : mouse_pos; - ImGuiPopupRef popup_ref = ImGuiPopupRef(id, window, window->GetID("##menus"), popup_pos, mouse_pos); // Tagged as new ref because constructor sets Window to NULL (we are passing the ParentWindow info here) + ImGuiPopupRef popup_ref = ImGuiPopupRef(id, window, window->GetID("##Menus"), popup_pos, mouse_pos); // Tagged as new ref because constructor sets Window to NULL (we are passing the ParentWindow info here) if (g.OpenPopupStack.Size < current_stack_size + 1) g.OpenPopupStack.push_back(popup_ref); else if (reopen_existing || g.OpenPopupStack[current_stack_size].PopupId != id) @@ -4672,9 +4672,9 @@ char name[20]; if (flags & ImGuiWindowFlags_ChildMenu) - ImFormatString(name, IM_ARRAYSIZE(name), "##menu_%d", g.CurrentPopupStack.Size); // Recycle windows based on depth + ImFormatString(name, IM_ARRAYSIZE(name), "##Menu_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth else - ImFormatString(name, IM_ARRAYSIZE(name), "##popup_%08x", id); // Not recycling, so we can close/open during the same frame + ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // Not recycling, so we can close/open during the same frame bool is_open = Begin(name, NULL, flags); if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) @@ -4931,7 +4931,7 @@ IM_ASSERT(p_backup == window->DC.StackSizesBackup + IM_ARRAYSIZE(window->DC.StackSizesBackup)); } -static ImVec2 FindBestPopupWindowPos(const ImVec2& base_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_avoid) +static ImVec2 FindBestWindowPosForPopup(const ImVec2& base_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_avoid, ImGuiWindowFlags flags) { const ImGuiStyle& style = GImGui->Style; @@ -4944,17 +4944,38 @@ //GImGui->OverlayDrawList.AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255,0,0,255)); //GImGui->OverlayDrawList.AddRect(r_outer.Min, r_outer.Max, IM_COL32(0,255,0,255)); + // Combo Box policy (we want a connecting edge) + if (flags & ImGuiWindowFlags_ComboBox) + { + const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = { ImGuiDir_Down, ImGuiDir_Right, ImGuiDir_Left, ImGuiDir_Up }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + ImVec2 pos; + if (dir == ImGuiDir_Down) pos = ImVec2(r_avoid.Min.x, r_avoid.Max.y); // Below, Toward Right (default) + if (dir == ImGuiDir_Right) pos = ImVec2(r_avoid.Min.x, r_avoid.Min.y - size.y); // Above, Toward Right + if (dir == ImGuiDir_Left) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Max.y); // Below, Toward Left + if (dir == ImGuiDir_Up) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Min.y - size.y); // Above, Toward Left + if (!r_outer.Contains(ImRect(pos, pos + size))) + continue; + *last_dir = dir; + return pos; + } + } + + // Default popup policy const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = { ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left }; for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) { const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); - if (avail_w < size.x) - continue; float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); - if (avail_h < size.y) + if (avail_w < size.x || avail_h < size.y) continue; - ImVec2 pos; pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : base_pos_clamped.x; pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : base_pos_clamped.y; @@ -5035,7 +5056,7 @@ return window; } -static ImVec2 CalcSizeFullWithConstraint(ImGuiWindow* window, ImVec2 new_size) +static ImVec2 CalcSizeAfterConstraint(ImGuiWindow* window, ImVec2 new_size) { ImGuiContext& g = *GImGui; if (g.SetNextWindowSizeConstraint) @@ -5055,6 +5076,8 @@ new_size = data.DesiredSize; } } + + // Minimum size if (!(window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize))) { new_size = ImMax(new_size, g.Style.WindowMinSize); @@ -5071,7 +5094,7 @@ return sz + window->WindowPadding; } -static ImVec2 CalcSizeAutoFit(ImGuiWindow* window) +static ImVec2 CalcSizeAutoFit(ImGuiWindow* window, const ImVec2& size_contents) { ImGuiContext& g = *GImGui; ImGuiStyle& style = g.Style; @@ -5080,16 +5103,16 @@ if ((flags & ImGuiWindowFlags_Tooltip) != 0) { // Tooltip always resize. We keep the spacing symmetric on both axises for aesthetic purpose. - size_auto_fit = window->SizeContents; + size_auto_fit = size_contents; } else { - // Handling case of auto fit window not fitting on the screen (on either axis): we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than DisplaySize-WindowPadding. - size_auto_fit = ImClamp(window->SizeContents, style.WindowMinSize, ImMax(style.WindowMinSize, g.IO.DisplaySize - g.Style.DisplaySafeAreaPadding)); - ImVec2 size_auto_fit_after_constraint = CalcSizeFullWithConstraint(window, size_auto_fit); - if (size_auto_fit_after_constraint.x < window->SizeContents.x && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)) + // When the window cannot fit all contents (either because of constraints, either because screen is too small): we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than DisplaySize-WindowPadding. + size_auto_fit = ImClamp(size_contents, style.WindowMinSize, ImMax(style.WindowMinSize, g.IO.DisplaySize - g.Style.DisplaySafeAreaPadding)); + ImVec2 size_auto_fit_after_constraint = CalcSizeAfterConstraint(window, size_auto_fit); + if (size_auto_fit_after_constraint.x < size_contents.x && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)) size_auto_fit.y += style.ScrollbarSize; - if (size_auto_fit_after_constraint.y < window->SizeContents.y && !(flags & ImGuiWindowFlags_NoScrollbar)) + if (size_auto_fit_after_constraint.y < size_contents.y && !(flags & ImGuiWindowFlags_NoScrollbar)) size_auto_fit.x += style.ScrollbarSize; } return size_auto_fit; @@ -5137,7 +5160,7 @@ ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm); // Expected window upper-left ImVec2 pos_max = ImLerp(window->Pos + window->Size, corner_target, corner_norm); // Expected window lower-right ImVec2 size_expected = pos_max - pos_min; - ImVec2 size_constrained = CalcSizeFullWithConstraint(window, size_expected); + ImVec2 size_constrained = CalcSizeAfterConstraint(window, size_expected); *out_pos = pos_min; if (corner_norm.x == 0.0f) out_pos->x -= (size_constrained.x - size_expected.x); @@ -5375,30 +5398,30 @@ const float window_border_size = window->WindowBorderSize; // Calculate auto-fit size, handle automatic resize - const ImVec2 size_auto_fit = CalcSizeAutoFit(window); - ImVec2 size_for_scrollbars_visibility = window->SizeFullAtLastBegin; + const ImVec2 size_auto_fit = CalcSizeAutoFit(window, window->SizeContents); + ImVec2 size_full_modified(FLT_MAX, FLT_MAX); if (flags & ImGuiWindowFlags_AlwaysAutoResize && !window->Collapsed) { // Using SetNextWindowSize() overrides ImGuiWindowFlags_AlwaysAutoResize, so it can be used on tooltips/popups, etc. if (!window_size_x_set_by_api) - window->SizeFull.x = size_for_scrollbars_visibility.x = size_auto_fit.x; + window->SizeFull.x = size_full_modified.x = size_auto_fit.x; if (!window_size_y_set_by_api) - window->SizeFull.y = size_for_scrollbars_visibility.y = size_auto_fit.y; + window->SizeFull.y = size_full_modified.y = size_auto_fit.y; } else if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) { // Auto-fit only grows during the first few frames - // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize ImGuiWindowFlags_AlwaysAutoResize. + // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize when collapsed. if (!window_size_x_set_by_api && window->AutoFitFramesX > 0) - window->SizeFull.x = size_for_scrollbars_visibility.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; + window->SizeFull.x = size_full_modified.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; if (!window_size_y_set_by_api && window->AutoFitFramesY > 0) - window->SizeFull.y = size_for_scrollbars_visibility.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; + window->SizeFull.y = size_full_modified.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; if (!window->Collapsed) MarkIniSettingsDirty(window); } // Apply minimum/maximum window size constraints and final size - window->SizeFull = CalcSizeFullWithConstraint(window, window->SizeFull); + window->SizeFull = CalcSizeAfterConstraint(window, window->SizeFull); window->Size = window->Collapsed ? window->TitleBarRect().GetSize() : window->SizeFull; if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup)) { @@ -5411,10 +5434,13 @@ // Update scrollbar status (based on the Size that was effective during last frame or the auto-resized Size). if (!window->Collapsed) { - window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((window->SizeContents.y > size_for_scrollbars_visibility.y) && !(flags & ImGuiWindowFlags_NoScrollbar)); - window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((window->SizeContents.x > size_for_scrollbars_visibility.x - (window->ScrollbarY ? style.ScrollbarSize : 0.0f) - window->WindowPadding.x) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); + // When reading the current size we need to read it after size constraints have been applied + float size_x_for_scrollbars = size_full_modified.x != FLT_MAX ? window->SizeFull.x : window->SizeFullAtLastBegin.x; + float size_y_for_scrollbars = size_full_modified.y != FLT_MAX ? window->SizeFull.y : window->SizeFullAtLastBegin.y; + window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((window->SizeContents.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); + window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((window->SizeContents.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f) - window->WindowPadding.x) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); if (window->ScrollbarX && !window->ScrollbarY) - window->ScrollbarY = (window->SizeContents.y > size_for_scrollbars_visibility.y + style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); + window->ScrollbarY = (window->SizeContents.y > size_y_for_scrollbars + style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); } @@ -5446,12 +5472,12 @@ rect_to_avoid = ImRect(-FLT_MAX, parent_window->Pos.y + parent_window->TitleBarHeight(), FLT_MAX, parent_window->Pos.y + parent_window->TitleBarHeight() + parent_window->MenuBarHeight()); else rect_to_avoid = ImRect(parent_window->Pos.x + horizontal_overlap, -FLT_MAX, parent_window->Pos.x + parent_window->Size.x - horizontal_overlap - parent_window->ScrollbarSizes.x, FLT_MAX); - window->PosFloat = FindBestPopupWindowPos(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid, flags); } else if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api && window_just_appearing_after_hidden_for_resize) { ImRect rect_to_avoid(window->PosFloat.x - 1, window->PosFloat.y - 1, window->PosFloat.x + 1, window->PosFloat.y + 1); - window->PosFloat = FindBestPopupWindowPos(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid, flags); } // Position tooltip (always follows mouse) @@ -5459,7 +5485,7 @@ { ImVec2 ref_pos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : g.IO.MousePos; ImRect rect_to_avoid(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24, ref_pos.y + 24); // FIXME: Completely hard-coded. Store boxes in mouse cursor data? Scale? Center on cursor hit-point? - window->PosFloat = FindBestPopupWindowPos(ref_pos, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + window->PosFloat = FindBestWindowPosForPopup(ref_pos, window->Size, &window->AutoPosLastDirection, rect_to_avoid, flags); if (window->AutoPosLastDirection == ImGuiDir_None) window->PosFloat = ref_pos + ImVec2(2,2); // If there's not enough room, for tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. } @@ -5555,7 +5581,7 @@ if (g.HoveredWindow == window && held && g.IO.MouseDoubleClicked[0] && resize_grip_n == 0) { // Manual auto-fit when double-clicking - size_target = CalcSizeFullWithConstraint(window, size_auto_fit); + size_target = CalcSizeAfterConstraint(window, size_auto_fit); ClearActiveID(); } else if (held) @@ -10150,6 +10176,161 @@ return InputIntN(label, v, 4, 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; + bool backup_has_next_window_size_constraint = g.SetNextWindowSizeConstraint; + g.SetNextWindowSizeConstraint = false; + + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + 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; + + const float arrow_size = SmallSquareSize(); + + 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)); + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + RenderFrame(ImVec2(frame_bb.Max.x-arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32(popup_open || hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button), true, style.FrameRounding); // FIXME-ROUNDING + RenderTriangle(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down); + + if (preview_value != NULL) + 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, false); + popup_open = true; + } + + if (!popup_open) + return false; + + if (backup_has_next_window_size_constraint) + { + g.SetNextWindowSizeConstraint = true; + g.SetNextWindowSizeConstraintRect.Min.x = ImMax(g.SetNextWindowSizeConstraintRect.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)) + { + ImVec2 size_contents = CalcSizeContents(popup_window); + ImVec2 size_expected = CalcSizeAfterConstraint(popup_window, CalcSizeAutoFit(popup_window, size_contents)); + if (flags & ImGuiComboFlags_PopupAlignLeft) + popup_window->AutoPosLastDirection = ImGuiDir_Left; + ImVec2 pos = FindBestWindowPosForPopup(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, frame_bb, ImGuiWindowFlags_ComboBox); + SetNextWindowPos(pos); + } + + ImGuiWindowFlags window_flags = ImGuiWindowFlags_ComboBox | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; + if (!Begin(name, NULL, window_flags)) + { + EndPopup(); + IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above + return false; + } + + // Horizontally align ourselves with the framed text + if (style.FramePadding.x != style.WindowPadding.x) + Indent(style.FramePadding.x - style.WindowPadding.x); + + return true; +} + +void ImGui::EndCombo() +{ + const ImGuiStyle& style = GImGui->Style; + if (style.FramePadding.x != style.WindowPadding.x) + Unindent(style.FramePadding.x - style.WindowPadding.x); + EndPopup(); +} + +// Combo box function. +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; + + const char* preview_text = NULL; + if (*current_item >= 0 && *current_item < items_count) + items_getter(data, *current_item, &preview_text); + + if (popup_max_height_in_items != -1 && !g.SetNextWindowSizeConstraint) + { + float popup_max_height = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items); + SetNextWindowSizeConstraints(ImVec2(0,0), ImVec2(FLT_MAX, popup_max_height)); + } + if (!BeginCombo(label, preview_text, 0)) + return false; + + // Display items + // FIXME-OPT: Use clipper + 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 && IsWindowAppearing()) + SetItemDefaultFocus(); //SetScrollHere(); + PopID(); + } + + EndCombo(); + return value_changed; +} + static bool Items_ArrayGetter(void* data, int idx, const char** out_text) { const char* const* items = (const char* const*)data; @@ -10199,133 +10380,6 @@ return value_changed; } -bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags, ImVec2 popup_size) -{ - (void)flags; // Unused - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID(label); - const float w = CalcItemWidth(); - - const ImVec2 label_size = CalcTextSize(label, NULL, true); - 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; - - const float arrow_size = SmallSquareSize(); - - 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)); - RenderNavHighlight(frame_bb, id); - RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); - RenderFrame(ImVec2(frame_bb.Max.x-arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32(popup_open || hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button), true, style.FrameRounding); // FIXME-ROUNDING - RenderTriangle(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down); - - if (preview_value != NULL) - 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, false); - popup_open = true; - } - - if (!popup_open) - return false; - - if (popup_size.x == 0.0f) - popup_size.x = w; - - float popup_y1 = frame_bb.Max.y; - float popup_y2 = ImClamp(popup_y1 + popup_size.y, popup_y1, g.IO.DisplaySize.y - style.DisplaySafeAreaPadding.y); - if ((popup_y2 - popup_y1) < ImMin(popup_size.y, frame_bb.Min.y - style.DisplaySafeAreaPadding.y)) - { - // Position our combo ABOVE because there's more space to fit! (FIXME: Handle in Begin() or use a shared helper. We have similar code in Begin() for popup placement) - popup_y1 = ImClamp(frame_bb.Min.y - popup_size.y, style.DisplaySafeAreaPadding.y, frame_bb.Min.y); - popup_y2 = frame_bb.Min.y; - SetNextWindowPos(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FrameBorderSize), ImGuiCond_Always, ImVec2(0.0f, 1.0f)); - } - else - { - // Position our combo below - SetNextWindowPos(ImVec2(frame_bb.Min.x, frame_bb.Max.y - style.FrameBorderSize), ImGuiCond_Always, ImVec2(0.0f, 0.0f)); - } - SetNextWindowSize(ImVec2(popup_size.x, popup_y2 - popup_y1), ImGuiCond_Appearing); - PushStyleVar(ImGuiStyleVar_WindowPadding, style.FramePadding); - - if (!BeginPopupEx(id, ImGuiWindowFlags_ComboBox)) - { - IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above - return false; - } - Spacing(); - - return true; -} - -void ImGui::EndCombo() -{ - EndPopup(); - PopStyleVar(); -} - -// Combo box function. -bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items) -{ - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - - const char* preview_text = NULL; - if (*current_item >= 0 && *current_item < items_count) - items_getter(data, *current_item, &preview_text); - - // Size default to hold ~7 items - if (height_in_items < 0) - height_in_items = 7; - float popup_height = (g.FontSize + style.ItemSpacing.y) * ImMin(items_count, height_in_items) + (style.FramePadding.y * 3); - - if (!BeginCombo(label, preview_text, 0, ImVec2(0.0f, popup_height))) - return false; - - // Display items - // FIXME-OPT: Use clipper - 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 && IsWindowAppearing()) - SetItemDefaultFocus(); //SetScrollHere(); - PopID(); - } - - EndCombo(); - 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. bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) @@ -10686,7 +10740,7 @@ bool pressed; bool menu_is_open = IsPopupOpen(id); - bool menuset_is_open = !(window->Flags & ImGuiWindowFlags_Popup) && (g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].ParentMenuSet == window->GetID("##menus")); + bool menuset_is_open = !(window->Flags & ImGuiWindowFlags_Popup) && (g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].ParentMenuSet == window->GetID("##Menus")); ImGuiWindow* backed_nav_window = g.NavWindow; if (menuset_is_open) g.NavWindow = window; // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent) diff --git a/imgui.cpp b/imgui.cpp index 0af2142..5d6e5df 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -694,7 +694,7 @@ static void CloseInactivePopups(ImGuiWindow* ref_window); static void ClosePopupToLevel(int remaining); static ImGuiWindow* GetFrontMostModalRootWindow(); -static ImVec2 FindBestPopupWindowPos(const ImVec2& base_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& rect_to_avoid); +static ImVec2 FindBestWindowPosForPopup(const ImVec2& base_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& rect_to_avoid); static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void* user_data); static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); @@ -4508,14 +4508,14 @@ { ImGuiContext& g = *GImGui; char window_name[16]; - ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip%02d", g.TooltipOverrideCount); + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", g.TooltipOverrideCount); if (override_previous_tooltip) if (ImGuiWindow* window = FindWindowByName(window_name)) if (window->Active) { // Hide previous tooltips. We can't easily "reset" the content of a window so we create a new one. window->HiddenFrames = 1; - ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip%02d", ++g.TooltipOverrideCount); + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", ++g.TooltipOverrideCount); } ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoNavFocus; Begin(window_name, NULL, flags | extra_flags); @@ -4558,7 +4558,7 @@ int current_stack_size = g.CurrentPopupStack.Size; ImVec2 mouse_pos = g.IO.MousePos; ImVec2 popup_pos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : mouse_pos; - ImGuiPopupRef popup_ref = ImGuiPopupRef(id, window, window->GetID("##menus"), popup_pos, mouse_pos); // Tagged as new ref because constructor sets Window to NULL (we are passing the ParentWindow info here) + ImGuiPopupRef popup_ref = ImGuiPopupRef(id, window, window->GetID("##Menus"), popup_pos, mouse_pos); // Tagged as new ref because constructor sets Window to NULL (we are passing the ParentWindow info here) if (g.OpenPopupStack.Size < current_stack_size + 1) g.OpenPopupStack.push_back(popup_ref); else if (reopen_existing || g.OpenPopupStack[current_stack_size].PopupId != id) @@ -4672,9 +4672,9 @@ char name[20]; if (flags & ImGuiWindowFlags_ChildMenu) - ImFormatString(name, IM_ARRAYSIZE(name), "##menu_%d", g.CurrentPopupStack.Size); // Recycle windows based on depth + ImFormatString(name, IM_ARRAYSIZE(name), "##Menu_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth else - ImFormatString(name, IM_ARRAYSIZE(name), "##popup_%08x", id); // Not recycling, so we can close/open during the same frame + ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // Not recycling, so we can close/open during the same frame bool is_open = Begin(name, NULL, flags); if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) @@ -4931,7 +4931,7 @@ IM_ASSERT(p_backup == window->DC.StackSizesBackup + IM_ARRAYSIZE(window->DC.StackSizesBackup)); } -static ImVec2 FindBestPopupWindowPos(const ImVec2& base_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_avoid) +static ImVec2 FindBestWindowPosForPopup(const ImVec2& base_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_avoid, ImGuiWindowFlags flags) { const ImGuiStyle& style = GImGui->Style; @@ -4944,17 +4944,38 @@ //GImGui->OverlayDrawList.AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255,0,0,255)); //GImGui->OverlayDrawList.AddRect(r_outer.Min, r_outer.Max, IM_COL32(0,255,0,255)); + // Combo Box policy (we want a connecting edge) + if (flags & ImGuiWindowFlags_ComboBox) + { + const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = { ImGuiDir_Down, ImGuiDir_Right, ImGuiDir_Left, ImGuiDir_Up }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + ImVec2 pos; + if (dir == ImGuiDir_Down) pos = ImVec2(r_avoid.Min.x, r_avoid.Max.y); // Below, Toward Right (default) + if (dir == ImGuiDir_Right) pos = ImVec2(r_avoid.Min.x, r_avoid.Min.y - size.y); // Above, Toward Right + if (dir == ImGuiDir_Left) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Max.y); // Below, Toward Left + if (dir == ImGuiDir_Up) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Min.y - size.y); // Above, Toward Left + if (!r_outer.Contains(ImRect(pos, pos + size))) + continue; + *last_dir = dir; + return pos; + } + } + + // Default popup policy const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = { ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left }; for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) { const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); - if (avail_w < size.x) - continue; float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); - if (avail_h < size.y) + if (avail_w < size.x || avail_h < size.y) continue; - ImVec2 pos; pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : base_pos_clamped.x; pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : base_pos_clamped.y; @@ -5035,7 +5056,7 @@ return window; } -static ImVec2 CalcSizeFullWithConstraint(ImGuiWindow* window, ImVec2 new_size) +static ImVec2 CalcSizeAfterConstraint(ImGuiWindow* window, ImVec2 new_size) { ImGuiContext& g = *GImGui; if (g.SetNextWindowSizeConstraint) @@ -5055,6 +5076,8 @@ new_size = data.DesiredSize; } } + + // Minimum size if (!(window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize))) { new_size = ImMax(new_size, g.Style.WindowMinSize); @@ -5071,7 +5094,7 @@ return sz + window->WindowPadding; } -static ImVec2 CalcSizeAutoFit(ImGuiWindow* window) +static ImVec2 CalcSizeAutoFit(ImGuiWindow* window, const ImVec2& size_contents) { ImGuiContext& g = *GImGui; ImGuiStyle& style = g.Style; @@ -5080,16 +5103,16 @@ if ((flags & ImGuiWindowFlags_Tooltip) != 0) { // Tooltip always resize. We keep the spacing symmetric on both axises for aesthetic purpose. - size_auto_fit = window->SizeContents; + size_auto_fit = size_contents; } else { - // Handling case of auto fit window not fitting on the screen (on either axis): we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than DisplaySize-WindowPadding. - size_auto_fit = ImClamp(window->SizeContents, style.WindowMinSize, ImMax(style.WindowMinSize, g.IO.DisplaySize - g.Style.DisplaySafeAreaPadding)); - ImVec2 size_auto_fit_after_constraint = CalcSizeFullWithConstraint(window, size_auto_fit); - if (size_auto_fit_after_constraint.x < window->SizeContents.x && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)) + // When the window cannot fit all contents (either because of constraints, either because screen is too small): we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than DisplaySize-WindowPadding. + size_auto_fit = ImClamp(size_contents, style.WindowMinSize, ImMax(style.WindowMinSize, g.IO.DisplaySize - g.Style.DisplaySafeAreaPadding)); + ImVec2 size_auto_fit_after_constraint = CalcSizeAfterConstraint(window, size_auto_fit); + if (size_auto_fit_after_constraint.x < size_contents.x && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)) size_auto_fit.y += style.ScrollbarSize; - if (size_auto_fit_after_constraint.y < window->SizeContents.y && !(flags & ImGuiWindowFlags_NoScrollbar)) + if (size_auto_fit_after_constraint.y < size_contents.y && !(flags & ImGuiWindowFlags_NoScrollbar)) size_auto_fit.x += style.ScrollbarSize; } return size_auto_fit; @@ -5137,7 +5160,7 @@ ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm); // Expected window upper-left ImVec2 pos_max = ImLerp(window->Pos + window->Size, corner_target, corner_norm); // Expected window lower-right ImVec2 size_expected = pos_max - pos_min; - ImVec2 size_constrained = CalcSizeFullWithConstraint(window, size_expected); + ImVec2 size_constrained = CalcSizeAfterConstraint(window, size_expected); *out_pos = pos_min; if (corner_norm.x == 0.0f) out_pos->x -= (size_constrained.x - size_expected.x); @@ -5375,30 +5398,30 @@ const float window_border_size = window->WindowBorderSize; // Calculate auto-fit size, handle automatic resize - const ImVec2 size_auto_fit = CalcSizeAutoFit(window); - ImVec2 size_for_scrollbars_visibility = window->SizeFullAtLastBegin; + const ImVec2 size_auto_fit = CalcSizeAutoFit(window, window->SizeContents); + ImVec2 size_full_modified(FLT_MAX, FLT_MAX); if (flags & ImGuiWindowFlags_AlwaysAutoResize && !window->Collapsed) { // Using SetNextWindowSize() overrides ImGuiWindowFlags_AlwaysAutoResize, so it can be used on tooltips/popups, etc. if (!window_size_x_set_by_api) - window->SizeFull.x = size_for_scrollbars_visibility.x = size_auto_fit.x; + window->SizeFull.x = size_full_modified.x = size_auto_fit.x; if (!window_size_y_set_by_api) - window->SizeFull.y = size_for_scrollbars_visibility.y = size_auto_fit.y; + window->SizeFull.y = size_full_modified.y = size_auto_fit.y; } else if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) { // Auto-fit only grows during the first few frames - // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize ImGuiWindowFlags_AlwaysAutoResize. + // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize when collapsed. if (!window_size_x_set_by_api && window->AutoFitFramesX > 0) - window->SizeFull.x = size_for_scrollbars_visibility.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; + window->SizeFull.x = size_full_modified.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; if (!window_size_y_set_by_api && window->AutoFitFramesY > 0) - window->SizeFull.y = size_for_scrollbars_visibility.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; + window->SizeFull.y = size_full_modified.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; if (!window->Collapsed) MarkIniSettingsDirty(window); } // Apply minimum/maximum window size constraints and final size - window->SizeFull = CalcSizeFullWithConstraint(window, window->SizeFull); + window->SizeFull = CalcSizeAfterConstraint(window, window->SizeFull); window->Size = window->Collapsed ? window->TitleBarRect().GetSize() : window->SizeFull; if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup)) { @@ -5411,10 +5434,13 @@ // Update scrollbar status (based on the Size that was effective during last frame or the auto-resized Size). if (!window->Collapsed) { - window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((window->SizeContents.y > size_for_scrollbars_visibility.y) && !(flags & ImGuiWindowFlags_NoScrollbar)); - window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((window->SizeContents.x > size_for_scrollbars_visibility.x - (window->ScrollbarY ? style.ScrollbarSize : 0.0f) - window->WindowPadding.x) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); + // When reading the current size we need to read it after size constraints have been applied + float size_x_for_scrollbars = size_full_modified.x != FLT_MAX ? window->SizeFull.x : window->SizeFullAtLastBegin.x; + float size_y_for_scrollbars = size_full_modified.y != FLT_MAX ? window->SizeFull.y : window->SizeFullAtLastBegin.y; + window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((window->SizeContents.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); + window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((window->SizeContents.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f) - window->WindowPadding.x) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); if (window->ScrollbarX && !window->ScrollbarY) - window->ScrollbarY = (window->SizeContents.y > size_for_scrollbars_visibility.y + style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); + window->ScrollbarY = (window->SizeContents.y > size_y_for_scrollbars + style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); } @@ -5446,12 +5472,12 @@ rect_to_avoid = ImRect(-FLT_MAX, parent_window->Pos.y + parent_window->TitleBarHeight(), FLT_MAX, parent_window->Pos.y + parent_window->TitleBarHeight() + parent_window->MenuBarHeight()); else rect_to_avoid = ImRect(parent_window->Pos.x + horizontal_overlap, -FLT_MAX, parent_window->Pos.x + parent_window->Size.x - horizontal_overlap - parent_window->ScrollbarSizes.x, FLT_MAX); - window->PosFloat = FindBestPopupWindowPos(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid, flags); } else if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api && window_just_appearing_after_hidden_for_resize) { ImRect rect_to_avoid(window->PosFloat.x - 1, window->PosFloat.y - 1, window->PosFloat.x + 1, window->PosFloat.y + 1); - window->PosFloat = FindBestPopupWindowPos(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid, flags); } // Position tooltip (always follows mouse) @@ -5459,7 +5485,7 @@ { ImVec2 ref_pos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : g.IO.MousePos; ImRect rect_to_avoid(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24, ref_pos.y + 24); // FIXME: Completely hard-coded. Store boxes in mouse cursor data? Scale? Center on cursor hit-point? - window->PosFloat = FindBestPopupWindowPos(ref_pos, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + window->PosFloat = FindBestWindowPosForPopup(ref_pos, window->Size, &window->AutoPosLastDirection, rect_to_avoid, flags); if (window->AutoPosLastDirection == ImGuiDir_None) window->PosFloat = ref_pos + ImVec2(2,2); // If there's not enough room, for tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. } @@ -5555,7 +5581,7 @@ if (g.HoveredWindow == window && held && g.IO.MouseDoubleClicked[0] && resize_grip_n == 0) { // Manual auto-fit when double-clicking - size_target = CalcSizeFullWithConstraint(window, size_auto_fit); + size_target = CalcSizeAfterConstraint(window, size_auto_fit); ClearActiveID(); } else if (held) @@ -10150,6 +10176,161 @@ return InputIntN(label, v, 4, 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; + bool backup_has_next_window_size_constraint = g.SetNextWindowSizeConstraint; + g.SetNextWindowSizeConstraint = false; + + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + 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; + + const float arrow_size = SmallSquareSize(); + + 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)); + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + RenderFrame(ImVec2(frame_bb.Max.x-arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32(popup_open || hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button), true, style.FrameRounding); // FIXME-ROUNDING + RenderTriangle(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down); + + if (preview_value != NULL) + 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, false); + popup_open = true; + } + + if (!popup_open) + return false; + + if (backup_has_next_window_size_constraint) + { + g.SetNextWindowSizeConstraint = true; + g.SetNextWindowSizeConstraintRect.Min.x = ImMax(g.SetNextWindowSizeConstraintRect.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)) + { + ImVec2 size_contents = CalcSizeContents(popup_window); + ImVec2 size_expected = CalcSizeAfterConstraint(popup_window, CalcSizeAutoFit(popup_window, size_contents)); + if (flags & ImGuiComboFlags_PopupAlignLeft) + popup_window->AutoPosLastDirection = ImGuiDir_Left; + ImVec2 pos = FindBestWindowPosForPopup(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, frame_bb, ImGuiWindowFlags_ComboBox); + SetNextWindowPos(pos); + } + + ImGuiWindowFlags window_flags = ImGuiWindowFlags_ComboBox | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; + if (!Begin(name, NULL, window_flags)) + { + EndPopup(); + IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above + return false; + } + + // Horizontally align ourselves with the framed text + if (style.FramePadding.x != style.WindowPadding.x) + Indent(style.FramePadding.x - style.WindowPadding.x); + + return true; +} + +void ImGui::EndCombo() +{ + const ImGuiStyle& style = GImGui->Style; + if (style.FramePadding.x != style.WindowPadding.x) + Unindent(style.FramePadding.x - style.WindowPadding.x); + EndPopup(); +} + +// Combo box function. +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; + + const char* preview_text = NULL; + if (*current_item >= 0 && *current_item < items_count) + items_getter(data, *current_item, &preview_text); + + if (popup_max_height_in_items != -1 && !g.SetNextWindowSizeConstraint) + { + float popup_max_height = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items); + SetNextWindowSizeConstraints(ImVec2(0,0), ImVec2(FLT_MAX, popup_max_height)); + } + if (!BeginCombo(label, preview_text, 0)) + return false; + + // Display items + // FIXME-OPT: Use clipper + 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 && IsWindowAppearing()) + SetItemDefaultFocus(); //SetScrollHere(); + PopID(); + } + + EndCombo(); + return value_changed; +} + static bool Items_ArrayGetter(void* data, int idx, const char** out_text) { const char* const* items = (const char* const*)data; @@ -10199,133 +10380,6 @@ return value_changed; } -bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags, ImVec2 popup_size) -{ - (void)flags; // Unused - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID(label); - const float w = CalcItemWidth(); - - const ImVec2 label_size = CalcTextSize(label, NULL, true); - 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; - - const float arrow_size = SmallSquareSize(); - - 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)); - RenderNavHighlight(frame_bb, id); - RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); - RenderFrame(ImVec2(frame_bb.Max.x-arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32(popup_open || hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button), true, style.FrameRounding); // FIXME-ROUNDING - RenderTriangle(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down); - - if (preview_value != NULL) - 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, false); - popup_open = true; - } - - if (!popup_open) - return false; - - if (popup_size.x == 0.0f) - popup_size.x = w; - - float popup_y1 = frame_bb.Max.y; - float popup_y2 = ImClamp(popup_y1 + popup_size.y, popup_y1, g.IO.DisplaySize.y - style.DisplaySafeAreaPadding.y); - if ((popup_y2 - popup_y1) < ImMin(popup_size.y, frame_bb.Min.y - style.DisplaySafeAreaPadding.y)) - { - // Position our combo ABOVE because there's more space to fit! (FIXME: Handle in Begin() or use a shared helper. We have similar code in Begin() for popup placement) - popup_y1 = ImClamp(frame_bb.Min.y - popup_size.y, style.DisplaySafeAreaPadding.y, frame_bb.Min.y); - popup_y2 = frame_bb.Min.y; - SetNextWindowPos(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FrameBorderSize), ImGuiCond_Always, ImVec2(0.0f, 1.0f)); - } - else - { - // Position our combo below - SetNextWindowPos(ImVec2(frame_bb.Min.x, frame_bb.Max.y - style.FrameBorderSize), ImGuiCond_Always, ImVec2(0.0f, 0.0f)); - } - SetNextWindowSize(ImVec2(popup_size.x, popup_y2 - popup_y1), ImGuiCond_Appearing); - PushStyleVar(ImGuiStyleVar_WindowPadding, style.FramePadding); - - if (!BeginPopupEx(id, ImGuiWindowFlags_ComboBox)) - { - IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above - return false; - } - Spacing(); - - return true; -} - -void ImGui::EndCombo() -{ - EndPopup(); - PopStyleVar(); -} - -// Combo box function. -bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items) -{ - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - - const char* preview_text = NULL; - if (*current_item >= 0 && *current_item < items_count) - items_getter(data, *current_item, &preview_text); - - // Size default to hold ~7 items - if (height_in_items < 0) - height_in_items = 7; - float popup_height = (g.FontSize + style.ItemSpacing.y) * ImMin(items_count, height_in_items) + (style.FramePadding.y * 3); - - if (!BeginCombo(label, preview_text, 0, ImVec2(0.0f, popup_height))) - return false; - - // Display items - // FIXME-OPT: Use clipper - 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 && IsWindowAppearing()) - SetItemDefaultFocus(); //SetScrollHere(); - PopID(); - } - - EndCombo(); - 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. bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) @@ -10686,7 +10740,7 @@ bool pressed; bool menu_is_open = IsPopupOpen(id); - bool menuset_is_open = !(window->Flags & ImGuiWindowFlags_Popup) && (g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].ParentMenuSet == window->GetID("##menus")); + bool menuset_is_open = !(window->Flags & ImGuiWindowFlags_Popup) && (g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].ParentMenuSet == window->GetID("##Menus")); ImGuiWindow* backed_nav_window = g.NavWindow; if (menuset_is_open) g.NavWindow = window; // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent) diff --git a/imgui_internal.h b/imgui_internal.h index 945b336..4e43f06 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -216,6 +216,14 @@ enum ImGuiComboFlags_ { + ImGuiComboFlags_PopupAlignLeft = 1 << 0, // Align the popup toward the left by default + + // If you want your combo popup to be a specific size you can use SetNextWindowSizeConstraints() prior to calling BeginCombo() + ImGuiComboFlags_HeightSmall = 1 << 1, // Max ~4 items visible + ImGuiComboFlags_HeightRegular = 1 << 2, // Max ~8 items visible (default) + ImGuiComboFlags_HeightLarge = 1 << 3, // Max ~20 items visible + ImGuiComboFlags_HeightLargest = 1 << 4, // As many fitting items as possible + ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest }; enum ImGuiSeparatorFlags_ @@ -957,7 +965,7 @@ IMGUI_API void PushColumnClipRect(int column_index = -1); // FIXME-WIP: New Combo API - IMGUI_API bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0, ImVec2 popup_size = ImVec2(0.0f,0.0f)); + IMGUI_API bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0); IMGUI_API void EndCombo(); // NB: All position are in absolute pixels coordinates (never using window coordinates internally)