From 89860f90db0b8527ba38ae137ddba141d90081ef Mon Sep 17 00:00:00 2001 From: omar Date: Fri, 1 Feb 2019 12:22:57 +0100 Subject: [PATCH] RangeSelect/MultiSelect: WIP range-select (#1861) --- imgui.cpp | 10 ++ imgui.h | 73 +++++++++++ imgui_demo.cpp | 72 ++++++++++- imgui_internal.h | 32 ++++- imgui_widgets.cpp | 324 +++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 490 insertions(+), 21 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 4805c673150f..d31f4932186b 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1106,6 +1106,7 @@ ImGuiStyle::ImGuiStyle() TabRounding = 4.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. TabBorderSize = 0.0f; // Thickness of border around tabs. ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. + SelectableSpacing = ImVec2(0,0); // Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing). SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text when button is larger than text. DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area by at least this amount. Only applies to regular windows. DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. @@ -1139,6 +1140,7 @@ void ImGuiStyle::ScaleAllSizes(float scale_factor) GrabMinSize = ImFloor(GrabMinSize * scale_factor); GrabRounding = ImFloor(GrabRounding * scale_factor); TabRounding = ImFloor(TabRounding * scale_factor); + SelectableSpacing = ImFloor(SelectableSpacing * scale_factor); DisplayWindowPadding = ImFloor(DisplayWindowPadding * scale_factor); DisplaySafeAreaPadding = ImFloor(DisplaySafeAreaPadding * scale_factor); MouseCursorScale = ImFloor(MouseCursorScale * scale_factor); @@ -3649,6 +3651,7 @@ void ImGui::Shutdown(ImGuiContext* context) g.ForegroundDrawList.ClearFreeMemory(); g.PrivateClipboard.clear(); g.InputTextState.ClearFreeMemory(); + g.MultiSelectScopeWindow = NULL; for (int i = 0; i < g.SettingsWindows.Size; i++) IM_DELETE(g.SettingsWindows[i].Name); @@ -4300,6 +4303,12 @@ bool ImGui::IsItemClicked(int mouse_button) return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_None); } +bool ImGui::IsItemToggledSelection() +{ + ImGuiContext& g = *GImGui; + return (g.CurrentWindow->DC.LastItemStatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false; +} + bool ImGui::IsAnyItemHovered() { ImGuiContext& g = *GImGui; @@ -5857,6 +5866,7 @@ static const ImGuiStyleVarInfo GStyleVarInfo[] = { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SelectableSpacing) }, // ImGuiStyleVar_SelectableSpacing { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign }; diff --git a/imgui.h b/imgui.h index 2ffe14dd86df..8be870149cbc 100644 --- a/imgui.h +++ b/imgui.h @@ -106,6 +106,7 @@ struct ImGuiContext; // Dear ImGui context (opaque structure, unl struct ImGuiIO; // Main configuration and I/O between your application and ImGui struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) struct ImGuiListClipper; // Helper to manually clip large list of items +struct ImGuiMultiSelectData; // State for a BeginMultiSelect() block struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame, used by IMGUI_ONCE_UPON_A_FRAME macro struct ImGuiPayload; // User data payload for drag and drop operations struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) @@ -141,6 +142,7 @@ typedef int ImGuiDragDropFlags; // -> enum ImGuiDragDropFlags_ // Flags: f typedef int ImGuiFocusedFlags; // -> enum ImGuiFocusedFlags_ // Flags: for IsWindowFocused() typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: for IsItemHovered(), IsWindowHovered() etc. typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText*() +typedef int ImGuiMultiSelectFlags; // -> enum ImGuiMultiSelectFlags_// Flags: for BeginMultiSelect() typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable() typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar() typedef int ImGuiTabItemFlags; // -> enum ImGuiTabItemFlags_ // Flags: for BeginTabItem() @@ -498,6 +500,14 @@ namespace ImGui IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. + // Multi-selection system for Selectable() and TreeNode() functions. + // This enables standard multi-selection/range-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow items to be fully clipped (= not submitted at all) when not visible. + // Read comments near ImGuiMultiSelectData for details. + // When enabled, Selectable() and TreeNode() functions will return true when selection needs toggling. + IMGUI_API ImGuiMultiSelectData* BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected); + IMGUI_API ImGuiMultiSelectData* EndMultiSelect(); + IMGUI_API void SetNextItemMultiSelectData(void* item_data); + // Widgets: List Boxes // - FIXME: To be consistent with all the newer API, ListBoxHeader/ListBoxFooter should in reality be called BeginListBox/EndListBox. Will rename them. IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); @@ -614,6 +624,7 @@ namespace ImGui IMGUI_API bool IsItemActivated(); // was the last item just made active (item was previously inactive). IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that requires continuous editing. IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that requires continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item). + IMGUI_API bool IsItemToggledSelection(); // was the last item selection toggled? (after Selectable(), TreeNode() etc. We only returns toggle _event_ in order to handle clipping correctly) IMGUI_API bool IsAnyItemHovered(); // is any item hovered? IMGUI_API bool IsAnyItemActive(); // is any item active? IMGUI_API bool IsAnyItemFocused(); // is any item focused? @@ -1092,6 +1103,7 @@ enum ImGuiStyleVar_ ImGuiStyleVar_GrabRounding, // float GrabRounding ImGuiStyleVar_TabRounding, // float TabRounding ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_SelectableSpacing, // ImVec2 SelectableSpacing ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign ImGuiStyleVar_COUNT @@ -1162,6 +1174,17 @@ enum ImGuiMouseCursor_ #endif }; +// Flags for BeginMultiSelect(). +// This system is designed to allow mouse/keyboard multi-selection, including support for range-selection (SHIFT + click) which is difficult to re-implement manually. +// If you disable multi-selection with ImGuiMultiSelectFlags_NoMultiSelect (which is provided for consistency and flexibility), the whole BeginMultiSelect() system +// becomes largely overkill as you can handle single-selection in a simpler manner by just calling Selectable() and reacting on clicks yourself. +enum ImGuiMultiSelectFlags_ +{ + ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, + ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc. + ImGuiMultiSelectFlags_NoSelectAll = 1 << 2 // Disable CTRL+A shortcut to set RequestSelectAll +}; + // Enumateration for ImGui::SetWindow***(), SetNextWindow***(), SetNextTreeNode***() functions // Represent a condition. // Important: Treat as a regular enum! Do NOT combine multiple values using binary operators! All the functions above treat 0 as a shortcut to ImGuiCond_Always. @@ -1274,6 +1297,7 @@ struct ImGuiStyle float TabRounding; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. float TabBorderSize; // Thickness of border around tabs. ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). + ImVec2 SelectableSpacing; // Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing). ImVec2 SelectableTextAlign; // Alignment of selectable text when selectable is larger than text. Defaults to (0.0f, 0.0f) (top-left aligned). ImVec2 DisplayWindowPadding; // Window position are clamped to be visible within the display area by at least this amount. Only applies to regular windows. ImVec2 DisplaySafeAreaPadding; // If you cannot see the edges of your screen (e.g. on a TV) increase the safe area padding. Apply to popups/tooltips as well regular windows. NB: Prefer configuring your TV sets correctly! @@ -1693,6 +1717,55 @@ struct ImGuiListClipper IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. }; +// Abstract: +// - This system implements standard multi-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow items to be +// fully clipped (= not submitted at all) when not visible. Clipping is typically provided by ImGuiListClipper. +// Handling all of this in a single pass imgui is a little tricky, and this is why we provide those functionalities. +// Note however that if you don't need SHIFT+Click/Arrow range-select, you can handle a simpler form of multi-selection yourself, +// by reacting to click/presses on Selectable() items and checking keyboard modifiers. +// The complexity of this system here is mostly caused by the handling of range-select while optionally allowing to clip elements. +// - The work involved to deal with multi-selection differs whether you want to only submit visible items (and clip others) or submit all items +// regardless of their visibility. Clipping items is more efficient and will allow you to deal with large lists (1k~100k items) with near zero +// performance penalty, but requires a little more work on the code. If you only have a few hundreds elements in your possible selection set, +// you may as well not bother with clipping, as the cost should be negligible (as least on imgui side). +// If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards. +// - The void* Src/Dst value represent a selectable object. They are the values you pass to SetNextItemMultiSelectData(). +// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points. But the code never assume that sortable integers are used. +// - In the spirit of imgui design, your code own the selection data. So this is designed to handle all kind of selection data: instructive (store a bool inside each object), +// external array (store an array aside from your objects), set (store only selected items in a hash/map/set), using intervals (store indices in an interval tree), etc. +// Usage flow: +// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc and its selection status. As a default value for the initial frame or when, +// resetting your selection state: you may use the value for your first item or a "null" value that matches the type stored in your void*. +// 2) Honor Clear/SelectAll requests by updating your selection data. [Only required if you are using a clipper in step 4] +// 3) Set RangeSrcPassedBy=true if the RangeSrc item is part of the items clipped before the first submitted/visible item. [Only required if you are using a clipper in step 4] +// This is because for range-selection we need to know if we are currently "inside" or "outside" the range. +// If you are using integer indices everywhere, this is easy to compute: if (clipper.DisplayStart > (int)data->RangeSrc) { data->RangeSrcPassedBy = true; } +// 4) Submit your items with SetNextItemMultiSelectData() + Selectable()/TreeNode() calls. +// Call IsItemSelectionToggled() to query with the selection state has been toggled, in which you need the info immediately (before EndMultiSelect()) for your display. +// When cannot reliably return a "IsItemSelected()" value because we need to consider clipped (unprocessed) item, this is why we return a toggle event instead. +// 5) Call EndMultiSelect(). Save the value of ->RangeSrc for the next frame (you may convert the value in a format that is safe for persistance) +// 6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Always process them in this order (as you will receive Clear+SetRange request simultaneously) +// If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable() on a per-item basis. +struct ImGuiMultiSelectData +{ + bool RequestClear; // Begin, End // Request user to clear selection + bool RequestSelectAll; // Begin, End // Request user to select all + bool RequestSetRange; // End // Request user to set or clear selection in the [RangeSrc..RangeDst] range + bool RangeSrcPassedBy; // After Begin // Need to be set by user is RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. + bool RangeValue; // End // End: parameter from RequestSetRange request. True = Select Range, False = Unselect range. + void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect() + void* RangeDst; // End // End: parameter from RequestSetRange request. + int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values. + + ImGuiMultiSelectData() { Clear(); } + void Clear() + { + RequestClear = RequestSelectAll = RequestSetRange = RangeSrcPassedBy = RangeValue = false; + RangeSrc = RangeDst = NULL; + RangeDirection = 0; + } +}; + // Helpers macros to generate 32-bits encoded colors #ifdef IMGUI_USE_BGRA_PACKED_COLOR #define IM_COL32_R_SHIFT 16 diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 45a21e484a0e..a8320ebf6d6f 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -835,7 +835,7 @@ static void ShowDemoWindowWidgets() } ImGui::TreePop(); } - if (ImGui::TreeNode("Selection State: Multiple Selection")) + if (ImGui::TreeNode("Selection State: Multiple Selection (Simplified)")) { ShowHelpMarker("Hold CTRL and click to select multiple items."); static bool selection[5] = { false, false, false, false, false }; @@ -852,6 +852,66 @@ static void ShowDemoWindowWidgets() } ImGui::TreePop(); } + if (ImGui::TreeNode("Selection State: Multiple Selection (Full)")) + { + // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. + // In this demo we use ImGuiStorage (simple key->value storage) to avoid external dependencies but it's probably not optimal. + // In your real code you could use e.g std::unordered_set<> or your own data structure for storing selection. + // If you don't mind being limited to one view over your objects, the simplest way is to use an intrusive selection (e.g. store bool inside object, as used in examples above). + // Otherwise external set/hash/map/interval trees (storing indices, etc.) may be appropriate. + struct MySelection + { + ImGuiStorage Storage; + void Clear() { Storage.Clear(); } + void SelectAll(int count) { Storage.Data.reserve(count); Storage.Data.resize(0); for (int n = 0; n < count; n++) Storage.Data.push_back(ImGuiStorage::Pair((ImGuiID)n, 1)); } + void SetRange(int a, int b, int sel) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) Storage.SetInt((ImGuiID)n, sel); } + bool GetSelected(int id) const { return Storage.GetInt((ImGuiID)id) != 0; } + void SetSelected(int id, bool v) { SetRange(id, id, v ? 1 : 0); } + }; + + static int selection_ref = 0; // Selection pivot (last clicked item, we need to preserve this to handle range-select) + static MySelection selection; + const char* random_names[] = + { + "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", + "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava", + "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" + }; + + int COUNT = 1000; + ShowHelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range."); + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int *)&ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); + + ImGui::ListBoxHeader("##Basket", ImVec2(-1, ImGui::GetFontSize() * 20)); + + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(0, (void*)(intptr_t)selection_ref, selection.GetSelected((int)selection_ref)); + if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); } + ImGuiListClipper clipper(COUNT); + while (clipper.Step()) + { + if (clipper.DisplayStart > (int)selection_ref) + multi_select_data->RangeSrcPassedBy = true; + for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) + { + ImGui::PushID(n); + char label[64]; + sprintf(label, "Object %05d (category: %s)", n, random_names[n % IM_ARRAYSIZE(random_names)]); + bool item_is_selected = selection.GetSelected(n); + ImGui::SetNextItemMultiSelectData((void*)(intptr_t)n); + if (ImGui::Selectable(label, item_is_selected)) + selection.SetSelected(n, !item_is_selected); + ImGui::PopID(); + } + } + multi_select_data = ImGui::EndMultiSelect(); + selection_ref = (int)(intptr_t)multi_select_data->RangeSrc; + ImGui::ListBoxFooter(); + if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); } + if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + ImGui::TreePop(); + } if (ImGui::TreeNode("Rendering more text into the same line")) { // Using the Selectable() override that takes "bool* p_selected" parameter and toggle your booleans automatically. @@ -877,6 +937,14 @@ static void ShowDemoWindowWidgets() if (ImGui::TreeNode("Grid")) { static bool selected[4*4] = { true, false, false, false, false, true, false, false, false, false, true, false, false, false, false, true }; + static float spacing = 0.0f; + ImGui::PushItemWidth(100); + ImGui::SliderFloat("SelectableSpacing", &spacing, 0, 20, "%.0f"); + ImGui::SameLine(); ShowHelpMarker("Selectable cancel out the regular spacing between items by extending itself by ItemSpacing/2 in each direction.\nThis has two purposes:\n- Avoid the gap between items so the mouse is always hitting something.\n- Avoid the gap between items so range-selected item looks connected.\nBy changing SelectableSpacing we can enforce spacing between selectables."); + ImGui::PopItemWidth(); + ImGui::Spacing(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 8)); + ImGui::PushStyleVar(ImGuiStyleVar_SelectableSpacing, ImVec2(spacing, spacing)); for (int i = 0; i < 4*4; i++) { ImGui::PushID(i); @@ -893,6 +961,7 @@ static void ShowDemoWindowWidgets() if ((i % 4) < 3) ImGui::SameLine(); ImGui::PopID(); } + ImGui::PopStyleVar(2); ImGui::TreePop(); } if (ImGui::TreeNode("Alignment")) @@ -2850,6 +2919,7 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2("SelectableSpacing", (float*)&style.SelectableSpacing, 0.0f, 20.0f, "%.0f"); ImGui::SameLine(); ShowHelpMarker("SelectableSpacing must be < ItemSpacing.\nSelectables display their highlight after canceling out the effect of ItemSpacing, so they can be look tightly packed. This setting allows to enforce spacing between them."); ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, 0.0f, 10.0f, "%.0f"); ImGui::SliderFloat("IndentSpacing", &style.IndentSpacing, 0.0f, 30.0f, "%.0f"); ImGui::SliderFloat("ScrollbarSize", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); diff --git a/imgui_internal.h b/imgui_internal.h index 4f5ac8841125..c28f98410538 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -69,6 +69,7 @@ struct ImGuiGroupData; // Stacked storage data for BeginGroup()/End struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box struct ImGuiItemHoveredDataBackup; // Backup and restore IsItemHovered() internal data struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only +struct ImGuiMultiSelectState; // Multi-selection state struct ImGuiNavMoveResult; // Result of a directional navigation move query result struct ImGuiNextWindowData; // Storage for SetNexWindow** functions struct ImGuiPopupRef; // Storage for current popup stack @@ -311,7 +312,8 @@ enum ImGuiButtonFlags_ ImGuiButtonFlags_NoKeyModifiers = 1 << 10, // disable interaction if a key modifier is held ImGuiButtonFlags_NoHoldingActiveID = 1 << 11, // don't set ActiveId while holding the mouse (ImGuiButtonFlags_PressedOnClick only) ImGuiButtonFlags_PressedOnDragDropHold = 1 << 12, // press when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) - ImGuiButtonFlags_NoNavFocus = 1 << 13 // don't override navigation focus when activated + ImGuiButtonFlags_NoNavFocus = 1 << 13, // don't override navigation focus when activated + ImGuiButtonFlags_NoHoveredOnNav = 1 << 14 // don't report as hovered when navigated on }; enum ImGuiSliderFlags_ @@ -372,7 +374,8 @@ enum ImGuiItemStatusFlags_ ImGuiItemStatusFlags_None = 0, ImGuiItemStatusFlags_HoveredRect = 1 << 0, ImGuiItemStatusFlags_HasDisplayRect = 1 << 1, - ImGuiItemStatusFlags_Edited = 1 << 2 // Value exposed by item was edited in the current frame (should match the bool return value of most widgets) + ImGuiItemStatusFlags_Edited = 1 << 2, // Value exposed by item was edited in the current frame (should match the bool return value of most widgets) + ImGuiItemStatusFlags_ToggledSelection = 1 << 3 // Set when Selectable(), TreeNode() reports toggling a selection. We don't report "Selected" because reporting the change allows us to handle clipping with less issues. #ifdef IMGUI_ENABLE_TEST_ENGINE , // [imgui-test only] @@ -689,6 +692,17 @@ struct ImGuiColumnsSet } }; +struct IMGUI_API ImGuiMultiSelectState +{ + ImGuiMultiSelectData In; // The In requests are set and returned by BeginMultiSelect() + ImGuiMultiSelectData Out; // The Out requests are finalized and returned by EndMultiSelect() + bool InRangeDstPassedBy; // (Internal) set by the the item that match NavJustMovedToId when InRequestRangeSetNav is set. + bool InRequestSetRangeNav; // (Internal) set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. + + ImGuiMultiSelectState() { Clear(); } + void Clear() { In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; } +}; + // Data shared between all ImDrawList instances struct IMGUI_API ImDrawListSharedData { @@ -848,6 +862,8 @@ struct ImGuiContext ImVector OpenPopupStack; // Which popups are open (persistent) ImVector BeginPopupStack; // Which level of BeginPopup() we are in (reset every frame) ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions + void* NextItemMultiSelectData; + bool NextItemMultiSelectDataIsSet; bool NextTreeNodeOpenVal; // Storage for SetNextTreeNode** functions ImGuiCond NextTreeNodeOpenCond; @@ -946,8 +962,10 @@ struct ImGuiContext ImVector PrivateClipboard; // If no custom clipboard handler is defined // Range-Select/Multi-Select - // [This is unused in this branch, but left here to facilitate merging/syncing multiple branches] ImGuiID MultiSelectScopeId; + ImGuiWindow* MultiSelectScopeWindow; + ImGuiMultiSelectFlags MultiSelectFlags; + ImGuiMultiSelectState MultiSelectState; // Platform support ImVec2 PlatformImePos; // Cursor position request & last passed to the OS Input Method Editor @@ -1019,6 +1037,8 @@ struct ImGuiContext LastActiveIdTimer = 0.0f; LastValidMousePos = ImVec2(0.0f, 0.0f); MovingWindow = NULL; + NextItemMultiSelectData = NULL; + NextItemMultiSelectDataIsSet = false; NextTreeNodeOpenVal = false; NextTreeNodeOpenCond = 0; @@ -1081,6 +1101,8 @@ struct ImGuiContext TooltipOverrideCount = 0; MultiSelectScopeId = 0; + MultiSelectScopeWindow = NULL; + MultiSelectFlags = 0; PlatformImePos = PlatformImeLastPos = ImVec2(FLT_MAX, FLT_MAX); @@ -1483,6 +1505,10 @@ namespace ImGui IMGUI_API void EndColumns(); // close columns IMGUI_API void PushColumnClipRect(int column_index = -1); + // New Multi-Selection/Range-Selection API (FIXME-WIP) + IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected); + IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); + // Tab Bars IMGUI_API bool BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& bb, ImGuiTabBarFlags flags); IMGUI_API ImGuiTabItem* TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index a36836131824..2765a5bb5900 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -18,6 +18,7 @@ Index of this file: // [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. // [SECTION] Widgets: TreeNode, CollapsingHeader, etc. // [SECTION] Widgets: Selectable +// [SECTION] Widgets: Multi-Selection System // [SECTION] Widgets: ListBox // [SECTION] Widgets: PlotLines, PlotHistogram // [SECTION] Widgets: Value helpers @@ -494,7 +495,8 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool // Gamepad/Keyboard navigation // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse. if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId)) - hovered = true; + if (!(flags & ImGuiButtonFlags_NoHoveredOnNav)) + hovered = true; if (g.NavActivateDownId == id) { @@ -4938,7 +4940,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l // - OpenOnDoubleClick .............. double-click anywhere to open // - OpenOnArrow .................... single-click on arrow to open // - OpenOnDoubleClick|OpenOnArrow .. single-click on arrow or double-click anywhere to open - ImGuiButtonFlags button_flags = ImGuiButtonFlags_NoKeyModifiers; + ImGuiButtonFlags button_flags = 0; if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) button_flags |= ImGuiButtonFlags_AllowItemOverlap; if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) @@ -4947,6 +4949,28 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0; + const bool was_selected = selected; + + // Multi-selection support (header) + const bool is_multi_select = (g.MultiSelectScopeWindow == window); + if (is_multi_select) + { + flags |= ImGuiTreeNodeFlags_OpenOnArrow; + MultiSelectItemHeader(id, &selected); + button_flags |= ImGuiButtonFlags_NoHoveredOnNav; + + // To handle drag and drop of multiple items we need to avoid clearing selection on click. + // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items. + if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressed)) + button_flags |= ImGuiButtonFlags_PressedOnClick; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + } + else + { + button_flags |= ImGuiButtonFlags_NoKeyModifiers; + } + bool hovered, held; bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); bool toggled = false; @@ -4954,7 +4978,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l { if (pressed) { - toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id); + toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id && !is_multi_select); if (flags & ImGuiTreeNodeFlags_OpenOnArrow) toggled |= IsMouseHoveringRect(interact_bb.Min, ImVec2(interact_bb.Min.x + text_offset_x, interact_bb.Max.y)) && (!g.NavDisableMouseHover); if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) @@ -4980,13 +5004,27 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l window->DC.StateStorage->SetInt(id, is_open); } } + + // Multi-selection support (footer) + if (is_multi_select) + { + bool pressed_copy = pressed && !toggled; + MultiSelectItemFooter(id, &selected, &pressed_copy); + if (pressed) + SetNavID(id, window->DC.NavLayerCurrent); + } + if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) SetItemAllowOverlap(); + if (selected != was_selected) + window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_ToggledSelection; // Render const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); const ImVec2 text_pos = frame_bb.Min + ImVec2(text_offset_x, text_base_offset_y); ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_TypeThin; + if (is_multi_select) + nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle if (display_frame) { // Framed type @@ -5011,10 +5049,8 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l { // Unframed typed for tree nodes if (hovered || selected) - { RenderFrame(frame_bb.Min, frame_bb.Max, col, false); - RenderNavHighlight(frame_bb, id, nav_highlight_flags); - } + RenderNavHighlight(frame_bb, id, nav_highlight_flags); if (flags & ImGuiTreeNodeFlags_Bullet) RenderBullet(frame_bb.Min + ImVec2(text_offset_x * 0.5f, g.FontSize*0.50f + text_base_offset_y)); @@ -5170,15 +5206,15 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_DrawFillAvailWidth)) bb.Max.x += window_padding.x; - // Selectables are tightly packed together, we extend the box to cover spacing between selectable. - float spacing_L = (float)(int)(style.ItemSpacing.x * 0.5f); - float spacing_U = (float)(int)(style.ItemSpacing.y * 0.5f); - float spacing_R = style.ItemSpacing.x - spacing_L; - float spacing_D = style.ItemSpacing.y - spacing_U; + // Selectables are tightly packed together so we extend the box to cover spacing between selectable. + float spacing_x = ImMax(style.ItemSpacing.x - style.SelectableSpacing.x, 0.0f); + float spacing_y = ImMax(style.ItemSpacing.y - style.SelectableSpacing.y, 0.0f); + float spacing_L = (float)(int)(spacing_x * 0.50f); + float spacing_U = (float)(int)(spacing_y * 0.50f); bb.Min.x -= spacing_L; bb.Min.y -= spacing_U; - bb.Max.x += spacing_R; - bb.Max.y += spacing_D; + bb.Max.x += (spacing_x - spacing_L); + bb.Max.y += (spacing_y - spacing_U); bool item_add; if (flags & ImGuiSelectableFlags_Disabled) @@ -5209,10 +5245,31 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (flags & ImGuiSelectableFlags_Disabled) selected = false; + // Multi-selection support (header) + const bool is_multi_select = (g.MultiSelectScopeWindow == window); + const bool was_selected = selected; + if (is_multi_select) + { + MultiSelectItemHeader(id, &selected); + button_flags |= ImGuiButtonFlags_NoHoveredOnNav; + + // To handle drag and drop of multiple items we need to avoid clearing selection on click. + // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items. + if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressed)) + button_flags |= ImGuiButtonFlags_PressedOnClick; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + } + bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); + + // Multi-selection support (footer) + if (is_multi_select) + MultiSelectItemFooter(id, &selected, &pressed); + // Hovering selectable with mouse updates NavId accordingly so navigation can be resumed with gamepad/keyboard (this doesn't happen on most widgets) - if (pressed || hovered) + if (pressed || (hovered && !is_multi_select)) if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent) { g.NavDisableHighlight = true; @@ -5224,10 +5281,18 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl // Render if (hovered || selected) { - const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + // FIXME-MULTISELECT, FIXME-STYLE: 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); - RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); } + ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding; + if (is_multi_select) + nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle + RenderNavHighlight(bb, id, nav_highlight_flags); if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) { @@ -5242,7 +5307,11 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl // Automatically close popups if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(window->DC.ItemFlags & ImGuiItemFlags_SelectableDontClosePopup)) CloseCurrentPopup(); - return pressed; + + if (selected != was_selected) + window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + + return pressed || (was_selected != selected); } bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) @@ -5255,6 +5324,227 @@ bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags return false; } +//------------------------------------------------------------------------- +// [SECTION] Widgets: Multi-Selection System +//------------------------------------------------------------------------- +// - BeginMultiSelect() +// - EndMultiSelect() +// - SetNextItemMultiSelectData() +// - MultiSelectItemHeader() [Internal] +// - MultiSelectItemFooter() [Internal] +//------------------------------------------------------------------------- + +ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) +{ + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiWindow* window = g.CurrentWindow; + + IM_ASSERT(g.MultiSelectScopeId == 0); // No recursion allowed yet (we could allow it if we deem it useful) + IM_ASSERT(g.MultiSelectFlags == 0); + + ImGuiMultiSelectState* state = &g.MultiSelectState; + g.MultiSelectScopeId = window->IDStack.back(); + g.MultiSelectScopeWindow = window; + g.MultiSelectFlags = flags; + state->Clear(); + + if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0) + { + state->In.RangeSrc = state->Out.RangeSrc = range_ref; + state->In.RangeValue = state->Out.RangeValue = range_ref_is_selected; + } + + // Auto clear when using Navigation to move within the selection (we compare SelectScopeId so it possible to use multiple lists inside a same window) + if (g.NavJustMovedToId != 0 && g.NavJustMovedToSelectScopeId == g.MultiSelectScopeId) + { + if (g.IO.KeyShift) + state->InRequestSetRangeNav = true; + if (!g.IO.KeyCtrl && !g.IO.KeyShift) + state->In.RequestClear = true; + } + + // Select All helper shortcut + if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (IsWindowFocused() && g.IO.KeyCtrl && IsKeyPressed(GetKeyIndex(ImGuiKey_A))) + state->In.RequestSelectAll = true; + +#if IMGUI_DEBUG_MULTISELECT + if (state->In.RequestClear) printf("[%05d] BeginMultiSelect: RequestClear\n", g.FrameCount); + if (state->In.RequestSelectAll) printf("[%05d] BeginMultiSelect: RequestSelectAll\n", g.FrameCount); +#endif + + return &state->In; +} + +ImGuiMultiSelectData* ImGui::EndMultiSelect() +{ + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiMultiSelectState* state = &g.MultiSelectState; + IM_ASSERT(g.MultiSelectScopeId != 0); + if (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect) + state->Out.RangeValue = true; + g.MultiSelectScopeId = 0; + g.MultiSelectScopeWindow = NULL; + g.MultiSelectFlags = 0; + +#if IMGUI_DEBUG_MULTISELECT + if (state->Out.RequestClear) printf("[%05d] EndMultiSelect: RequestClear\n", g.FrameCount); + if (state->Out.RequestSelectAll) printf("[%05d] EndMultiSelect: RequestSelectAll\n", g.FrameCount); + if (state->Out.RequestSetRange) printf("[%05d] EndMultiSelect: RequestSetRange %p..%p = %d\n", g.FrameCount, state->Out.RangeSrc, state->Out.RangeDst, state->Out.RangeValue); +#endif + + return &state->Out; +} + +void ImGui::SetNextItemMultiSelectData(void* item_data) +{ + ImGuiContext& g = *GImGui; + g.NextItemMultiSelectData = item_data; + g.NextItemMultiSelectDataIsSet = true; +} + +void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) +{ + ImGuiContext& g = *GImGui; + ImGuiMultiSelectState* state = &g.MultiSelectState; + + IM_ASSERT(g.NextItemMultiSelectDataIsSet && "Forgot to call SetNextItemMultiSelectData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); + void* item_data = g.NextItemMultiSelectData; + + // Apply Clear/SelectAll requests requested by BeginMultiSelect(). + // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. + // If you are using a clipper (aka not submitting every element of the list) you need to process the Clear/SelectAll request after calling BeginMultiSelect() + bool selected = *p_selected; + if (state->In.RequestClear) + selected = false; + else if (state->In.RequestSelectAll) + selected = true; + + const bool is_range_src = (state->In.RangeSrc == item_data); + if (is_range_src) + state->In.RangeSrcPassedBy = true; + + // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) + // For this to work, IF the user is clipping items, they need to set RangeSrcPassedBy = true to notify the system. + if (state->InRequestSetRangeNav) + { + IM_ASSERT(id != 0); + IM_ASSERT(g.IO.KeyShift); + const bool is_range_dst = !state->InRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. + if (is_range_dst) + state->InRangeDstPassedBy = true; + if (is_range_src || is_range_dst || state->In.RangeSrcPassedBy != state->InRangeDstPassedBy) + selected = state->In.RangeValue; + else if (!g.IO.KeyCtrl) + selected = false; + } + + *p_selected = selected; +} + +void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiMultiSelectState* state = &g.MultiSelectState; + + void* item_data = g.NextItemMultiSelectData; + g.NextItemMultiSelectDataIsSet = false; + + bool selected = *p_selected; + bool pressed = *p_pressed; + bool is_ctrl = g.IO.KeyCtrl; + bool is_shift = g.IO.KeyShift; + const bool is_multiselect = (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoMultiSelect) == 0; + + // Auto-select as you navigate a list + if (g.NavJustMovedToId == id) + { + if (!g.IO.KeyCtrl) + selected = pressed = true; + else if (g.IO.KeyCtrl && g.IO.KeyShift) + pressed = true; + } + + // Right-click handling: this could be moved at the Selectable() level. + bool hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); + if (hovered && IsMouseClicked(1)) + { + SetFocusID(window->DC.LastItemId, window); + if (!pressed && !selected) + { + pressed = true; + is_ctrl = is_shift = false; + } + } + + if (pressed) + { + //------------------------------------------------------------------------------------------------------------------------------------------------- + // ACTION | Begin | Item Old | Item New | End + //------------------------------------------------------------------------------------------------------------------------------------------------- + // Keys Navigated, Ctrl=0, Shift=0 | In.Clear | Clear -> Sel=0 | Src=item, Pressed -> Sel=1 | + // Keys Navigated, Ctrl=0, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange + // Keys Navigated, Ctrl=1, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=Src, Out.Clear, Out.SetRange=Src | Clear + SetRange + // Mouse Pressed, Ctrl=0, Shift=0 | n/a | n/a (Sel=1) | Src=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange + // Mouse Pressed, Ctrl=0, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange + //------------------------------------------------------------------------------------------------------------------------------------------------- + + ImGuiInputSource input_source = (g.NavJustMovedToId != 0 && g.NavWindow == window && g.NavJustMovedToId == window->DC.LastItemId) ? ImGuiInputSource_Nav : ImGuiInputSource_Mouse; + if (is_shift && is_multiselect) + { + state->Out.RequestSetRange = true; + state->Out.RangeDst = item_data; + if (!is_ctrl) + state->Out.RangeValue = true; + state->Out.RangeDirection = state->In.RangeSrcPassedBy ? +1 : -1; + } + else + { + selected = (!is_ctrl || (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)) ? true : !selected; + state->Out.RangeSrc = state->Out.RangeDst = item_data; + state->Out.RangeValue = selected; + } + + if (input_source == ImGuiInputSource_Mouse) + { + // Mouse click without CTRL clears the selection, unless the clicked item is already selected + bool preserve_existing_selection = g.DragDropActive; + if (is_multiselect && !is_ctrl && !preserve_existing_selection) + state->Out.RequestClear = true; + if (is_multiselect && !is_shift && !preserve_existing_selection && state->Out.RequestClear) + { + // For toggle selection unless there is a Clear request, we can handle it completely locally without sending a RangeSet request. + IM_ASSERT(state->Out.RangeSrc == state->Out.RangeDst); // Setup by block above + state->Out.RequestSetRange = true; + state->Out.RangeValue = selected; + state->Out.RangeDirection = +1; + } + if (!is_multiselect) + { + // Clear selection, set single item range + IM_ASSERT(state->Out.RangeSrc == item_data && state->Out.RangeDst == item_data); // Setup by block above + state->Out.RequestClear = true; + state->Out.RequestSetRange = true; + } + } + else if (input_source == ImGuiInputSource_Nav) + { + if (!is_multiselect) + state->Out.RequestClear = true; + else if (is_shift && !is_ctrl && is_multiselect) + state->Out.RequestClear = true; + } + } + + // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) + if (state->Out.RangeSrc == item_data && is_ctrl && is_shift && is_multiselect && !(g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)) + state->Out.RangeValue = selected; + + *p_selected = selected; + *p_pressed = pressed; +} + //------------------------------------------------------------------------- // [SECTION] Widgets: ListBox //-------------------------------------------------------------------------