From e1fd25051e1c4d4fef53eca80412668505a6908f Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Jun 2024 17:14:00 +0200 Subject: [PATCH] MultiSelect: ImGuiSelectionBasicStorage: added GetNextSelectedItem() to abstract selection storage from user. Amend Assets Browser demo to handle drag and drop correctly. --- imgui.h | 34 +++++++++++++++++++--------------- imgui_demo.cpp | 31 ++++++++++++++++++++++--------- imgui_widgets.cpp | 21 ++++++++++++++++++++- 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/imgui.h b/imgui.h index bfaa0182e..93ecaf77b 100644 --- a/imgui.h +++ b/imgui.h @@ -2817,35 +2817,39 @@ struct ImGuiSelectionRequest // Optional helper to store multi-selection state + apply multi-selection requests. // - Used by our demos and provided as a convenience to easily implement basic multi-selection. +// - Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }' +// Or you can check 'if (Contains(id)) { ... }' for each possible object if their number is not too high to iterate. // - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. // To store a multi-selection, in your application you could: -// - A) Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. -// - B) Use your own external storage: e.g. std::set, std::vector, interval trees, etc. -// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Cannot have multiple views over same objects. +// - Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. +// - Use your own external storage: e.g. std::set, std::vector, interval trees, intrusively stored selection etc. // In ImGuiSelectionBasicStorage we: // - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) // - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index. +// - use decently optimized logic to allow queries and insertion of very large selection sets. +// - do not preserve selection order. // Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. // Large applications are likely to eventually want to get rid of this indirection layer and do their own thing. // See https://github.com/ocornut/imgui/wiki/Multi-Select for details and pseudo-code using this helper. struct ImGuiSelectionBasicStorage { // Members - ImGuiStorage Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set + ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. Prefer not accessing directly: iterate with GetNextSelectedItem(). int Size; // Number of selected items (== number of 1 in the Storage), maintained by this helper. void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; - // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. Uses 'items_count' passed to BeginMultiSelect() - IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); + // Methods + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io);// Apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. It uses 'items_count' passed to BeginMultiSelect() + IMGUI_API ImGuiID GetNextSelectedItem(void** opaque_it); // Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }' + bool Contains(ImGuiID id) const { return _Storage.GetInt(id, 0) != 0; } // Query if an item id is in selection. + ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } // Convert index to item id based on provided adapter. - // Methods: selection storage - ImGuiSelectionBasicStorage() { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } - void Clear() { Storage.Data.resize(0); Size = 0; } - void Swap(ImGuiSelectionBasicStorage& r) { Storage.Data.swap(r.Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; } - bool Contains(ImGuiID id) const { return Storage.GetInt(id, 0) != 0; } - void SetItemSelected(ImGuiID id, bool v) { int* p_int = Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } - ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } + // [Internal, rarely called directly by end-user] + ImGuiSelectionBasicStorage() { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } + void Clear() { _Storage.Data.resize(0); Size = 0; } + void Swap(ImGuiSelectionBasicStorage& r) { _Storage.Data.swap(r._Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; } + void SetItemSelected(ImGuiID id, bool v) { int* p_int = _Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } }; // Optional helper to apply multi-selection requests to existing randomly accessible storage. @@ -2857,8 +2861,8 @@ struct ImGuiSelectionExternalStorage void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; } // Methods - ImGuiSelectionExternalStorage() { UserData = NULL; AdapterSetItemSelected = NULL; } - IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Generic function, using AdapterSetItemSelected() + ImGuiSelectionExternalStorage() { UserData = NULL; AdapterSetItemSelected = NULL; } + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Generic function, using AdapterSetItemSelected() }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 3cd2046a1..d5ff2972d 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3441,24 +3441,24 @@ static void ShowDemoWindowMultiSelect() // Drag and Drop if (use_drag_drop && ImGui::BeginDragDropSource()) { - // Consider payload to be full selection OR single unselected item. + // Create payload with full selection OR single unselected item. // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease) if (ImGui::GetDragDropPayload() == NULL) { ImVector payload_items; + void* it = NULL; if (!item_is_selected) payload_items.push_back(item_id); else - for (const ImGuiStoragePair& pair : selection.Storage.Data) - if (pair.val_i) - payload_items.push_back((int)pair.key); + while (int id = (int)selection.GetNextSelectedItem(&it)) + payload_items.push_back(id); ImGui::SetDragDropPayload("MULTISELECT_DEMO_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes()); } // Display payload content in tooltip const ImGuiPayload* payload = ImGui::GetDragDropPayload(); const int* payload_items = (int*)payload->Data; - const int payload_count = (int)payload->DataSize / (int)sizeof(payload_items[0]); + const int payload_count = (int)payload->DataSize / (int)sizeof(int); if (payload_count == 1) ImGui::Text("Object %05d: %s", payload_items[0], ExampleNames[payload_items[0] % IM_ARRAYSIZE(ExampleNames)]); else @@ -9896,13 +9896,26 @@ struct ExampleAssetsBrowser // Drag and drop if (ImGui::BeginDragDropSource()) { - // Consider payload to be full selection OR single unselected item + // Create payload with full selection OR single unselected item. // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease) - int payload_size = item_is_selected ? Selection.Size : 1; if (ImGui::GetDragDropPayload() == NULL) - ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", "Dummy", 5); // Dummy payload + { + ImVector payload_items; + void* it = NULL; + if (!item_is_selected) + payload_items.push_back(item_data->ID); + else + while (ImGuiID id = Selection.GetNextSelectedItem(&it)) + payload_items.push_back(id); + ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes()); + } + + // Display payload content in tooltip, by extracting it from the payload data + // (we could read from selection, but it is more correct and reusable to read from payload) + const ImGuiPayload* payload = ImGui::GetDragDropPayload(); + const int payload_count = (int)payload->DataSize / (int)sizeof(ImGuiID); + ImGui::Text("%d assets", payload_count); - ImGui::Text("%d assets", payload_size); ImGui::EndDragDropSource(); } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index be02fe140..bf9c9139a 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7821,6 +7821,23 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) // - ImGuiSelectionExternalStorage //------------------------------------------------------------------------- +// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user. +// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector instead of ImGuiStorage...) +ImGuiID ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it) +{ + ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it; + ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size; + if (it == NULL) + it = _Storage.Data.Data; + IM_ASSERT(it >= _Storage.Data.Data && it <= it_end); + if (it != it_end) + while (it->val_i == 0 && it < it_end) + it++; + const bool has_more = (it != it_end); + *opaque_it = has_more ? (void**)(it + 1) : (void**)(it); + return has_more ? it->key : 0; +} + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). // - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. // - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. @@ -7852,7 +7869,7 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) Clear(); if (req.Selected) { - Storage.Data.reserve(ms_io->ItemsCount); + _Storage.Data.reserve(ms_io->ItemsCount); for (int idx = 0; idx < ms_io->ItemsCount; idx++) SetItemSelected(GetStorageIdFromIndex(idx), true); } @@ -7863,6 +7880,8 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) } } +//------------------------------------------------------------------------- + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). // We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage // This makes no assumption about underlying storage.