From 15391762ddbd4ad03c11dc1746f0603c75036edc Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 20 Dec 2023 21:14:09 +0100 Subject: [PATCH] MultiSelect: Box-Select: Added ImGuiMultiSelectFlags_BoxSelect2d support. Enabled in Asset Browser. Selectable() supports it. --- imgui.cpp | 4 +++ imgui.h | 17 +++++++------ imgui_demo.cpp | 47 +++++++++++++++++----------------- imgui_internal.h | 2 ++ imgui_widgets.cpp | 65 ++++++++++++++++++++++++++++++++--------------- 5 files changed, 84 insertions(+), 51 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index bee410a72..8f9a8fbc9 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3091,6 +3091,10 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) // As a workaround we currently half ItemSpacing worth on each side. min_y -= g.Style.ItemSpacing.y; max_y += g.Style.ItemSpacing.y; + + // Box-select on 2D area requires different clipping. + if (ms->BoxSelectUnclipMode) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(ms->BoxSelectUnclipRect.Min.y, ms->BoxSelectUnclipRect.Max.y, 0, 0)); } const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; diff --git a/imgui.h b/imgui.h index 63159a14b..c4b8afa69 100644 --- a/imgui.h +++ b/imgui.h @@ -2774,14 +2774,15 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut sending a SelectAll request. - ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection. Box-selection + clipper is currently only supported for 1D list (not with 2D grid). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 3, // Disable scrolling when box-selecting near edges of scope. - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 4, // Clear selection when pressing Escape while scope is focused. - ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 5, // Clear selection when clicking on empty location within scope. - ImGuiMultiSelectFlags_ScopeWindow = 1 << 6, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if BeginMultiSelect() covers a whole window. - ImGuiMultiSelectFlags_ScopeRect = 1 << 7, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. - ImGuiMultiSelectFlags_SelectOnClick = 1 << 8, // Apply selection on mouse down when clicking on unselected item. (Default) - ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 9, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 3, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 4, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 5, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 6, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 7, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if BeginMultiSelect() covers a whole window. + ImGuiMultiSelectFlags_ScopeRect = 1 << 8, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 9, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 10, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; enum ImGuiSelectionRequestType diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 004f89ca1..b84a0ca9f 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -9622,7 +9622,7 @@ struct ExampleAssetsBrowser // Options bool ShowTypeOverlay = true; bool AllowDragUnselected = false; - bool AllowBoxSelect = false; // Unsupported for 2D selection for now. + bool AllowBoxSelect = true; float IconSize = 32.0f; 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. @@ -9726,9 +9726,7 @@ struct ExampleAssetsBrowser ImGui::SeparatorText("Selection Behavior"); ImGui::Checkbox("Allow dragging unselected item", &AllowDragUnselected); - ImGui::BeginDisabled(); // Unsupported for 2D selection for now. ImGui::Checkbox("Allow box-selection", &AllowBoxSelect); - ImGui::EndDisabled(); ImGui::SeparatorText("Layout"); ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); @@ -9778,7 +9776,7 @@ struct ExampleAssetsBrowser if (AllowDragUnselected) ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. if (AllowBoxSelect) - ms_flags |= ImGuiMultiSelectFlags_BoxSelect; // FIXME-MULTISELECT: Box-select not yet supported for 2D selection when using clipper. + ms_flags |= ImGuiMultiSelectFlags_BoxSelect2d; // Enable box-select in 2D mode. ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags); // Use custom selection adapter: store ID in selection (recommended) @@ -9798,7 +9796,6 @@ struct ExampleAssetsBrowser ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(LayoutSelectableSpacing, LayoutSelectableSpacing)); // Rendering parameters - const ImU32 icon_bg_color = IM_COL32(48, 48, 48, 128); const ImU32 icon_type_overlay_colors[3] = { 0, IM_COL32(200, 70, 70, 255), IM_COL32(70, 170, 70, 255) }; const ImVec2 icon_type_overlay_size = ImVec2(4.0f, 4.0f); const bool display_label = (LayoutItemSize.x >= ImGui::CalcTextSize("999").x); @@ -9825,13 +9822,9 @@ struct ExampleAssetsBrowser ImVec2 pos = ImVec2(start_pos.x + (item_idx % column_count) * LayoutItemStep.x, start_pos.y + line_idx * LayoutItemStep.y); ImGui::SetCursorScreenPos(pos); - // Draw box - ImVec2 box_min(pos.x - 1, pos.y - 1); - ImVec2 box_max(box_min.x + LayoutItemSize.x + 2, box_min.y + LayoutItemSize.y + 2); - draw_list->AddRect(box_min, box_max, IM_COL32(90, 90, 90, 255)); - ImGui::SetNextItemSelectionUserData(item_idx); bool item_is_selected = Selection.Contains((ImGuiID)item_data->ID); + bool item_is_visible = ImGui::IsRectVisible(LayoutItemSize); ImGui::Selectable("", item_is_selected, ImGuiSelectableFlags_None, LayoutItemSize); // Update our selection state immediately (without waiting for EndMultiSelect() requests) @@ -9856,19 +9849,26 @@ struct ExampleAssetsBrowser ImGui::EndDragDropSource(); } - // A real app would likely display an image/thumbnail here. - draw_list->AddRectFilled(box_min, box_max, icon_bg_color); - if (ShowTypeOverlay && item_data->Type != 0) - { - ImU32 type_col = icon_type_overlay_colors[item_data->Type % IM_ARRAYSIZE(icon_type_overlay_colors)]; - draw_list->AddRectFilled(ImVec2(box_max.x - 2 - icon_type_overlay_size.x, box_min.y + 2), ImVec2(box_max.x - 2, box_min.y + 2 + icon_type_overlay_size.y), type_col); - } - if (display_label) + // Render icon (a real app would likely display an image/thumbnail here) + // Because we use ImGuiMultiSelectFlags_BoxSelect2d mode, + // clipping vertical range may occasionally be larger so we coarse-clip our rendering. + if (item_is_visible) { - ImU32 label_col = item_is_selected ? IM_COL32(255, 255, 255, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled); - char label[32]; - sprintf(label, "%d", item_data->ID); - draw_list->AddText(ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), label_col, label); + ImVec2 box_min(pos.x - 1, pos.y - 1); + ImVec2 box_max(box_min.x + LayoutItemSize.x + 2, box_min.y + LayoutItemSize.y + 2); // Dubious + draw_list->AddRectFilled(box_min, box_max, IM_COL32(48, 48, 48, 200)); // Background color + if (ShowTypeOverlay && item_data->Type != 0) + { + ImU32 type_col = icon_type_overlay_colors[item_data->Type % IM_ARRAYSIZE(icon_type_overlay_colors)]; + draw_list->AddRectFilled(ImVec2(box_max.x - 2 - icon_type_overlay_size.x, box_min.y + 2), ImVec2(box_max.x - 2, box_min.y + 2 + icon_type_overlay_size.y), type_col); + } + if (display_label) + { + ImU32 label_col = item_is_selected ? IM_COL32(255, 255, 255, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled); + char label[32]; + sprintf(label, "%d", item_data->ID); + draw_list->AddText(ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), label_col, label); + } } ImGui::PopID(); @@ -9893,7 +9893,8 @@ struct ExampleAssetsBrowser if (want_delete) Selection.ApplyDeletionPostLoop(ms_io, Items, item_curr_idx_to_focus); - // FIXME-MULTISELECT: Find a way to expose this in public API. This currently requires "imgui_internal.h" + // Keyboard/Gamepad Wrapping + // FIXME-MULTISELECT: Currently an imgui_internal.h API. Find a design/way to expose this in public API. //ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); // Zooming with CTRL+Wheel diff --git a/imgui_internal.h b/imgui_internal.h index 0b05404e1..968a9413f 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1726,8 +1726,10 @@ struct IMGUI_API ImGuiMultiSelectTempData ImGuiID BoxSelectId; ImRect BoxSelectRectPrev; ImRect BoxSelectRectCurr; // Selection rectangle in absolute coordinates (derived every frame from Storage->BoxSelectStartPosRel + MousePos) + ImRect BoxSelectUnclipRect;// Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. ImGuiSelectionUserData BoxSelectLastitem; ImGuiKeyChord KeyMods; + bool BoxSelectUnclipMode; bool LoopRequestClear; bool LoopRequestSelectAll; bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 52a342e5a..050136a88 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6775,7 +6775,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; const bool is_multi_select = (g.NextItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; // Before ItemAdd() - const bool item_add = ItemAdd(bb, id, NULL, disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None); + const bool is_visible = ItemAdd(bb, id, NULL, disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None); if (span_all_columns) { @@ -6783,8 +6783,14 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl window->ClipRect.Max.x = backup_clip_rect_max_x; } - if (!item_add) - return false; + if (!is_visible) + { + if (!is_multi_select) + return false; + // Extra layer of "no logic clip" for box-select support + if (!g.CurrentMultiSelect->BoxSelectUnclipMode || !g.CurrentMultiSelect->BoxSelectUnclipRect.Overlaps(bb)) + return false; + } const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; if (disabled_item && !disabled_global) // Only testing this as an optimization @@ -6857,22 +6863,25 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; // Render - if (hovered || selected) - { - // FIXME-MULTISELECT: Styling: Color for 'selected' elements? ImGuiCol_HeaderSelected - ImU32 col; - if (selected && !hovered) - col = GetColorU32(ImLerp(GetStyleColorVec4(ImGuiCol_Header), GetStyleColorVec4(ImGuiCol_HeaderHovered), 0.5f)); - else - col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); - RenderFrame(bb.Min, bb.Max, col, false, 0.0f); - } - if (g.NavId == id) + if (is_visible) { - ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding; - if (is_multi_select) - nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle - RenderNavHighlight(bb, id, nav_highlight_flags); + if (hovered || selected) + { + // FIXME-MULTISELECT: Styling: Color for 'selected' elements? ImGuiCol_HeaderSelected + ImU32 col; + if (selected && !hovered) + col = GetColorU32(ImLerp(GetStyleColorVec4(ImGuiCol_Header), GetStyleColorVec4(ImGuiCol_HeaderHovered), 0.5f)); + else + col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(bb.Min, bb.Max, col, false, 0.0f); + } + if (g.NavId == id) + { + ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding; + if (is_multi_select) + nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle + RenderNavHighlight(bb, id, nav_highlight_flags); + } } if (span_all_columns) @@ -6883,7 +6892,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl PopColumnsBackground(); } - RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb); + if (is_visible) + RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb); // Automatically close popups if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.InFlags & ImGuiItemFlags_AutoClosePopups)) @@ -7169,7 +7179,9 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) flags |= ImGuiMultiSelectFlags_ScopeWindow; if (flags & ImGuiMultiSelectFlags_SingleSelect) - flags &= ~ImGuiMultiSelectFlags_BoxSelect; + flags &= ~(ImGuiMultiSelectFlags_BoxSelect | ImGuiMultiSelectFlags_BoxSelect2d); + if (flags & ImGuiMultiSelectFlags_BoxSelect2d) + flags |= ImGuiMultiSelectFlags_BoxSelect; // FIXME: BeginFocusScope() const ImGuiID id = window->IDStack.back(); @@ -7233,6 +7245,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) } // Box-select handling: update active state. + ms->BoxSelectUnclipMode = false; if (flags & ImGuiMultiSelectFlags_BoxSelect) { ms->BoxSelectId = GetID("##BoxSelect"); @@ -7267,6 +7280,18 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); ms->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); ms->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); + + // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) + // Storing an extra rect used by widgets supporting box-select. + if (flags & ImGuiMultiSelectFlags_BoxSelect2d) + if (ms->BoxSelectRectPrev.Min.x != ms->BoxSelectRectCurr.Min.x || ms->BoxSelectRectPrev.Max.x != ms->BoxSelectRectCurr.Max.x) + { + ms->BoxSelectUnclipRect = ms->BoxSelectRectPrev; + ms->BoxSelectUnclipRect.Add(ms->BoxSelectRectCurr); + ms->BoxSelectUnclipMode = true; + } + + //GetForegroundDrawList()->AddRect(ms->BoxSelectNoClipRect.Min, ms->BoxSelectNoClipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); //GetForegroundDrawList()->AddRect(ms->BoxSelectRectPrev.Min, ms->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); //GetForegroundDrawList()->AddRect(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); }