From d4783bd5534d984f728edff9ed31888ecb0002f4 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 16 Apr 2026 16:39:07 +0200 Subject: [PATCH 01/10] Added missing Test Engine hooks for PlotXXX(), VSliderXXX(), TableHeader(). --- docs/CHANGELOG.txt | 1 + imgui_tables.cpp | 2 ++ imgui_widgets.cpp | 2 ++ 3 files changed, 5 insertions(+) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 208c58656..5c79b3461 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -90,6 +90,7 @@ Other Changes: a better recoverable error. (#9350) - Misc: - Minor optimization: reduce redudant label scanning in common widgets. + - Added missing Test Engine hooks for PlotXXX(), VSliderXXX(), TableHeader(). - Backends: - Metal: avoid redundant vertex buffer bind in `SetupRenderState()`, which leads to validation issue. (#9343) [@Hunam6] diff --git a/imgui_tables.cpp b/imgui_tables.cpp index c58694bc3..84d29e65b 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -3311,6 +3311,8 @@ void ImGui::TableHeader(const char* label) // We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden if (IsPopupOpenRequestForItem(ImGuiPopupFlags_None, id)) TableOpenContextMenu(column_n); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); } // Unlike TableHeadersRow() it is not expected that you can reimplement or customize this with custom widgets. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index bdec3440e..e26b33888 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -3542,6 +3542,7 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d if (label_size.x > 0.0f) RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return value_changed; } @@ -8933,6 +8934,7 @@ int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_get // Return hovered index or -1 if none are hovered. // This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx(). + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return idx_hovered; } From 2cbdb7a337af7da5cfe6adf320f0e84ef56bee01 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 16 Apr 2026 16:47:13 +0200 Subject: [PATCH 02/10] TextLink(), BeginCombo(): fixed two remainig instances of needlessly scanning for ##. --- imgui_widgets.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index e26b33888..1e5058af7 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -1558,7 +1558,7 @@ bool ImGui::TextLink(const char* label) window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf), 1.0f * (float)(int)g.Style._MainScale); // FIXME-TEXT: Underline mode // FIXME-DPI PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf)); - RenderText(bb.Min, label, label_end); + RenderText(bb.Min, label, label_end, false); PopStyleColor(); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); @@ -1950,7 +1950,7 @@ bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboF const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight(); const char* label_end = FindRenderedTextEnd(label); const ImVec2 label_size = CalcTextSize(label, label_end, false); - const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f; + const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, false).x : 0.0f; const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth()); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); From a2916923a12fd4c6caaa3fc7508e0bf817913263 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 16 Apr 2026 23:32:34 +0200 Subject: [PATCH 03/10] MultiSelect: Box-Select + Clipper: extend UnclipRect by ItemSpacing. (#7994, #8250, #7821, #7850, #7970) Amend 1ac469b50f --- docs/CHANGELOG.txt | 4 +++- imgui.cpp | 8 +++++--- imgui.h | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 5c79b3461..fe77e73ed 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -74,7 +74,9 @@ Other Changes: and ImGuiSelectionUserData is technically opaque storage. (#7994, #1861) (we will probably bring this back as a minor optimization if we have a way to for user to tell us ImGuiSelectionUserData are indices) - - Box-Select, Tables: fixed an issue when calling `BeginMultiSelect()` in a table + - Box-Select + Clipper: fixed an issue selecting items while scrolling while a clipper + active. (#7994, #8250, #7821, #7850, #7970) + - Box-Select + Tables: fixed an issue when calling `BeginMultiSelect()` in a table before layout has been locked (first row or headers row submitted). (#8250) - Style: - Fixed vertical scrollbar top coordinates when using thick borders on windows diff --git a/imgui.cpp b/imgui.cpp index 66a4a18d2..4735960e7 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3454,12 +3454,14 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. // As a workaround we currently half ItemSpacing worth on each side. - min_y -= g.Style.ItemSpacing.y; - max_y += g.Style.ItemSpacing.y; + float pad_y = g.Style.ItemSpacing.y; + min_y -= pad_y; + max_y += pad_y; // Box-select on 2D area requires different clipping. + // (best adding pad_y here than in BeginBoxSelect() as we are closer to current state) if (bs->UnclipMode) - data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->UnclipRect.Min.y, bs->UnclipRect.Max.y, 0, 0)); + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->UnclipRect.Min.y - pad_y, bs->UnclipRect.Max.y + pad_y, 0, 0)); } // Add main visible range diff --git a/imgui.h b/imgui.h index d9f6a8cd1..5539bc7e3 100644 --- a/imgui.h +++ b/imgui.h @@ -30,7 +30,7 @@ // Library Version // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') #define IMGUI_VERSION "1.92.8 WIP" -#define IMGUI_VERSION_NUM 19272 +#define IMGUI_VERSION_NUM 19273 #define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000 #define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198 From a2eb6d99ed6ccbf453a3bdefe6b576af442e8e1a Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 15 Apr 2026 16:42:56 +0200 Subject: [PATCH 04/10] MultiSelect: Box-Select +Tables: revert 4d00bf8ad which seems unneeded since enforce table layout in BeginMultiSelect(). (#7970, #7821). --- imgui_widgets.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 1e5058af7..0ea586a49 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7914,7 +7914,6 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window) { - ImGuiContext& g = *GImGui; if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) { // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only @@ -7923,10 +7922,10 @@ static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window) } else { - // When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970) + //// When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970) ImRect scope_rect = window->InnerClipRect; - if (g.CurrentTable != NULL) - scope_rect = g.CurrentTable->HostClipRect; + //if (g.CurrentTable != NULL) + // scope_rect = g.CurrentTable->HostClipRect; // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect? scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max); From c91b03060de6a758b51623ff7ddf99978b6edbb7 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 16 Apr 2026 23:59:19 +0200 Subject: [PATCH 05/10] Multi-Select: Box-Select: improve dirty unclip rectangle calculation + use in ImGuiMultiSelectFlags_BoxSelect1d mode when needed (e.g. wheel scrolling up). (#7994, #8250, #7821, #7850, #7970) --- docs/CHANGELOG.txt | 4 +++ imgui_internal.h | 3 ++ imgui_tables.cpp | 2 ++ imgui_widgets.cpp | 69 ++++++++++++++++++++++++++++++++++++---------- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index fe77e73ed..32f6a9767 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -61,6 +61,10 @@ Other Changes: - Multi-Select: - Fixed an issue using Multi-Select within a Table causing column width measurement to be invalid when trailing column contents is not submitted in the last row. (#9341, #8250) + - Box-Select: fixed an issue using ImGuiMultiSelectFlags_BoxSelect1d mode while scrolling. + Notably, using mouse wheel while holding a box-selection could lead items close to windows + edges from not being correctly unselected. (#7994, #8250, #7821, #7850, #7970) + - Box-Select: improved dirty/unclip rectangle logic for ImGuiMultiSelectFlags_BoxSelect2d. - Box-Select: fixed an issue using ImGuiMultiSelectFlags_BoxSelect2d mode, where items out of view wouldn't be properly selected while scrolling while mouse cursor is hovering outside of selection scope. (#7994, #1861, #6518) diff --git a/imgui_internal.h b/imgui_internal.h index 9bc0f9a50..f1f4d357e 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -606,6 +606,8 @@ struct IMGUI_API ImRect bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; } void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; } void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; } + void AddX(float x) { if (Min.x > x) Min.x = x; if (Max.x < x) Max.x = x; } + void AddY(float y) { if (Min.y > y) Min.y = y; if (Max.y < y) Max.y = y; } void Expand(const float amount) { Min.x -= amount; Min.y -= amount; Max.x += amount; Max.y += amount; } void Expand(const ImVec2& amount) { Min.x -= amount.x; Min.y -= amount.y; Max.x += amount.x; Max.y += amount.y; } void Translate(const ImVec2& d) { Min.x += d.x; Min.y += d.y; Max.x += d.x; Max.y += d.y; } @@ -1893,6 +1895,7 @@ struct ImGuiBoxSelectState // Temporary/Transient data bool UnclipMode; // (Temp/Transient, here in hot area). Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select. ImRect UnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. + ImRect UnclipRects[2]; // Per-axis versions. ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos) ImRect BoxSelectRectCurr; diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 84d29e65b..c590dac80 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -1330,6 +1330,8 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // When starting a BeginMultiSelect() after table has been layout we update IsRequestOutput fields. void ImGui::TableApplyExternalUnclipRect(ImGuiTable* table, ImRect& rect) { + if (rect.IsInverted()) + return; for (int column_n = 0; column_n < table->ColumnsCount; column_n++) { ImGuiTableColumn* column = &table->Columns[column_n]; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 0ea586a49..4998c6a8c 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7508,8 +7508,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, label_end, &label_size, style.SelectableTextAlign, &bb); #ifdef IMGUI_DEBUG_BOXSELECT - if (g.BoxSelectState.UnclipMode) - GetForegroundDrawList()->AddText(pos, IM_COL32(255,255,0,200), label); + if (g.BoxSelectState.UnclipMode) { GetForegroundDrawList()->AddText(pos, IM_COL32(255,255,0,200), label, label_end); } #endif // Automatically close popups @@ -7827,7 +7826,7 @@ bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiI return false; // Current frame absolute prev/current rectangles are used to toggle selection. - // They are derived from positions relative to scrolling space. + // They are derived from positions relative to scrolling space, so "previous" rectangle is reprojected for current frame coordinates. ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel); ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already ImVec2 curr_end_pos_abs = g.IO.MousePos; @@ -7837,26 +7836,68 @@ bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiI bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); + //IMGUI_DEBUG_LOG("StartPosRel (%.2f,%.2f) EndPosRel (%.2f,%.2f) -> (%.2f,%.2f)\n", bs->StartPosRel.x, bs->StartPosRel.y, bs->EndPosRel.x, bs->EndPosRel.y, WindowPosAbsToRel(window, g.IO.MousePos).x, WindowPosAbsToRel(window, g.IO.MousePos).y); // Box-select 2D mode detects change of the rectangle. - // Storing unclip rect used by widgets supporting box-select. - if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) + // Storing unclip rects which will be tested by widgets supporting box-select. Always update rectangles when active (even if we don't use them). + // To facilitate understanding this: enable IMGUI_DEBUG_BOXSELECT and visualize all geometry. + if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) { - if (bs->BoxSelectRectPrev.Min != bs->BoxSelectRectCurr.Min || bs->BoxSelectRectPrev.Max != bs->BoxSelectRectCurr.Max) - bs->UnclipMode = true; + // For both sides, compute the area differing between Prev and Curr rectangles. + bs->UnclipRects[0] = bs->UnclipRects[1] = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + for (int side = 0; side < 2; side++) + { + ImVec2 d_min = (side == 0) ? ImMin(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMin(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max); + ImVec2 d_max = (side == 0) ? ImMax(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMax(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max); + if (d_min.x != d_max.x) + { + bs->UnclipRects[0].AddX(d_min.x); + bs->UnclipRects[0].AddX(d_max.x); + } + if (d_min.y != d_max.y) + { + bs->UnclipRects[1].AddY(d_min.y); + bs->UnclipRects[1].AddY(d_max.y); + } + } - // Always update rect even if we don't use it. - bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect X coordinates could be intersection of Prev and Curr rect on X axis. - bs->UnclipRect.Add(bs->BoxSelectRectCurr); + ImRect box_select_intersection = bs->BoxSelectRectPrev; + box_select_intersection.Add(bs->BoxSelectRectCurr); + if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) + if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) + { + bs->UnclipRects[0].AddY(box_select_intersection.Min.y); + bs->UnclipRects[0].AddY(box_select_intersection.Max.y); + } + if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) + if (bs->BoxSelectRectPrev.Min.y != bs->BoxSelectRectCurr.Min.y || bs->BoxSelectRectPrev.Max.y != bs->BoxSelectRectCurr.Max.y) + { + bs->UnclipRects[1].AddX(box_select_intersection.Min.x); + bs->UnclipRects[1].AddX(box_select_intersection.Max.x); + } + + // Merge both rectangles into one. + // FIXME-OPT: When UnclipRect.Area() is much larger than the sum of UnclipRects[0]/[1] Areas, widgets should + // ideally first use UnclipRect as a first coarse cull layer + the individual ones as a second validation. + bs->UnclipRect = bs->UnclipRects[0]; + bs->UnclipRect.Add(bs->UnclipRects[1]); + if (!bs->UnclipRect.IsInverted() && (!window->ClipRect.Contains(bs->UnclipRect.Min) || !window->ClipRect.Contains(bs->UnclipRect.Max))) // !! Don't use Contains(ImRect) + bs->UnclipMode = true; + if (bs->UnclipMode && g.CurrentTable != NULL) + TableApplyExternalUnclipRect(g.CurrentTable, bs->UnclipRect); // No need submitting both } - if (bs->UnclipMode && g.CurrentTable != NULL) - TableApplyExternalUnclipRect(g.CurrentTable, bs->UnclipRect); #ifdef IMGUI_DEBUG_BOXSELECT - if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) - GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, bs->UnclipMode ? IM_COL32(255,255,0,200) : IM_COL32(255,0,0,200), 0.0f, 0, 4.0f); + //GetForegroundDrawList()->AddRect(scope_rect.Min, scope_rect.Max, IM_COL32(0, 255, 0, 200), 0.0f, 0, 4.0f); //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); + if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) + { + for (ImRect& unclip_r : bs->UnclipRects) + if (!unclip_r.IsInverted()) + GetForegroundDrawList()->AddRect(unclip_r.Min, unclip_r.Max, bs->UnclipMode ? IM_COL32(255, 255, 0, 200) : IM_COL32(255, 0, 0, 200), 0.0f, 0, 4.0f); + GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, bs->UnclipMode ? IM_COL32(255, 255, 0, 200) : IM_COL32(255, 0, 0, 200), 0.0f, 0, 2.0f); + } #endif return true; } From 97939e6837afbd4c440db323fcfbeb9915b1da84 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 17 Apr 2026 16:30:53 +0200 Subject: [PATCH 06/10] Multi-Select: Box-Select: fixes for using accross nested child windows. (#8364) - IsFocused scan nav focus route. - When covering multiple windows, draw in front most ones (grabbed FindFrontMostVisibleChildWindow() from docking branch). --- docs/CHANGELOG.txt | 1 + imgui.cpp | 19 +++++++++++++++++++ imgui_internal.h | 2 ++ imgui_widgets.cpp | 9 +++++---- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 32f6a9767..42e4f7909 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -78,6 +78,7 @@ Other Changes: and ImGuiSelectionUserData is technically opaque storage. (#7994, #1861) (we will probably bring this back as a minor optimization if we have a way to for user to tell us ImGuiSelectionUserData are indices) + - Box-Select: fixes for using accross nested child windows. (#8364) - Box-Select + Clipper: fixed an issue selecting items while scrolling while a clipper active. (#7994, #8250, #7821, #7850, #7970) - Box-Select + Tables: fixed an issue when calling `BeginMultiSelect()` in a table diff --git a/imgui.cpp b/imgui.cpp index 4735960e7..836ba0cc8 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -6588,6 +6588,14 @@ void ImGui::EndChild() g.LogLinePosY = -FLT_MAX; // To enforce a carriage return } +ImGuiWindow* ImGui::FindFrontMostVisibleChildWindow(ImGuiWindow* window) +{ + for (int n = window->DC.ChildWindows.Size - 1; n >= 0; n--) + if (IsWindowActiveAndVisible(window->DC.ChildWindows[n])) + return FindFrontMostVisibleChildWindow(window->DC.ChildWindows[n]); + return window; +} + static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled) { window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); @@ -8754,6 +8762,17 @@ void ImGui::PopFocusScope() g.CurrentFocusScopeId = g.FocusScopeStack.Size ? g.FocusScopeStack.back().ID : 0; } +bool ImGui::IsInNavFocusRoute(ImGuiID focus_scope_id) +{ + ImGuiContext& g = *GImGui; + if (g.NavFocusScopeId == focus_scope_id) + return true; + for (const ImGuiFocusScopeData& focus_scope : g.NavFocusRoute) + if (focus_scope.ID == focus_scope_id) + return true; + return false; +} + void ImGui::SetNavFocusScope(ImGuiID focus_scope_id) { ImGuiContext& g = *GImGui; diff --git a/imgui_internal.h b/imgui_internal.h index f1f4d357e..cd8e22005 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3328,6 +3328,7 @@ namespace ImGui // Childs IMGUI_API bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags); + IMGUI_API ImGuiWindow* FindFrontMostVisibleChildWindow(ImGuiWindow* window); // Popups, Modals IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags); @@ -3481,6 +3482,7 @@ namespace ImGui // We don't use the ID Stack for this as it is common to want them separate. IMGUI_API void PushFocusScope(ImGuiID id); IMGUI_API void PopFocusScope(); + IMGUI_API bool IsInNavFocusRoute(ImGuiID focus_scope_id); inline ImGuiID GetCurrentFocusScope() { ImGuiContext& g = *GImGui; return g.CurrentFocusScopeId; } // Focus scope we are outputting into, set by PushFocusScope() // Drag and Drop diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 4998c6a8c..d4f7c0eea 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7914,8 +7914,9 @@ void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flag bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view ImRect box_select_r = bs->BoxSelectRectCurr; box_select_r.ClipWith(scope_rect); - window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling - window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling + ImGuiWindow* draw_window = FindFrontMostVisibleChildWindow(window); + draw_window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling + draw_window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling // Scroll const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0; @@ -8014,10 +8015,10 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel ms->Clear(); ms->FocusScopeId = id; ms->Flags = flags; - ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); ms->BackupCursorMaxPos = window->DC.CursorMaxPos; ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; // CalcScopeRect() for ImGuiMultiSelectFlags_ScopeRect will measure in EndMultiSelect(). PushFocusScope(ms->FocusScopeId); + ms->IsFocused = IsInNavFocusRoute(g.CurrentFocusScopeId); if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items. window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main; @@ -8145,7 +8146,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() // Clear selection when clicking void? // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! // The InnerRect test is necessary for non-child/decorated windows. - bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos); + bool scope_hovered = window->InnerRect.Contains(g.IO.MousePos) && IsWindowHovered(ImGuiHoveredFlags_ChildWindows); if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)) scope_hovered &= scope_rect.Contains(g.IO.MousePos); if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) From cdb046ac3f8df9a6adf3844f5e420709da46527e Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 17 Apr 2026 19:19:21 +0200 Subject: [PATCH 07/10] Demo: Assets Browser: fixed not submitting anything after a SetCursorScreenPos() call when no items. Would not manifest because BeginMultiSelect() altered CursorMaxPos. (#5548) cc: 2819ab32f8, edcd5b113e1 --- imgui.cpp | 3 ++- imgui_demo.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/imgui.cpp b/imgui.cpp index 836ba0cc8..188b53d45 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -10839,7 +10839,8 @@ bool ImGui::DebugCheckVersionAndDataLayout(const char* version, size_t sz_io, si // to extend contents size of our parent container (e.g. window contents size, which is used for auto-resizing // windows, table column contents size used for auto-resizing columns, group size). // This was causing issues and ambiguities and we needed to retire that. -// From 1.89, extending contents size boundaries REQUIRES AN ITEM TO BE SUBMITTED. +// 2022/08/05 (1.89): extending contents size boundaries REQUIRES AN ITEM TO BE SUBMITTED. However we gated the new logic behind a '#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS' block. +// 2025/06/25 (1.92): removed the legacy path and turned into an assert. It was a mistake that there was a #ifndef before: our obsolescence schedule gets pushed back a bit more :( // // Previously this would make the window content size ~200x200: // Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End(); // NOT OK ANYMORE diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 338a55cb3..08924ea9e 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -11016,6 +11016,8 @@ struct ExampleAssetsBrowser } } clipper.End(); + if (Items.Size == 0) + ImGui::Dummy(ImVec2(0, 0)); ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing // Context menu From ea1c04f7abc31f3089e5b163eef69d933e0a6209 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 17 Apr 2026 19:21:51 +0200 Subject: [PATCH 08/10] Multi-Select + Tables: fixed an issue which could lead to an extra vertical offset in the Header row. (#8250, #7994) Because BeginMultiSelect() does `ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos` at a time where CursorPos is already past MaxPos.y because of ItemSpacing.y. Accumulate spacing. --- docs/CHANGELOG.txt | 2 ++ imgui_widgets.cpp | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 42e4f7909..9a3e84f99 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -61,6 +61,8 @@ Other Changes: - Multi-Select: - Fixed an issue using Multi-Select within a Table causing column width measurement to be invalid when trailing column contents is not submitted in the last row. (#9341, #8250) + - Fixed an issue using Multi-Select within a Table with the right-most column visible, + which could lead to an extra vertical offset in the Header row. (#8250) - Box-Select: fixed an issue using ImGuiMultiSelectFlags_BoxSelect1d mode while scrolling. Notably, using mouse wheel while holding a box-selection could lead items close to windows edges from not being correctly unselected. (#7994, #8250, #7821, #7850, #7970) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index d4f7c0eea..3d9f2bec0 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -8016,7 +8016,9 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel ms->FocusScopeId = id; ms->Flags = flags; ms->BackupCursorMaxPos = window->DC.CursorMaxPos; - ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; // CalcScopeRect() for ImGuiMultiSelectFlags_ScopeRect will measure in EndMultiSelect(). + ms->ScopeRectMin = window->DC.CursorPos; + if (flags & ImGuiMultiSelectFlags_ScopeRect) + window->DC.CursorMaxPos = ms->ScopeRectMin; // CalcScopeRect() for ImGuiMultiSelectFlags_ScopeRect will measure in EndMultiSelect(). PushFocusScope(ms->FocusScopeId); ms->IsFocused = IsInNavFocusRoute(g.CurrentFocusScopeId); if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items. From ac88294b4a28b3b56ed2aaec013acfc5e2a9eda8 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 20 Apr 2026 11:07:45 +0200 Subject: [PATCH 09/10] MultiSelect: Box-Select +Tables: fix usage of box-selection columns with items straying out of columns. (#7994, #2221) Use 00d3f9295e. + Assets Browser toggle to enable ScrollX. --- docs/CHANGELOG.txt | 2 ++ imgui_demo.cpp | 11 ++++++++--- imgui_widgets.cpp | 11 +++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 9a3e84f99..a907de658 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -83,6 +83,8 @@ Other Changes: - Box-Select: fixes for using accross nested child windows. (#8364) - Box-Select + Clipper: fixed an issue selecting items while scrolling while a clipper active. (#7994, #8250, #7821, #7850, #7970) + - Box-Select + Tables: fixed an issue using box-selection in a tables with + items straying out of columns boundaries. (#7994, #2221) - Box-Select + Tables: fixed an issue when calling `BeginMultiSelect()` in a table before layout has been locked (first row or headers row submitted). (#8250) - Style: diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 08924ea9e..bbdfa1c3e 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -10726,8 +10726,9 @@ struct ExampleAssetsBrowser bool AllowDragUnselected = false; // Will set ImGuiMultiSelectFlags_SelectOnClickRelease float IconSize = 0; int IconSpacing = 10; - int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. + int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. bool StretchSpacing = true; + bool UseScrollX = false; // Debug: submit twice the number of items per line (overflow horizontally to exercise ScrollX + box-select) // State ImVector Items; // Our items @@ -10778,12 +10779,15 @@ struct ExampleAssetsBrowser // Layout: calculate number of icon per line and number of lines LayoutItemSize = ImVec2(floorf(IconSize), floorf(IconSize)); LayoutColumnCount = IM_MAX((int)(avail_width / (LayoutItemSize.x + LayoutItemSpacing)), 1); - LayoutLineCount = (Items.Size + LayoutColumnCount - 1) / LayoutColumnCount; // Layout: when stretching: allocate remaining space to more spacing. Round before division, so item_spacing may be non-integer. if (StretchSpacing && LayoutColumnCount > 1) LayoutItemSpacing = floorf(avail_width - LayoutItemSize.x * LayoutColumnCount) / LayoutColumnCount; + if (UseScrollX) + LayoutColumnCount *= 2; + LayoutLineCount = (Items.Size + LayoutColumnCount - 1) / LayoutColumnCount; + LayoutItemStep = ImVec2(LayoutItemSize.x + LayoutItemSpacing, LayoutItemSize.y + LayoutItemSpacing); LayoutSelectableSpacing = IM_MAX(floorf(LayoutItemSpacing) - IconHitSpacing, 0.0f); LayoutOuterPadding = floorf(LayoutItemSpacing * 0.5f); @@ -10843,6 +10847,7 @@ struct ExampleAssetsBrowser ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32); ImGui::SliderInt("Icon Hit Spacing", &IconHitSpacing, 0, 32); ImGui::Checkbox("Stretch Spacing", &StretchSpacing); + ImGui::Checkbox("Use ScrollX", &UseScrollX); ImGui::PopItemWidth(); ImGui::EndMenu(); } @@ -10872,7 +10877,7 @@ struct ExampleAssetsBrowser ImGuiIO& io = ImGui::GetIO(); ImGui::SetNextWindowContentSize(ImVec2(0.0f, LayoutOuterPadding + LayoutLineCount * (LayoutItemSize.y + LayoutItemSpacing))); - if (ImGui::BeginChild("Assets", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing()), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove)) + if (ImGui::BeginChild("Assets", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing()), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_HorizontalScrollbar)) { ImDrawList* draw_list = ImGui::GetWindowDrawList(); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 3d9f2bec0..9eb007621 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -8358,8 +8358,15 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if (ms->BoxSelectId != 0) if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) { - const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); - const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); + ImRect item_rect = g.LastItemData.Rect; + if (!window->DC.NavIsScrollPushableX) + { + const ImRect& clip_rect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect : window->ClipRect; + item_rect.Min.x = ImMax(item_rect.Min.x, clip_rect.Min.x); + item_rect.Max.x = ImMin(item_rect.Max.x, clip_rect.Max.x); + } + const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(item_rect); + const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(item_rect); if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) { if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce) From d7b40ab9a9799ba0ce94e6e8ee96d5b3ac55483a Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 20 Apr 2026 11:40:01 +0200 Subject: [PATCH 10/10] MultiSelect: Box-Select + Tables: Amend ac88294. fix usage of box-selection columns with items straying out of columns. (#7994, #2221) --- imgui.cpp | 2 +- imgui_widgets.cpp | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 188b53d45..1d2364669 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -13349,7 +13349,7 @@ static void ImGui::NavProcessItem() const ImGuiID id = g.LastItemData.ID; const ImGuiItemFlags item_flags = g.LastItemData.ItemFlags; - // When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221, #8816) + // When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221, #8816, #7994) ImRect nav_bb = g.LastItemData.NavRect; if (window->DC.NavIsScrollPushableX == false) { diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 9eb007621..d26e407da 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -8359,12 +8359,16 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) { ImRect item_rect = g.LastItemData.Rect; - if (!window->DC.NavIsScrollPushableX) - { - const ImRect& clip_rect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect : window->ClipRect; - item_rect.Min.x = ImMax(item_rect.Min.x, clip_rect.Min.x); - item_rect.Max.x = ImMin(item_rect.Max.x, clip_rect.Max.x); - } + if (!window->DC.NavIsScrollPushableX) // FIXME: Rename to be more generic. + if (ImGuiTable* table = g.CurrentTable) + if (table->CurrentColumn != -1) + { + // FIXME: We cannot use current ClipRect as it includes HostClipRect. + // A more generic version would be nice, but window->WorkRect.Min/Max exclude CellPadding. (#7994) + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + item_rect.Min.x = ImMax(item_rect.Min.x, column->MinX); + item_rect.Max.x = ImMin(item_rect.Max.x, column->MaxX); + } const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(item_rect); const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(item_rect); if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))