├── .github ├── CMakeLists.txt ├── example_imsearch.cpp └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── imsearch.cpp ├── imsearch.h ├── imsearch_demo.cpp └── imsearch_internal.h /.github/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This build script is not meant for general use, it is for CI use only! 2 | cmake_minimum_required(VERSION 3.0) 3 | project(imsearch) 4 | 5 | # 6 | # Global options 7 | # 8 | 9 | # Same as Dear ImGui 10 | set(CMAKE_CXX_STANDARD 11) 11 | 12 | # Arch option for linux 13 | if (NOT APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU" AND DEFINED GCC_ARCH) 14 | if ("${GCC_ARCH}" MATCHES "Win32|x86|32") 15 | add_compile_options(-m32) 16 | add_link_options(-m32) 17 | elseif ("${GCC_ARCH}" MATCHES "Win64|x64|64") 18 | add_compile_options(-m64) 19 | add_link_options(-m64) 20 | endif () 21 | endif () 22 | 23 | # Arch option for Mac: arm64 for M1 or x86_64 for intel (32 bits build are deprecated on Mac) 24 | if(APPLE AND DEFINED OSX_ARCH) 25 | if ("${OSX_ARCH}" MATCHES "x86_64") 26 | set(CMAKE_OSX_ARCHITECTURES "x86_64") 27 | elseif ("${OSX_ARCH}" MATCHES "arm64") 28 | set(CMAKE_OSX_ARCHITECTURES "arm64") 29 | else() 30 | message(FATAL_ERROR "Unhandled OSX_ARCH=${OSX_ARCH}") 31 | endif() 32 | endif() 33 | 34 | # 35 | # Dear ImGui library with no backend 36 | # 37 | 38 | set(imgui_sources 39 | ../imgui/imconfig.h 40 | ../imgui/imgui.cpp 41 | ../imgui/imgui.h 42 | ../imgui/imgui_demo.cpp 43 | ../imgui/imgui_draw.cpp 44 | ../imgui/imgui_internal.h 45 | ../imgui/imgui_tables.cpp 46 | ../imgui/imgui_widgets.cpp 47 | ../imgui/imstb_rectpack.h 48 | ../imgui/imstb_textedit.h 49 | ../imgui/imstb_truetype.h 50 | ) 51 | add_library(imgui ${imgui_sources}) 52 | target_include_directories(imgui PUBLIC ../imgui) 53 | 54 | # 55 | # ImSearch library 56 | # 57 | 58 | file(GLOB SOURCE_CODE ../imsearch*.*) 59 | add_library(imsearch STATIC ${SOURCE_CODE}) 60 | 61 | if(MSVC) 62 | target_compile_options(imsearch PRIVATE /W4 /WX) 63 | else() 64 | target_compile_options(imsearch PRIVATE -Wall -Werror -pedantic) 65 | endif() 66 | 67 | target_include_directories(imsearch PUBLIC ${CMAKE_CURRENT_LIST_DIR}/..) 68 | target_link_libraries(imsearch PUBLIC imgui) 69 | 70 | if (UNIX) 71 | target_link_libraries(imsearch PUBLIC m stdc++) 72 | endif() 73 | 74 | # 75 | # imsearch example binary application (with no backend) 76 | # 77 | add_executable(example_imsearch example_imsearch.cpp) 78 | target_link_libraries(example_imsearch PRIVATE imsearch) 79 | -------------------------------------------------------------------------------- /.github/example_imsearch.cpp: -------------------------------------------------------------------------------- 1 | // Sample app built with Dear ImGui and ImSearch 2 | // This app uses imsearch and imgui, but does not output to any backend! It only serves as a proof that an app can be built, linked, and run. 3 | 4 | #include "imgui.h" 5 | #include "imsearch.h" 6 | #include 7 | 8 | int main(int, char**) 9 | { 10 | puts("sample_imsearch: start\n"); 11 | 12 | IMGUI_CHECKVERSION(); 13 | ImGui::CreateContext(); 14 | ImSearch::CreateContext(); 15 | 16 | // Additional imgui initialization needed when no backend is present 17 | ImGui::GetIO().DisplaySize = ImVec2(400.f, 400.f); 18 | ImGui::GetIO().Fonts->Build(); 19 | 20 | // Render 500 frames 21 | for(int counter = 0; counter < 500; ++counter) 22 | { 23 | ImGui::NewFrame(); 24 | ImSearch::ShowDemoWindow(); 25 | ImGui::Render(); 26 | } 27 | 28 | ImSearch::DestroyContext(); 29 | ImGui::DestroyContext(); 30 | puts("sample_imsearch: end\n"); 31 | return 0; 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | Linux: 8 | runs-on: ubuntu-22.04 9 | 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | build_type: 14 | - debug 15 | - release 16 | compiler: 17 | - gcc 18 | - clang 19 | arch: 20 | - x86 21 | - x64 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - uses: actions/checkout@v3 27 | with: 28 | repository: ocornut/imgui 29 | path: imgui 30 | 31 | - name: Update package list 32 | run: sudo apt-get update 33 | 34 | - name: Dependencies 35 | run: sudo apt-get install g++-multilib 36 | 37 | - name: Configure 38 | run: cmake -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DCMAKE_C_COMPILER=${{ matrix.compiler }} -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DGCC_ARCH=${{ matrix.arch }} -B cmake-build -S .github 39 | 40 | - name: Build 41 | run: cmake --build cmake-build --parallel $(nproc) 42 | 43 | - name: Run 44 | run: | 45 | file cmake-build/example_imsearch 46 | cmake-build/example_imsearch 47 | 48 | MacOS: 49 | runs-on: macos-latest 50 | 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | build_type: 55 | - debug 56 | - release 57 | arch: 58 | - x86_64 59 | - arm64 60 | 61 | steps: 62 | - uses: actions/checkout@v3 63 | 64 | - uses: actions/checkout@v3 65 | with: 66 | repository: ocornut/imgui 67 | path: imgui 68 | 69 | - name: Configure 70 | shell: bash 71 | run: cmake -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DOSX_ARCH=${{ matrix.arch }} -B cmake-build -S .github 72 | 73 | - name: Build 74 | shell: bash 75 | run: cmake --build cmake-build --parallel $(sysctl -n hw.ncpu) 76 | 77 | - name: Run 78 | if: matrix.arch == 'x86_64' # github's CI hosts seem to be running intel and can not run ARM 79 | run: | 80 | file cmake-build/example_imsearch 81 | cmake-build/example_imsearch 82 | 83 | Windows_MSVC: 84 | runs-on: windows-2022 85 | 86 | strategy: 87 | fail-fast: false 88 | matrix: 89 | build_type: 90 | - debug 91 | - release 92 | arch: 93 | - Win32 94 | - x64 95 | 96 | steps: 97 | - uses: actions/checkout@v3 98 | 99 | - uses: actions/checkout@v3 100 | with: 101 | repository: ocornut/imgui 102 | path: imgui 103 | 104 | - name: Configure 105 | shell: bash 106 | run: cmake -G 'Visual Studio 17 2022' -A ${{ matrix.arch }} -B cmake-build -S .github 107 | 108 | - name: Build 109 | shell: bash 110 | run: cmake --build cmake-build -- -p:Configuration=${{ matrix.build_type }} -maxcpucount:$NUMBER_OF_PROCESSORS 111 | 112 | - name: Run 113 | run: .\cmake-build\${{matrix.build_type}}\example_imsearch.exe 114 | 115 | Windows_MingW: # MingW on Github CI does not fully support 32 bits: link fails when it tries to link 64 bits system libraries. 116 | runs-on: windows-2022 117 | 118 | strategy: 119 | fail-fast: false 120 | matrix: 121 | build_type: 122 | - debug 123 | - release 124 | arch: 125 | - x64 126 | # - Win32 127 | 128 | steps: 129 | - uses: actions/checkout@v3 130 | 131 | - uses: actions/checkout@v3 132 | with: 133 | repository: ocornut/imgui 134 | path: imgui 135 | 136 | - name: Configure 137 | shell: bash 138 | run: cmake -G 'MinGW Makefiles' -DGCC_ARCH=${{ matrix.arch }} -B cmake-build -S .github 139 | 140 | - name: Build 141 | shell: bash 142 | run: cmake --build cmake-build --parallel $NUMBER_OF_PROCESSORS 143 | 144 | - name: Run (MingW) 145 | run: .\cmake-build\example_imsearch.exe 146 | 147 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 GuusKemperman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ImSearch 3 | 4 | ImSearch is an immediate mode extension for [Dear ImGui](https://github.com/ocornut/imgui), allowing you to create searchbars for ImGui's buttons, trees, selectable, and every other widget you can imagine. 5 | 6 | Just like ImGui, ImSearch's API does not burden the end user with state management, use STL containers or include any C++ headers, and has no public dependencies except for ImGui itself. This extension is compatible with C++11 and above. 7 | 8 | ## Examples 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

Combos, previews and autocomplete

Hierarchies

Collapsing headers and nested search bars

Custom search bars

20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
24 |

Real world examples from Coral Engine

25 |

World outliner, details panel, and asset selection

Content browser

Finding functions and types for easier scripting

33 | 34 | ## Usage 35 | 36 | You'll find that the API still feels very familiar to ImGui, albeit *slightly* different from what you might expect from an ImGui extension; because this extension also takes control over the order to display items in, the library uses callbacks for submitting your widgets. First, start a new search context by calling `ImSearch::BeginSearch`. Next, submit as many items as you want with the Push/PopSearchable functions, to which you provide callbacks to display your ImGui widget (e.g. `Selectable`, `Button`, `TreeNode`, etc). Finally, wrap things up with a call to `ImSearch::EndSearch()`. That's it! 37 | 38 | 39 | 40 | ```cpp 41 | static const char* selectedString = sImguiExtensions[0]; 42 | 43 | if (ImGui::BeginCombo("##Extensions", selectedString)) 44 | { 45 | if (ImSearch::BeginSearch()) 46 | { 47 | ImSearch::SearchBar(); 48 | 49 | for (const char* extension : sImguiExtensions) 50 | { 51 | ImSearch::SearchableItem(extension, 52 | [&](const char* name) 53 | { 54 | const bool isSelected = name == selectedString; 55 | if (ImGui::Selectable(name, isSelected)) 56 | { 57 | selectedString = name; 58 | } 59 | }); 60 | } 61 | ImSearch::EndSearch(); 62 | } 63 | ImGui::EndCombo(); 64 | } 65 | ``` 66 | 67 | ![](https://github.com/user-attachments/assets/66b73ee6-3134-472e-889f-f8dcb81f9757) 68 | 69 | ## Demos 70 | 71 | More examples of ImSearch's features and usages can be found in `imsearch_demo.cpp`. Add this file to your sources and call `ImSearch::ShowDemoWindow()` somewhere in your update loop. You are encouraged to use this file as a reference whenever you need it. The demo is always updated to show new features as they are added, so check back with each release! 72 | 73 | ## Integration 74 | 75 | 0) Set up an [ImGui](https://github.com/ocornut/imgui) environment if you don't already have one. 76 | 1) Add `imsearch.h`, `imsearch_internal.h`, `imsearch.cpp`, and optionally `imsearch_demo.cpp` to your sources. 77 | 2) Create and destroy an `ImSearchContext` wherever you do so for your `ImGuiContext`: 78 | 79 | ```cpp 80 | ImGui::CreateContext(); 81 | ImSearch::CreateContext(); 82 | ... 83 | ImSearch::DestroyContext(); 84 | ImGui::DestroyContext(); 85 | ``` 86 | 87 | You should be good to go! 88 | -------------------------------------------------------------------------------- /imsearch.cpp: -------------------------------------------------------------------------------- 1 | #ifndef IMGUI_DISABLE 2 | 3 | #include "imsearch.h" 4 | #include "imsearch_internal.h" 5 | #include "imgui.h" 6 | #include "imgui_internal.h" 7 | 8 | namespace ImSearch 9 | { 10 | static bool IsResultUpToDate(const ImSearch::Result& oldResult, const ImSearch::Input& currentInput); 11 | static void BringResultUpToDate(ImSearch::Result& result); 12 | 13 | static void AssignInitialScores(const Input& input, ReusableBuffers& buffers); 14 | static void PropagateScoreToChildren(const Input& input, ReusableBuffers& buffers); 15 | static void PropagateScoreToParents(const Input& input, ReusableBuffers& buffers); 16 | 17 | static void GenerateDisplayOrder(const Input& input, ReusableBuffers& buffers, Output& output); 18 | static void AppendToDisplayOrder(const Input& input, ReusableBuffers& buffers, IndexT startInIndicesBuffer, IndexT endInIndicesBuffer, Output& output); 19 | 20 | static void FindStringToAppendOnAutoComplete(const Input& input, Output& output); 21 | 22 | static void DisplayToUser(const ImSearch::LocalContext& context, const ImSearch::Result& result); 23 | 24 | static bool DoDrawnGlyphsMatch( 25 | const ImDrawList& lhsList, 26 | const ImDrawIdx* lhsStart, 27 | const ImDrawList& rhsList, 28 | const ImDrawIdx* rhsStart, 29 | int length); 30 | 31 | static void HighlightSubstrings(const char* substrStart, 32 | const char* substrEnd, 33 | ImDrawList* drawList, 34 | int startIdxIdx, 35 | int endIdxIdx); 36 | 37 | static ImSearch::ImSearchContext* sContext{}; 38 | } 39 | 40 | //----------------------------------------------------------------------------- 41 | // [SECTION] Definitions from imsearch.h 42 | //----------------------------------------------------------------------------- 43 | 44 | ImSearchStyle::ImSearchStyle() 45 | { 46 | Colors[ImSearchCol_TextHighlighted] = { 0.0f, 0.0f, 0.0f, 1.0f }; 47 | Colors[ImSearchCol_TextHighlightedBg] = { 1.0f, 1.0f, 0.0f, 1.0f }; 48 | } 49 | 50 | ImSearch::ImSearchContext* ImSearch::CreateContext() 51 | { 52 | ImSearchContext* ctx = IM_NEW(ImSearchContext)(); 53 | if (sContext == nullptr) 54 | { 55 | SetCurrentContext(ctx); 56 | } 57 | return ctx; 58 | } 59 | 60 | void ImSearch::DestroyContext(ImSearchContext* ctx) 61 | { 62 | if (ctx == nullptr) 63 | { 64 | ctx = sContext; 65 | } 66 | 67 | if (sContext == ctx) 68 | { 69 | SetCurrentContext(nullptr); 70 | } 71 | IM_DELETE(ctx); 72 | } 73 | 74 | ImSearch::ImSearchContext* ImSearch::GetCurrentContext() 75 | { 76 | return sContext; 77 | } 78 | 79 | void ImSearch::SetCurrentContext(ImSearchContext* ctx) 80 | { 81 | sContext = ctx; 82 | } 83 | 84 | bool ImSearch::BeginSearch(ImSearchFlags flags) 85 | { 86 | const ImGuiID imId = ImGui::GetID("Search"); 87 | ImGui::PushID(static_cast(imId)); 88 | 89 | ImSearchContext& context = GetImSearchContext(); 90 | LocalContext& localContext = context.Contexts[imId]; 91 | context.ContextStack.emplace(localContext); 92 | 93 | localContext.mHasSubmitted = false; 94 | localContext.mInput.mFlags = flags; 95 | 96 | return true; 97 | } 98 | 99 | void ImSearch::SearchBar(const char* hint) 100 | { 101 | LocalContext& context = GetLocalContext(); 102 | 103 | ImGui::SetNextItemWidth(-FLT_MIN); 104 | ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_F); 105 | 106 | std::string& userQuery = context.mInput.mUserQuery; 107 | 108 | if (ImGui::IsWindowAppearing()) 109 | { 110 | ImGui::SetKeyboardFocusHere(); 111 | userQuery.clear(); 112 | } 113 | 114 | ImGui::InputTextWithHint("##SearchBar", 115 | hint, 116 | const_cast(userQuery.c_str()), 117 | userQuery.capacity() + 1, 118 | ImGuiInputTextFlags_CallbackResize | ImGuiInputTextFlags_CallbackCompletion, 119 | +[](ImGuiInputTextCallbackData* data) -> int 120 | { 121 | LocalContext* capturedContext = static_cast(data->UserData); 122 | std::string& capturedQuery = capturedContext->mInput.mUserQuery; 123 | 124 | if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) 125 | { 126 | // Resize string callback 127 | // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want. 128 | IM_ASSERT(data->Buf == capturedQuery.c_str()); 129 | capturedQuery.resize(static_cast(data->BufTextLen)); 130 | data->Buf = const_cast(capturedQuery.c_str()); 131 | } 132 | if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) 133 | { 134 | const std::string& newLastToken = capturedContext->mResult.mOutput.mPreviewText; 135 | 136 | if (newLastToken.empty()) 137 | { 138 | return 0; 139 | } 140 | 141 | const std::vector splitQuery = SplitTokens(capturedQuery); 142 | 143 | if (splitQuery.empty()) 144 | { 145 | return 0; 146 | } 147 | 148 | const StrView oldLastToken = splitQuery.back(); 149 | 150 | // Find the start of the last token 151 | IndexT startOfLastToken = static_cast(capturedQuery.size() - oldLastToken.size()); 152 | while (startOfLastToken > 0) 153 | { 154 | if (StrView{ capturedQuery.data() + startOfLastToken, oldLastToken.size() } == oldLastToken) 155 | { 156 | break; 157 | } 158 | --startOfLastToken; 159 | } 160 | 161 | data->DeleteChars(static_cast(startOfLastToken), static_cast(capturedQuery.size()) - static_cast(startOfLastToken)); 162 | data->InsertChars(static_cast(startOfLastToken), newLastToken.c_str(), newLastToken.c_str() + newLastToken.size()); 163 | } 164 | return 0; 165 | }, 166 | &context); 167 | 168 | // Some logic for drawing the search icon 169 | const ImVec2 hintSize = ImGui::CalcTextSize(hint); 170 | ImRect availRect{ ImGui::GetItemRectMin(), ImGui::GetItemRectMax() }; 171 | availRect.Min.x += hintSize.x + ImGui::GetStyle().FramePadding.x * 2.0f; 172 | availRect.Max.x = std::max(availRect.Min.x, availRect.Max.x); 173 | 174 | const float barHalfHeight = availRect.GetHeight() * .5f; 175 | const ImVec2 iconCentre = { availRect.Max.x - barHalfHeight, (availRect.Min.y + availRect.Max.y) * 0.5f }; 176 | 177 | const float lensRadius = barHalfHeight * .5f; 178 | const ImVec2 lensCentre = { iconCentre.x - barHalfHeight * .2f, iconCentre.y - barHalfHeight * .2f }; 179 | 180 | const float handleLength = barHalfHeight * .4f; 181 | static constexpr float halfSqrt2 = 0.707106781187f; 182 | 183 | const ImVec2 handleTopLeft = { lensCentre.x + lensRadius * halfSqrt2, lensCentre.y + lensRadius * halfSqrt2 }; 184 | const ImVec2 handleBottomRight = { handleTopLeft.x + handleLength, handleTopLeft.y + handleLength }; 185 | 186 | const ImVec2 iconTopLeft{ lensCentre.x - lensRadius, lensCentre.y - lensRadius }; 187 | const ImRect iconRect{ iconTopLeft, handleBottomRight }; 188 | 189 | ImDrawList* drawList = ImGui::GetWindowDrawList(); 190 | const ImU32 disabledTextCol = ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); 191 | 192 | if (availRect.Contains(iconRect)) 193 | { 194 | drawList->AddCircle(lensCentre, lensRadius, disabledTextCol); 195 | drawList->AddLine(handleTopLeft, handleBottomRight, disabledTextCol); 196 | } 197 | 198 | if (!userQuery.empty() 199 | && ImGui::IsItemFocused()) 200 | { 201 | // If the preview text was not set by us, 202 | // it may not 'complete' what has currently 203 | // been typed. Check if this is the case. 204 | const std::vector splitQuery = SplitTokens(userQuery); 205 | 206 | if (!splitQuery.empty()) 207 | { 208 | StrView previewToDisplay = GetStringNeededToCompletePartial(splitQuery.back(), context.mResult.mOutput.mPreviewText); 209 | 210 | if (previewToDisplay.size() > 0) 211 | { 212 | ImVec2 completePreviewPos = ImGui::GetItemRectMin(); 213 | completePreviewPos.x += ImGui::CalcTextSize(userQuery.c_str(), userQuery.c_str() + userQuery.size()).x; 214 | 215 | completePreviewPos.x += ImGui::GetStyle().FramePadding.x; 216 | completePreviewPos.y += ImGui::GetStyle().FramePadding.y; 217 | 218 | drawList->AddText(completePreviewPos, disabledTextCol, previewToDisplay.begin(), previewToDisplay.end()); 219 | } 220 | } 221 | } 222 | } 223 | 224 | void ImSearch::Submit() 225 | { 226 | LocalContext& context = GetLocalContext(); 227 | Result& lastValidResult = context.mResult; 228 | 229 | if (!IsResultUpToDate(lastValidResult, context.mInput)) 230 | { 231 | lastValidResult.mInput = context.mInput; 232 | BringResultUpToDate(lastValidResult); 233 | } 234 | 235 | DisplayToUser(context, lastValidResult); 236 | 237 | context.mHasSubmitted = true; 238 | context.mInput.mEntries.clear(); 239 | context.mDisplayCallbacks.clear(); 240 | IM_ASSERT(context.mPushStackLevel == 0 && "There were more calls to PushSearchable than to PopSearchable"); 241 | } 242 | 243 | void ImSearch::EndSearch() 244 | { 245 | LocalContext& localContext = GetLocalContext(); 246 | 247 | if (!localContext.mHasSubmitted) 248 | { 249 | Submit(); 250 | } 251 | 252 | ImGui::PopID(); 253 | 254 | ImSearch::ImSearchContext& context = GetImSearchContext(); 255 | context.ContextStack.pop(); 256 | } 257 | 258 | bool ImSearch::Internal::PushSearchable(const char* name, void* functor, VTable vTable) 259 | { 260 | LocalContext& context = GetLocalContext(); 261 | 262 | IM_ASSERT(name != nullptr); 263 | IM_ASSERT(!context.mHasSubmitted && "Tried calling PushSearchable after EndSearch or Submit"); 264 | 265 | if (!CanCollectSubmissions()) 266 | { 267 | // Invoke the callback immediately if the user 268 | // is not actively searching, for performance 269 | // and memory reasons. 270 | const bool pushResult = Callback::InvokeAsPushSearchable(vTable, functor, name); 271 | 272 | // We only expect a call to PopSearchable if the callback returned true. 273 | // So we only increment the depth if the callback returned true. 274 | if (pushResult) 275 | { 276 | context.mPushStackLevel++; 277 | } 278 | 279 | return pushResult; 280 | } 281 | 282 | context.mInput.mEntries.emplace_back(); 283 | Searchable& searchable = context.mInput.mEntries.back(); 284 | searchable.mText = std::string{ name }; 285 | 286 | context.mDisplayCallbacks.emplace_back(); 287 | if (functor != nullptr 288 | && vTable != nullptr) 289 | { 290 | context.mDisplayCallbacks.back().mOnDisplayStart = Callback{ functor, vTable }; 291 | } 292 | 293 | const IndexT currentIndex = static_cast(context.mInput.mEntries.size() - static_cast(1ull)); 294 | 295 | if (!context.mPushStack.empty()) 296 | { 297 | const IndexT parentIndex = context.mPushStack.top(); 298 | searchable.mIndexOfParent = parentIndex; 299 | 300 | Searchable& parent = context.mInput.mEntries[parentIndex]; 301 | 302 | if (parent.mIndexOfFirstChild == sNullIndex) 303 | { 304 | parent.mIndexOfFirstChild = currentIndex; 305 | parent.mIndexOfLastChild = currentIndex; 306 | } 307 | else 308 | { 309 | const IndexT prevIndex = parent.mIndexOfLastChild; 310 | parent.mIndexOfLastChild = currentIndex; 311 | IM_ASSERT(prevIndex != sNullIndex); 312 | 313 | Searchable& prevSibling = context.mInput.mEntries[prevIndex]; 314 | prevSibling.mIndexOfNextSibling = currentIndex; 315 | } 316 | } 317 | 318 | context.mPushStack.emplace(currentIndex); 319 | context.mPushStackLevel++; 320 | return true; 321 | } 322 | 323 | void ImSearch::PopSearchable() 324 | { 325 | Internal::PopSearchable(nullptr, nullptr); 326 | } 327 | 328 | void ImSearch::Internal::PopSearchable(void* functor, VTable vTable) 329 | { 330 | LocalContext& context = GetLocalContext(); 331 | 332 | context.mPushStackLevel--; 333 | IM_ASSERT(context.mPushStackLevel >= 0 && "Called PopSearchable too many times!"); 334 | 335 | if (!CanCollectSubmissions()) 336 | { 337 | if (functor != nullptr 338 | && vTable != nullptr) 339 | { 340 | Callback::InvokeAsPopSearchable(vTable, functor); 341 | } 342 | 343 | return; 344 | } 345 | 346 | const IndexT indexOfCurrentCategory = GetCurrentItem(context); 347 | 348 | if (functor != nullptr 349 | && vTable != nullptr) 350 | { 351 | context.mDisplayCallbacks[indexOfCurrentCategory].mOnDisplayEnd = Callback{ functor, vTable }; 352 | } 353 | 354 | context.mPushStack.pop(); 355 | } 356 | 357 | void ImSearch::SetRelevancyBonus(float bonus) 358 | { 359 | if (!CanCollectSubmissions()) 360 | { 361 | return; 362 | } 363 | 364 | LocalContext& context = GetLocalContext(); 365 | const IndexT indexOfCurrentCategory = GetCurrentItem(context); 366 | 367 | context.mInput.mBonuses.resize(indexOfCurrentCategory + 1); 368 | context.mInput.mBonuses[indexOfCurrentCategory] = bonus; 369 | } 370 | 371 | void ImSearch::AddSynonym(const char* synonym) 372 | { 373 | // Ensure there is an active 'parent' item 374 | // to add the synonym to; no point in adding 375 | // synonyms at the root level, and if you did, 376 | // you did something wrong. 377 | LocalContext& context = GetLocalContext(); 378 | 379 | IM_UNUSED(context); 380 | IM_ASSERT(context.mPushStackLevel > 0 && "AddSynonym must be called between PushSearchable and PopSearchable. See imsearch_demo.cpp"); 381 | 382 | if (!CanCollectSubmissions()) 383 | { 384 | return; 385 | } 386 | 387 | if (Internal::PushSearchable(synonym, nullptr, nullptr)) 388 | { 389 | PopSearchable(); 390 | } 391 | } 392 | 393 | void ImSearch::SetUserQuery(const char* query) 394 | { 395 | LocalContext& context = GetLocalContext(); 396 | context.mInput.mUserQuery = std::string{ query }; 397 | } 398 | 399 | const char* ImSearch::GetUserQuery() 400 | { 401 | LocalContext& context = GetLocalContext(); 402 | return context.mInput.mUserQuery.c_str(); 403 | } 404 | 405 | 406 | ImSearchStyle& ImSearch::GetStyle() 407 | { 408 | ImSearchContext& context = GetImSearchContext(); 409 | return context.Style; 410 | } 411 | 412 | ImU32 ImSearch::GetColorU32(ImSearchCol idx, float alpha_mul) 413 | { 414 | ImVec4 c = GetStyleColorVec4(idx); 415 | c.w *= ImGui::GetStyle().Alpha * alpha_mul; 416 | return ImGui::ColorConvertFloat4ToU32(c); 417 | } 418 | 419 | const ImVec4& ImSearch::GetStyleColorVec4(ImSearchCol idx) 420 | { 421 | ImSearchStyle& style = GetStyle(); 422 | return style.Colors[idx]; 423 | } 424 | 425 | void ImSearch::PushStyleColor(ImSearchCol idx, ImU32 col) 426 | { 427 | PushStyleColor(idx, ImGui::ColorConvertU32ToFloat4(col)); 428 | } 429 | 430 | void ImSearch::PushStyleColor(ImSearchCol idx, const ImVec4& col) 431 | { 432 | ImSearchContext& context = GetImSearchContext(); 433 | 434 | ImGuiColorMod backup; 435 | backup.Col = static_cast(idx); 436 | backup.BackupValue = context.Style.Colors[idx]; 437 | context.ColorModifiers.push_back(backup); 438 | context.Style.Colors[idx] = col; 439 | } 440 | 441 | void ImSearch::PopStyleColor(int count) 442 | { 443 | ImSearchContext& context = GetImSearchContext(); 444 | IM_ASSERT_USER_ERROR(count <= context.ColorModifiers.size(), "You can't pop more modifiers than have been pushed!"); 445 | while (count > 0) 446 | { 447 | ImGuiColorMod& backup = context.ColorModifiers.back(); 448 | context.Style.Colors[backup.Col] = backup.BackupValue; 449 | context.ColorModifiers.pop_back(); 450 | count--; 451 | } 452 | } 453 | 454 | //----------------------------------------------------------------------------- 455 | // [SECTION] Definitions from static functions 456 | //----------------------------------------------------------------------------- 457 | 458 | bool ImSearch::IsResultUpToDate(const Result& oldResult, const Input& currentInput) 459 | { 460 | return oldResult.mInput == currentInput; 461 | } 462 | 463 | void ImSearch::BringResultUpToDate(Result& result) 464 | { 465 | AssignInitialScores(result.mInput, result.mBuffers); 466 | PropagateScoreToChildren(result.mInput, result.mBuffers); 467 | PropagateScoreToParents(result.mInput, result.mBuffers); 468 | GenerateDisplayOrder(result.mInput, result.mBuffers, result.mOutput); 469 | FindStringToAppendOnAutoComplete(result.mInput, result.mOutput); 470 | } 471 | 472 | void ImSearch::AssignInitialScores(const Input& input, ReusableBuffers& buffers) 473 | { 474 | buffers.mScores.clear(); 475 | buffers.mScores.resize(input.mEntries.size()); 476 | 477 | const StrView query = input.mUserQuery; 478 | const std::string tokenSortedQuery = MakeTokenisedString(input.mUserQuery); 479 | 480 | for (IndexT i = 0; i < static_cast(input.mEntries.size()); i++) 481 | { 482 | const Searchable& entry = input.mEntries[i]; 483 | 484 | float score = WeightedRatio(query, 485 | tokenSortedQuery, 486 | entry.mText, 487 | GetMemoizedTokenisedString(entry.mText), 488 | buffers); 489 | 490 | if (i < static_cast(input.mBonuses.size())) 491 | { 492 | score += input.mBonuses[i]; 493 | } 494 | 495 | buffers.mScores[i] = score; 496 | } 497 | } 498 | 499 | void ImSearch::PropagateScoreToChildren(const Input& input, ReusableBuffers& buffers) 500 | { 501 | // Each node can only be the child of ONE parent. 502 | // Children can only be submitted AFTER their parent. 503 | // When iterating over the nodes, we will always 504 | // encounter a parent before a child. 505 | for (IndexT parentIndex = 0; parentIndex < static_cast(input.mEntries.size()); parentIndex++) 506 | { 507 | const Searchable& parent = input.mEntries[parentIndex]; 508 | const float parentScore = buffers.mScores[parentIndex]; 509 | 510 | for (IndexT childIndex = parent.mIndexOfFirstChild; 511 | childIndex != sNullIndex; 512 | childIndex = input.mEntries[childIndex].mIndexOfNextSibling) 513 | { 514 | float& childScore = buffers.mScores[childIndex]; 515 | childScore = std::max(childScore, parentScore); 516 | } 517 | } 518 | } 519 | 520 | void ImSearch::PropagateScoreToParents(const Input& input, ReusableBuffers& buffers) 521 | { 522 | // Children can only be submitted AFTER their parent. 523 | // When iterating over the entries in reverse, we will 524 | // always reach a node's child before the node itself. 525 | for (IndexT childIndex = static_cast(input.mEntries.size()); childIndex-- > 0;) 526 | { 527 | const IndexT parentIndex = input.mEntries[childIndex].mIndexOfParent; 528 | 529 | if (parentIndex == sNullIndex) 530 | { 531 | continue; 532 | } 533 | 534 | const float childScore = buffers.mScores[childIndex]; 535 | float& parentScore = buffers.mScores[parentIndex]; 536 | parentScore = std::max(parentScore, childScore); 537 | } 538 | } 539 | 540 | void ImSearch::GenerateDisplayOrder(const Input& input, ReusableBuffers& buffers, Output& output) 541 | { 542 | output.mDisplayOrder.clear(); 543 | buffers.mTempIndices.clear(); 544 | 545 | for (IndexT i = 0; i < static_cast(input.mEntries.size()); i++) 546 | { 547 | if (input.mEntries[i].mIndexOfParent == sNullIndex 548 | && buffers.mScores[i] >= sCutOffStrength) 549 | { 550 | buffers.mTempIndices.emplace_back(i); 551 | } 552 | } 553 | 554 | AppendToDisplayOrder(input, 555 | buffers, 556 | 0, 557 | static_cast(buffers.mTempIndices.size()), 558 | output); 559 | } 560 | 561 | void ImSearch::AppendToDisplayOrder(const Input& input, 562 | ReusableBuffers& buffers, 563 | IndexT startInIndicesBuffer, 564 | IndexT endInIndicesBuffer, 565 | Output& output) 566 | { 567 | std::stable_sort(buffers.mTempIndices.begin() + startInIndicesBuffer, 568 | buffers.mTempIndices.begin() + endInIndicesBuffer, 569 | [&](IndexT lhsIndex, IndexT rhsIndex) -> bool 570 | { 571 | const float lhsScore = buffers.mScores[lhsIndex]; 572 | const float rhsScore = buffers.mScores[rhsIndex]; 573 | 574 | return lhsScore > rhsScore; 575 | }); 576 | 577 | for (IndexT indexInIndicesBuffer = startInIndicesBuffer; indexInIndicesBuffer < endInIndicesBuffer; indexInIndicesBuffer++) 578 | { 579 | IndexT searchableIndex = buffers.mTempIndices[indexInIndicesBuffer]; 580 | 581 | const IndexT nextStartInIndicesBuffer = static_cast(buffers.mTempIndices.size()); 582 | 583 | output.mDisplayOrder.emplace_back(searchableIndex); 584 | 585 | const Searchable& searchable = input.mEntries[searchableIndex]; 586 | for (IndexT childIndex = searchable.mIndexOfFirstChild; 587 | childIndex != sNullIndex; 588 | childIndex = input.mEntries[childIndex].mIndexOfNextSibling) 589 | { 590 | if (buffers.mScores[childIndex] >= sCutOffStrength) 591 | { 592 | buffers.mTempIndices.emplace_back(childIndex); 593 | } 594 | } 595 | 596 | const IndexT nextEndInIndicesBuffer = static_cast(buffers.mTempIndices.size()); 597 | 598 | AppendToDisplayOrder(input, 599 | buffers, 600 | nextStartInIndicesBuffer, 601 | nextEndInIndicesBuffer, 602 | output); 603 | 604 | output.mDisplayOrder.emplace_back(searchableIndex | Output::sDisplayEndFlag); 605 | } 606 | } 607 | 608 | void ImSearch::FindStringToAppendOnAutoComplete(const Input& input, Output& output) 609 | { 610 | output.mPreviewText.clear(); 611 | 612 | const std::vector tokensInQuery = SplitTokens(input.mUserQuery); 613 | 614 | if (tokensInQuery.empty()) 615 | { 616 | return; 617 | } 618 | 619 | const StrView tokenToComplete = tokensInQuery.back(); 620 | 621 | const std::vector& displayOrder = output.mDisplayOrder; 622 | 623 | for (auto it = displayOrder.begin(); it != displayOrder.end(); ++it) 624 | { 625 | const IndexT indexAndFlag = *it; 626 | const IndexT index = indexAndFlag & ~Output::sDisplayEndFlag; 627 | const IndexT isEnd = indexAndFlag & Output::sDisplayEndFlag; 628 | 629 | if (isEnd) 630 | { 631 | continue; 632 | } 633 | 634 | const Searchable& searchable = input.mEntries[index]; 635 | const std::vector tokens = SplitTokens(searchable.mText); 636 | 637 | for (const StrView token : tokens) 638 | { 639 | if (GetStringNeededToCompletePartial(tokenToComplete, token).mSize != 0) 640 | { 641 | output.mPreviewText = { token.data(), token.size() }; 642 | return; 643 | } 644 | } 645 | } 646 | } 647 | 648 | void ImSearch::DisplayToUser(const LocalContext& context, const Result& result) 649 | { 650 | const std::string& userQuery = result.mInput.mUserQuery; 651 | const bool isUserSearching = !userQuery.empty(); 652 | ImGui::PushID(isUserSearching); 653 | 654 | const std::vector& displayOrder = result.mOutput.mDisplayOrder; 655 | 656 | const bool hasHighlighting = (context.mInput.mFlags & ImSearchFlags_NoTextHighlighting) == 0; 657 | if (hasHighlighting) 658 | { 659 | BeginHighlightZone(userQuery.c_str()); 660 | } 661 | 662 | for (auto it = displayOrder.begin(); it != displayOrder.end(); ++it) 663 | { 664 | const IndexT indexAndFlag = *it; 665 | const IndexT index = indexAndFlag & ~Output::sDisplayEndFlag; 666 | const IndexT isEnd = indexAndFlag & Output::sDisplayEndFlag; 667 | 668 | const Searchable& searchable = result.mInput.mEntries[index]; 669 | const DisplayCallbacks& callbacks = context.mDisplayCallbacks[index]; 670 | 671 | if (isEnd) 672 | { 673 | if (callbacks.mOnDisplayEnd) 674 | { 675 | callbacks.mOnDisplayEnd(); 676 | } 677 | continue; 678 | } 679 | 680 | if (!callbacks.mOnDisplayStart) 681 | { 682 | continue; 683 | } 684 | 685 | if (isUserSearching) 686 | { 687 | ImGui::SetNextItemOpen(true, ImGuiCond_Once); 688 | } 689 | 690 | if (callbacks.mOnDisplayStart(searchable.mText.c_str())) 691 | { 692 | continue; 693 | } 694 | 695 | // The user start function was called, but returned false. 696 | // We need to avoid displaying this searchable's children, 697 | // and make sure we call the corresponding mOnDisplayEnd 698 | it = std::find(it + 1, displayOrder.end(), index | Output::sDisplayEndFlag); 699 | IM_ASSERT(it != displayOrder.end()); 700 | } 701 | 702 | if (hasHighlighting) 703 | { 704 | EndHighlightZone(); 705 | } 706 | 707 | ImGui::PopID(); 708 | } 709 | 710 | bool ImSearch::DoDrawnGlyphsMatch(const ImDrawList& lhsList, 711 | const ImDrawIdx* lhsStart, 712 | const ImDrawList& rhsList, 713 | const ImDrawIdx* rhsStart, 714 | int length) 715 | { 716 | for (int i = 0; i < length; i++) 717 | { 718 | const ImVec2 expectedCurr = lhsList.VtxBuffer[lhsStart[i]].uv; 719 | const ImVec2 actualCurr = rhsList.VtxBuffer[rhsStart[i]].uv; 720 | 721 | // use std::not_equal_to instead of != to silence -Wfloat-equal 722 | // warnings, for those that want to integrate this library 723 | // with a stricter codebase. 724 | if (std::not_equal_to{}(expectedCurr.x, actualCurr.x) 725 | || std::not_equal_to{}(expectedCurr.y, actualCurr.y)) 726 | { 727 | return false; 728 | } 729 | } 730 | 731 | return true; 732 | } 733 | 734 | //----------------------------------------------------------------------------- 735 | // [SECTION] Definitions from imsearch_internal.h 736 | //----------------------------------------------------------------------------- 737 | 738 | ImSearch::Callback::Callback(void* originalFunctor, ImSearch::Internal::VTable vTable) : 739 | mVTable(vTable) 740 | { 741 | if (mVTable == nullptr) 742 | { 743 | return; 744 | } 745 | 746 | int size; 747 | vTable(VTableModes::GetSize, &size, nullptr); 748 | mUserFunctor = ImGui::MemAlloc(static_cast(size)); 749 | IM_ASSERT(mUserFunctor != nullptr); 750 | vTable(VTableModes::MoveConstruct, originalFunctor, mUserFunctor); 751 | } 752 | 753 | ImSearch::Callback::Callback(Callback&& other) noexcept : 754 | mVTable(other.mVTable), 755 | mUserFunctor(other.mUserFunctor) 756 | { 757 | other.mVTable = nullptr; 758 | other.mUserFunctor = nullptr; 759 | } 760 | 761 | ImSearch::Callback& ImSearch::Callback::operator=(Callback&& other) noexcept 762 | { 763 | if (this == &other) 764 | { 765 | return *this; 766 | } 767 | 768 | ClearData(); 769 | 770 | mVTable = other.mVTable; 771 | mUserFunctor = other.mUserFunctor; 772 | 773 | other.mVTable = nullptr; 774 | other.mUserFunctor = nullptr; 775 | 776 | return *this; 777 | } 778 | 779 | ImSearch::Callback::~Callback() 780 | { 781 | ClearData(); 782 | } 783 | 784 | bool ImSearch::Callback::operator()(const char* name) const 785 | { 786 | return InvokeAsPushSearchable(mVTable, mUserFunctor, name); 787 | } 788 | 789 | void ImSearch::Callback::operator()() const 790 | { 791 | InvokeAsPopSearchable(mVTable, mUserFunctor); 792 | } 793 | 794 | bool ImSearch::Callback::InvokeAsPushSearchable(ImSearch::Internal::VTable vTable, void* userFunctor, const char* name) 795 | { 796 | return vTable(VTableModes::Invoke, userFunctor, const_cast(name)); 797 | } 798 | 799 | void ImSearch::Callback::InvokeAsPopSearchable(ImSearch::Internal::VTable vTable, void* userFunctor) 800 | { 801 | vTable(VTableModes::Invoke, userFunctor, nullptr); 802 | } 803 | 804 | void ImSearch::Callback::ClearData() 805 | { 806 | if (mUserFunctor == nullptr) 807 | { 808 | return; 809 | } 810 | 811 | mVTable(VTableModes::Destruct, mUserFunctor, nullptr); 812 | 813 | ImGui::MemFree(mUserFunctor); 814 | mUserFunctor = nullptr; 815 | } 816 | 817 | bool ImSearch::operator==(const StrView& lhs, const StrView& rhs) 818 | { 819 | return lhs.mSize == rhs.mSize && 820 | ( 821 | (lhs.mData == nullptr && rhs.mData == nullptr) 822 | || memcmp(lhs.mData, rhs.mData, lhs.mSize) == 0 823 | ); 824 | } 825 | 826 | bool ImSearch::operator==(const Searchable& lhs, const Searchable& rhs) 827 | { 828 | return lhs.mText == rhs.mText 829 | && lhs.mIndexOfFirstChild == rhs.mIndexOfFirstChild 830 | && lhs.mIndexOfLastChild == rhs.mIndexOfLastChild 831 | && lhs.mIndexOfParent == rhs.mIndexOfParent 832 | && lhs.mIndexOfNextSibling == rhs.mIndexOfNextSibling; 833 | } 834 | 835 | bool ImSearch::operator==(const Input& lhs, const Input& rhs) 836 | { 837 | return lhs.mFlags == rhs.mFlags 838 | && lhs.mUserQuery == rhs.mUserQuery 839 | && lhs.mEntries == rhs.mEntries 840 | && lhs.mBonuses == rhs.mBonuses; 841 | } 842 | 843 | ImSearch::LocalContext& ImSearch::GetLocalContext() 844 | { 845 | ImSearch::ImSearchContext& ImSearchContext = GetImSearchContext(); 846 | IM_ASSERT(!ImSearchContext.ContextStack.empty() && "Not currently in between a ImSearch::BeginSearch and ImSearch::EndSearch"); 847 | return ImSearchContext.ContextStack.top(); 848 | } 849 | 850 | ImSearch::ImSearchContext& ImSearch::GetImSearchContext() 851 | { 852 | ImSearch::ImSearchContext* context = ImSearch::GetCurrentContext(); 853 | IM_ASSERT(sContext != nullptr && "An ImSearchContext has not been created, see ImSearch::CreateContext"); 854 | return *context; 855 | } 856 | 857 | ImSearch::IndexT ImSearch::GetCurrentItem(ImSearch::LocalContext& context) 858 | { 859 | IM_ASSERT(!context.mPushStack.empty() && "No active object; can only be called after PushSearchable"); 860 | IM_ASSERT(!context.mHasSubmitted && "No active object; Submit (or EndSearch) has already been called"); 861 | return context.mPushStack.top(); 862 | } 863 | 864 | bool ImSearch::CanCollectSubmissions() 865 | { 866 | return *ImSearch::GetUserQuery() != '\0'; 867 | } 868 | 869 | float ImSearch::GetScore(size_t index) 870 | { 871 | LocalContext& context = GetLocalContext(); 872 | auto& scores = context.mResult.mBuffers.mScores; 873 | 874 | if (index >= scores.size()) 875 | { 876 | return -1.0f; 877 | } 878 | 879 | return scores[index]; 880 | } 881 | 882 | size_t ImSearch::GetDisplayOrderEntry(size_t index) 883 | { 884 | LocalContext& context = GetLocalContext(); 885 | auto& displayOrder = context.mResult.mOutput.mDisplayOrder; 886 | 887 | if (index >= displayOrder.size()) 888 | { 889 | return std::numeric_limits::max(); 890 | } 891 | return displayOrder[index]; 892 | } 893 | 894 | int ImSearch::GetNumItemsFilteredOut() 895 | { 896 | const LocalContext& context = GetLocalContext(); 897 | const auto& displayOrder = context.mResult.mOutput.mDisplayOrder; 898 | 899 | int numInDisplayOrder{}; 900 | 901 | for (IndexT flagAndIndex : displayOrder) 902 | { 903 | numInDisplayOrder += (flagAndIndex & Output::sDisplayEndFlag) == 0; 904 | } 905 | const int numSubmitted = static_cast(context.mResult.mInput.mEntries.size()); 906 | IM_ASSERT(numInDisplayOrder <= numSubmitted); 907 | 908 | return numSubmitted - numInDisplayOrder; 909 | } 910 | 911 | void ImSearch::SetPreviewText(const char* preview) 912 | { 913 | GetLocalContext().mResult.mOutput.mPreviewText = std::string{ preview }; 914 | } 915 | 916 | const char* ImSearch::GetPreviewText() 917 | { 918 | return GetLocalContext().mResult.mOutput.mPreviewText.c_str(); 919 | } 920 | 921 | void ImSearch::BeginHighlightZone(const char* textToHighlight) 922 | { 923 | ImGuiStorage* storage = ImGui::GetStateStorage(); 924 | 925 | std::string* str = new std::string(textToHighlight); 926 | storage->SetVoidPtr(ImGui::GetID("TextToHighlight"), str); 927 | 928 | ImDrawList* drawList = ImGui::GetWindowDrawList(); 929 | storage->SetInt(ImGui::GetID("StartIdxIdx"), drawList->IdxBuffer.size()); 930 | } 931 | 932 | void ImSearch::EndHighlightZone() 933 | { 934 | ImGuiStorage* storage = ImGui::GetStateStorage(); 935 | ImDrawList* drawList = ImGui::GetWindowDrawList(); 936 | 937 | std::string* str = static_cast(storage->GetVoidPtr(ImGui::GetID("TextToHighlight"))); 938 | const int prevIdxEnd = storage->GetInt(ImGui::GetID("StartIdxIdx")); 939 | 940 | HighlightSubstrings(str->c_str(), 941 | str->c_str() + str->size(), 942 | drawList, 943 | prevIdxEnd, 944 | drawList->IdxBuffer.size()); 945 | 946 | delete str; 947 | } 948 | 949 | void ImSearch::HighlightSubstrings(const char* substrStart, 950 | const char* substrEnd, 951 | ImDrawList* drawList, 952 | int startIdxIdx, 953 | int endIdxIdx) 954 | { 955 | const ImU32 textCol = ImSearch::GetColorU32(ImSearchCol_TextHighlighted); 956 | const ImU32 textBgCol = ImSearch::GetColorU32(ImSearchCol_TextHighlightedBg); 957 | 958 | ImDrawListSharedData* sharedData = ImGui::GetDrawListSharedData(); 959 | ImDrawList queryDrawList{ sharedData }; 960 | queryDrawList.AddDrawCmd(); 961 | queryDrawList.PushTextureID(ImGui::GetFont()->ContainerAtlas->TexID); 962 | queryDrawList.PushClipRect({ -INFINITY, -INFINITY }, { INFINITY, INFINITY }); 963 | 964 | struct DrawnGlyph 965 | { 966 | DrawnGlyph(ImDrawList& l, char character) 967 | { 968 | mIdxIdxStart = l.IdxBuffer.size(); 969 | l.AddText( 970 | {}, 971 | 0xffffffff, 972 | &character, 973 | &character + 1); 974 | mIdxIdxEnd = l.IdxBuffer.size(); 975 | } 976 | int mIdxIdxStart{}; 977 | int mIdxIdxEnd{}; 978 | }; 979 | ImVector glyphs{}; 980 | 981 | for (const char* ch = substrStart; ch < substrEnd; ch++) 982 | { 983 | const char lower = static_cast(std::tolower(static_cast(*ch))); 984 | const char upper = static_cast(std::toupper(static_cast(*ch))); 985 | 986 | glyphs.push_back(DrawnGlyph{ queryDrawList, lower }); 987 | glyphs.push_back(DrawnGlyph{ queryDrawList, upper }); 988 | } 989 | 990 | queryDrawList.PopClipRect(); 991 | queryDrawList.PopTextureID(); 992 | 993 | for (int matchStart = startIdxIdx; matchStart < endIdxIdx; matchStart++) 994 | { 995 | int matchEnd = matchStart; 996 | 997 | for (int chIdx = 0; chIdx < substrEnd - substrStart; chIdx++) 998 | { 999 | bool anyGlyphMatching = false; 1000 | 1001 | for (int i = 0; i < 2; i++) 1002 | { 1003 | const DrawnGlyph& glyph = glyphs[(chIdx * 2) + i]; 1004 | const int glyphLength = glyph.mIdxIdxEnd - glyph.mIdxIdxStart; 1005 | 1006 | const int nextMatchEnd = matchEnd + glyphLength; 1007 | 1008 | if (nextMatchEnd <= endIdxIdx 1009 | && DoDrawnGlyphsMatch( 1010 | queryDrawList, 1011 | queryDrawList.IdxBuffer.Data + glyph.mIdxIdxStart, 1012 | *drawList, 1013 | &drawList->IdxBuffer[matchEnd], 1014 | glyphLength)) 1015 | { 1016 | matchEnd = nextMatchEnd; 1017 | anyGlyphMatching = true; 1018 | break; 1019 | } 1020 | } 1021 | 1022 | if (!anyGlyphMatching) 1023 | { 1024 | matchEnd = matchStart; 1025 | break; 1026 | } 1027 | } 1028 | 1029 | if (matchStart == matchEnd) 1030 | { 1031 | continue; 1032 | } 1033 | 1034 | ImVec2 min{ INFINITY, INFINITY }; 1035 | ImVec2 max{ -INFINITY, -INFINITY }; 1036 | 1037 | for (int idxIdx = matchStart; idxIdx < matchEnd; idxIdx++) 1038 | { 1039 | ImDrawVert& vert = drawList->VtxBuffer[drawList->IdxBuffer[idxIdx]]; 1040 | vert.col = textCol; 1041 | 1042 | min.x = std::min(min.x, vert.pos.x); 1043 | min.y = std::min(min.y, vert.pos.y); 1044 | 1045 | max.x = std::max(max.x, vert.pos.x); 1046 | max.y = std::max(max.y, vert.pos.y); 1047 | } 1048 | 1049 | const ImVec2 padding = ImGui::GetStyle().FramePadding; 1050 | min.x -= padding.x * .5f; 1051 | min.y -= padding.y * .5f; 1052 | max.x += padding.x * .5f; 1053 | max.y += padding.y * .5f; 1054 | 1055 | drawList->AddRectFilled(min, max, textBgCol); 1056 | 1057 | const int count = matchEnd - matchStart; 1058 | drawList->PrimReserve(count, 0); 1059 | 1060 | for (int idxIdx = matchStart; idxIdx < matchEnd; idxIdx++) 1061 | { 1062 | drawList->PrimWriteIdx(drawList->IdxBuffer[idxIdx]); 1063 | } 1064 | } 1065 | } 1066 | 1067 | std::vector ImSearch::SplitTokens(StrView s) 1068 | { 1069 | std::vector tokens{}; 1070 | std::string current{}; 1071 | for (char c : s) 1072 | { 1073 | if (std::isalnum(static_cast(c))) 1074 | { 1075 | current += c; 1076 | continue; 1077 | } 1078 | 1079 | if (!current.empty()) 1080 | { 1081 | tokens.push_back(current); 1082 | current.clear(); 1083 | } 1084 | } 1085 | 1086 | if (!current.empty()) 1087 | { 1088 | tokens.push_back(current); 1089 | } 1090 | return tokens; 1091 | } 1092 | 1093 | std::string ImSearch::Join(const std::vector& tokens) 1094 | { 1095 | if (tokens.empty()) 1096 | { 1097 | return {}; 1098 | } 1099 | 1100 | std::string res = tokens[0]; 1101 | for (size_t i = 1; i < tokens.size(); i++) 1102 | { 1103 | res += ' ' + tokens[i]; 1104 | } 1105 | return res; 1106 | } 1107 | 1108 | ImSearch::StrView ImSearch::GetStringNeededToCompletePartial(StrView partial, StrView complete) 1109 | { 1110 | if (complete.size() <= partial.size()) 1111 | { 1112 | return {}; 1113 | } 1114 | 1115 | const StrView tokenSubStr = { complete.mData, partial.size() }; 1116 | 1117 | for (IndexT i = 0; i < partial.size(); i++) 1118 | { 1119 | if (std::tolower(static_cast(tokenSubStr[i])) != std::tolower(static_cast(partial[i]))) 1120 | { 1121 | return {}; 1122 | } 1123 | } 1124 | 1125 | return { complete.data() + partial.size(), complete.size() - partial.size() }; 1126 | } 1127 | 1128 | std::string ImSearch::MakeTokenisedString(StrView original) 1129 | { 1130 | std::vector targetTokens = SplitTokens(original); 1131 | std::sort(targetTokens.begin(), targetTokens.end()); 1132 | std::string processedTarget = Join(targetTokens); 1133 | 1134 | for (char& c : processedTarget) 1135 | { 1136 | c = static_cast(std::tolower(static_cast(c))); 1137 | } 1138 | 1139 | return processedTarget; 1140 | } 1141 | 1142 | ImSearch::StrView ImSearch::GetMemoizedTokenisedString(const std::string& original) 1143 | { 1144 | ImSearch::ImSearchContext& context = GetImSearchContext(); 1145 | auto& preprocessedStrings = context.mTokenisedStrings; 1146 | 1147 | auto it = context.mTokenisedStrings.find(original); 1148 | 1149 | if (it == preprocessedStrings.end()) 1150 | { 1151 | it = preprocessedStrings.emplace(original, MakeTokenisedString(original)).first; 1152 | } 1153 | 1154 | return it->second; 1155 | } 1156 | 1157 | // This file has been altered to better fit ImSearch. 1158 | // The original can be found here https://github.com/Tmplt/python-Levenshtein/blob/master/Levenshtein.c 1159 | ImSearch::IndexT ImSearch::LevenshteinDistance( 1160 | StrView s1, 1161 | StrView s2, 1162 | ReusableBuffers& buffers) 1163 | { 1164 | IndexT i; 1165 | IndexT* row; /* we only need to keep one row of costs */ 1166 | IndexT* end; 1167 | 1168 | /* strip common prefix */ 1169 | while (s1.mSize > 0 1170 | && s2.mSize > 0 1171 | && *s1.mData == *s2.mData) 1172 | { 1173 | s1.mSize--; 1174 | s2.mSize--; 1175 | s1.mData++; 1176 | s2.mData++; 1177 | } 1178 | 1179 | /* strip common suffix */ 1180 | while (s1.mSize > 0 && s2.mSize > 0 && s1.mData[s1.mSize - 1] == s2.mData[s2.mSize - 1]) 1181 | { 1182 | s1.mSize--; 1183 | s2.mSize--; 1184 | } 1185 | 1186 | /* catch trivial cases */ 1187 | if (s1.mSize == 0) 1188 | { 1189 | return s2.mSize; 1190 | } 1191 | if (s2.mSize == 0) 1192 | { 1193 | return s1.mSize; 1194 | } 1195 | 1196 | /* make the inner cycle (i.e. s2.mData) the longer one */ 1197 | if (s1.mSize > s2.mSize) 1198 | { 1199 | IndexT nx = s1.mSize; 1200 | const char* sx = s1.mData; 1201 | s1.mSize = s2.mSize; 1202 | s2.mSize = nx; 1203 | s1.mData = s2.mData; 1204 | s2.mData = sx; 1205 | } 1206 | /* check s1.mSize == 1 separately */ 1207 | if (s1.mSize == 1) 1208 | { 1209 | return s2.mSize + 1 - 2 * (memchr(s2.mData, *s1.mData, s2.mSize) != nullptr); 1210 | } 1211 | s1.mSize++; 1212 | s2.mSize++; 1213 | 1214 | /* initialize first row */ 1215 | buffers.mTempIndices.resize(s2.mSize); 1216 | row = buffers.mTempIndices.data(); 1217 | end = row + s2.mSize - 1; 1218 | for (i = 0; i < s2.mSize; i++) 1219 | { 1220 | row[i] = i; 1221 | } 1222 | 1223 | /* go through the matrix and compute the costs. yes, this is an extremely 1224 | * obfuscated version, but also extremely memory-conservative and relatively 1225 | * fast. */ 1226 | for (i = 1; i < s1.mSize; i++) 1227 | { 1228 | IndexT* p = row + 1; 1229 | const char char1 = s1.mData[i - 1]; 1230 | const char* char2p = s2.mData; 1231 | IndexT D = i; 1232 | IndexT x = i; 1233 | while (p <= end) 1234 | { 1235 | if (char1 == *(char2p++)) 1236 | { 1237 | x = --D; 1238 | } 1239 | else 1240 | { 1241 | x++; 1242 | } 1243 | D = *p; 1244 | D++; 1245 | if (x > D) 1246 | { 1247 | x = D; 1248 | } 1249 | *(p++) = x; 1250 | } 1251 | } 1252 | 1253 | i = *end; 1254 | return i; 1255 | } 1256 | 1257 | float ImSearch::Ratio(StrView s1, 1258 | StrView s2, 1259 | ReusableBuffers& buffers) 1260 | { 1261 | const IndexT distance = LevenshteinDistance(s1, 1262 | s2, 1263 | buffers); 1264 | const float ratio = 1.0f - static_cast(distance) / static_cast(s1.size() + s2.size()); 1265 | return ratio; 1266 | } 1267 | 1268 | float ImSearch::PartialRatio(StrView shorter, 1269 | StrView longer, 1270 | ReusableBuffers& buffers) 1271 | { 1272 | if (shorter.size() == 0 1273 | || longer.size() == 0) 1274 | { 1275 | return 0.0f; 1276 | } 1277 | 1278 | if (shorter.size() > longer.size()) 1279 | { 1280 | std::swap(shorter, longer); 1281 | } 1282 | 1283 | float maxRatio = 0.0f; 1284 | for (IndexT i = 0; i <= longer.size() - shorter.size(); i++) 1285 | { 1286 | const float ratio = Ratio(shorter, 1287 | { &longer[i], shorter.size() }, // Window into longer 1288 | buffers); 1289 | maxRatio = std::max(maxRatio, ratio); 1290 | 1291 | if (maxRatio >= 1.0f) 1292 | { 1293 | break; 1294 | } 1295 | } 1296 | return maxRatio; 1297 | } 1298 | 1299 | float ImSearch::WeightedRatio(StrView s1, 1300 | StrView s1Tokenised, 1301 | StrView s2, 1302 | StrView s2Tokenised, 1303 | ReusableBuffers& buffers) 1304 | { 1305 | float score = Ratio(s1, s2, buffers); 1306 | 1307 | const IndexT shorterSize = std::min(s1.size(), s2.size()); 1308 | const IndexT longerSize = std::max(s1.size(), s2.size()); 1309 | 1310 | if (longerSize <= shorterSize + shorterSize / 2) 1311 | { 1312 | score = std::max(score, 1313 | Ratio(s1Tokenised, s2Tokenised, buffers) * 0.95f); 1314 | } 1315 | else 1316 | { 1317 | const float weight = longerSize > shorterSize * 8 ? 0.5f : .8f; 1318 | 1319 | score = std::max(score, 1320 | PartialRatio(s1, s2, buffers) * weight); 1321 | 1322 | score = std::max(score, 1323 | PartialRatio(s1Tokenised, s2Tokenised, buffers) * 0.95f * weight); 1324 | } 1325 | 1326 | return score; 1327 | } 1328 | 1329 | #endif // #ifndef IMGUI_DISABLE 1330 | 1331 | 1332 | -------------------------------------------------------------------------------- /imsearch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "imgui.h" 3 | #ifndef IMGUI_DISABLE 4 | 5 | typedef int ImSearchCol; // -> enum ImSearchCol_ 6 | typedef int ImSearchFlags; // -> enum ImSearchFlags_ // Flags: for BeginSearch() 7 | 8 | enum ImSearchFlags_ 9 | { 10 | ImSearchFlags_None = 0, 11 | ImSearchFlags_NoTextHighlighting = 1 << 0 12 | }; 13 | 14 | // Plot styling colors. 15 | enum ImSearchCol_ 16 | { 17 | // item styling colors 18 | ImSearchCol_TextHighlighted, // All matching substrings are highlighted by default, and will be this colour 19 | ImSearchCol_TextHighlightedBg, // Background colour of all highlighted text 20 | ImSearchCol_COUNT 21 | }; 22 | 23 | // Plot style structure 24 | struct ImSearchStyle 25 | { 26 | // style colors 27 | ImVec4 Colors[ImSearchCol_COUNT]; // Array of styling colors. Indexable with ImSearchCol_ enums. 28 | 29 | ImSearchStyle(); 30 | }; 31 | 32 | namespace ImSearch 33 | { 34 | //----------------------------------------------------------------------------- 35 | // [SECTION] Contexts 36 | //----------------------------------------------------------------------------- 37 | 38 | struct ImSearchContext; 39 | 40 | // Creates a new ImSearch context. Call this after ImGui::CreateContext. 41 | ImSearchContext* CreateContext(); 42 | 43 | // Destroys an ImSearch context. Call this before ImGui::DestroyContext. nullptr = destroy current context 44 | void DestroyContext(ImSearchContext* ctx = nullptr); 45 | 46 | // Returns the current context. nullptr if not context has been set. 47 | ImSearchContext* GetCurrentContext(); 48 | 49 | // Sets the current context. 50 | void SetCurrentContext(ImSearchContext* ctx); 51 | 52 | //----------------------------------------------------------------------------- 53 | // [SECTION] Begin/End Search contexts 54 | //----------------------------------------------------------------------------- 55 | 56 | // Starts a searching context. If this function returns true, EndSearch() MUST 57 | // be called! You are encouraged to use the following convention: 58 | // 59 | // if (BeginSearch()) 60 | // { 61 | // SearchBar(); 62 | // 63 | // ImSearch::SearchableItem("Hello world!", 64 | // [](const char* str) 65 | // { 66 | // ImGui::Button(str); 67 | // }); 68 | // 69 | // ... 70 | // 71 | // EndSearch(); 72 | // } 73 | // 74 | // See imsearch_demo.cpp for more examples. 75 | // 76 | // Important notes: 77 | // 78 | // - BeginSearch must be unique to the current ImGui ID scope, having multiple 79 | // calls to BeginSearch leads to ID collisions. If you need to avoid ID 80 | // collisions, use ImGui::PushId 81 | bool BeginSearch(ImSearchFlags flags = 0); 82 | 83 | // Only call EndSearch() if BeginSearch() returns true! Typically called at the end 84 | // of an if statement conditioned on BeginSearch(). See example above. 85 | // Calls Submit, if Submit was not already explicitly called by the user. 86 | // See documentation for Submit below. 87 | void EndSearch(); 88 | 89 | // ImSearch is free to invoke your callbacks anywhere between you submitting them, 90 | // and you calling EndSearch. Sometimes you need more control over when the callbacks 91 | // are invoked by ImSearch, which is what Submit is for. 92 | // 93 | // For example, when you want the callbacks to be invoked within an ImGui child window: 94 | // 95 | // if (ImSearch::BeginSearch()) 96 | // { 97 | // ImSearch::SearchBar(); 98 | // 99 | // if (ImGui::BeginChild("Submissions", {}, ImGuiChildFlags_Borders)) 100 | // { 101 | // ImSearch::SearchableItem("Hello world!", 102 | // [](const char* str) 103 | // { 104 | // ImGui::Button(str); 105 | // }); 106 | // 107 | // // Call Submit explicitly; all the callbacks 108 | // // will be invoked through submit. If we 109 | // // had waited for EndSearch to do this for us, 110 | // // the callbacks would've been invoked after 111 | // // ImGui::EndChild, leaving our searchables 112 | // // to be displayed outside of the child window. 113 | // ImSearch::Submit(); 114 | // } ImGui::EndChild(); 115 | // 116 | // ImSearch::EndSearch(); 117 | // } 118 | void Submit(); 119 | 120 | //----------------------------------------------------------------------------- 121 | // [SECTION] Submitting Searchables 122 | //----------------------------------------------------------------------------- 123 | 124 | // Add a searchable with a callback, wrapping some ImGui function calls, for example: 125 | // 126 | // ImSearch::SearchableItem("Hello world!", 127 | // [](const char* str) 128 | // { 129 | // ImGui::Button(str); 130 | // }); 131 | // 132 | // Note that callbacks are called in order of relevancy, or maybe not even called at all, 133 | // if they are not relevant. ImSearch is free to invoke your callbacks anywhere between you 134 | // submitting them, and you calling EndSearch or Submit. The callback may have data 135 | // associated with (e.g., lambda captures). Make sure that your callback object 136 | // is not referencing anything that will be out of scope by then. For more information, see 137 | // 'How do callbacks work?' in imsearch_demo.cpp. 138 | // 139 | // Callback is an object or function pointer with a function of the form: void Func(const char* name). 140 | template 141 | void SearchableItem(const char* name, T&& callback); 142 | 143 | // Push a searchable with a callback, wrapping some ImGui function calls. 144 | // If this function returns true, EndSearch() MUST 145 | // be called! You are encouraged to use the following convention: 146 | // 147 | // if (PushSearchable("Hello world!", [](const char* name) { return ImGui::TreeNode(name); }) 148 | // { 149 | // PopSearchable([](){ ImGui::TreePop(); }); 150 | // } 151 | // 152 | // The provided callback function should return true if the 'children' should also be 153 | // displayed, similar to ImGui's TreeNodes. 154 | // 155 | // Callback is an object or function pointer with a function of the form: bool Func(const char* name). 156 | // 157 | // Note that callbacks are called in order of relevancy, or maybe not even called at all, 158 | // if they are not relevant. ImSearch is free to invoke your callbacks anywhere between you 159 | // submitting them, and you calling EndSearch or Submit. The callback may have data 160 | // associated with (e.g., lambda captures). Make sure that your callback object 161 | // is not referencing anything that will be out of scope by then. For more information, see 162 | // 'How do callbacks work?' in imsearch_demo.cpp. 163 | template 164 | bool PushSearchable(const char* name, T&& callback); 165 | 166 | // Only call PopSearchable() if PushSearchable() returns true! Typically called at the end 167 | // of an if statement conditioned on PushSearchable(). See example above PushSearchable. 168 | void PopSearchable(); 169 | 170 | // Only call PopSearchable() if PushSearchable() returns true! Typically called at the end 171 | // of an if statement conditioned on PushSearchable(). 172 | // You can use a callback for wrapping all the ImGui calls needed for 'ending' your widget, 173 | // a common example being `ImGui::TreePop()`, see example above PushSearchable. 174 | // 175 | // Callback is an object or function pointer with a function of the form: void Func(). 176 | template 177 | void PopSearchable(T&& callback); 178 | 179 | //----------------------------------------------------------------------------- 180 | // [SECTION] Modifiers 181 | //----------------------------------------------------------------------------- 182 | 183 | // You can artificially increase the relevancy for items you think might 184 | // be more likely to be what the user is looking for. 185 | // 186 | // You could, for example, give priority to more commonly referenced functions: 187 | // 188 | // if (ImSearch::PushSearchable(func.name, &DisplayFuncWidget)) 189 | // { 190 | // float frequency = func.timesUsedInCodebase / gTotalFunctionsUsedInCodeBase; 191 | // ImSearch::SetRelevancyBonus(frequency); 192 | // ImSearch::PopSearchable(); 193 | // } 194 | // 195 | // ImSearch places no restrictions on the range of your provided bonus, 196 | // but keep in mind the 'default' relevancy of items, as scored by ImSearch, 197 | // is within the range of 0.0f to 1.0f. Normalization is encouraged, to avoid 198 | // your bonus dwarfing the similarity score of the text. 199 | void SetRelevancyBonus(float bonus); 200 | 201 | // You can add synonyms using the following syntax: 202 | // 203 | // if (ImSearch::PushSearchable("Function", selectableCallback)) 204 | // { 205 | // ImSearch::AddSynonym("Method"); 206 | // ImSearch::AddSynonym("Procedure"); 207 | // 208 | // ImSearch::PopSearchable(); 209 | // } 210 | // 211 | void AddSynonym(const char* synonym); 212 | 213 | //----------------------------------------------------------------------------- 214 | // [SECTION] Searchbars 215 | //----------------------------------------------------------------------------- 216 | 217 | // Shows the default searchbar. Usually placed right after BeginSearch, or 218 | // right after calling Submit. 219 | void SearchBar(const char* hint = "Search"); 220 | 221 | // API for setting the user query, the text that the user has typed 222 | // and is currently searching for. Used for making custom searchbars. 223 | void SetUserQuery(const char* query); 224 | 225 | // Will return the text that the user has typed 226 | // and is currently searching for. 227 | const char* GetUserQuery(); 228 | 229 | //----------------------------------------------------------------------------- 230 | // [SECTION] Styling 231 | //----------------------------------------------------------------------------- 232 | 233 | // Like ImGui, all style colors are stored in indexable array in ImSearchStyle. 234 | // You can permanently modify these values through GetStyle().Colors, or 235 | // temporarily modify them with Push/Pop functions below. 236 | 237 | ImSearchStyle& GetStyle(); 238 | 239 | ImU32 GetColorU32(ImSearchCol idx, float alpha_mul = 1.0f); 240 | const ImVec4& GetStyleColorVec4(ImSearchCol idx); 241 | 242 | // Use PushStyleX to temporarily modify your ImSearchStyle. The modification 243 | // will last until the matching call to PopStyleX. You MUST call a pop for 244 | // every push, otherwise you will leak memory! This behaves just like ImGui. 245 | 246 | // Temporarily modify a style color. Don't forget to call PopStyleColor! 247 | void PushStyleColor(ImSearchCol idx, ImU32 col); 248 | void PushStyleColor(ImSearchCol idx, const ImVec4& col); 249 | 250 | // Undo temporary style color modification(s). Undo multiple pushes at once by increasing count. 251 | void PopStyleColor(int count = 1); 252 | 253 | //----------------------------------------------------------------------------- 254 | // [SECTION] Demo 255 | //----------------------------------------------------------------------------- 256 | 257 | // Shows the ImSearch demo window (add imsearch_demo.cpp to your sources!) 258 | void ShowDemoWindow(bool* p_open = nullptr); 259 | 260 | //----------------------------------------------------------------------------- 261 | // [SECTION] Internal 262 | //----------------------------------------------------------------------------- 263 | 264 | // End of public API! 265 | // Starting from here until the end of the file, 266 | // forwards compatibility is not guaranteed! 267 | 268 | namespace Internal 269 | { 270 | using VTable = bool(*)(int mode, void* ptr1, void* ptr2); 271 | 272 | bool PushSearchable(const char* name, void* callback, VTable vTable); 273 | void PopSearchable(void* callback, VTable vTable); 274 | 275 | template struct remove_reference { typedef T type; }; 276 | template struct remove_reference { typedef T type; }; 277 | template struct remove_reference { typedef T type; }; 278 | } 279 | } 280 | 281 | template 282 | void ImSearch::SearchableItem(const char* name, T&& callback) 283 | { 284 | using TNonRef = typename Internal::remove_reference::type; 285 | 286 | struct CallbackWrapper 287 | { 288 | TNonRef mUserCallback; 289 | 290 | bool operator()(const char* name) const 291 | { 292 | (void)mUserCallback(name); 293 | return false; 294 | } 295 | }; 296 | 297 | if (PushSearchable(name, CallbackWrapper{ static_cast(callback) })) 298 | { 299 | PopSearchable(); 300 | } 301 | } 302 | 303 | template 304 | bool ImSearch::PushSearchable(const char* name, T&& callback) 305 | { 306 | using TNonRef = typename Internal::remove_reference::type; 307 | TNonRef moveable{ static_cast(callback) }; 308 | return Internal::PushSearchable( 309 | name, 310 | &moveable, 311 | +[](int mode, void* ptr1, void* ptr2) -> bool 312 | { 313 | switch (mode) 314 | { 315 | case 0: // Invoke 316 | { 317 | TNonRef* func = static_cast(ptr1); 318 | const char* nameArg = static_cast(ptr2); 319 | return (*func)(nameArg); 320 | } 321 | case 1: // Move-construct 322 | { 323 | TNonRef* src = static_cast(ptr1); 324 | TNonRef* dst = static_cast(ptr2); 325 | new(dst)TNonRef(static_cast(*src)); 326 | return true; 327 | } 328 | case 2: // Destructor 329 | { 330 | TNonRef* src = static_cast(ptr1); 331 | src->~TNonRef(); 332 | return true; 333 | } 334 | case 3: // Get size 335 | { 336 | int& ret = *static_cast(ptr1); 337 | ret = sizeof(TNonRef); 338 | return true; 339 | } 340 | default: 341 | return false; 342 | } 343 | }); 344 | } 345 | 346 | template 347 | void ImSearch::PopSearchable(T&& callback) 348 | { 349 | using TNonRef = typename Internal::remove_reference::type; 350 | TNonRef moveable{ static_cast(callback) }; 351 | Internal::PopSearchable( 352 | &moveable, 353 | +[](int mode, void* ptr1, void* ptr2) 354 | { 355 | switch (mode) 356 | { 357 | case 0: // Invoke 358 | { 359 | TNonRef* func = static_cast(ptr1); 360 | (*func)(); 361 | return true; 362 | } 363 | case 1: // Move-construct 364 | { 365 | TNonRef* src = static_cast(ptr1); 366 | TNonRef* dst = static_cast(ptr2); 367 | new(dst)TNonRef(static_cast(*src)); 368 | return true; 369 | } 370 | case 2: // Destructor 371 | { 372 | TNonRef* src = static_cast(ptr1); 373 | src->~TNonRef(); 374 | return true; 375 | } 376 | case 3: // Get size 377 | { 378 | int& ret = *static_cast(ptr1); 379 | ret = sizeof(TNonRef); 380 | return true; 381 | } 382 | default: 383 | return false; 384 | } 385 | }); 386 | } 387 | 388 | #endif // #ifndef IMGUI_DISABLE 389 | -------------------------------------------------------------------------------- /imsearch_demo.cpp: -------------------------------------------------------------------------------- 1 | #ifndef IMGUI_DISABLE 2 | 3 | #include "imsearch.h" 4 | #include "imsearch_internal.h" 5 | #include "imgui.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace 13 | { 14 | std::array nouns{ "people","history","way","art","world","information","map","two","family","government","health","system","computer","meat","year","thanks","music","person","reading","method","data","food","understanding","theory","law","bird","literature","problem","software","control","knowledge","power","ability","economics","love","internet","television","science","library","nature","fact","product","idea","temperature","investment","area","society","activity","story","industry","media","thing","oven","community","definition","safety","quality","development","language","management","player","variety","video","week","security","country","exam","movie","organization","equipment","physics","analysis","policy","series","thought","basis","boyfriend","direction","strategy","technology","army","camera","freedom","paper","environment","child","instance","month","truth","marketing","university","writing","article","department","difference","goal","news","audience","fishing","growth" }; 15 | std::array adjectives{ "different","used","important","every","large","available","popular","able","basic","known","various","difficult","several","united","historical","hot","useful","mental","scared","additional","emotional","old","political","similar","healthy","financial","medical","traditional","federal","entire","strong","actual","significant","successful","electrical","expensive","pregnant","intelligent","interesting","poor","happy","responsible","cute","helpful","recent","willing","nice","wonderful","impossible","serious","huge","rare","technical","typical","competitive","critical","electronic","immediate","aware","educational","environmental","global","legal","relevant","accurate","capable","dangerous","dramatic","efficient","powerful","foreign","hungry","practical","psychological","severe","suitable","numerous","sufficient","unusual","consistent","cultural","existing","famous","pure","afraid","obvious","careful","latter","unhappy","acceptable","aggressive","boring","distinct","eastern","logical","reasonable","strict","administrative","automatic","civil" }; 16 | std::array sImguiExtensions = { "imgui_test_engine","ImGuiColorTextEdit","Zep","Scintilla integration","imgui-node-editor","ImNodes","imnodes","ImNodeFlow","imgui_memory_editor","imgui_hex_editor","ImPlot","ImPlot3D","imgui-plot","SimpleImGuiFlameGraph","imgui-flame-graph","Plot Var Helper","Cubic Bezier / Curve Editor","ImSequencer","ImGradient","ImCurveEdit","Gradient Color Generator","im-neo-sequencer","HImGuiAnimation","ImFileDialog","imfile","ImGuiFD","L2DFileDialog","aiekick/ImGuiFileDialog","OverShifted's Directory tree view","AirGuanZ's imgui-filebrowser","gallickgunner's ImGui-Addons","Flix01's ImGui-Addons","imgui_markdown","imgui_md","UntitledImGuiTextUtils","url/hyperlinks","DearImGui-with-IMM32","UntitledIBusHandwriting","imgui-rs-knobs","imgui-knobs","imspinner","Spinner/Loading progress indicators","Toggle Button (under Toggles)","imgui_toggle","Splitters","Stack Layout (pr/branch)","ImRAD","ImStudio","ImGuiBuilder","ImGuiDesigner","Fellow ImGui","HImGuiEditor","Dear-Design-Manager","Thread","imgui-spectrum","Software Renderer for Dear ImGui","ImSoft","Fast Software Rasterizer","Backend for Xlux","netImGui","UnrealNetImgui","imgui-ws","ImGui_WS","RemoteImGui","AndroidAppViewer","ImTui","tear imgui","midi2osc","devmidi","shric/midi","Desktop+","ImGuiVR","BIMXplorer","mpFluid CAVE Front End","imgInspect","ImGuiTexInspect","ImGuiDatePicker","BrotBoxEngine's ImGuiExtensions.cpp","Flix01's ImGui-Addons","ImGuizmo","imGuiZMO.quat","ImGui-2D-HArrow","ImOGuizmo","ImGui::Auto()","ImQuick","imgui-inspect","imgui_stdlib","TextFmt()","imgui_scoped","imgui_sugar","imguiwrap","Explicit context pointer PR/patch","Multi-Context Compositor","Cog","PropertyWatcher","UnrealImGuiTools","UnrealNetImgui","SrgImGui","ImSearch","ImGuiTextSelect","ImZoomSlider","Slider 2D and Slider 3D","imgui-notify","ImHotKey","IP Entry Box","Pie Menu","nnview","ImGui Command Palette","imlottie","ImCoolBar","InAppGpuProfiler","Flix01's ImGui-Addons","@leiradel's snippets","@nem0's snippets","@aoterodelaroza's snippets","MetricsGui","nakedeyes' UnrealImGuiTools" }; 17 | 18 | size_t Rand(size_t& seed); 19 | const char* GetRandomString(size_t& seed, std::string& str); 20 | 21 | void HelpMarker(const char* desc); 22 | 23 | bool ImSearchDemo_TreeNode(const char* name); 24 | void ImSearchDemo_TreeLeaf(const char* name); 25 | void ImSearchDemo_TreePop(); 26 | 27 | bool ImSearchDemo_CollapsingHeader(const char* name); 28 | } 29 | 30 | #if defined(_MSVC_LANG) // MS compiler has different __cplusplus value. 31 | # if _MSVC_LANG >= 201402L 32 | #define HAS_CPP14 33 | # endif 34 | #else // All other compilers. 35 | # if __cplusplus >= 201402L 36 | #define HAS_CPP14 37 | # endif 38 | #endif 39 | 40 | void ImSearch::ShowDemoWindow(bool* p_open) 41 | { 42 | if (!ImGui::Begin("ImSearch Demo", p_open)) 43 | { 44 | ImGui::End(); 45 | return; 46 | } 47 | 48 | size_t seed = static_cast(0xbadC0ffee); 49 | // Reuse the same string 50 | // when generating random strings, 51 | // to reduce heap allocations 52 | std::string randStr{}; 53 | 54 | if (ImGui::TreeNode("Basic")) 55 | { 56 | if (ImSearch::BeginSearch()) 57 | { 58 | ImSearch::SearchBar(); 59 | 60 | ImSearch::SearchableItem("Hey there!", 61 | [](const char* name) 62 | { 63 | ImGui::Selectable(name); 64 | return true; 65 | }); 66 | 67 | ImSearch::SearchableItem("Howdy partner!", 68 | [](const char* name) 69 | { 70 | ImGui::Button(name); 71 | ImGui::SetItemTooltip("Click me!"); 72 | return true; 73 | }); 74 | 75 | ImSearch::EndSearch(); 76 | } 77 | 78 | ImGui::TreePop(); 79 | } 80 | 81 | if (ImGui::TreeNode("Combo")) 82 | { 83 | static const char* selectedString = sImguiExtensions[0]; 84 | 85 | if (ImGui::BeginCombo("##Extensions", selectedString)) 86 | { 87 | if (ImSearch::BeginSearch()) 88 | { 89 | ImSearch::SearchBar(); 90 | 91 | for (const char* extension : sImguiExtensions) 92 | { 93 | ImSearch::SearchableItem(extension, 94 | [&](const char* name) 95 | { 96 | const bool isSelected = name == selectedString; 97 | if (ImGui::Selectable(name, isSelected)) 98 | { 99 | selectedString = name; 100 | } 101 | }); 102 | } 103 | ImSearch::EndSearch(); 104 | } 105 | ImGui::EndCombo(); 106 | } 107 | 108 | ImGui::TreePop(); 109 | } 110 | 111 | if (ImGui::TreeNode("Custom Search bar")) 112 | { 113 | if (ImSearch::BeginSearch()) 114 | { 115 | static char query[2048]{}; 116 | 117 | ImGui::SetNextItemWidth(-FLT_MIN); 118 | 119 | const float spaceWidth = ImGui::CalcTextSize(" ").x; 120 | const float searchbarWidth = ImGui::GetContentRegionAvail().x; 121 | const int totalNumCharacters = static_cast(searchbarWidth / spaceWidth); 122 | 123 | const int timeAsInt = static_cast(ImGui::GetTime() * 10.0); 124 | 125 | constexpr int length = 31; 126 | constexpr char hint[length + 1] = "I'm a custom search bar! "; 127 | std::string hintWithSpacing{}; 128 | 129 | for (int i = 0; i < totalNumCharacters; i++) 130 | { 131 | int index = (i + timeAsInt) % length; 132 | hintWithSpacing.push_back(hint[index]); 133 | } 134 | 135 | if (ImGui::InputTextWithHint("##Searchbar", hintWithSpacing.c_str(), query, sizeof(query))) 136 | { 137 | ImSearch::SetUserQuery(query); 138 | } 139 | 140 | for (int i = 0; i < 3; i++) 141 | { 142 | ImSearch::SearchableItem(GetRandomString(seed, randStr), 143 | [](const char* str) 144 | { 145 | ImGui::Selectable(str); 146 | return true; 147 | }); 148 | } 149 | 150 | ImSearch::EndSearch(); 151 | } 152 | 153 | ImGui::TreePop(); 154 | } 155 | 156 | if (ImGui::TreeNode("How do callbacks work?")) 157 | { 158 | HelpMarker("This displayed section probably won't make a lot of sense if you're not also looking at the code behind it."); 159 | 160 | if (ImSearch::BeginSearch()) 161 | { 162 | ImSearch::SearchBar(); 163 | 164 | if (ImSearchDemo_CollapsingHeader("std::functions")) 165 | { 166 | std::function myDisplayStart = 167 | [](const char* str) -> bool 168 | { 169 | return ImGui::TreeNode(str); 170 | }; 171 | 172 | std::function myDisplayEnd = 173 | []() 174 | { 175 | return ImGui::TreePop(); 176 | }; 177 | 178 | if (ImSearch::PushSearchable("std::function!", myDisplayStart)) 179 | { 180 | ImSearch::PopSearchable(myDisplayEnd); 181 | } 182 | 183 | ImSearch::PopSearchable(); 184 | } 185 | 186 | 187 | 188 | #ifdef HAS_CPP14 // C++11 didnt support lambda captures by value. 189 | if (ImSearchDemo_CollapsingHeader("Lambdas and captures")) 190 | { 191 | const std::string tooltip = GetRandomString(seed, randStr); 192 | ImSearch::SearchableItem(GetRandomString(seed, randStr), 193 | // You can capture anything in the lambda you might need. 194 | // The easiest way, works with any C++ lambda. 195 | [=](const char* str) 196 | { 197 | ImGui::TextUnformatted(str); 198 | 199 | if (ImGui::BeginItemTooltip()) 200 | { 201 | ImGui::TextUnformatted(tooltip.c_str()); 202 | ImGui::EndTooltip(); 203 | } 204 | }); 205 | 206 | ImSearch::PopSearchable(); 207 | } 208 | #endif 209 | 210 | if (ImSearchDemo_CollapsingHeader("Free functions")) 211 | { 212 | // C++ may sometimes require you to cast, to avoid ambiguity between different overloads. 213 | if (ImSearch::PushSearchable("Tree", static_cast(&ImGui::TreeNode))) 214 | { 215 | // Sometimes you can even use functions directly from ImGui! 216 | ImSearch::PopSearchable(&ImGui::TreePop); 217 | } 218 | 219 | ImSearch::PopSearchable(); 220 | } 221 | 222 | if (ImSearchDemo_CollapsingHeader("Common pitfall: dangling references")) 223 | { 224 | { 225 | int hiIWentOutOfScope{}; 226 | 227 | ImSearch::SearchableItem("Undefined behaviour, variable out of scope!", 228 | [&hiIWentOutOfScope](const char* name) // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 229 | { /*^*/ 230 | ImGui::TextUnformatted(name); /*^*/ 231 | /*^*/ 232 | // This would be invalid! /*^*/ 233 | // ImGui::InputInt("DontDoThis", &hiIWentOutOfScope); /*^*/ 234 | /*^*/ 235 | (void)(hiIWentOutOfScope); // (just to silence warnings of it being unused /*^*/ 236 | }); /*^*/ 237 | } /*^*/ 238 | /*^*/ 239 | ImSearch::PopSearchable(); /*^*/ 240 | /*^*/ 241 | // Your callbacks can be invoked at any point between your call to PushSearchable and the next /*^*/ 242 | // ImSearch::Submit or ImSearch::EndSearch is reached. Make sure your callbacks remain valid, /*^*/ 243 | // with nothing dangling. /*^*/ 244 | ImSearch::Submit(); // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>^ 245 | } 246 | 247 | ImSearch::EndSearch(); 248 | } 249 | 250 | ImGui::TreePop(); 251 | } 252 | 253 | if (ImGui::TreeNode("Many")) 254 | { 255 | if (ImSearch::BeginSearch()) 256 | { 257 | ImGui::TextWrapped("SearchBar's can be placed anywhere between BeginSearch and EndSearch; even outside the child window"); 258 | ImSearch::SearchBar(); 259 | ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); 260 | 261 | if (ImGui::BeginChild("Submissions", {}, ImGuiChildFlags_Borders)) 262 | { 263 | for (int i = 0; i < 1000; i++) 264 | { 265 | ImSearch::SearchableItem(GetRandomString(seed, randStr), 266 | [](const char* str) 267 | { 268 | ImGui::TextUnformatted(str); 269 | }); 270 | } 271 | 272 | // Call Submit explicitly; all the callbacks 273 | // will be invoked through submit. If we 274 | // had waited for EndSearch to do this for us, 275 | // the callbacks would've been invoked after 276 | // ImGui::EndChild, leaving our searchables 277 | // to be displayed outside of the child window. 278 | ImSearch::Submit(); 279 | } ImGui::EndChild(); 280 | 281 | ImGui::PopStyleColor(); 282 | ImSearch::EndSearch(); 283 | } 284 | 285 | ImGui::TreePop(); 286 | } 287 | 288 | if (ImGui::TreeNode("Tree Nodes")) 289 | { 290 | if (ImSearch::BeginSearch()) 291 | { 292 | ImSearch::SearchBar(); 293 | 294 | if (ImSearchDemo_TreeNode("Professions")) 295 | { 296 | if (ImSearchDemo_TreeNode("Farmers")) 297 | { 298 | if (ImSearchDemo_TreeNode("Tools")) 299 | { 300 | ImSearchDemo_TreeLeaf("Hoe"); 301 | ImSearchDemo_TreeLeaf("Sickle"); 302 | ImSearchDemo_TreeLeaf("Plow"); 303 | ImSearchDemo_TreeLeaf("Wheelbarrow"); 304 | ImSearchDemo_TreeLeaf("Rake"); 305 | ImSearchDemo_TreeLeaf("Pitchfork"); 306 | ImSearchDemo_TreeLeaf("Scythe"); 307 | ImSearchDemo_TreeLeaf("Hand Trowel"); 308 | ImSearchDemo_TreeLeaf("Pruning Shears"); 309 | ImSearchDemo_TreeLeaf("Seed Drill"); 310 | ImSearchDemo_TreePop(); 311 | } 312 | if (ImSearchDemo_TreeNode("Crops")) 313 | { 314 | ImSearchDemo_TreeLeaf("Wheat"); 315 | ImSearchDemo_TreeLeaf("Corn"); 316 | ImSearchDemo_TreeLeaf("Rice"); 317 | ImSearchDemo_TreeLeaf("Soybeans"); 318 | ImSearchDemo_TreeLeaf("Barley"); 319 | ImSearchDemo_TreeLeaf("Oats"); 320 | ImSearchDemo_TreeLeaf("Cotton"); 321 | ImSearchDemo_TreeLeaf("Sugarcane"); 322 | ImSearchDemo_TreeLeaf("Potatoes"); 323 | ImSearchDemo_TreeLeaf("Tomatoes"); 324 | ImSearchDemo_TreePop(); 325 | } 326 | if (ImSearchDemo_TreeNode("Livestock")) 327 | { 328 | ImSearchDemo_TreeLeaf("Cattle"); 329 | ImSearchDemo_TreeLeaf("Sheep"); 330 | ImSearchDemo_TreeLeaf("Goats"); 331 | ImSearchDemo_TreeLeaf("Pigs"); 332 | ImSearchDemo_TreeLeaf("Chickens"); 333 | ImSearchDemo_TreeLeaf("Ducks"); 334 | ImSearchDemo_TreeLeaf("Horses"); 335 | ImSearchDemo_TreeLeaf("Bees"); 336 | ImSearchDemo_TreeLeaf("Turkeys"); 337 | ImSearchDemo_TreeLeaf("Llamas"); 338 | ImSearchDemo_TreePop(); 339 | } 340 | ImSearchDemo_TreePop(); 341 | } 342 | 343 | if (ImSearchDemo_TreeNode("Blacksmiths")) 344 | { 345 | if (ImSearchDemo_TreeNode("Tools")) 346 | { 347 | ImSearchDemo_TreeLeaf("Hammer"); 348 | ImSearchDemo_TreeLeaf("Anvil"); 349 | ImSearchDemo_TreeLeaf("Tongs"); 350 | ImSearchDemo_TreeLeaf("Forge"); 351 | ImSearchDemo_TreeLeaf("Quenching Tank"); 352 | ImSearchDemo_TreeLeaf("Files"); 353 | ImSearchDemo_TreeLeaf("Chisels"); 354 | ImSearchDemo_TreeLeaf("Punches"); 355 | ImSearchDemo_TreeLeaf("Swage Block"); 356 | ImSearchDemo_TreeLeaf("Bellows"); 357 | ImSearchDemo_TreePop(); 358 | } 359 | if (ImSearchDemo_TreeNode("Materials")) 360 | { 361 | ImSearchDemo_TreeLeaf("Iron Ore"); 362 | ImSearchDemo_TreeLeaf("Coal"); 363 | ImSearchDemo_TreeLeaf("Charcoal"); 364 | ImSearchDemo_TreeLeaf("Steel Ingots"); 365 | ImSearchDemo_TreeLeaf("Copper"); 366 | ImSearchDemo_TreeLeaf("Bronze"); 367 | ImSearchDemo_TreeLeaf("Nickel"); 368 | ImSearchDemo_TreeLeaf("Cobalt"); 369 | ImSearchDemo_TreeLeaf("Manganese"); 370 | ImSearchDemo_TreeLeaf("Flux"); 371 | ImSearchDemo_TreePop(); 372 | } 373 | if (ImSearchDemo_TreeNode("Products")) 374 | { 375 | ImSearchDemo_TreeLeaf("Horseshoes"); 376 | ImSearchDemo_TreeLeaf("Nails"); 377 | ImSearchDemo_TreeLeaf("Swords"); 378 | ImSearchDemo_TreeLeaf("Axes"); 379 | ImSearchDemo_TreeLeaf("Armor Plates"); 380 | ImSearchDemo_TreeLeaf("Tools"); 381 | ImSearchDemo_TreeLeaf("Chains"); 382 | ImSearchDemo_TreeLeaf("Iron Gates"); 383 | ImSearchDemo_TreeLeaf("Rail Tracks"); 384 | ImSearchDemo_TreeLeaf("Decorative Grills"); 385 | ImSearchDemo_TreePop(); 386 | } 387 | ImSearchDemo_TreePop(); 388 | } 389 | 390 | if (ImSearchDemo_TreeNode("Fishermen")) 391 | { 392 | if (ImSearchDemo_TreeNode("Equipment")) 393 | { 394 | ImSearchDemo_TreeLeaf("Fishing Rod"); 395 | ImSearchDemo_TreeLeaf("Net"); 396 | ImSearchDemo_TreeLeaf("Tackle Box"); 397 | ImSearchDemo_TreeLeaf("Hooks"); 398 | ImSearchDemo_TreeLeaf("Lures"); 399 | ImSearchDemo_TreeLeaf("Bobbers"); 400 | ImSearchDemo_TreeLeaf("Sinkers"); 401 | ImSearchDemo_TreeLeaf("Gaff"); 402 | ImSearchDemo_TreeLeaf("Gill Net"); 403 | ImSearchDemo_TreeLeaf("Crab Pot"); 404 | ImSearchDemo_TreePop(); 405 | } 406 | if (ImSearchDemo_TreeNode("Catch")) 407 | { 408 | ImSearchDemo_TreeLeaf("Salmon"); 409 | ImSearchDemo_TreeLeaf("Tuna"); 410 | ImSearchDemo_TreeLeaf("Trout"); 411 | ImSearchDemo_TreeLeaf("Cod"); 412 | ImSearchDemo_TreeLeaf("Haddock"); 413 | ImSearchDemo_TreeLeaf("Shrimp"); 414 | ImSearchDemo_TreeLeaf("Crab"); 415 | ImSearchDemo_TreeLeaf("Lobster"); 416 | ImSearchDemo_TreeLeaf("Sardines"); 417 | ImSearchDemo_TreeLeaf("Mussels"); 418 | ImSearchDemo_TreePop(); 419 | } 420 | if (ImSearchDemo_TreeNode("Boats")) 421 | { 422 | ImSearchDemo_TreeLeaf("Rowboat"); 423 | ImSearchDemo_TreeLeaf("Sailboat"); 424 | ImSearchDemo_TreeLeaf("Trawler"); 425 | ImSearchDemo_TreeLeaf("Catamaran"); 426 | ImSearchDemo_TreeLeaf("Kayak"); 427 | ImSearchDemo_TreeLeaf("Dinghy"); 428 | ImSearchDemo_TreeLeaf("Canoe"); 429 | ImSearchDemo_TreeLeaf("Fishing Trawler"); 430 | ImSearchDemo_TreeLeaf("Longliner"); 431 | ImSearchDemo_TreeLeaf("Gillnetter"); 432 | ImSearchDemo_TreePop(); 433 | } 434 | ImSearchDemo_TreePop(); 435 | } 436 | 437 | ImSearchDemo_TreePop(); 438 | } 439 | 440 | if (ImSearchDemo_TreeNode("Technologies")) 441 | { 442 | if (ImSearchDemo_TreeNode("Computers")) 443 | { 444 | if (ImSearchDemo_TreeNode("Hardware")) 445 | { 446 | ImSearchDemo_TreeLeaf("CPU"); 447 | ImSearchDemo_TreeLeaf("GPU"); 448 | ImSearchDemo_TreeLeaf("RAM"); 449 | ImSearchDemo_TreeLeaf("Motherboard"); 450 | ImSearchDemo_TreeLeaf("SSD"); 451 | ImSearchDemo_TreeLeaf("HDD"); 452 | ImSearchDemo_TreeLeaf("Power Supply"); 453 | ImSearchDemo_TreeLeaf("Cooler"); 454 | ImSearchDemo_TreeLeaf("Case"); 455 | ImSearchDemo_TreeLeaf("Network Card"); 456 | ImSearchDemo_TreePop(); 457 | } 458 | if (ImSearchDemo_TreeNode("Software")) 459 | { 460 | ImSearchDemo_TreeLeaf("Operating System"); 461 | ImSearchDemo_TreeLeaf("Web Browser"); 462 | ImSearchDemo_TreeLeaf("Office Suite"); 463 | ImSearchDemo_TreeLeaf("IDE"); 464 | ImSearchDemo_TreeLeaf("Antivirus"); 465 | ImSearchDemo_TreeLeaf("Drivers"); 466 | ImSearchDemo_TreeLeaf("Database"); 467 | ImSearchDemo_TreeLeaf("Virtual Machine"); 468 | ImSearchDemo_TreeLeaf("Compiler"); 469 | ImSearchDemo_TreeLeaf("Text Editor"); 470 | ImSearchDemo_TreePop(); 471 | } 472 | if (ImSearchDemo_TreeNode("Networking")) 473 | { 474 | ImSearchDemo_TreeLeaf("Router"); 475 | ImSearchDemo_TreeLeaf("Switch"); 476 | ImSearchDemo_TreeLeaf("Firewall"); 477 | ImSearchDemo_TreeLeaf("Modem"); 478 | ImSearchDemo_TreeLeaf("Access Point"); 479 | ImSearchDemo_TreeLeaf("Ethernet Cable"); 480 | ImSearchDemo_TreeLeaf("Fiber Optic Cable"); 481 | ImSearchDemo_TreeLeaf("VPN"); 482 | ImSearchDemo_TreeLeaf("DNS"); 483 | ImSearchDemo_TreeLeaf("DHCP"); 484 | ImSearchDemo_TreePop(); 485 | } 486 | ImSearchDemo_TreePop(); 487 | } 488 | 489 | if (ImSearchDemo_TreeNode("Vehicles")) 490 | { 491 | if (ImSearchDemo_TreeNode("Land")) 492 | { 493 | ImSearchDemo_TreeLeaf("Car"); 494 | ImSearchDemo_TreeLeaf("Truck"); 495 | ImSearchDemo_TreeLeaf("Motorcycle"); 496 | ImSearchDemo_TreeLeaf("Bicycle"); 497 | ImSearchDemo_TreeLeaf("Bus"); 498 | ImSearchDemo_TreeLeaf("Train"); 499 | ImSearchDemo_TreeLeaf("Tram"); 500 | ImSearchDemo_TreeLeaf("Tank"); 501 | ImSearchDemo_TreeLeaf("ATV"); 502 | ImSearchDemo_TreeLeaf("Segway"); 503 | ImSearchDemo_TreePop(); 504 | } 505 | if (ImSearchDemo_TreeNode("Air")) 506 | { 507 | ImSearchDemo_TreeLeaf("Airplane"); 508 | ImSearchDemo_TreeLeaf("Helicopter"); 509 | ImSearchDemo_TreeLeaf("Drone"); 510 | ImSearchDemo_TreeLeaf("Glider"); 511 | ImSearchDemo_TreeLeaf("Hot Air Balloon"); 512 | ImSearchDemo_TreeLeaf("Jet"); 513 | ImSearchDemo_TreeLeaf("Blimp"); 514 | ImSearchDemo_TreeLeaf("Autogyro"); 515 | ImSearchDemo_TreeLeaf("Seaplane"); 516 | ImSearchDemo_TreeLeaf("Hang Glider"); 517 | ImSearchDemo_TreePop(); 518 | } 519 | if (ImSearchDemo_TreeNode("Sea")) 520 | { 521 | ImSearchDemo_TreeLeaf("Ship"); 522 | ImSearchDemo_TreeLeaf("Boat"); 523 | ImSearchDemo_TreeLeaf("Submarine"); 524 | ImSearchDemo_TreeLeaf("Yacht"); 525 | ImSearchDemo_TreeLeaf("Canoe"); 526 | ImSearchDemo_TreeLeaf("Ferry"); 527 | ImSearchDemo_TreeLeaf("Sailboat"); 528 | ImSearchDemo_TreeLeaf("Tugboat"); 529 | ImSearchDemo_TreeLeaf("Catamaran"); 530 | ImSearchDemo_TreeLeaf("Dinghy"); 531 | ImSearchDemo_TreePop(); 532 | } 533 | ImSearchDemo_TreePop(); 534 | } 535 | 536 | ImSearchDemo_TreePop(); 537 | } 538 | 539 | if (ImSearchDemo_TreeNode("Nature")) 540 | { 541 | if (ImSearchDemo_TreeNode("Animals")) 542 | { 543 | if (ImSearchDemo_TreeNode("Mammals")) 544 | { 545 | ImSearchDemo_TreeLeaf("Lion"); 546 | ImSearchDemo_TreeLeaf("Tiger"); 547 | ImSearchDemo_TreeLeaf("Elephant"); 548 | ImSearchDemo_TreeLeaf("Whale"); 549 | ImSearchDemo_TreeLeaf("Dolphin"); 550 | ImSearchDemo_TreeLeaf("Bat"); 551 | ImSearchDemo_TreeLeaf("Kangaroo"); 552 | ImSearchDemo_TreeLeaf("Human"); 553 | ImSearchDemo_TreeLeaf("Bear"); 554 | ImSearchDemo_TreeLeaf("Wolf"); 555 | ImSearchDemo_TreePop(); 556 | } 557 | if (ImSearchDemo_TreeNode("Birds")) 558 | { 559 | ImSearchDemo_TreeLeaf("Eagle"); 560 | ImSearchDemo_TreeLeaf("Sparrow"); 561 | ImSearchDemo_TreeLeaf("Penguin"); 562 | ImSearchDemo_TreeLeaf("Owl"); 563 | ImSearchDemo_TreeLeaf("Parrot"); 564 | ImSearchDemo_TreeLeaf("Flamingo"); 565 | ImSearchDemo_TreeLeaf("Duck"); 566 | ImSearchDemo_TreeLeaf("Goose"); 567 | ImSearchDemo_TreeLeaf("Hawk"); 568 | ImSearchDemo_TreeLeaf("Crow"); 569 | ImSearchDemo_TreePop(); 570 | } 571 | if (ImSearchDemo_TreeNode("Reptiles")) 572 | { 573 | ImSearchDemo_TreeLeaf("Crocodile"); 574 | ImSearchDemo_TreeLeaf("Snake"); 575 | ImSearchDemo_TreeLeaf("Lizard"); 576 | ImSearchDemo_TreeLeaf("Turtle"); 577 | ImSearchDemo_TreeLeaf("Chameleon"); 578 | ImSearchDemo_TreeLeaf("Gecko"); 579 | ImSearchDemo_TreeLeaf("Alligator"); 580 | ImSearchDemo_TreeLeaf("Komodo Dragon"); 581 | ImSearchDemo_TreeLeaf("Iguana"); 582 | ImSearchDemo_TreeLeaf("Rattlesnake"); 583 | ImSearchDemo_TreePop(); 584 | } 585 | ImSearchDemo_TreePop(); 586 | } 587 | if (ImSearchDemo_TreeNode("Plants")) 588 | { 589 | if (ImSearchDemo_TreeNode("Trees")) 590 | { 591 | ImSearchDemo_TreeLeaf("Oak"); 592 | ImSearchDemo_TreeLeaf("Pine"); 593 | ImSearchDemo_TreeLeaf("Maple"); 594 | ImSearchDemo_TreeLeaf("Birch"); 595 | ImSearchDemo_TreeLeaf("Cedar"); 596 | ImSearchDemo_TreeLeaf("Redwood"); 597 | ImSearchDemo_TreeLeaf("Palm"); 598 | ImSearchDemo_TreeLeaf("Willow"); 599 | ImSearchDemo_TreeLeaf("Spruce"); 600 | ImSearchDemo_TreeLeaf("Cypress"); 601 | ImSearchDemo_TreePop(); 602 | } 603 | if (ImSearchDemo_TreeNode("Flowers")) 604 | { 605 | ImSearchDemo_TreeLeaf("Rose"); 606 | ImSearchDemo_TreeLeaf("Tulip"); 607 | ImSearchDemo_TreeLeaf("Sunflower"); 608 | ImSearchDemo_TreeLeaf("Daisy"); 609 | ImSearchDemo_TreeLeaf("Orchid"); 610 | ImSearchDemo_TreeLeaf("Lily"); 611 | ImSearchDemo_TreeLeaf("Marigold"); 612 | ImSearchDemo_TreeLeaf("Daffodil"); 613 | ImSearchDemo_TreeLeaf("Chrysanthemum"); 614 | ImSearchDemo_TreeLeaf("Iris"); 615 | ImSearchDemo_TreePop(); 616 | } 617 | if (ImSearchDemo_TreeNode("Fungi")) 618 | { 619 | ImSearchDemo_TreeLeaf("Button Mushroom"); 620 | ImSearchDemo_TreeLeaf("Shiitake"); 621 | ImSearchDemo_TreeLeaf("Oyster Mushroom"); 622 | ImSearchDemo_TreeLeaf("Morel"); 623 | ImSearchDemo_TreeLeaf("Chanterelle"); 624 | ImSearchDemo_TreeLeaf("Truffle"); 625 | ImSearchDemo_TreeLeaf("Fly Agaric"); 626 | ImSearchDemo_TreeLeaf("Porcini"); 627 | ImSearchDemo_TreeLeaf("Puffball"); 628 | ImSearchDemo_TreeLeaf("Enoki"); 629 | ImSearchDemo_TreePop(); 630 | } 631 | ImSearchDemo_TreePop(); 632 | } 633 | 634 | ImSearchDemo_TreePop(); 635 | } 636 | if (ImSearchDemo_TreeNode("Culinary")) 637 | { 638 | if (ImSearchDemo_TreeNode("Ingredients")) 639 | { 640 | if (ImSearchDemo_TreeNode("Spices")) 641 | { 642 | ImSearchDemo_TreeLeaf("Salt"); 643 | ImSearchDemo_TreeLeaf("Pepper"); 644 | ImSearchDemo_TreeLeaf("Paprika"); 645 | ImSearchDemo_TreeLeaf("Cumin"); 646 | ImSearchDemo_TreeLeaf("Turmeric"); 647 | ImSearchDemo_TreeLeaf("Oregano"); 648 | ImSearchDemo_TreeLeaf("Basil"); 649 | ImSearchDemo_TreeLeaf("Thyme"); 650 | ImSearchDemo_TreeLeaf("Cinnamon"); 651 | ImSearchDemo_TreeLeaf("Nutmeg"); 652 | ImSearchDemo_TreePop(); 653 | } 654 | if (ImSearchDemo_TreeNode("Produce")) 655 | { 656 | ImSearchDemo_TreeLeaf("Carrot"); 657 | ImSearchDemo_TreeLeaf("Onion"); 658 | ImSearchDemo_TreeLeaf("Garlic"); 659 | ImSearchDemo_TreeLeaf("Pepper"); 660 | ImSearchDemo_TreeLeaf("Tomato"); 661 | ImSearchDemo_TreeLeaf("Lettuce"); 662 | ImSearchDemo_TreeLeaf("Spinach"); 663 | ImSearchDemo_TreeLeaf("Broccoli"); 664 | ImSearchDemo_TreeLeaf("Eggplant"); 665 | ImSearchDemo_TreeLeaf("Zucchini"); 666 | ImSearchDemo_TreePop(); 667 | } 668 | if (ImSearchDemo_TreeNode("Proteins")) 669 | { 670 | ImSearchDemo_TreeLeaf("Chicken"); 671 | ImSearchDemo_TreeLeaf("Beef"); 672 | ImSearchDemo_TreeLeaf("Pork"); 673 | ImSearchDemo_TreeLeaf("Tofu"); 674 | ImSearchDemo_TreeLeaf("Lentils"); 675 | ImSearchDemo_TreeLeaf("Fish"); 676 | ImSearchDemo_TreeLeaf("Eggs"); 677 | ImSearchDemo_TreeLeaf("Beans"); 678 | ImSearchDemo_TreeLeaf("Lamb"); 679 | ImSearchDemo_TreeLeaf("Turkey"); 680 | ImSearchDemo_TreePop(); 681 | } 682 | ImSearchDemo_TreePop(); 683 | } 684 | if (ImSearchDemo_TreeNode("Recipes")) 685 | { 686 | if (ImSearchDemo_TreeNode("Soups")) 687 | { 688 | ImSearchDemo_TreeLeaf("Chicken Noodle Soup"); 689 | ImSearchDemo_TreeLeaf("Tomato Soup"); 690 | ImSearchDemo_TreeLeaf("Miso Soup"); 691 | ImSearchDemo_TreeLeaf("Minestrone"); 692 | ImSearchDemo_TreeLeaf("Clam Chowder"); 693 | ImSearchDemo_TreeLeaf("Pho"); 694 | ImSearchDemo_TreeLeaf("Ramen"); 695 | ImSearchDemo_TreeLeaf("Gazpacho"); 696 | ImSearchDemo_TreeLeaf("Pumpkin Soup"); 697 | ImSearchDemo_TreeLeaf("Lentil Soup"); 698 | ImSearchDemo_TreePop(); 699 | } 700 | if (ImSearchDemo_TreeNode("Desserts")) 701 | { 702 | ImSearchDemo_TreeLeaf("Chocolate Cake"); 703 | ImSearchDemo_TreeLeaf("Apple Pie"); 704 | ImSearchDemo_TreeLeaf("Ice Cream"); 705 | ImSearchDemo_TreeLeaf("Brownies"); 706 | ImSearchDemo_TreeLeaf("Cheesecake"); 707 | ImSearchDemo_TreeLeaf("Pudding"); 708 | ImSearchDemo_TreeLeaf("Tiramisu"); 709 | ImSearchDemo_TreeLeaf("Crepes"); 710 | ImSearchDemo_TreeLeaf("Cupcakes"); 711 | ImSearchDemo_TreeLeaf("Macarons"); 712 | ImSearchDemo_TreePop(); 713 | } 714 | ImSearchDemo_TreePop(); 715 | } 716 | 717 | ImSearchDemo_TreePop(); 718 | } 719 | 720 | ImSearch::EndSearch(); 721 | } 722 | 723 | ImGui::TreePop(); 724 | } 725 | 726 | if (ImGui::TreeNode("Collapsing headers")) 727 | { 728 | if (ImSearch::BeginSearch()) 729 | { 730 | ImSearch::SearchBar(); 731 | 732 | if (ImSearchDemo_CollapsingHeader("TransformComponent")) 733 | { 734 | ImSearch::SearchableItem("Position", 735 | [](const char* name) 736 | { 737 | static float v[3]{}; 738 | ImGui::InputFloat3(name, v); 739 | return true; 740 | }); 741 | 742 | ImSearch::SearchableItem("Scale", 743 | [](const char* name) 744 | { 745 | static float v[3]{}; 746 | ImGui::InputFloat3(name, v); 747 | }); 748 | 749 | ImSearch::SearchableItem("Orientation", 750 | [](const char* name) 751 | { 752 | static float v[3]{}; 753 | ImGui::InputFloat3(name, v); 754 | }); 755 | 756 | ImSearch::PopSearchable(); 757 | } 758 | 759 | if (ImSearchDemo_CollapsingHeader("StaticMeshComponent")) 760 | { 761 | ImSearch::SearchableItem("Mesh", 762 | [](const char* fieldName) 763 | { 764 | static const char* selectedString = nouns[0]; 765 | if (ImGui::BeginCombo(fieldName, selectedString)) 766 | { 767 | if (ImSearch::BeginSearch()) 768 | { 769 | ImSearch::SearchBar(); 770 | for (const char* noun : nouns) 771 | { 772 | ImSearch::SearchableItem(noun, 773 | [&](const char* meshName) 774 | { 775 | const bool isSelected = meshName == selectedString; 776 | if (ImGui::Selectable(meshName, isSelected)) 777 | { 778 | selectedString = meshName; 779 | } 780 | return true; 781 | }); 782 | } 783 | 784 | ImSearch::EndSearch(); 785 | } 786 | ImGui::EndCombo(); 787 | } 788 | }); 789 | 790 | ImSearch::PopSearchable(); 791 | } 792 | 793 | if (ImSearchDemo_CollapsingHeader("PhysicsBodyComponent")) 794 | { 795 | ImSearch::SearchableItem("Mass", 796 | [](const char* name) 797 | { 798 | static float v{}; 799 | ImGui::InputFloat(name, &v); 800 | }); 801 | 802 | ImSearch::SearchableItem("Collision Enabled", 803 | [](const char* name) 804 | { 805 | static bool b{}; 806 | ImGui::Checkbox(name, &b); 807 | }); 808 | 809 | ImSearch::PopSearchable(); 810 | } 811 | 812 | ImSearch::EndSearch(); 813 | } 814 | 815 | ImGui::TreePop(); 816 | } 817 | 818 | if (ImGui::TreeNode("Artificially increasing priority for popular items")) 819 | { 820 | if (ImSearch::BeginSearch()) 821 | { 822 | HelpMarker( 823 | "A bonus can be applied which, ONLY when the user is searching,\n" 824 | "influences which items are shown to the user first.\n\n" 825 | "Gus Goose is textually irrelevant, but because of his high\n" 826 | "bonus, he is the first result."); 827 | ImSearch::SearchBar(); 828 | static bool doOnceDummy = []{ ImSearch::SetUserQuery("Duck"); return true; }(); 829 | (void)doOnceDummy; 830 | 831 | static std::array, 15> namesAndBonuses 832 | { 833 | std::pair{ "Scrooge McDuck", 0.95f }, 834 | std::pair{ "Della Duck", 0.88f }, 835 | std::pair{ "Huey Dewey & Louie Duck", 0.93f }, 836 | std::pair{ "Donald Duck", 1.0f }, 837 | std::pair{ "Grandma Duck", 0.82f }, 838 | std::pair{ "Gus Goose", 5.0f }, 839 | std::pair{ "Sir Quackly McDuck", 0.68f }, 840 | std::pair{ "Fethry Duck", 0.75f }, 841 | std::pair{ "Dugan Duck", 0.65f }, 842 | std::pair{ "Sir Roast McDuck", 0.42f }, 843 | std::pair{ "Dudly D. Duck", 0.45f }, 844 | std::pair{ "Matilda McDuck", 0.58f }, 845 | std::pair{ "Donna Duck", 0.55f }, 846 | std::pair{ "Pothole McDuck", 0.52f }, 847 | std::pair{ "Daphne Duck-Gander", 0.60f }, 848 | }; 849 | 850 | for (auto& nameAndBonus : namesAndBonuses) 851 | { 852 | if (ImSearch::PushSearchable(nameAndBonus.first, 853 | [&](const char* name) 854 | { 855 | ImGui::SliderFloat(name, &nameAndBonus.second, -1.0f, 1.0f); 856 | return false; 857 | })) 858 | { 859 | ImSearch::SetRelevancyBonus(nameAndBonus.second); 860 | ImSearch::PopSearchable(); 861 | } 862 | } 863 | 864 | ImSearch::EndSearch(); 865 | } 866 | ImGui::TreePop(); 867 | } 868 | 869 | if (ImGui::TreeNode("Synonyms")) 870 | { 871 | if (ImSearch::BeginSearch()) 872 | { 873 | HelpMarker("ImSearch supports synonyms, useful, if no one can agree on a single name. " 874 | 875 | "For example, \"Class\" has the synonyms \"Type\" and \"Struct\"." 876 | "Try searching for \"Struct\"; you'll see \"Class\" rise up to the top!"); 877 | ImSearch::SearchBar(); 878 | 879 | auto selectableCallback = [](const char* name) { ImGui::Selectable(name); return false; }; 880 | 881 | if (ImSearch::PushSearchable("Branch", selectableCallback)) 882 | { 883 | ImSearch::AddSynonym("If"); 884 | ImSearch::AddSynonym("Condition"); 885 | 886 | ImSearch::PopSearchable(); 887 | } 888 | 889 | if (ImSearch::PushSearchable("Function", selectableCallback)) 890 | { 891 | ImSearch::AddSynonym("Method"); 892 | ImSearch::AddSynonym("Procedure"); 893 | 894 | ImSearch::PopSearchable(); 895 | } 896 | 897 | if (ImSearch::PushSearchable("Variable", selectableCallback)) 898 | { 899 | ImSearch::AddSynonym("Identifier"); 900 | ImSearch::AddSynonym("Container"); 901 | 902 | ImSearch::PopSearchable(); 903 | } 904 | 905 | if (ImSearch::PushSearchable("Array", selectableCallback)) 906 | { 907 | ImSearch::AddSynonym("List"); 908 | ImSearch::AddSynonym("Collection"); 909 | 910 | ImSearch::PopSearchable(); 911 | } 912 | 913 | if (ImSearch::PushSearchable("Class", selectableCallback)) 914 | { 915 | ImSearch::AddSynonym("Type"); 916 | ImSearch::AddSynonym("Struct"); 917 | 918 | ImSearch::PopSearchable(); 919 | } 920 | 921 | ImSearch::EndSearch(); 922 | } 923 | 924 | ImGui::TreePop(); 925 | } 926 | 927 | ImGui::End(); 928 | } 929 | 930 | namespace 931 | { 932 | size_t Rand(size_t& seed) 933 | { 934 | seed ^= seed << 13; 935 | seed ^= seed >> 17; 936 | seed ^= seed << 5; 937 | return seed; 938 | } 939 | 940 | const char* GetRandomString(size_t& seed, std::string& str) 941 | { 942 | const char* adjective = adjectives[Rand(seed) % adjectives.size()]; 943 | const char* noun = nouns[Rand(seed) % nouns.size()]; 944 | 945 | str.clear(); 946 | str.append(adjective); 947 | str.push_back(' '); 948 | str.append(noun); 949 | 950 | return str.c_str(); 951 | } 952 | 953 | void HelpMarker(const char* desc) 954 | { 955 | ImGui::TextDisabled("(?)"); 956 | if (ImGui::BeginItemTooltip()) 957 | { 958 | ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); 959 | ImGui::TextUnformatted(desc); 960 | ImGui::PopTextWrapPos(); 961 | ImGui::EndTooltip(); 962 | } 963 | } 964 | 965 | bool ImSearchDemo_TreeNode(const char* name) 966 | { 967 | return ImSearch::PushSearchable(name, 968 | [](const char* nodeName) 969 | { 970 | return ImGui::TreeNode(nodeName); 971 | }); 972 | } 973 | 974 | void ImSearchDemo_TreeLeaf(const char* name) 975 | { 976 | ImSearch::SearchableItem(name, 977 | [](const char* leafName) 978 | { 979 | ImGui::Selectable(leafName); 980 | }); 981 | } 982 | 983 | void ImSearchDemo_TreePop() 984 | { 985 | ImSearch::PopSearchable( 986 | []() 987 | { 988 | ImGui::TreePop(); 989 | }); 990 | } 991 | 992 | bool ImSearchDemo_CollapsingHeader(const char* name) 993 | { 994 | return ImSearch::PushSearchable(name, 995 | [](const char* headerName) 996 | { 997 | return ImGui::CollapsingHeader(headerName); 998 | }); 999 | } 1000 | } 1001 | #endif // #ifndef IMGUI_DISABLE 1002 | -------------------------------------------------------------------------------- /imsearch_internal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "imgui.h" 3 | #include "imgui_internal.h" 4 | 5 | #ifndef IMGUI_DISABLE 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace ImSearch 23 | { 24 | using IndexT = std::uint32_t; 25 | 26 | //----------------------------------------------------------------------------- 27 | // [SECTION] Constants 28 | //----------------------------------------------------------------------------- 29 | 30 | // Anything below this score is not displayed to the user. 31 | // Will be adjustable at runtime in the future 32 | constexpr float sCutOffStrength = .5f; 33 | 34 | constexpr IndexT sNullIndex = std::numeric_limits::max(); 35 | 36 | //----------------------------------------------------------------------------- 37 | // [SECTION] Structs 38 | //----------------------------------------------------------------------------- 39 | 40 | struct StrView 41 | { 42 | StrView() = default; 43 | StrView(const std::string& str) : 44 | mData(str.data()), 45 | mSize(static_cast(str.size())) {} 46 | StrView(const char* data, IndexT size) : 47 | mData(data), 48 | mSize(size) {} 49 | 50 | const char* begin() const { return mData; } 51 | const char* end() const { return mData + mSize; } 52 | const char* data() const { return mData; } 53 | IndexT size() const { return mSize; } 54 | 55 | const char& operator[](IndexT i) const { IM_ASSERT(i < mSize); return mData[i]; } 56 | 57 | const char* mData{}; 58 | IndexT mSize{}; 59 | }; 60 | 61 | // Here is why we are using VTables instead of just std::function: 62 | // std::function's SBO optimisation means it might consume 63 | // more memory than needed if the user has not captured anything, 64 | // and if the user is capturing more than fits in the SBO storage, 65 | // performance tanks due to the many heap allocations. Now, 66 | // the Callback is slower than using std::function, it makes 67 | // the implementation and API slightly more complicated, BUT, if we 68 | // were to release with std::function in the public API, I would not 69 | // be able to get rid of std::function without breaking forward 70 | // compatibility in the future. By keeping the concept of how functors 71 | // are stored abstracted away in the backend, it can be optimised in 72 | // the future without breaking API. 73 | struct Callback 74 | { 75 | Callback() = default; 76 | 77 | Callback(void* originalFunctor, ImSearch::Internal::VTable vTable); 78 | 79 | Callback(const Callback&) = delete; 80 | Callback(Callback&& other) noexcept; 81 | 82 | Callback& operator=(const Callback&) = delete; 83 | Callback& operator=(Callback&& other) noexcept; 84 | 85 | ~Callback(); 86 | 87 | operator bool() const { return mUserFunctor != nullptr; } 88 | 89 | // PushSearchable 90 | bool operator()(const char* name) const; 91 | 92 | // PopSearchable 93 | void operator()() const; 94 | 95 | static bool InvokeAsPushSearchable(ImSearch::Internal::VTable vTable, void* userFunctor, const char* name); 96 | static void InvokeAsPopSearchable(ImSearch::Internal::VTable vTable, void* userFunctor); 97 | 98 | void ClearData(); 99 | 100 | enum VTableModes 101 | { 102 | Invoke = 0, 103 | MoveConstruct = 1, 104 | Destruct = 2, 105 | GetSize = 3 106 | }; 107 | 108 | Internal::VTable mVTable{}; 109 | void* mUserFunctor{}; 110 | }; 111 | 112 | struct Searchable 113 | { 114 | std::string mText{}; 115 | 116 | IndexT mIndexOfFirstChild = sNullIndex; 117 | IndexT mIndexOfLastChild = sNullIndex; 118 | IndexT mIndexOfParent = sNullIndex; 119 | IndexT mIndexOfNextSibling = sNullIndex; 120 | }; 121 | 122 | struct Input 123 | { 124 | ImSearchFlags mFlags{}; 125 | std::vector mEntries{}; 126 | std::vector mBonuses{}; 127 | std::string mUserQuery{}; 128 | }; 129 | 130 | struct ReusableBuffers 131 | { 132 | std::vector mScores{}; 133 | std::vector mTempIndices{}; 134 | }; 135 | 136 | struct Output 137 | { 138 | std::vector mDisplayOrder{}; 139 | static constexpr IndexT sDisplayEndFlag = static_cast(1) << static_cast(std::numeric_limits::digits - 1); 140 | 141 | std::string mPreviewText{}; 142 | }; 143 | 144 | struct Result 145 | { 146 | Input mInput{}; 147 | ReusableBuffers mBuffers{}; 148 | Output mOutput{}; 149 | }; 150 | 151 | struct DisplayCallbacks 152 | { 153 | Callback mOnDisplayStart{}; 154 | Callback mOnDisplayEnd{}; 155 | }; 156 | 157 | struct LocalContext 158 | { 159 | Input mInput{}; 160 | 161 | std::vector mDisplayCallbacks{}; 162 | std::stack mPushStack{}; 163 | 164 | // Used for debugging. We don't need 165 | // to keep track of the actual stack 166 | // when the user is not actively searching, 167 | // so we only track the size to detect 168 | // push/pop underflows and overflows. 169 | int mPushStackLevel{}; 170 | 171 | Result mResult{}; 172 | bool mHasSubmitted{}; 173 | }; 174 | 175 | struct ImSearchContext 176 | { 177 | std::unordered_map Contexts{}; 178 | std::stack> ContextStack{}; 179 | std::unordered_map mTokenisedStrings{}; 180 | 181 | // Style and Colormaps 182 | ImSearchStyle Style; 183 | ImVector ColorModifiers; 184 | }; 185 | 186 | bool operator==(const StrView& lhs, const StrView& rhs); 187 | bool operator==(const Searchable& lhs, const Searchable& rhs); 188 | bool operator==(const Input& lhs, const Input& rhs); 189 | 190 | //----------------------------------------------------------------------------- 191 | // [SECTION] Context 192 | //----------------------------------------------------------------------------- 193 | 194 | // Some helper functions for generalising error-reporting. 195 | LocalContext& GetLocalContext(); 196 | ImSearch::ImSearchContext& GetImSearchContext(); 197 | IndexT GetCurrentItem(LocalContext& context); 198 | 199 | // Is the context currently collecting submissions? 200 | // ImSearch does not store anything the programm is submitting if the user 201 | // is not actively searching, for performance and memory reasons. 202 | bool CanCollectSubmissions(); 203 | 204 | //----------------------------------------------------------------------------- 205 | // [SECTION] Testing 206 | //----------------------------------------------------------------------------- 207 | 208 | float GetScore(size_t index); 209 | 210 | size_t GetDisplayOrderEntry(size_t index); 211 | 212 | //----------------------------------------------------------------------------- 213 | // [SECTION] Future API candidates 214 | //----------------------------------------------------------------------------- 215 | 216 | int GetNumItemsFilteredOut(); 217 | 218 | void SetPreviewText(const char* preview); 219 | 220 | const char* GetPreviewText(); 221 | 222 | void BeginHighlightZone(const char* textToHighlight); 223 | 224 | void EndHighlightZone(); 225 | 226 | //----------------------------------------------------------------------------- 227 | // [SECTION] Fuzzy Searching & String Functions 228 | //----------------------------------------------------------------------------- 229 | 230 | std::vector SplitTokens(StrView s); 231 | 232 | std::string Join(const std::vector& tokens); 233 | 234 | StrView GetStringNeededToCompletePartial(StrView partial, StrView complete); 235 | 236 | std::string MakeTokenisedString(StrView original); 237 | 238 | StrView GetMemoizedTokenisedString(const std::string& original); 239 | 240 | IndexT LevenshteinDistance( 241 | StrView s1, 242 | StrView s2, 243 | ReusableBuffers& buffers); 244 | 245 | float Ratio(StrView s1, 246 | StrView s2, 247 | ReusableBuffers& buffers); 248 | 249 | float PartialRatio(StrView s1, 250 | StrView s2, 251 | ReusableBuffers& buffers); 252 | 253 | // The function used internally to score strings 254 | float WeightedRatio(StrView s1, 255 | StrView s1Tokenised, 256 | StrView s2, 257 | StrView s2Tokenised, 258 | ReusableBuffers& buffers); 259 | } 260 | 261 | #endif // #ifndef IMGUI_DISABLE 262 | --------------------------------------------------------------------------------