From 140a2f0565b4f07056673c3922ba2995640f70dc Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 7 Aug 2023 12:34:20 +0200 Subject: [PATCH] MultiSelect: Comments, tweaks. + Alignment to reduce noise on next commit. --- imgui.h | 75 ++++++++++++++++++++++++----------------------- imgui_demo.cpp | 5 +++- imgui_widgets.cpp | 3 +- 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/imgui.h b/imgui.h index aa9774759..811bf940e 100644 --- a/imgui.h +++ b/imgui.h @@ -2737,21 +2737,23 @@ enum ImGuiMultiSelectFlags_ }; // Multi-selection system -// - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) in a way that -// allow a clipper to be used (so most non-visible items won't be submitted). Handling this correctly is tricky, this is why -// we provide the functionality. Note however that if you don't need SHIFT+Mouse/Keyboard range-select + clipping, you could use -// a simpler form of multi-selection yourself, by reacting to click/presses on Selectable() items and checking keyboard modifiers. -// The unusual complexity of this system is mostly caused by supporting SHIFT+Click/Arrow range-select with clipped elements. -// - In the spirit of Dear ImGui design, your code owns the selection data. -// So this is designed to handle all kind of selection data: e.g. instructive selection (store a bool inside each object), -// external array (store an array aside from your objects), hash/map/set (store only selected items in a hash/map/set), -// or other structures (store indices in an interval tree), etc. -// - TreeNode() and Selectable() are supported. -// - 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 Dear ImGui side). -// If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards. +// - Refer to 'Demo->Widgets->Selection State' for references using this. +// - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) +// and supports a clipper being used. Handling this manually may be tricky, this is why we provide the functionality. +// If you don't need SHIFT+Mouse/Keyboard range-select + clipping, you can use a simpler form of multi-selection yourself, +// by reacting to click/presses on Selectable() items and checking keyboard modifiers. +// The complexity of this system is mostly caused by supporting SHIFT+Click/Arrow range-select with clipped elements. +// - TreeNode() and Selectable() are supported but custom widgets may use it as well. +// - In the spirit of Dear ImGui design, your code owns actual selection data. +// This is designed to allow all kinds of selection storage you may use in your application: +// e.g. instructive selection (store a bool inside each object), external array (store an array in your view data, next +// to your objects), set/map/hash (store only selected items), or other structures (store indices in an interval tree), etc. +// - 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 no performance penalty, but requires a little more work on the code. +// For small selection set (<100 items), you might want to not bother with using the clipper, as the cost you should +// be negligible (as least on Dear ImGui side). +// If you are not sure, always start without clipping and you can work your way to the optimized version afterwards. // - The void* RangeSrcItem/RangeDstItem value represent a selectable object. They are the value you pass to SetNextItemSelectionUserData(). // Most likely you will want to store an index here. // Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points and you will need to interpolate @@ -2759,34 +2761,35 @@ enum ImGuiMultiSelectFlags_ // and then from the pointer have your own way of iterating from RangeSrcItem to RangeDstItem). // Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (2) [If using a clipper] Honor Clear/SelectAll/SetRange requests by updating your selection data. Can use same code as Step 6. -// LOOP - (3) [If using a clipper] Set RangeSrcPassedBy=true if the RangeSrcItem item is part of the items clipped before the first submitted/visible item. +// - (2) [If using clipper] Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 6. +// LOOP - (3) [If using clipper] Set RangeSrcPassedBy=true if the RangeSrcItem item is part of the items clipped before the first submitted/visible item. // 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->RangeSrcItem) { data->RangeSrcPassedBy = true; } -// - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. -// (optionally call IsItemToggledSelection() to query if the selection state has been toggled for a given visible item, if you need that info immediately for your display, before EndMultiSelect()) +// - If you are using integer indices, this is easy to compute: if (clipper.DisplayStart > data->RangeSrcItem) { data->RangeSrcPassedBy = true; } +// - If you are using pointers, you may need additional processing in each clipper step to tell if current DisplayStart comes after RangeSrcItem.. +// - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. (optionally call IsItemToggledSelection() if you need that info immediately for displaying your item, before EndMultiSelect()) // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (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). Can use same code as Step 2. +// - (6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 2. // If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable()/TreeNode on a per-item basis. +// However it is perfectly fine to honor all steps even if you don't use a clipper. struct ImGuiMultiSelectIO { - // - Always process requests in this order: Clear, SelectAll, SetRange. + // - Always process requests in this order: Clear, SelectAll, SetRange. Use 'Debug Log->Selection' to see requests as they happen. // - Some fields are only necessary if your list is dynamic and allows deletion (getting "post-deletion" state right is exhibited in the demo) // - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code, 'BEGIN'=BeginMultiSelect() and after, 'END'=EndMultiSelect() and after. - // REQUESTS ----------------// BEGIN / LOOP / END - bool RequestClear; // ms:w, app:r / / ms:w, app:r // 1. Request app/user to clear selection. - bool RequestSelectAll; // ms:w, app:r / / ms:w, app:r // 2. Request app/user to select all. - bool RequestSetRange; // / / ms:w, app:r // 3. Request app/user to select/unselect [RangeSrcItem..RangeDstItem] items, based on RangeSelected. In practice, only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. - void* RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). - // STATE/ARGUMENTS ---------// BEGIN / LOOP / END - void* RangeSrcItem; // ms:w / app:r / ms:w, app:r // Begin: Last known SetNextItemSelectionUserData() value for RangeSrcItem. End: parameter from RequestSetRange request. - void* RangeDstItem; // / / ms:w, app:r // End: parameter from RequestSetRange request. - ImS8 RangeDirection; // / / ms:w, app:r // End: parameter from RequestSetRange request. +1 if RangeSrcItem came before RangeDstItem, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values. If your void* values are storing indices you will never need this. - bool RangeSelected; // / / ms:w, app:r // End: parameter from RequestSetRange request. true = Select Range, false = Unselect Range. - bool RangeSrcPassedBy; // / ms:rw app:w / ms:r // (If using clipper) Need to be set by app/user if RangeSrcItem was part of the clipped set before submitting the visible items. Ignore if not clipping. - bool RangeSrcReset; // app:w / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). - bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). - void* NavIdItem; // ms:w, app:r / / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + // REQUESTS --------------------------------// BEGIN / LOOP / END + bool RequestClear; // ms:w, app:r / / ms:w, app:r // 1. Request app/user to clear selection. + bool RequestSelectAll; // ms:w, app:r / / ms:w, app:r // 2. Request app/user to select all. + bool RequestSetRange; // / / ms:w, app:r // 3. Request app/user to select/unselect [RangeSrcItem..RangeDstItem] items, based on RangeSelected. In practice, only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. + void* RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). + // STATE/ARGUMENTS -------------------------// BEGIN / LOOP / END + void* RangeSrcItem; // ms:w / app:r / ms:w, app:r // Begin: Last known SetNextItemSelectionUserData() value for RangeSrcItem. End: parameter from RequestSetRange request. + void* RangeDstItem; // / / ms:w, app:r // End: parameter from RequestSetRange request. + ImS8 RangeDirection; // / / ms:w, app:r // End: parameter from RequestSetRange request. +1 if RangeSrcItem came before RangeDstItem, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values. If your void* values are storing indices you will never need this. + bool RangeSelected; // / / ms:w, app:r // End: parameter from RequestSetRange request. true = Select Range, false = Unselect Range. + bool RangeSrcPassedBy; // / ms:rw app:w / ms:r // (If using clipper) Need to be set by app/user if RangeSrcItem was part of the clipped set before submitting the visible items. Ignore if not clipping. + bool RangeSrcReset; // app:w / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). + bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). + void* NavIdItem; // ms:w, app:r / / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). ImGuiMultiSelectIO() { Clear(); } void Clear() { memset(this, 0, sizeof(*this)); RequestFocusItem = NavIdItem = RangeSrcItem = RangeDstItem = (void*)-1; } diff --git a/imgui_demo.cpp b/imgui_demo.cpp index c532ff9b4..bd4618e32 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2796,7 +2796,8 @@ struct ExampleSelection void SetRange(int a, int b, bool v) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) SetSelected(n, v); } void SelectAll(int count) { Storage.Data.resize(count); for (int idx = 0; idx < count; idx++) Storage.Data[idx] = ImGuiStoragePair((ImGuiID)idx, 1); SelectionSize = count; } // This could be using SetRange(), but it this way is faster. - // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). Must be done in this order! Order->SelectAll->SetRange. + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). Must be done in this order! Clear->SelectAll->SetRange. + // Enable 'Debug Log->Selection' to see selection requests as they happen. void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) { if (ms_io->RequestClear) { Clear(); } @@ -2934,6 +2935,8 @@ static void ShowDemoWindowMultiSelect() { static ExampleSelection selection; + ImGui::Text("Tips: Use 'Debug Log->Selection' to see selection requests as they happen."); + ImGui::Text("Supported features:"); ImGui::BulletText("Keyboard navigation (arrows, page up/down, home/end, space)."); ImGui::BulletText("Ctrl modifier to preserve and toggle selection."); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 58467a1de..9dacb010d 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7455,11 +7455,10 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) #ifndef IMGUI_DISABLE_DEBUG_TOOLS const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here. if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } - bool open = TreeNode((void*)(intptr_t)storage->ID, "MultiSelect 0x%08X%s", storage->ID, is_active ? "" : " *Inactive*"); + bool open = TreeNode((void*)(intptr_t)storage->ID, "MultiSelect 0x%08X in '%s'%s", storage->ID, storage->Window ? storage->Window->Name : "N/A", is_active ? "" : " *Inactive*"); if (!is_active) { PopStyleColor(); } if (!open) return; - Text("ID = 0x%08X", storage->ID); Text("RangeSrcItem = %p, RangeSelected = %d", storage->RangeSrcItem, storage->RangeSelected); Text("NavIdData = %p, NavIdSelected = %d", storage->NavIdItem, storage->NavIdSelected); TreePop();