├── .github ├── example_imsearch.cpp ├── CMakeLists.txt └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── imsearch_internal.h ├── imsearch.h ├── imsearch_demo.cpp └── imsearch.cpp /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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.5) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | schedule: 5 | # runs at 00:00 on the first day of every month. 6 | - cron: '0 0 1 * *' 7 | pull_request: 8 | 9 | jobs: 10 | Linux: 11 | runs-on: ubuntu-22.04 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | build_type: 17 | - debug 18 | - release 19 | compiler: 20 | - gcc 21 | - clang 22 | arch: 23 | - x86 24 | - x64 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - uses: actions/checkout@v3 30 | with: 31 | repository: ocornut/imgui 32 | path: imgui 33 | 34 | - name: Update package list 35 | run: sudo apt-get update 36 | 37 | - name: Dependencies 38 | run: sudo apt-get install g++-multilib 39 | 40 | - name: Configure 41 | 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 42 | 43 | - name: Build 44 | run: cmake --build cmake-build --parallel $(nproc) 45 | 46 | - name: Run 47 | run: | 48 | file cmake-build/example_imsearch 49 | cmake-build/example_imsearch 50 | 51 | MacOS: 52 | runs-on: macos-latest 53 | 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | build_type: 58 | - debug 59 | - release 60 | arch: 61 | - x86_64 62 | - arm64 63 | 64 | steps: 65 | - uses: actions/checkout@v3 66 | 67 | - uses: actions/checkout@v3 68 | with: 69 | repository: ocornut/imgui 70 | path: imgui 71 | 72 | - name: Configure 73 | shell: bash 74 | run: cmake -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DOSX_ARCH=${{ matrix.arch }} -B cmake-build -S .github 75 | 76 | - name: Build 77 | shell: bash 78 | run: cmake --build cmake-build --parallel $(sysctl -n hw.ncpu) 79 | 80 | - name: Run 81 | if: matrix.arch == 'x86_64' # github's CI hosts seem to be running intel and can not run ARM 82 | run: | 83 | file cmake-build/example_imsearch 84 | cmake-build/example_imsearch 85 | 86 | Windows_MSVC: 87 | runs-on: windows-2022 88 | 89 | strategy: 90 | fail-fast: false 91 | matrix: 92 | build_type: 93 | - debug 94 | - release 95 | arch: 96 | - Win32 97 | - x64 98 | 99 | steps: 100 | - uses: actions/checkout@v3 101 | 102 | - uses: actions/checkout@v3 103 | with: 104 | repository: ocornut/imgui 105 | path: imgui 106 | 107 | - name: Configure 108 | shell: bash 109 | run: cmake -G 'Visual Studio 17 2022' -A ${{ matrix.arch }} -B cmake-build -S .github 110 | 111 | - name: Build 112 | shell: bash 113 | run: cmake --build cmake-build -- -p:Configuration=${{ matrix.build_type }} -maxcpucount:$NUMBER_OF_PROCESSORS 114 | 115 | - name: Run 116 | run: .\cmake-build\${{matrix.build_type}}\example_imsearch.exe 117 | 118 | Windows_MingW: # MingW on Github CI does not fully support 32 bits: link fails when it tries to link 64 bits system libraries. 119 | runs-on: windows-2022 120 | 121 | strategy: 122 | fail-fast: false 123 | matrix: 124 | build_type: 125 | - debug 126 | - release 127 | arch: 128 | - x64 129 | # - Win32 130 | 131 | steps: 132 | - uses: actions/checkout@v3 133 | 134 | - uses: actions/checkout@v3 135 | with: 136 | repository: ocornut/imgui 137 | path: imgui 138 | 139 | - name: Configure 140 | shell: bash 141 | run: cmake -G 'MinGW Makefiles' -DGCC_ARCH=${{ matrix.arch }} -B cmake-build -S .github 142 | 143 | - name: Build 144 | shell: bash 145 | run: cmake --build cmake-build --parallel $NUMBER_OF_PROCESSORS 146 | 147 | - name: Run (MingW) 148 | run: .\cmake-build\example_imsearch.exe 149 | 150 | -------------------------------------------------------------------------------- /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 | constexpr float sDefaultCutoffStrength = .5f; 32 | 33 | constexpr IndexT sNullIndex = std::numeric_limits::max(); 34 | 35 | //----------------------------------------------------------------------------- 36 | // [SECTION] Structs 37 | //----------------------------------------------------------------------------- 38 | 39 | struct StrView 40 | { 41 | StrView() = default; 42 | StrView(const std::string& str) : 43 | mData(str.data()), 44 | mSize(static_cast(str.size())) {} 45 | StrView(const char* data, IndexT size) : 46 | mData(data), 47 | mSize(size) {} 48 | 49 | const char* begin() const { return mData; } 50 | const char* end() const { return mData + mSize; } 51 | const char* data() const { return mData; } 52 | IndexT size() const { return mSize; } 53 | 54 | const char& operator[](IndexT i) const { IM_ASSERT(i < mSize); return mData[i]; } 55 | 56 | const char* mData{}; 57 | IndexT mSize{}; 58 | }; 59 | 60 | // Here is why we are using VTables instead of just std::function: 61 | // std::function's SBO optimisation means it might consume 62 | // more memory than needed if the user has not captured anything, 63 | // and if the user is capturing more than fits in the SBO storage, 64 | // performance tanks due to the many heap allocations. Now, 65 | // the Callback is slower than using std::function, it makes 66 | // the implementation and API slightly more complicated, BUT, if we 67 | // were to release with std::function in the public API, I would not 68 | // be able to get rid of std::function without breaking forward 69 | // compatibility in the future. By keeping the concept of how functors 70 | // are stored abstracted away in the backend, it can be optimised in 71 | // the future without breaking API. 72 | struct Callback 73 | { 74 | Callback() = default; 75 | 76 | Callback(void* originalFunctor, ImSearch::Internal::VTable vTable); 77 | 78 | Callback(const Callback&) = delete; 79 | Callback(Callback&& other) noexcept; 80 | 81 | Callback& operator=(const Callback&) = delete; 82 | Callback& operator=(Callback&& other) noexcept; 83 | 84 | ~Callback(); 85 | 86 | operator bool() const { return mUserFunctor != nullptr; } 87 | 88 | // PushSearchable 89 | bool operator()(const char* name) const; 90 | 91 | // PopSearchable 92 | void operator()() const; 93 | 94 | static bool InvokeAsPushSearchable(ImSearch::Internal::VTable vTable, void* userFunctor, const char* name); 95 | static void InvokeAsPopSearchable(ImSearch::Internal::VTable vTable, void* userFunctor); 96 | 97 | void ClearData(); 98 | 99 | enum VTableModes 100 | { 101 | Invoke = 0, 102 | MoveConstruct = 1, 103 | Destruct = 2, 104 | GetSize = 3 105 | }; 106 | 107 | Internal::VTable mVTable{}; 108 | void* mUserFunctor{}; 109 | }; 110 | 111 | struct Searchable 112 | { 113 | std::string mText{}; 114 | 115 | IndexT mIndexOfFirstChild = sNullIndex; 116 | IndexT mIndexOfLastChild = sNullIndex; 117 | IndexT mIndexOfParent = sNullIndex; 118 | IndexT mIndexOfNextSibling = sNullIndex; 119 | }; 120 | 121 | struct Input 122 | { 123 | ImSearchFlags mFlags{}; 124 | std::vector mEntries{}; 125 | std::vector mBonuses{}; 126 | std::string mUserQuery{}; 127 | }; 128 | 129 | struct ReusableBuffers 130 | { 131 | std::vector mScores{}; 132 | std::vector mTempIndices{}; 133 | }; 134 | 135 | struct Output 136 | { 137 | std::vector mDisplayOrder{}; 138 | static constexpr IndexT sDisplayEndFlag = static_cast(1) << static_cast(std::numeric_limits::digits - 1); 139 | 140 | std::string mPreviewText{}; 141 | }; 142 | 143 | struct Result 144 | { 145 | Input mInput{}; 146 | ReusableBuffers mBuffers{}; 147 | Output mOutput{}; 148 | }; 149 | 150 | struct DisplayCallbacks 151 | { 152 | Callback mOnDisplayStart{}; 153 | Callback mOnDisplayEnd{}; 154 | }; 155 | 156 | struct LocalContext 157 | { 158 | Input mInput{}; 159 | 160 | std::vector mDisplayCallbacks{}; 161 | std::stack mPushStack{}; 162 | 163 | // Used for debugging. We don't need 164 | // to keep track of the actual stack 165 | // when the user is not actively searching, 166 | // so we only track the size to detect 167 | // push/pop underflows and overflows. 168 | int mPushStackLevel{}; 169 | 170 | Result mResult{}; 171 | bool mHasSubmitted{}; 172 | }; 173 | 174 | struct ImSearchContext 175 | { 176 | std::unordered_map mContexts{}; 177 | std::stack> mContextStack{}; 178 | std::unordered_map mTokenisedStrings{}; 179 | 180 | std::stack mCutoffStack{}; 181 | 182 | // Style and Colormaps 183 | ImSearchStyle Style; 184 | ImVector ColorModifiers; 185 | }; 186 | 187 | bool operator==(const StrView& lhs, const StrView& rhs); 188 | bool operator==(const Searchable& lhs, const Searchable& rhs); 189 | bool operator==(const Input& lhs, const Input& rhs); 190 | 191 | //----------------------------------------------------------------------------- 192 | // [SECTION] Context 193 | //----------------------------------------------------------------------------- 194 | 195 | // Some helper functions for generalising error-reporting. 196 | LocalContext& GetLocalContext(); 197 | ImSearch::ImSearchContext& GetImSearchContext(); 198 | IndexT GetCurrentItem(LocalContext& context); 199 | 200 | // Is the context currently collecting submissions? 201 | // ImSearch does not store anything the programm is submitting if the user 202 | // is not actively searching, for performance and memory reasons. 203 | bool CanCollectSubmissions(); 204 | 205 | //----------------------------------------------------------------------------- 206 | // [SECTION] Testing 207 | //----------------------------------------------------------------------------- 208 | 209 | float GetScore(size_t index); 210 | 211 | size_t GetDisplayOrderEntry(size_t index); 212 | 213 | //----------------------------------------------------------------------------- 214 | // [SECTION] Future API candidates 215 | //----------------------------------------------------------------------------- 216 | 217 | int GetNumItemsFilteredOut(); 218 | 219 | float GetCutoffStrength(); 220 | 221 | void SetPreviewText(const char* preview); 222 | 223 | const char* GetPreviewText(); 224 | 225 | void BeginHighlightZone(const char* textToHighlight); 226 | 227 | void EndHighlightZone(); 228 | 229 | //----------------------------------------------------------------------------- 230 | // [SECTION] Fuzzy Searching & String Functions 231 | //----------------------------------------------------------------------------- 232 | 233 | std::vector SplitTokens(StrView s); 234 | 235 | std::string Join(const std::vector& tokens); 236 | 237 | StrView GetStringNeededToCompletePartial(StrView partial, StrView complete); 238 | 239 | std::string MakeTokenisedString(StrView original); 240 | 241 | StrView GetMemoizedTokenisedString(const std::string& original); 242 | 243 | IndexT LevenshteinDistance( 244 | StrView s1, 245 | StrView s2, 246 | ReusableBuffers& buffers); 247 | 248 | float Ratio(StrView s1, 249 | StrView s2, 250 | ReusableBuffers& buffers); 251 | 252 | float PartialRatio(StrView s1, 253 | StrView s2, 254 | ReusableBuffers& buffers); 255 | 256 | // The function used internally to score strings 257 | float WeightedRatio(StrView s1, 258 | StrView s1Tokenised, 259 | StrView s2, 260 | StrView s2Tokenised, 261 | ReusableBuffers& buffers); 262 | } 263 | 264 | #endif // #ifndef IMGUI_DISABLE 265 | -------------------------------------------------------------------------------- /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, PopSearchable() 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 | // Anything that has a relevancy score below the cutoff strength 184 | // is not displayed to the user. Note that the relevancy of items 185 | // is within the range of 0.0f to 1.0f. (unless you are using SetRelevancyBonus). 186 | // 187 | // The default cut off strength is sDefaultCutoffStrength (see imsearch_internal.h). 188 | void PushCutoffStrength(float value); 189 | 190 | void PopCutoffStrength(); 191 | 192 | // You can artificially increase the relevancy for items you think might 193 | // be more likely to be what the user is looking for. 194 | // 195 | // You could, for example, give priority to more commonly referenced functions: 196 | // 197 | // if (ImSearch::PushSearchable(func.name, &DisplayFuncWidget)) 198 | // { 199 | // float frequency = func.timesUsedInCodebase / gTotalFunctionsUsedInCodeBase; 200 | // ImSearch::SetRelevancyBonus(frequency); 201 | // ImSearch::PopSearchable(); 202 | // } 203 | // 204 | // ImSearch places no restrictions on the range of your provided bonus, 205 | // but keep in mind the 'default' relevancy of items, as scored by ImSearch, 206 | // is within the range of 0.0f to 1.0f. Normalization is encouraged, to avoid 207 | // your bonus dwarfing the similarity score of the text. 208 | void SetRelevancyBonus(float bonus); 209 | 210 | // You can add synonyms using the following syntax: 211 | // 212 | // if (ImSearch::PushSearchable("Function", selectableCallback)) 213 | // { 214 | // ImSearch::AddSynonym("Method"); 215 | // ImSearch::AddSynonym("Procedure"); 216 | // 217 | // ImSearch::PopSearchable(); 218 | // } 219 | // 220 | void AddSynonym(const char* synonym); 221 | 222 | //----------------------------------------------------------------------------- 223 | // [SECTION] Searchbars 224 | //----------------------------------------------------------------------------- 225 | 226 | // Shows the default searchbar. Usually placed right after BeginSearch, or 227 | // right after calling Submit. 228 | void SearchBar(const char* hint = "Search"); 229 | 230 | // API for setting the user query, the text that the user has typed 231 | // and is currently searching for. Used for making custom searchbars. 232 | void SetUserQuery(const char* query); 233 | 234 | // Will return the text that the user has typed 235 | // and is currently searching for. 236 | const char* GetUserQuery(); 237 | 238 | //----------------------------------------------------------------------------- 239 | // [SECTION] Styling 240 | //----------------------------------------------------------------------------- 241 | 242 | // Like ImGui, all style colors are stored in indexable array in ImSearchStyle. 243 | // You can permanently modify these values through GetStyle().Colors, or 244 | // temporarily modify them with Push/Pop functions below. 245 | 246 | ImSearchStyle& GetStyle(); 247 | 248 | ImU32 GetColorU32(ImSearchCol idx, float alpha_mul = 1.0f); 249 | const ImVec4& GetStyleColorVec4(ImSearchCol idx); 250 | 251 | // Use PushStyleX to temporarily modify your ImSearchStyle. The modification 252 | // will last until the matching call to PopStyleX. You MUST call a pop for 253 | // every push, otherwise you will leak memory! This behaves just like ImGui. 254 | 255 | // Temporarily modify a style color. Don't forget to call PopStyleColor! 256 | void PushStyleColor(ImSearchCol idx, ImU32 col); 257 | void PushStyleColor(ImSearchCol idx, const ImVec4& col); 258 | 259 | // Undo temporary style color modification(s). Undo multiple pushes at once by increasing count. 260 | void PopStyleColor(int count = 1); 261 | 262 | //----------------------------------------------------------------------------- 263 | // [SECTION] Demo 264 | //----------------------------------------------------------------------------- 265 | 266 | // Shows the ImSearch demo window (add imsearch_demo.cpp to your sources!) 267 | void ShowDemoWindow(bool* p_open = nullptr); 268 | 269 | //----------------------------------------------------------------------------- 270 | // [SECTION] Internal 271 | //----------------------------------------------------------------------------- 272 | 273 | // End of public API! 274 | // Starting from here until the end of the file, 275 | // forwards compatibility is not guaranteed! 276 | 277 | namespace Internal 278 | { 279 | using VTable = bool(*)(int mode, void* ptr1, void* ptr2); 280 | 281 | bool PushSearchable(const char* name, void* callback, VTable vTable); 282 | void PopSearchable(void* callback, VTable vTable); 283 | 284 | template struct remove_reference { typedef T type; }; 285 | template struct remove_reference { typedef T type; }; 286 | template struct remove_reference { typedef T type; }; 287 | } 288 | } 289 | 290 | template 291 | void ImSearch::SearchableItem(const char* name, T&& callback) 292 | { 293 | using TNonRef = typename Internal::remove_reference::type; 294 | 295 | struct CallbackWrapper 296 | { 297 | TNonRef mUserCallback; 298 | 299 | bool operator()(const char* name) const 300 | { 301 | (void)mUserCallback(name); 302 | return false; 303 | } 304 | }; 305 | 306 | if (PushSearchable(name, CallbackWrapper{ static_cast(callback) })) 307 | { 308 | PopSearchable(); 309 | } 310 | } 311 | 312 | template 313 | bool ImSearch::PushSearchable(const char* name, T&& callback) 314 | { 315 | using TNonRef = typename Internal::remove_reference::type; 316 | TNonRef moveable{ static_cast(callback) }; 317 | return Internal::PushSearchable( 318 | name, 319 | &moveable, 320 | +[](int mode, void* ptr1, void* ptr2) -> bool 321 | { 322 | switch (mode) 323 | { 324 | case 0: // Invoke 325 | { 326 | TNonRef* func = static_cast(ptr1); 327 | const char* nameArg = static_cast(ptr2); 328 | return (*func)(nameArg); 329 | } 330 | case 1: // Move-construct 331 | { 332 | TNonRef* src = static_cast(ptr1); 333 | TNonRef* dst = static_cast(ptr2); 334 | new(dst)TNonRef(static_cast(*src)); 335 | return true; 336 | } 337 | case 2: // Destructor 338 | { 339 | TNonRef* src = static_cast(ptr1); 340 | src->~TNonRef(); 341 | return true; 342 | } 343 | case 3: // Get size 344 | { 345 | int& ret = *static_cast(ptr1); 346 | ret = sizeof(TNonRef); 347 | return true; 348 | } 349 | default: 350 | return false; 351 | } 352 | }); 353 | } 354 | 355 | template 356 | void ImSearch::PopSearchable(T&& callback) 357 | { 358 | using TNonRef = typename Internal::remove_reference::type; 359 | TNonRef moveable{ static_cast(callback) }; 360 | Internal::PopSearchable( 361 | &moveable, 362 | +[](int mode, void* ptr1, void* ptr2) 363 | { 364 | switch (mode) 365 | { 366 | case 0: // Invoke 367 | { 368 | TNonRef* func = static_cast(ptr1); 369 | (*func)(); 370 | return true; 371 | } 372 | case 1: // Move-construct 373 | { 374 | TNonRef* src = static_cast(ptr1); 375 | TNonRef* dst = static_cast(ptr2); 376 | new(dst)TNonRef(static_cast(*src)); 377 | return true; 378 | } 379 | case 2: // Destructor 380 | { 381 | TNonRef* src = static_cast(ptr1); 382 | src->~TNonRef(); 383 | return true; 384 | } 385 | case 3: // Get size 386 | { 387 | int& ret = *static_cast(ptr1); 388 | ret = sizeof(TNonRef); 389 | return true; 390 | } 391 | default: 392 | return false; 393 | } 394 | }); 395 | } 396 | 397 | #endif // #ifndef IMGUI_DISABLE 398 | -------------------------------------------------------------------------------- /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 | 11 | namespace 12 | { 13 | 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" }; 14 | 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" }; 15 | std::array imguiExtensions = { "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" }; 16 | 17 | struct Person 18 | { 19 | const char* mFirstName; 20 | const char* mLastName; 21 | const char* mCountry; 22 | const char* mHobby; 23 | }; 24 | 25 | std::array people{ {{"John", "Doe", "USA", "Photography"},{"Jane", "Smith", "Canada", "Painting"},{"Carlos", "Garcia", "Spain", "Cooking"},{"Li", "Wei", "China", "Chess"},{"Anna", "Muller", "Germany", "Hiking"},{"Yuki", "Tanaka", "Japan", "Origami"},{"Marco", "Rossi", "Italy", "Soccer"},{"Olga", "Ivanova", "Russia", "Reading"},{"Pierre", "Dubois", "France", "Wine Tasting"},{"Maria", "Silva", "Brazil", "Dancing"},{"Ahmed", "Khan", "Egypt", "Swimming"},{"Emma", "Johnson", "Australia", "Surfing"},{"Lars", "Andersen", "Denmark", "Lego Building"},{"Sofia", "Papadopoulos", "Greece", "Philosophy"},{"James", "Wilson", "UK", "Gardening"},{"Isabella", "Lopez", "Mexico", "Guitar"},{"David", "Kim", "South Korea", "Taekwondo"},{"Fatima", "Ali", "Pakistan", "Calligraphy"},{"Michael", "Brown", "New Zealand", "Rugby"},{"Amina", "Juma", "Kenya", "Running"},{"Elena", "Vasile", "Romania", "Poetry"},{"George", "Peterson", "Sweden", "Skiing"},{"Chloe", "Martin", "Belgium", "Comics"},{"Raj", "Patel", "India", "Yoga"},{"Lily", "Chen", "Singapore", "Food Blogging"}} }; 26 | 27 | size_t Rand(size_t& seed); 28 | const char* GetRandomString(size_t& seed, std::string& str); 29 | 30 | void HelpMarker(const char* desc); 31 | 32 | bool ImSearchDemo_TreeNode(const char* name); 33 | void ImSearchDemo_TreeLeaf(const char* name); 34 | void ImSearchDemo_TreePop(); 35 | 36 | bool ImSearchDemo_CollapsingHeader(const char* name); 37 | } 38 | 39 | #if defined(_MSVC_LANG) // MS compiler has different __cplusplus value. 40 | # if _MSVC_LANG >= 201402L 41 | #define HAS_CPP14 42 | # endif 43 | #else // All other compilers. 44 | # if __cplusplus >= 201402L 45 | #define HAS_CPP14 46 | # endif 47 | #endif 48 | 49 | void ImSearch::ShowDemoWindow(bool* p_open) 50 | { 51 | if (!ImGui::Begin("ImSearch Demo", p_open)) 52 | { 53 | ImGui::End(); 54 | return; 55 | } 56 | 57 | size_t seed = static_cast(0xbadC0ffee); 58 | // Reuse the same string 59 | // when generating random strings, 60 | // to reduce heap allocations 61 | std::string randStr{}; 62 | 63 | if (ImGui::TreeNode("Basic")) 64 | { 65 | if (ImSearch::BeginSearch()) 66 | { 67 | ImSearch::SearchBar(); 68 | 69 | ImSearch::SearchableItem("Hey there!", 70 | [](const char* name) 71 | { 72 | ImGui::Selectable(name); 73 | return true; 74 | }); 75 | 76 | ImSearch::SearchableItem("Howdy partner!", 77 | [](const char* name) 78 | { 79 | ImGui::Button(name); 80 | ImGui::SetItemTooltip("Click me!"); 81 | return true; 82 | }); 83 | 84 | ImSearch::EndSearch(); 85 | } 86 | 87 | ImGui::TreePop(); 88 | } 89 | 90 | if (ImGui::TreeNode("Combo")) 91 | { 92 | static const char* selectedString = imguiExtensions[0]; 93 | 94 | if (ImGui::BeginCombo("##Extensions", selectedString)) 95 | { 96 | if (ImSearch::BeginSearch()) 97 | { 98 | ImSearch::SearchBar(); 99 | 100 | for (const char* extension : imguiExtensions) 101 | { 102 | ImSearch::SearchableItem(extension, 103 | [&](const char* name) 104 | { 105 | const bool isSelected = name == selectedString; 106 | if (ImGui::Selectable(name, isSelected)) 107 | { 108 | selectedString = name; 109 | } 110 | }); 111 | } 112 | ImSearch::EndSearch(); 113 | } 114 | ImGui::EndCombo(); 115 | } 116 | 117 | ImGui::TreePop(); 118 | } 119 | 120 | if (ImGui::TreeNode("Custom Search bar")) 121 | { 122 | if (ImSearch::BeginSearch()) 123 | { 124 | static char query[2048]{}; 125 | 126 | ImGui::SetNextItemWidth(-FLT_MIN); 127 | 128 | const float spaceWidth = ImGui::CalcTextSize(" ").x; 129 | const float searchbarWidth = ImGui::GetContentRegionAvail().x; 130 | const int totalNumCharacters = static_cast(searchbarWidth / spaceWidth); 131 | 132 | const int timeAsInt = static_cast(ImGui::GetTime() * 10.0); 133 | 134 | constexpr int length = 31; 135 | constexpr char hint[length + 1] = "I'm a custom search bar! "; 136 | std::string hintWithSpacing{}; 137 | 138 | for (int i = 0; i < totalNumCharacters; i++) 139 | { 140 | int index = (i + timeAsInt) % length; 141 | hintWithSpacing.push_back(hint[index]); 142 | } 143 | 144 | if (ImGui::InputTextWithHint("##Searchbar", hintWithSpacing.c_str(), query, sizeof(query))) 145 | { 146 | ImSearch::SetUserQuery(query); 147 | } 148 | 149 | for (int i = 0; i < 3; i++) 150 | { 151 | ImSearch::SearchableItem(GetRandomString(seed, randStr), 152 | [](const char* str) 153 | { 154 | ImGui::Selectable(str); 155 | return true; 156 | }); 157 | } 158 | 159 | ImSearch::EndSearch(); 160 | } 161 | 162 | ImGui::TreePop(); 163 | } 164 | 165 | if (ImGui::TreeNode("How do callbacks work?")) 166 | { 167 | HelpMarker("This displayed section probably won't make a lot of sense if you're not also looking at the code behind it."); 168 | 169 | if (ImSearch::BeginSearch()) 170 | { 171 | ImSearch::SearchBar(); 172 | 173 | if (ImSearchDemo_CollapsingHeader("std::functions")) 174 | { 175 | std::function myDisplayStart = 176 | [](const char* str) -> bool 177 | { 178 | return ImGui::TreeNode(str); 179 | }; 180 | 181 | std::function myDisplayEnd = 182 | []() 183 | { 184 | return ImGui::TreePop(); 185 | }; 186 | 187 | if (ImSearch::PushSearchable("std::function!", myDisplayStart)) 188 | { 189 | ImSearch::PopSearchable(myDisplayEnd); 190 | } 191 | 192 | ImSearch::PopSearchable(); 193 | } 194 | 195 | 196 | 197 | #ifdef HAS_CPP14 // C++11 didnt support lambda captures by value. 198 | if (ImSearchDemo_CollapsingHeader("Lambdas and captures")) 199 | { 200 | const std::string tooltip = GetRandomString(seed, randStr); 201 | ImSearch::SearchableItem(GetRandomString(seed, randStr), 202 | // You can capture anything in the lambda you might need. 203 | // The easiest way, works with any C++ lambda. 204 | [=](const char* str) 205 | { 206 | ImGui::TextUnformatted(str); 207 | 208 | if (ImGui::BeginItemTooltip()) 209 | { 210 | ImGui::TextUnformatted(tooltip.c_str()); 211 | ImGui::EndTooltip(); 212 | } 213 | }); 214 | 215 | ImSearch::PopSearchable(); 216 | } 217 | #endif 218 | 219 | if (ImSearchDemo_CollapsingHeader("Free functions")) 220 | { 221 | // C++ may sometimes require you to cast, to avoid ambiguity between different overloads. 222 | if (ImSearch::PushSearchable("Tree", static_cast(&ImGui::TreeNode))) 223 | { 224 | // Sometimes you can even use functions directly from ImGui! 225 | ImSearch::PopSearchable(&ImGui::TreePop); 226 | } 227 | 228 | ImSearch::PopSearchable(); 229 | } 230 | 231 | if (ImSearchDemo_CollapsingHeader("Common pitfall: dangling references")) 232 | { 233 | { 234 | int hiIWentOutOfScope{}; 235 | 236 | ImSearch::SearchableItem("Undefined behaviour, variable out of scope!", 237 | [&hiIWentOutOfScope](const char* name) // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 238 | { /*^*/ 239 | ImGui::TextUnformatted(name); /*^*/ 240 | /*^*/ 241 | // This would be invalid! /*^*/ 242 | // ImGui::InputInt("DontDoThis", &hiIWentOutOfScope); /*^*/ 243 | /*^*/ 244 | (void)(hiIWentOutOfScope); // (just to silence warnings of it being unused /*^*/ 245 | }); /*^*/ 246 | } /*^*/ 247 | /*^*/ 248 | ImSearch::PopSearchable(); /*^*/ 249 | /*^*/ 250 | // Your callbacks can be invoked at any point between your call to PushSearchable and the next /*^*/ 251 | // ImSearch::Submit or ImSearch::EndSearch is reached. Make sure your callbacks remain valid, /*^*/ 252 | // with nothing dangling. /*^*/ 253 | ImSearch::Submit(); // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>^ 254 | } 255 | 256 | ImSearch::EndSearch(); 257 | } 258 | 259 | ImGui::TreePop(); 260 | } 261 | 262 | if (ImGui::TreeNode("Many")) 263 | { 264 | if (ImSearch::BeginSearch()) 265 | { 266 | ImGui::TextWrapped("SearchBar's can be placed anywhere between BeginSearch and EndSearch; even outside the child window"); 267 | ImSearch::SearchBar(); 268 | ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); 269 | 270 | if (ImGui::BeginChild("Submissions", {}, ImGuiChildFlags_Borders)) 271 | { 272 | for (int i = 0; i < 1000; i++) 273 | { 274 | ImSearch::SearchableItem(GetRandomString(seed, randStr), 275 | [](const char* str) 276 | { 277 | ImGui::TextUnformatted(str); 278 | }); 279 | } 280 | 281 | // Call Submit explicitly; all the callbacks 282 | // will be invoked through submit. If we 283 | // had waited for EndSearch to do this for us, 284 | // the callbacks would've been invoked after 285 | // ImGui::EndChild, leaving our searchables 286 | // to be displayed outside of the child window. 287 | ImSearch::Submit(); 288 | } ImGui::EndChild(); 289 | 290 | ImGui::PopStyleColor(); 291 | ImSearch::EndSearch(); 292 | } 293 | 294 | ImGui::TreePop(); 295 | } 296 | 297 | if (ImGui::TreeNode("Table")) 298 | { 299 | if (ImSearch::BeginSearch(ImSearchFlags_NoTextHighlighting)) 300 | { 301 | ImSearch::SearchBar(); 302 | 303 | ImSearch::BeginHighlightZone(ImSearch::GetUserQuery()); 304 | 305 | static ImGuiTableFlags flags = 306 | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti 307 | | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_NoBordersInBody; 308 | 309 | if (ImGui::BeginTable("My beautiful table!", 4, flags)) 310 | { 311 | enum UserId { Person_FirstName, Person_LastName, Person_Country, Person_Hobby }; 312 | 313 | ImGui::TableSetupColumn("First Name", ImGuiTableColumnFlags_DefaultSort, 0.0f, Person_FirstName); 314 | ImGui::TableSetupColumn("Last Name", ImGuiTableColumnFlags_DefaultSort, 0.0f, Person_LastName); 315 | ImGui::TableSetupColumn("Country", ImGuiTableColumnFlags_DefaultSort, 0.0f, Person_Country); 316 | ImGui::TableSetupColumn("Hobby", ImGuiTableColumnFlags_DefaultSort, 0.0f, Person_Hobby); 317 | ImGui::TableHeadersRow(); 318 | 319 | if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) 320 | { 321 | if (sort_specs->SpecsDirty) 322 | { 323 | std::sort(people.begin(), people.end(), 324 | [&](Person& lhs, Person& rhs) 325 | { 326 | switch (sort_specs->Specs->ColumnUserID) 327 | { 328 | case Person_FirstName: return strcmp(lhs.mFirstName, rhs.mFirstName) > 0; 329 | case Person_LastName: return strcmp(lhs.mLastName, rhs.mLastName) > 0; 330 | case Person_Country: return strcmp(lhs.mCountry, rhs.mCountry) > 0; 331 | case Person_Hobby: return strcmp(lhs.mHobby, rhs.mHobby) > 0; 332 | default: return true; 333 | } 334 | }); 335 | 336 | if (sort_specs->Specs->SortDirection == ImGuiSortDirection_Descending) 337 | { 338 | std::reverse(people.begin(), people.end()); 339 | } 340 | 341 | sort_specs->SpecsDirty = false; 342 | } 343 | } 344 | 345 | for (const Person& person : people) 346 | { 347 | if (ImSearch::PushSearchable( 348 | "", 349 | [&](const char*) 350 | { 351 | ImGui::TableNextRow(); 352 | 353 | ImGui::TableNextColumn(); 354 | ImGui::TextUnformatted(person.mFirstName); 355 | 356 | ImGui::TableNextColumn(); 357 | ImGui::TextUnformatted(person.mLastName); 358 | 359 | ImGui::TableNextColumn(); 360 | ImGui::TextUnformatted(person.mCountry); 361 | 362 | ImGui::TableNextColumn(); 363 | ImGui::TextUnformatted(person.mHobby); 364 | 365 | return true; 366 | })) 367 | { 368 | ImSearch::AddSynonym(person.mFirstName); 369 | ImSearch::AddSynonym(person.mLastName); 370 | ImSearch::AddSynonym(person.mCountry); 371 | ImSearch::AddSynonym(person.mHobby); 372 | ImSearch::PopSearchable(); 373 | } 374 | } 375 | 376 | ImSearch::Submit(); 377 | ImGui::EndTable(); 378 | } 379 | 380 | ImSearch::EndHighlightZone(); 381 | 382 | ImSearch::EndSearch(); 383 | } 384 | 385 | ImGui::TreePop(); 386 | } 387 | 388 | if (ImGui::TreeNode("Tree Nodes")) 389 | { 390 | if (ImSearch::BeginSearch()) 391 | { 392 | ImSearch::SearchBar(); 393 | 394 | if (ImSearchDemo_TreeNode("Professions")) 395 | { 396 | if (ImSearchDemo_TreeNode("Farmers")) 397 | { 398 | if (ImSearchDemo_TreeNode("Tools")) 399 | { 400 | ImSearchDemo_TreeLeaf("Hoe"); 401 | ImSearchDemo_TreeLeaf("Sickle"); 402 | ImSearchDemo_TreeLeaf("Plow"); 403 | ImSearchDemo_TreeLeaf("Wheelbarrow"); 404 | ImSearchDemo_TreeLeaf("Rake"); 405 | ImSearchDemo_TreeLeaf("Pitchfork"); 406 | ImSearchDemo_TreeLeaf("Scythe"); 407 | ImSearchDemo_TreeLeaf("Hand Trowel"); 408 | ImSearchDemo_TreeLeaf("Pruning Shears"); 409 | ImSearchDemo_TreeLeaf("Seed Drill"); 410 | ImSearchDemo_TreePop(); 411 | } 412 | if (ImSearchDemo_TreeNode("Crops")) 413 | { 414 | ImSearchDemo_TreeLeaf("Wheat"); 415 | ImSearchDemo_TreeLeaf("Corn"); 416 | ImSearchDemo_TreeLeaf("Rice"); 417 | ImSearchDemo_TreeLeaf("Soybeans"); 418 | ImSearchDemo_TreeLeaf("Barley"); 419 | ImSearchDemo_TreeLeaf("Oats"); 420 | ImSearchDemo_TreeLeaf("Cotton"); 421 | ImSearchDemo_TreeLeaf("Sugarcane"); 422 | ImSearchDemo_TreeLeaf("Potatoes"); 423 | ImSearchDemo_TreeLeaf("Tomatoes"); 424 | ImSearchDemo_TreePop(); 425 | } 426 | if (ImSearchDemo_TreeNode("Livestock")) 427 | { 428 | ImSearchDemo_TreeLeaf("Cattle"); 429 | ImSearchDemo_TreeLeaf("Sheep"); 430 | ImSearchDemo_TreeLeaf("Goats"); 431 | ImSearchDemo_TreeLeaf("Pigs"); 432 | ImSearchDemo_TreeLeaf("Chickens"); 433 | ImSearchDemo_TreeLeaf("Ducks"); 434 | ImSearchDemo_TreeLeaf("Horses"); 435 | ImSearchDemo_TreeLeaf("Bees"); 436 | ImSearchDemo_TreeLeaf("Turkeys"); 437 | ImSearchDemo_TreeLeaf("Llamas"); 438 | ImSearchDemo_TreePop(); 439 | } 440 | ImSearchDemo_TreePop(); 441 | } 442 | 443 | if (ImSearchDemo_TreeNode("Blacksmiths")) 444 | { 445 | if (ImSearchDemo_TreeNode("Tools")) 446 | { 447 | ImSearchDemo_TreeLeaf("Hammer"); 448 | ImSearchDemo_TreeLeaf("Anvil"); 449 | ImSearchDemo_TreeLeaf("Tongs"); 450 | ImSearchDemo_TreeLeaf("Forge"); 451 | ImSearchDemo_TreeLeaf("Quenching Tank"); 452 | ImSearchDemo_TreeLeaf("Files"); 453 | ImSearchDemo_TreeLeaf("Chisels"); 454 | ImSearchDemo_TreeLeaf("Punches"); 455 | ImSearchDemo_TreeLeaf("Swage Block"); 456 | ImSearchDemo_TreeLeaf("Bellows"); 457 | ImSearchDemo_TreePop(); 458 | } 459 | if (ImSearchDemo_TreeNode("Materials")) 460 | { 461 | ImSearchDemo_TreeLeaf("Iron Ore"); 462 | ImSearchDemo_TreeLeaf("Coal"); 463 | ImSearchDemo_TreeLeaf("Charcoal"); 464 | ImSearchDemo_TreeLeaf("Steel Ingots"); 465 | ImSearchDemo_TreeLeaf("Copper"); 466 | ImSearchDemo_TreeLeaf("Bronze"); 467 | ImSearchDemo_TreeLeaf("Nickel"); 468 | ImSearchDemo_TreeLeaf("Cobalt"); 469 | ImSearchDemo_TreeLeaf("Manganese"); 470 | ImSearchDemo_TreeLeaf("Flux"); 471 | ImSearchDemo_TreePop(); 472 | } 473 | if (ImSearchDemo_TreeNode("Products")) 474 | { 475 | ImSearchDemo_TreeLeaf("Horseshoes"); 476 | ImSearchDemo_TreeLeaf("Nails"); 477 | ImSearchDemo_TreeLeaf("Swords"); 478 | ImSearchDemo_TreeLeaf("Axes"); 479 | ImSearchDemo_TreeLeaf("Armor Plates"); 480 | ImSearchDemo_TreeLeaf("Tools"); 481 | ImSearchDemo_TreeLeaf("Chains"); 482 | ImSearchDemo_TreeLeaf("Iron Gates"); 483 | ImSearchDemo_TreeLeaf("Rail Tracks"); 484 | ImSearchDemo_TreeLeaf("Decorative Grills"); 485 | ImSearchDemo_TreePop(); 486 | } 487 | ImSearchDemo_TreePop(); 488 | } 489 | 490 | if (ImSearchDemo_TreeNode("Fishermen")) 491 | { 492 | if (ImSearchDemo_TreeNode("Equipment")) 493 | { 494 | ImSearchDemo_TreeLeaf("Fishing Rod"); 495 | ImSearchDemo_TreeLeaf("Net"); 496 | ImSearchDemo_TreeLeaf("Tackle Box"); 497 | ImSearchDemo_TreeLeaf("Hooks"); 498 | ImSearchDemo_TreeLeaf("Lures"); 499 | ImSearchDemo_TreeLeaf("Bobbers"); 500 | ImSearchDemo_TreeLeaf("Sinkers"); 501 | ImSearchDemo_TreeLeaf("Gaff"); 502 | ImSearchDemo_TreeLeaf("Gill Net"); 503 | ImSearchDemo_TreeLeaf("Crab Pot"); 504 | ImSearchDemo_TreePop(); 505 | } 506 | if (ImSearchDemo_TreeNode("Catch")) 507 | { 508 | ImSearchDemo_TreeLeaf("Salmon"); 509 | ImSearchDemo_TreeLeaf("Tuna"); 510 | ImSearchDemo_TreeLeaf("Trout"); 511 | ImSearchDemo_TreeLeaf("Cod"); 512 | ImSearchDemo_TreeLeaf("Haddock"); 513 | ImSearchDemo_TreeLeaf("Shrimp"); 514 | ImSearchDemo_TreeLeaf("Crab"); 515 | ImSearchDemo_TreeLeaf("Lobster"); 516 | ImSearchDemo_TreeLeaf("Sardines"); 517 | ImSearchDemo_TreeLeaf("Mussels"); 518 | ImSearchDemo_TreePop(); 519 | } 520 | if (ImSearchDemo_TreeNode("Boats")) 521 | { 522 | ImSearchDemo_TreeLeaf("Rowboat"); 523 | ImSearchDemo_TreeLeaf("Sailboat"); 524 | ImSearchDemo_TreeLeaf("Trawler"); 525 | ImSearchDemo_TreeLeaf("Catamaran"); 526 | ImSearchDemo_TreeLeaf("Kayak"); 527 | ImSearchDemo_TreeLeaf("Dinghy"); 528 | ImSearchDemo_TreeLeaf("Canoe"); 529 | ImSearchDemo_TreeLeaf("Fishing Trawler"); 530 | ImSearchDemo_TreeLeaf("Longliner"); 531 | ImSearchDemo_TreeLeaf("Gillnetter"); 532 | ImSearchDemo_TreePop(); 533 | } 534 | ImSearchDemo_TreePop(); 535 | } 536 | 537 | ImSearchDemo_TreePop(); 538 | } 539 | 540 | if (ImSearchDemo_TreeNode("Technologies")) 541 | { 542 | if (ImSearchDemo_TreeNode("Computers")) 543 | { 544 | if (ImSearchDemo_TreeNode("Hardware")) 545 | { 546 | ImSearchDemo_TreeLeaf("CPU"); 547 | ImSearchDemo_TreeLeaf("GPU"); 548 | ImSearchDemo_TreeLeaf("RAM"); 549 | ImSearchDemo_TreeLeaf("Motherboard"); 550 | ImSearchDemo_TreeLeaf("SSD"); 551 | ImSearchDemo_TreeLeaf("HDD"); 552 | ImSearchDemo_TreeLeaf("Power Supply"); 553 | ImSearchDemo_TreeLeaf("Cooler"); 554 | ImSearchDemo_TreeLeaf("Case"); 555 | ImSearchDemo_TreeLeaf("Network Card"); 556 | ImSearchDemo_TreePop(); 557 | } 558 | if (ImSearchDemo_TreeNode("Software")) 559 | { 560 | ImSearchDemo_TreeLeaf("Operating System"); 561 | ImSearchDemo_TreeLeaf("Web Browser"); 562 | ImSearchDemo_TreeLeaf("Office Suite"); 563 | ImSearchDemo_TreeLeaf("IDE"); 564 | ImSearchDemo_TreeLeaf("Antivirus"); 565 | ImSearchDemo_TreeLeaf("Drivers"); 566 | ImSearchDemo_TreeLeaf("Database"); 567 | ImSearchDemo_TreeLeaf("Virtual Machine"); 568 | ImSearchDemo_TreeLeaf("Compiler"); 569 | ImSearchDemo_TreeLeaf("Text Editor"); 570 | ImSearchDemo_TreePop(); 571 | } 572 | if (ImSearchDemo_TreeNode("Networking")) 573 | { 574 | ImSearchDemo_TreeLeaf("Router"); 575 | ImSearchDemo_TreeLeaf("Switch"); 576 | ImSearchDemo_TreeLeaf("Firewall"); 577 | ImSearchDemo_TreeLeaf("Modem"); 578 | ImSearchDemo_TreeLeaf("Access Point"); 579 | ImSearchDemo_TreeLeaf("Ethernet Cable"); 580 | ImSearchDemo_TreeLeaf("Fiber Optic Cable"); 581 | ImSearchDemo_TreeLeaf("VPN"); 582 | ImSearchDemo_TreeLeaf("DNS"); 583 | ImSearchDemo_TreeLeaf("DHCP"); 584 | ImSearchDemo_TreePop(); 585 | } 586 | ImSearchDemo_TreePop(); 587 | } 588 | 589 | if (ImSearchDemo_TreeNode("Vehicles")) 590 | { 591 | if (ImSearchDemo_TreeNode("Land")) 592 | { 593 | ImSearchDemo_TreeLeaf("Car"); 594 | ImSearchDemo_TreeLeaf("Truck"); 595 | ImSearchDemo_TreeLeaf("Motorcycle"); 596 | ImSearchDemo_TreeLeaf("Bicycle"); 597 | ImSearchDemo_TreeLeaf("Bus"); 598 | ImSearchDemo_TreeLeaf("Train"); 599 | ImSearchDemo_TreeLeaf("Tram"); 600 | ImSearchDemo_TreeLeaf("Tank"); 601 | ImSearchDemo_TreeLeaf("ATV"); 602 | ImSearchDemo_TreeLeaf("Segway"); 603 | ImSearchDemo_TreePop(); 604 | } 605 | if (ImSearchDemo_TreeNode("Air")) 606 | { 607 | ImSearchDemo_TreeLeaf("Airplane"); 608 | ImSearchDemo_TreeLeaf("Helicopter"); 609 | ImSearchDemo_TreeLeaf("Drone"); 610 | ImSearchDemo_TreeLeaf("Glider"); 611 | ImSearchDemo_TreeLeaf("Hot Air Balloon"); 612 | ImSearchDemo_TreeLeaf("Jet"); 613 | ImSearchDemo_TreeLeaf("Blimp"); 614 | ImSearchDemo_TreeLeaf("Autogyro"); 615 | ImSearchDemo_TreeLeaf("Seaplane"); 616 | ImSearchDemo_TreeLeaf("Hang Glider"); 617 | ImSearchDemo_TreePop(); 618 | } 619 | if (ImSearchDemo_TreeNode("Sea")) 620 | { 621 | ImSearchDemo_TreeLeaf("Ship"); 622 | ImSearchDemo_TreeLeaf("Boat"); 623 | ImSearchDemo_TreeLeaf("Submarine"); 624 | ImSearchDemo_TreeLeaf("Yacht"); 625 | ImSearchDemo_TreeLeaf("Canoe"); 626 | ImSearchDemo_TreeLeaf("Ferry"); 627 | ImSearchDemo_TreeLeaf("Sailboat"); 628 | ImSearchDemo_TreeLeaf("Tugboat"); 629 | ImSearchDemo_TreeLeaf("Catamaran"); 630 | ImSearchDemo_TreeLeaf("Dinghy"); 631 | ImSearchDemo_TreePop(); 632 | } 633 | ImSearchDemo_TreePop(); 634 | } 635 | 636 | ImSearchDemo_TreePop(); 637 | } 638 | 639 | if (ImSearchDemo_TreeNode("Nature")) 640 | { 641 | if (ImSearchDemo_TreeNode("Animals")) 642 | { 643 | if (ImSearchDemo_TreeNode("Mammals")) 644 | { 645 | ImSearchDemo_TreeLeaf("Lion"); 646 | ImSearchDemo_TreeLeaf("Tiger"); 647 | ImSearchDemo_TreeLeaf("Elephant"); 648 | ImSearchDemo_TreeLeaf("Whale"); 649 | ImSearchDemo_TreeLeaf("Dolphin"); 650 | ImSearchDemo_TreeLeaf("Bat"); 651 | ImSearchDemo_TreeLeaf("Kangaroo"); 652 | ImSearchDemo_TreeLeaf("Human"); 653 | ImSearchDemo_TreeLeaf("Bear"); 654 | ImSearchDemo_TreeLeaf("Wolf"); 655 | ImSearchDemo_TreePop(); 656 | } 657 | if (ImSearchDemo_TreeNode("Birds")) 658 | { 659 | ImSearchDemo_TreeLeaf("Eagle"); 660 | ImSearchDemo_TreeLeaf("Sparrow"); 661 | ImSearchDemo_TreeLeaf("Penguin"); 662 | ImSearchDemo_TreeLeaf("Owl"); 663 | ImSearchDemo_TreeLeaf("Parrot"); 664 | ImSearchDemo_TreeLeaf("Flamingo"); 665 | ImSearchDemo_TreeLeaf("Duck"); 666 | ImSearchDemo_TreeLeaf("Goose"); 667 | ImSearchDemo_TreeLeaf("Hawk"); 668 | ImSearchDemo_TreeLeaf("Crow"); 669 | ImSearchDemo_TreePop(); 670 | } 671 | if (ImSearchDemo_TreeNode("Reptiles")) 672 | { 673 | ImSearchDemo_TreeLeaf("Crocodile"); 674 | ImSearchDemo_TreeLeaf("Snake"); 675 | ImSearchDemo_TreeLeaf("Lizard"); 676 | ImSearchDemo_TreeLeaf("Turtle"); 677 | ImSearchDemo_TreeLeaf("Chameleon"); 678 | ImSearchDemo_TreeLeaf("Gecko"); 679 | ImSearchDemo_TreeLeaf("Alligator"); 680 | ImSearchDemo_TreeLeaf("Komodo Dragon"); 681 | ImSearchDemo_TreeLeaf("Iguana"); 682 | ImSearchDemo_TreeLeaf("Rattlesnake"); 683 | ImSearchDemo_TreePop(); 684 | } 685 | ImSearchDemo_TreePop(); 686 | } 687 | if (ImSearchDemo_TreeNode("Plants")) 688 | { 689 | if (ImSearchDemo_TreeNode("Trees")) 690 | { 691 | ImSearchDemo_TreeLeaf("Oak"); 692 | ImSearchDemo_TreeLeaf("Pine"); 693 | ImSearchDemo_TreeLeaf("Maple"); 694 | ImSearchDemo_TreeLeaf("Birch"); 695 | ImSearchDemo_TreeLeaf("Cedar"); 696 | ImSearchDemo_TreeLeaf("Redwood"); 697 | ImSearchDemo_TreeLeaf("Palm"); 698 | ImSearchDemo_TreeLeaf("Willow"); 699 | ImSearchDemo_TreeLeaf("Spruce"); 700 | ImSearchDemo_TreeLeaf("Cypress"); 701 | ImSearchDemo_TreePop(); 702 | } 703 | if (ImSearchDemo_TreeNode("Flowers")) 704 | { 705 | ImSearchDemo_TreeLeaf("Rose"); 706 | ImSearchDemo_TreeLeaf("Tulip"); 707 | ImSearchDemo_TreeLeaf("Sunflower"); 708 | ImSearchDemo_TreeLeaf("Daisy"); 709 | ImSearchDemo_TreeLeaf("Orchid"); 710 | ImSearchDemo_TreeLeaf("Lily"); 711 | ImSearchDemo_TreeLeaf("Marigold"); 712 | ImSearchDemo_TreeLeaf("Daffodil"); 713 | ImSearchDemo_TreeLeaf("Chrysanthemum"); 714 | ImSearchDemo_TreeLeaf("Iris"); 715 | ImSearchDemo_TreePop(); 716 | } 717 | if (ImSearchDemo_TreeNode("Fungi")) 718 | { 719 | ImSearchDemo_TreeLeaf("Button Mushroom"); 720 | ImSearchDemo_TreeLeaf("Shiitake"); 721 | ImSearchDemo_TreeLeaf("Oyster Mushroom"); 722 | ImSearchDemo_TreeLeaf("Morel"); 723 | ImSearchDemo_TreeLeaf("Chanterelle"); 724 | ImSearchDemo_TreeLeaf("Truffle"); 725 | ImSearchDemo_TreeLeaf("Fly Agaric"); 726 | ImSearchDemo_TreeLeaf("Porcini"); 727 | ImSearchDemo_TreeLeaf("Puffball"); 728 | ImSearchDemo_TreeLeaf("Enoki"); 729 | ImSearchDemo_TreePop(); 730 | } 731 | ImSearchDemo_TreePop(); 732 | } 733 | 734 | ImSearchDemo_TreePop(); 735 | } 736 | if (ImSearchDemo_TreeNode("Culinary")) 737 | { 738 | if (ImSearchDemo_TreeNode("Ingredients")) 739 | { 740 | if (ImSearchDemo_TreeNode("Spices")) 741 | { 742 | ImSearchDemo_TreeLeaf("Salt"); 743 | ImSearchDemo_TreeLeaf("Pepper"); 744 | ImSearchDemo_TreeLeaf("Paprika"); 745 | ImSearchDemo_TreeLeaf("Cumin"); 746 | ImSearchDemo_TreeLeaf("Turmeric"); 747 | ImSearchDemo_TreeLeaf("Oregano"); 748 | ImSearchDemo_TreeLeaf("Basil"); 749 | ImSearchDemo_TreeLeaf("Thyme"); 750 | ImSearchDemo_TreeLeaf("Cinnamon"); 751 | ImSearchDemo_TreeLeaf("Nutmeg"); 752 | ImSearchDemo_TreePop(); 753 | } 754 | if (ImSearchDemo_TreeNode("Produce")) 755 | { 756 | ImSearchDemo_TreeLeaf("Carrot"); 757 | ImSearchDemo_TreeLeaf("Onion"); 758 | ImSearchDemo_TreeLeaf("Garlic"); 759 | ImSearchDemo_TreeLeaf("Pepper"); 760 | ImSearchDemo_TreeLeaf("Tomato"); 761 | ImSearchDemo_TreeLeaf("Lettuce"); 762 | ImSearchDemo_TreeLeaf("Spinach"); 763 | ImSearchDemo_TreeLeaf("Broccoli"); 764 | ImSearchDemo_TreeLeaf("Eggplant"); 765 | ImSearchDemo_TreeLeaf("Zucchini"); 766 | ImSearchDemo_TreePop(); 767 | } 768 | if (ImSearchDemo_TreeNode("Proteins")) 769 | { 770 | ImSearchDemo_TreeLeaf("Chicken"); 771 | ImSearchDemo_TreeLeaf("Beef"); 772 | ImSearchDemo_TreeLeaf("Pork"); 773 | ImSearchDemo_TreeLeaf("Tofu"); 774 | ImSearchDemo_TreeLeaf("Lentils"); 775 | ImSearchDemo_TreeLeaf("Fish"); 776 | ImSearchDemo_TreeLeaf("Eggs"); 777 | ImSearchDemo_TreeLeaf("Beans"); 778 | ImSearchDemo_TreeLeaf("Lamb"); 779 | ImSearchDemo_TreeLeaf("Turkey"); 780 | ImSearchDemo_TreePop(); 781 | } 782 | ImSearchDemo_TreePop(); 783 | } 784 | if (ImSearchDemo_TreeNode("Recipes")) 785 | { 786 | if (ImSearchDemo_TreeNode("Soups")) 787 | { 788 | ImSearchDemo_TreeLeaf("Chicken Noodle Soup"); 789 | ImSearchDemo_TreeLeaf("Tomato Soup"); 790 | ImSearchDemo_TreeLeaf("Miso Soup"); 791 | ImSearchDemo_TreeLeaf("Minestrone"); 792 | ImSearchDemo_TreeLeaf("Clam Chowder"); 793 | ImSearchDemo_TreeLeaf("Pho"); 794 | ImSearchDemo_TreeLeaf("Ramen"); 795 | ImSearchDemo_TreeLeaf("Gazpacho"); 796 | ImSearchDemo_TreeLeaf("Pumpkin Soup"); 797 | ImSearchDemo_TreeLeaf("Lentil Soup"); 798 | ImSearchDemo_TreePop(); 799 | } 800 | if (ImSearchDemo_TreeNode("Desserts")) 801 | { 802 | ImSearchDemo_TreeLeaf("Chocolate Cake"); 803 | ImSearchDemo_TreeLeaf("Apple Pie"); 804 | ImSearchDemo_TreeLeaf("Ice Cream"); 805 | ImSearchDemo_TreeLeaf("Brownies"); 806 | ImSearchDemo_TreeLeaf("Cheesecake"); 807 | ImSearchDemo_TreeLeaf("Pudding"); 808 | ImSearchDemo_TreeLeaf("Tiramisu"); 809 | ImSearchDemo_TreeLeaf("Crepes"); 810 | ImSearchDemo_TreeLeaf("Cupcakes"); 811 | ImSearchDemo_TreeLeaf("Macarons"); 812 | ImSearchDemo_TreePop(); 813 | } 814 | ImSearchDemo_TreePop(); 815 | } 816 | 817 | ImSearchDemo_TreePop(); 818 | } 819 | 820 | ImSearch::EndSearch(); 821 | } 822 | 823 | ImGui::TreePop(); 824 | } 825 | 826 | if (ImGui::TreeNode("Collapsing headers")) 827 | { 828 | if (ImSearch::BeginSearch()) 829 | { 830 | ImSearch::SearchBar(); 831 | 832 | if (ImSearchDemo_CollapsingHeader("TransformComponent")) 833 | { 834 | ImSearch::SearchableItem("Position", 835 | [](const char* name) 836 | { 837 | static float v[3]{}; 838 | ImGui::InputFloat3(name, v); 839 | return true; 840 | }); 841 | 842 | ImSearch::SearchableItem("Scale", 843 | [](const char* name) 844 | { 845 | static float v[3]{}; 846 | ImGui::InputFloat3(name, v); 847 | }); 848 | 849 | ImSearch::SearchableItem("Orientation", 850 | [](const char* name) 851 | { 852 | static float v[3]{}; 853 | ImGui::InputFloat3(name, v); 854 | }); 855 | 856 | ImSearch::PopSearchable(); 857 | } 858 | 859 | if (ImSearchDemo_CollapsingHeader("StaticMeshComponent")) 860 | { 861 | ImSearch::SearchableItem("Mesh", 862 | [](const char* fieldName) 863 | { 864 | static const char* selectedString = nouns[0]; 865 | if (ImGui::BeginCombo(fieldName, selectedString)) 866 | { 867 | if (ImSearch::BeginSearch()) 868 | { 869 | ImSearch::SearchBar(); 870 | for (const char* noun : nouns) 871 | { 872 | ImSearch::SearchableItem(noun, 873 | [&](const char* meshName) 874 | { 875 | const bool isSelected = meshName == selectedString; 876 | if (ImGui::Selectable(meshName, isSelected)) 877 | { 878 | selectedString = meshName; 879 | } 880 | return true; 881 | }); 882 | } 883 | 884 | ImSearch::EndSearch(); 885 | } 886 | ImGui::EndCombo(); 887 | } 888 | }); 889 | 890 | ImSearch::PopSearchable(); 891 | } 892 | 893 | if (ImSearchDemo_CollapsingHeader("PhysicsBodyComponent")) 894 | { 895 | ImSearch::SearchableItem("Mass", 896 | [](const char* name) 897 | { 898 | static float v{}; 899 | ImGui::InputFloat(name, &v); 900 | }); 901 | 902 | ImSearch::SearchableItem("Collision Enabled", 903 | [](const char* name) 904 | { 905 | static bool b{}; 906 | ImGui::Checkbox(name, &b); 907 | }); 908 | 909 | ImSearch::PopSearchable(); 910 | } 911 | 912 | ImSearch::EndSearch(); 913 | } 914 | 915 | ImGui::TreePop(); 916 | } 917 | 918 | if (ImGui::TreeNode("Artificially increasing priority for popular items")) 919 | { 920 | if (ImSearch::BeginSearch()) 921 | { 922 | HelpMarker( 923 | "A bonus can be applied which, ONLY when the user is searching,\n" 924 | "influences which items are shown to the user first.\n\n" 925 | "Gus Goose is textually irrelevant, but because of his high\n" 926 | "bonus, he is the first result."); 927 | ImSearch::SearchBar(); 928 | static bool doOnceDummy = [] { ImSearch::SetUserQuery("Duck"); return true; }(); 929 | (void)doOnceDummy; 930 | 931 | static std::array, 15> namesAndBonuses 932 | { 933 | std::pair{ "Scrooge McDuck", 0.95f }, 934 | std::pair{ "Della Duck", 0.88f }, 935 | std::pair{ "Huey Dewey & Louie Duck", 0.93f }, 936 | std::pair{ "Donald Duck", 1.0f }, 937 | std::pair{ "Grandma Duck", 0.82f }, 938 | std::pair{ "Gus Goose", 5.0f }, 939 | std::pair{ "Sir Quackly McDuck", 0.68f }, 940 | std::pair{ "Fethry Duck", 0.75f }, 941 | std::pair{ "Dugan Duck", 0.65f }, 942 | std::pair{ "Sir Roast McDuck", 0.42f }, 943 | std::pair{ "Dudly D. Duck", 0.45f }, 944 | std::pair{ "Matilda McDuck", 0.58f }, 945 | std::pair{ "Donna Duck", 0.55f }, 946 | std::pair{ "Pothole McDuck", 0.52f }, 947 | std::pair{ "Daphne Duck-Gander", 0.60f }, 948 | }; 949 | 950 | for (auto& nameAndBonus : namesAndBonuses) 951 | { 952 | if (ImSearch::PushSearchable(nameAndBonus.first, 953 | [&](const char* name) 954 | { 955 | ImGui::SliderFloat(name, &nameAndBonus.second, -1.0f, 1.0f); 956 | return false; 957 | })) 958 | { 959 | ImSearch::SetRelevancyBonus(nameAndBonus.second); 960 | ImSearch::PopSearchable(); 961 | } 962 | } 963 | 964 | ImSearch::EndSearch(); 965 | } 966 | ImGui::TreePop(); 967 | } 968 | 969 | if (ImGui::TreeNode("Synonyms")) 970 | { 971 | if (ImSearch::BeginSearch()) 972 | { 973 | HelpMarker("ImSearch supports synonyms, useful, if no one can agree on a single name. " 974 | 975 | "For example, \"Class\" has the synonyms \"Type\" and \"Struct\"." 976 | "Try searching for \"Struct\"; you'll see \"Class\" rise up to the top!"); 977 | ImSearch::SearchBar(); 978 | 979 | auto selectableCallback = [](const char* name) { ImGui::Selectable(name); return false; }; 980 | 981 | if (ImSearch::PushSearchable("Branch", selectableCallback)) 982 | { 983 | ImSearch::AddSynonym("If"); 984 | ImSearch::AddSynonym("Condition"); 985 | 986 | ImSearch::PopSearchable(); 987 | } 988 | 989 | if (ImSearch::PushSearchable("Function", selectableCallback)) 990 | { 991 | ImSearch::AddSynonym("Method"); 992 | ImSearch::AddSynonym("Procedure"); 993 | 994 | ImSearch::PopSearchable(); 995 | } 996 | 997 | if (ImSearch::PushSearchable("Variable", selectableCallback)) 998 | { 999 | ImSearch::AddSynonym("Identifier"); 1000 | ImSearch::AddSynonym("Container"); 1001 | 1002 | ImSearch::PopSearchable(); 1003 | } 1004 | 1005 | if (ImSearch::PushSearchable("Array", selectableCallback)) 1006 | { 1007 | ImSearch::AddSynonym("List"); 1008 | ImSearch::AddSynonym("Collection"); 1009 | 1010 | ImSearch::PopSearchable(); 1011 | } 1012 | 1013 | if (ImSearch::PushSearchable("Class", selectableCallback)) 1014 | { 1015 | ImSearch::AddSynonym("Type"); 1016 | ImSearch::AddSynonym("Struct"); 1017 | 1018 | ImSearch::PopSearchable(); 1019 | } 1020 | 1021 | ImSearch::EndSearch(); 1022 | } 1023 | 1024 | ImGui::TreePop(); 1025 | } 1026 | 1027 | ImGui::End(); 1028 | } 1029 | 1030 | namespace 1031 | { 1032 | size_t Rand(size_t& seed) 1033 | { 1034 | seed ^= seed << 13; 1035 | seed ^= seed >> 17; 1036 | seed ^= seed << 5; 1037 | return seed; 1038 | } 1039 | 1040 | const char* GetRandomString(size_t& seed, std::string& str) 1041 | { 1042 | const char* adjective = adjectives[Rand(seed) % adjectives.size()]; 1043 | const char* noun = nouns[Rand(seed) % nouns.size()]; 1044 | 1045 | str.clear(); 1046 | str.append(adjective); 1047 | str.push_back(' '); 1048 | str.append(noun); 1049 | 1050 | return str.c_str(); 1051 | } 1052 | 1053 | void HelpMarker(const char* desc) 1054 | { 1055 | ImGui::TextDisabled("(?)"); 1056 | if (ImGui::BeginItemTooltip()) 1057 | { 1058 | ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); 1059 | ImGui::TextUnformatted(desc); 1060 | ImGui::PopTextWrapPos(); 1061 | ImGui::EndTooltip(); 1062 | } 1063 | } 1064 | 1065 | bool ImSearchDemo_TreeNode(const char* name) 1066 | { 1067 | return ImSearch::PushSearchable(name, 1068 | [](const char* nodeName) 1069 | { 1070 | return ImGui::TreeNode(nodeName); 1071 | }); 1072 | } 1073 | 1074 | void ImSearchDemo_TreeLeaf(const char* name) 1075 | { 1076 | ImSearch::SearchableItem(name, 1077 | [](const char* leafName) 1078 | { 1079 | ImGui::Selectable(leafName); 1080 | }); 1081 | } 1082 | 1083 | void ImSearchDemo_TreePop() 1084 | { 1085 | ImSearch::PopSearchable( 1086 | []() 1087 | { 1088 | ImGui::TreePop(); 1089 | }); 1090 | } 1091 | 1092 | bool ImSearchDemo_CollapsingHeader(const char* name) 1093 | { 1094 | return ImSearch::PushSearchable(name, 1095 | [](const char* headerName) 1096 | { 1097 | return ImGui::CollapsingHeader(headerName); 1098 | }); 1099 | } 1100 | } 1101 | #endif // #ifndef IMGUI_DISABLE 1102 | -------------------------------------------------------------------------------- /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 | IM_ASSERT(ctx != nullptr); 68 | IM_ASSERT(ctx->mCutoffStack.empty() && "There were more calls to PushCutoffStrength than to PopCutoffStrength"); 69 | 70 | if (sContext == ctx) 71 | { 72 | SetCurrentContext(nullptr); 73 | } 74 | IM_DELETE(ctx); 75 | } 76 | 77 | ImSearch::ImSearchContext* ImSearch::GetCurrentContext() 78 | { 79 | return sContext; 80 | } 81 | 82 | void ImSearch::SetCurrentContext(ImSearchContext* ctx) 83 | { 84 | sContext = ctx; 85 | } 86 | 87 | bool ImSearch::BeginSearch(ImSearchFlags flags) 88 | { 89 | const ImGuiID imId = ImGui::GetID("Search"); 90 | ImGui::PushID(static_cast(imId)); 91 | 92 | ImSearchContext& context = GetImSearchContext(); 93 | LocalContext& localContext = context.mContexts[imId]; 94 | context.mContextStack.emplace(localContext); 95 | 96 | localContext.mHasSubmitted = false; 97 | localContext.mInput.mFlags = flags; 98 | 99 | return true; 100 | } 101 | 102 | void ImSearch::SearchBar(const char* hint) 103 | { 104 | LocalContext& context = GetLocalContext(); 105 | 106 | ImGui::SetNextItemWidth(-FLT_MIN); 107 | ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_F); 108 | 109 | std::string& userQuery = context.mInput.mUserQuery; 110 | 111 | if (ImGui::IsWindowAppearing()) 112 | { 113 | ImGui::SetKeyboardFocusHere(); 114 | userQuery.clear(); 115 | } 116 | 117 | ImGui::InputTextWithHint("##SearchBar", 118 | hint, 119 | const_cast(userQuery.c_str()), 120 | userQuery.capacity() + 1, 121 | ImGuiInputTextFlags_CallbackResize | ImGuiInputTextFlags_CallbackCompletion, 122 | +[](ImGuiInputTextCallbackData* data) -> int 123 | { 124 | LocalContext* capturedContext = static_cast(data->UserData); 125 | std::string& capturedQuery = capturedContext->mInput.mUserQuery; 126 | 127 | if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) 128 | { 129 | // Resize string callback 130 | // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want. 131 | IM_ASSERT(data->Buf == capturedQuery.c_str()); 132 | capturedQuery.resize(static_cast(data->BufTextLen)); 133 | data->Buf = const_cast(capturedQuery.c_str()); 134 | } 135 | if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) 136 | { 137 | const std::string& newLastToken = capturedContext->mResult.mOutput.mPreviewText; 138 | 139 | if (newLastToken.empty()) 140 | { 141 | return 0; 142 | } 143 | 144 | const std::vector splitQuery = SplitTokens(capturedQuery); 145 | 146 | if (splitQuery.empty()) 147 | { 148 | return 0; 149 | } 150 | 151 | const StrView oldLastToken = splitQuery.back(); 152 | 153 | // Find the start of the last token 154 | IndexT startOfLastToken = static_cast(capturedQuery.size() - oldLastToken.size()); 155 | while (startOfLastToken > 0) 156 | { 157 | if (StrView{ capturedQuery.data() + startOfLastToken, oldLastToken.size() } == oldLastToken) 158 | { 159 | break; 160 | } 161 | --startOfLastToken; 162 | } 163 | 164 | data->DeleteChars(static_cast(startOfLastToken), static_cast(capturedQuery.size()) - static_cast(startOfLastToken)); 165 | data->InsertChars(static_cast(startOfLastToken), newLastToken.c_str(), newLastToken.c_str() + newLastToken.size()); 166 | } 167 | return 0; 168 | }, 169 | &context); 170 | 171 | // Some logic for drawing the search icon 172 | const ImVec2 hintSize = ImGui::CalcTextSize(hint); 173 | ImRect availRect{ ImGui::GetItemRectMin(), ImGui::GetItemRectMax() }; 174 | availRect.Min.x += hintSize.x + ImGui::GetStyle().FramePadding.x * 2.0f; 175 | availRect.Max.x = std::max(availRect.Min.x, availRect.Max.x); 176 | 177 | const float barHalfHeight = availRect.GetHeight() * .5f; 178 | const ImVec2 iconCentre = { availRect.Max.x - barHalfHeight, (availRect.Min.y + availRect.Max.y) * 0.5f }; 179 | 180 | const float lensRadius = barHalfHeight * .5f; 181 | const ImVec2 lensCentre = { iconCentre.x - barHalfHeight * .2f, iconCentre.y - barHalfHeight * .2f }; 182 | 183 | const float handleLength = barHalfHeight * .4f; 184 | static constexpr float halfSqrt2 = 0.707106781187f; 185 | 186 | const ImVec2 handleTopLeft = { lensCentre.x + lensRadius * halfSqrt2, lensCentre.y + lensRadius * halfSqrt2 }; 187 | const ImVec2 handleBottomRight = { handleTopLeft.x + handleLength, handleTopLeft.y + handleLength }; 188 | 189 | const ImVec2 iconTopLeft{ lensCentre.x - lensRadius, lensCentre.y - lensRadius }; 190 | const ImRect iconRect{ iconTopLeft, handleBottomRight }; 191 | 192 | ImDrawList* drawList = ImGui::GetWindowDrawList(); 193 | const ImU32 disabledTextCol = ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); 194 | 195 | if (availRect.Contains(iconRect)) 196 | { 197 | drawList->AddCircle(lensCentre, lensRadius, disabledTextCol); 198 | drawList->AddLine(handleTopLeft, handleBottomRight, disabledTextCol); 199 | } 200 | 201 | if (!userQuery.empty() 202 | && ImGui::IsItemFocused()) 203 | { 204 | // If the preview text was not set by us, 205 | // it may not 'complete' what has currently 206 | // been typed. Check if this is the case. 207 | const std::vector splitQuery = SplitTokens(userQuery); 208 | 209 | if (!splitQuery.empty()) 210 | { 211 | StrView previewToDisplay = GetStringNeededToCompletePartial(splitQuery.back(), context.mResult.mOutput.mPreviewText); 212 | 213 | if (previewToDisplay.size() > 0) 214 | { 215 | ImVec2 completePreviewPos = ImGui::GetItemRectMin(); 216 | completePreviewPos.x += ImGui::CalcTextSize(userQuery.c_str(), userQuery.c_str() + userQuery.size()).x; 217 | 218 | completePreviewPos.x += ImGui::GetStyle().FramePadding.x; 219 | completePreviewPos.y += ImGui::GetStyle().FramePadding.y; 220 | 221 | drawList->AddText(completePreviewPos, disabledTextCol, previewToDisplay.begin(), previewToDisplay.end()); 222 | } 223 | } 224 | } 225 | } 226 | 227 | void ImSearch::Submit() 228 | { 229 | LocalContext& context = GetLocalContext(); 230 | Result& lastValidResult = context.mResult; 231 | 232 | if (!IsResultUpToDate(lastValidResult, context.mInput)) 233 | { 234 | lastValidResult.mInput = context.mInput; 235 | BringResultUpToDate(lastValidResult); 236 | } 237 | 238 | DisplayToUser(context, lastValidResult); 239 | 240 | context.mHasSubmitted = true; 241 | context.mInput.mEntries.clear(); 242 | context.mDisplayCallbacks.clear(); 243 | IM_ASSERT(context.mPushStackLevel == 0 && "There were more calls to PushSearchable than to PopSearchable"); 244 | } 245 | 246 | void ImSearch::EndSearch() 247 | { 248 | LocalContext& localContext = GetLocalContext(); 249 | 250 | if (!localContext.mHasSubmitted) 251 | { 252 | Submit(); 253 | } 254 | 255 | ImGui::PopID(); 256 | 257 | ImSearch::ImSearchContext& context = GetImSearchContext(); 258 | context.mContextStack.pop(); 259 | } 260 | 261 | bool ImSearch::Internal::PushSearchable(const char* name, void* functor, VTable vTable) 262 | { 263 | LocalContext& context = GetLocalContext(); 264 | 265 | IM_ASSERT(name != nullptr); 266 | IM_ASSERT(!context.mHasSubmitted && "Tried calling PushSearchable after EndSearch or Submit"); 267 | 268 | if (!CanCollectSubmissions()) 269 | { 270 | // Invoke the callback immediately if the user 271 | // is not actively searching, for performance 272 | // and memory reasons. 273 | const bool pushResult = Callback::InvokeAsPushSearchable(vTable, functor, name); 274 | 275 | // We only expect a call to PopSearchable if the callback returned true. 276 | // So we only increment the depth if the callback returned true. 277 | if (pushResult) 278 | { 279 | context.mPushStackLevel++; 280 | } 281 | 282 | return pushResult; 283 | } 284 | 285 | context.mInput.mEntries.emplace_back(); 286 | Searchable& searchable = context.mInput.mEntries.back(); 287 | searchable.mText = std::string{ name }; 288 | 289 | context.mDisplayCallbacks.emplace_back(); 290 | if (functor != nullptr 291 | && vTable != nullptr) 292 | { 293 | context.mDisplayCallbacks.back().mOnDisplayStart = Callback{ functor, vTable }; 294 | } 295 | 296 | const IndexT currentIndex = static_cast(context.mInput.mEntries.size() - static_cast(1ull)); 297 | 298 | if (!context.mPushStack.empty()) 299 | { 300 | const IndexT parentIndex = context.mPushStack.top(); 301 | searchable.mIndexOfParent = parentIndex; 302 | 303 | Searchable& parent = context.mInput.mEntries[parentIndex]; 304 | 305 | if (parent.mIndexOfFirstChild == sNullIndex) 306 | { 307 | parent.mIndexOfFirstChild = currentIndex; 308 | parent.mIndexOfLastChild = currentIndex; 309 | } 310 | else 311 | { 312 | const IndexT prevIndex = parent.mIndexOfLastChild; 313 | parent.mIndexOfLastChild = currentIndex; 314 | IM_ASSERT(prevIndex != sNullIndex); 315 | 316 | Searchable& prevSibling = context.mInput.mEntries[prevIndex]; 317 | prevSibling.mIndexOfNextSibling = currentIndex; 318 | } 319 | } 320 | 321 | context.mPushStack.emplace(currentIndex); 322 | context.mPushStackLevel++; 323 | return true; 324 | } 325 | 326 | void ImSearch::PopSearchable() 327 | { 328 | Internal::PopSearchable(nullptr, nullptr); 329 | } 330 | 331 | void ImSearch::PushCutoffStrength(float value) 332 | { 333 | ImSearchContext& context = GetImSearchContext(); 334 | context.mCutoffStack.emplace(value); 335 | } 336 | 337 | void ImSearch::PopCutoffStrength() 338 | { 339 | ImSearchContext& context = GetImSearchContext(); 340 | 341 | IM_ASSERT(!context.mCutoffStack.empty() && "Called PopCutoffStrength too many times!"); 342 | context.mCutoffStack.pop(); 343 | } 344 | 345 | void ImSearch::Internal::PopSearchable(void* functor, VTable vTable) 346 | { 347 | LocalContext& context = GetLocalContext(); 348 | 349 | context.mPushStackLevel--; 350 | IM_ASSERT(context.mPushStackLevel >= 0 && "Called PopSearchable too many times!"); 351 | 352 | if (!CanCollectSubmissions()) 353 | { 354 | if (functor != nullptr 355 | && vTable != nullptr) 356 | { 357 | Callback::InvokeAsPopSearchable(vTable, functor); 358 | } 359 | 360 | return; 361 | } 362 | 363 | const IndexT indexOfCurrentCategory = GetCurrentItem(context); 364 | 365 | if (functor != nullptr 366 | && vTable != nullptr) 367 | { 368 | context.mDisplayCallbacks[indexOfCurrentCategory].mOnDisplayEnd = Callback{ functor, vTable }; 369 | } 370 | 371 | context.mPushStack.pop(); 372 | } 373 | 374 | void ImSearch::SetRelevancyBonus(float bonus) 375 | { 376 | if (!CanCollectSubmissions()) 377 | { 378 | return; 379 | } 380 | 381 | LocalContext& context = GetLocalContext(); 382 | const IndexT indexOfCurrentCategory = GetCurrentItem(context); 383 | 384 | context.mInput.mBonuses.resize(indexOfCurrentCategory + 1); 385 | context.mInput.mBonuses[indexOfCurrentCategory] = bonus; 386 | } 387 | 388 | void ImSearch::AddSynonym(const char* synonym) 389 | { 390 | // Ensure there is an active 'parent' item 391 | // to add the synonym to; no point in adding 392 | // synonyms at the root level, and if you did, 393 | // you did something wrong. 394 | LocalContext& context = GetLocalContext(); 395 | 396 | IM_UNUSED(context); 397 | IM_ASSERT(context.mPushStackLevel > 0 && "AddSynonym must be called between PushSearchable and PopSearchable. See imsearch_demo.cpp"); 398 | 399 | if (!CanCollectSubmissions()) 400 | { 401 | return; 402 | } 403 | 404 | if (Internal::PushSearchable(synonym, nullptr, nullptr)) 405 | { 406 | PopSearchable(); 407 | } 408 | } 409 | 410 | void ImSearch::SetUserQuery(const char* query) 411 | { 412 | LocalContext& context = GetLocalContext(); 413 | context.mInput.mUserQuery = std::string{ query }; 414 | } 415 | 416 | const char* ImSearch::GetUserQuery() 417 | { 418 | LocalContext& context = GetLocalContext(); 419 | return context.mInput.mUserQuery.c_str(); 420 | } 421 | 422 | ImSearchStyle& ImSearch::GetStyle() 423 | { 424 | ImSearchContext& context = GetImSearchContext(); 425 | return context.Style; 426 | } 427 | 428 | ImU32 ImSearch::GetColorU32(ImSearchCol idx, float alpha_mul) 429 | { 430 | ImVec4 c = GetStyleColorVec4(idx); 431 | c.w *= ImGui::GetStyle().Alpha * alpha_mul; 432 | return ImGui::ColorConvertFloat4ToU32(c); 433 | } 434 | 435 | const ImVec4& ImSearch::GetStyleColorVec4(ImSearchCol idx) 436 | { 437 | ImSearchStyle& style = GetStyle(); 438 | return style.Colors[idx]; 439 | } 440 | 441 | void ImSearch::PushStyleColor(ImSearchCol idx, ImU32 col) 442 | { 443 | PushStyleColor(idx, ImGui::ColorConvertU32ToFloat4(col)); 444 | } 445 | 446 | void ImSearch::PushStyleColor(ImSearchCol idx, const ImVec4& col) 447 | { 448 | ImSearchContext& context = GetImSearchContext(); 449 | 450 | ImGuiColorMod backup; 451 | backup.Col = static_cast(idx); 452 | backup.BackupValue = context.Style.Colors[idx]; 453 | context.ColorModifiers.push_back(backup); 454 | context.Style.Colors[idx] = col; 455 | } 456 | 457 | void ImSearch::PopStyleColor(int count) 458 | { 459 | ImSearchContext& context = GetImSearchContext(); 460 | IM_ASSERT_USER_ERROR(count <= context.ColorModifiers.size(), "You can't pop more modifiers than have been pushed!"); 461 | while (count > 0) 462 | { 463 | ImGuiColorMod& backup = context.ColorModifiers.back(); 464 | context.Style.Colors[backup.Col] = backup.BackupValue; 465 | context.ColorModifiers.pop_back(); 466 | count--; 467 | } 468 | } 469 | 470 | //----------------------------------------------------------------------------- 471 | // [SECTION] Definitions from static functions 472 | //----------------------------------------------------------------------------- 473 | 474 | bool ImSearch::IsResultUpToDate(const Result& oldResult, const Input& currentInput) 475 | { 476 | return oldResult.mInput == currentInput; 477 | } 478 | 479 | void ImSearch::BringResultUpToDate(Result& result) 480 | { 481 | AssignInitialScores(result.mInput, result.mBuffers); 482 | PropagateScoreToChildren(result.mInput, result.mBuffers); 483 | PropagateScoreToParents(result.mInput, result.mBuffers); 484 | GenerateDisplayOrder(result.mInput, result.mBuffers, result.mOutput); 485 | FindStringToAppendOnAutoComplete(result.mInput, result.mOutput); 486 | } 487 | 488 | void ImSearch::AssignInitialScores(const Input& input, ReusableBuffers& buffers) 489 | { 490 | buffers.mScores.clear(); 491 | buffers.mScores.resize(input.mEntries.size()); 492 | 493 | const StrView query = input.mUserQuery; 494 | const std::string tokenSortedQuery = MakeTokenisedString(input.mUserQuery); 495 | 496 | for (IndexT i = 0; i < static_cast(input.mEntries.size()); i++) 497 | { 498 | const Searchable& entry = input.mEntries[i]; 499 | 500 | float score = WeightedRatio(query, 501 | tokenSortedQuery, 502 | entry.mText, 503 | GetMemoizedTokenisedString(entry.mText), 504 | buffers); 505 | 506 | if (i < static_cast(input.mBonuses.size())) 507 | { 508 | score += input.mBonuses[i]; 509 | } 510 | 511 | buffers.mScores[i] = score; 512 | } 513 | } 514 | 515 | void ImSearch::PropagateScoreToChildren(const Input& input, ReusableBuffers& buffers) 516 | { 517 | // Each node can only be the child of ONE parent. 518 | // Children can only be submitted AFTER their parent. 519 | // When iterating over the nodes, we will always 520 | // encounter a parent before a child. 521 | for (IndexT parentIndex = 0; parentIndex < static_cast(input.mEntries.size()); parentIndex++) 522 | { 523 | const Searchable& parent = input.mEntries[parentIndex]; 524 | const float parentScore = buffers.mScores[parentIndex]; 525 | 526 | for (IndexT childIndex = parent.mIndexOfFirstChild; 527 | childIndex != sNullIndex; 528 | childIndex = input.mEntries[childIndex].mIndexOfNextSibling) 529 | { 530 | float& childScore = buffers.mScores[childIndex]; 531 | childScore = std::max(childScore, parentScore); 532 | } 533 | } 534 | } 535 | 536 | void ImSearch::PropagateScoreToParents(const Input& input, ReusableBuffers& buffers) 537 | { 538 | // Children can only be submitted AFTER their parent. 539 | // When iterating over the entries in reverse, we will 540 | // always reach a node's child before the node itself. 541 | for (IndexT childIndex = static_cast(input.mEntries.size()); childIndex-- > 0;) 542 | { 543 | const IndexT parentIndex = input.mEntries[childIndex].mIndexOfParent; 544 | 545 | if (parentIndex == sNullIndex) 546 | { 547 | continue; 548 | } 549 | 550 | const float childScore = buffers.mScores[childIndex]; 551 | float& parentScore = buffers.mScores[parentIndex]; 552 | parentScore = std::max(parentScore, childScore); 553 | } 554 | } 555 | 556 | void ImSearch::GenerateDisplayOrder(const Input& input, ReusableBuffers& buffers, Output& output) 557 | { 558 | output.mDisplayOrder.clear(); 559 | buffers.mTempIndices.clear(); 560 | 561 | float cutoffStrength = GetCutoffStrength(); 562 | 563 | for (IndexT i = 0; i < static_cast(input.mEntries.size()); i++) 564 | { 565 | if (input.mEntries[i].mIndexOfParent == sNullIndex 566 | && buffers.mScores[i] >= cutoffStrength) 567 | { 568 | buffers.mTempIndices.emplace_back(i); 569 | } 570 | } 571 | 572 | AppendToDisplayOrder(input, 573 | buffers, 574 | 0, 575 | static_cast(buffers.mTempIndices.size()), 576 | output); 577 | } 578 | 579 | void ImSearch::AppendToDisplayOrder(const Input& input, 580 | ReusableBuffers& buffers, 581 | IndexT startInIndicesBuffer, 582 | IndexT endInIndicesBuffer, 583 | Output& output) 584 | { 585 | float cutoffStrength = GetCutoffStrength(); 586 | 587 | std::stable_sort(buffers.mTempIndices.begin() + startInIndicesBuffer, 588 | buffers.mTempIndices.begin() + endInIndicesBuffer, 589 | [&](IndexT lhsIndex, IndexT rhsIndex) -> bool 590 | { 591 | const float lhsScore = buffers.mScores[lhsIndex]; 592 | const float rhsScore = buffers.mScores[rhsIndex]; 593 | 594 | return lhsScore > rhsScore; 595 | }); 596 | 597 | for (IndexT indexInIndicesBuffer = startInIndicesBuffer; indexInIndicesBuffer < endInIndicesBuffer; indexInIndicesBuffer++) 598 | { 599 | IndexT searchableIndex = buffers.mTempIndices[indexInIndicesBuffer]; 600 | 601 | const IndexT nextStartInIndicesBuffer = static_cast(buffers.mTempIndices.size()); 602 | 603 | output.mDisplayOrder.emplace_back(searchableIndex); 604 | 605 | const Searchable& searchable = input.mEntries[searchableIndex]; 606 | for (IndexT childIndex = searchable.mIndexOfFirstChild; 607 | childIndex != sNullIndex; 608 | childIndex = input.mEntries[childIndex].mIndexOfNextSibling) 609 | { 610 | if (buffers.mScores[childIndex] >= cutoffStrength) 611 | { 612 | buffers.mTempIndices.emplace_back(childIndex); 613 | } 614 | } 615 | 616 | const IndexT nextEndInIndicesBuffer = static_cast(buffers.mTempIndices.size()); 617 | 618 | AppendToDisplayOrder(input, 619 | buffers, 620 | nextStartInIndicesBuffer, 621 | nextEndInIndicesBuffer, 622 | output); 623 | 624 | output.mDisplayOrder.emplace_back(searchableIndex | Output::sDisplayEndFlag); 625 | } 626 | } 627 | 628 | void ImSearch::FindStringToAppendOnAutoComplete(const Input& input, Output& output) 629 | { 630 | output.mPreviewText.clear(); 631 | 632 | const std::vector tokensInQuery = SplitTokens(input.mUserQuery); 633 | 634 | if (tokensInQuery.empty()) 635 | { 636 | return; 637 | } 638 | 639 | const StrView tokenToComplete = tokensInQuery.back(); 640 | 641 | const std::vector& displayOrder = output.mDisplayOrder; 642 | 643 | for (auto it = displayOrder.begin(); it != displayOrder.end(); ++it) 644 | { 645 | const IndexT indexAndFlag = *it; 646 | const IndexT index = indexAndFlag & ~Output::sDisplayEndFlag; 647 | const IndexT isEnd = indexAndFlag & Output::sDisplayEndFlag; 648 | 649 | if (isEnd) 650 | { 651 | continue; 652 | } 653 | 654 | const Searchable& searchable = input.mEntries[index]; 655 | const std::vector tokens = SplitTokens(searchable.mText); 656 | 657 | for (const StrView token : tokens) 658 | { 659 | if (GetStringNeededToCompletePartial(tokenToComplete, token).mSize != 0) 660 | { 661 | output.mPreviewText = { token.data(), token.size() }; 662 | return; 663 | } 664 | } 665 | } 666 | } 667 | 668 | void ImSearch::DisplayToUser(const LocalContext& context, const Result& result) 669 | { 670 | const std::string& userQuery = result.mInput.mUserQuery; 671 | const bool isUserSearching = !userQuery.empty(); 672 | ImGui::PushID(isUserSearching); 673 | 674 | const std::vector& displayOrder = result.mOutput.mDisplayOrder; 675 | 676 | const bool hasHighlighting = (context.mInput.mFlags & ImSearchFlags_NoTextHighlighting) == 0; 677 | if (hasHighlighting) 678 | { 679 | BeginHighlightZone(userQuery.c_str()); 680 | } 681 | 682 | for (auto it = displayOrder.begin(); it != displayOrder.end(); ++it) 683 | { 684 | const IndexT indexAndFlag = *it; 685 | const IndexT index = indexAndFlag & ~Output::sDisplayEndFlag; 686 | const IndexT isEnd = indexAndFlag & Output::sDisplayEndFlag; 687 | 688 | const Searchable& searchable = result.mInput.mEntries[index]; 689 | const DisplayCallbacks& callbacks = context.mDisplayCallbacks[index]; 690 | 691 | if (isEnd) 692 | { 693 | if (callbacks.mOnDisplayEnd) 694 | { 695 | callbacks.mOnDisplayEnd(); 696 | } 697 | continue; 698 | } 699 | 700 | if (!callbacks.mOnDisplayStart) 701 | { 702 | continue; 703 | } 704 | 705 | if (isUserSearching) 706 | { 707 | ImGui::SetNextItemOpen(true, ImGuiCond_Once); 708 | } 709 | 710 | if (callbacks.mOnDisplayStart(searchable.mText.c_str())) 711 | { 712 | continue; 713 | } 714 | 715 | // The user start function was called, but returned false. 716 | // We need to avoid displaying this searchable's children, 717 | // and make sure we call the corresponding mOnDisplayEnd 718 | it = std::find(it + 1, displayOrder.end(), index | Output::sDisplayEndFlag); 719 | IM_ASSERT(it != displayOrder.end()); 720 | } 721 | 722 | if (hasHighlighting) 723 | { 724 | EndHighlightZone(); 725 | } 726 | 727 | ImGui::PopID(); 728 | } 729 | 730 | bool ImSearch::DoDrawnGlyphsMatch(const ImDrawList& lhsList, 731 | const ImDrawIdx* lhsStart, 732 | const ImDrawList& rhsList, 733 | const ImDrawIdx* rhsStart, 734 | int length) 735 | { 736 | for (int i = 0; i < length; i++) 737 | { 738 | const ImVec2 expectedCurr = lhsList.VtxBuffer[lhsStart[i]].uv; 739 | const ImVec2 actualCurr = rhsList.VtxBuffer[rhsStart[i]].uv; 740 | 741 | // use std::not_equal_to instead of != to silence -Wfloat-equal 742 | // warnings, for those that want to integrate this library 743 | // with a stricter codebase. 744 | if (std::not_equal_to{}(expectedCurr.x, actualCurr.x) 745 | || std::not_equal_to{}(expectedCurr.y, actualCurr.y)) 746 | { 747 | return false; 748 | } 749 | } 750 | 751 | return true; 752 | } 753 | 754 | //----------------------------------------------------------------------------- 755 | // [SECTION] Definitions from imsearch_internal.h 756 | //----------------------------------------------------------------------------- 757 | 758 | ImSearch::Callback::Callback(void* originalFunctor, ImSearch::Internal::VTable vTable) : 759 | mVTable(vTable) 760 | { 761 | if (mVTable == nullptr) 762 | { 763 | return; 764 | } 765 | 766 | int size; 767 | vTable(VTableModes::GetSize, &size, nullptr); 768 | mUserFunctor = ImGui::MemAlloc(static_cast(size)); 769 | IM_ASSERT(mUserFunctor != nullptr); 770 | vTable(VTableModes::MoveConstruct, originalFunctor, mUserFunctor); 771 | } 772 | 773 | ImSearch::Callback::Callback(Callback&& other) noexcept : 774 | mVTable(other.mVTable), 775 | mUserFunctor(other.mUserFunctor) 776 | { 777 | other.mVTable = nullptr; 778 | other.mUserFunctor = nullptr; 779 | } 780 | 781 | ImSearch::Callback& ImSearch::Callback::operator=(Callback&& other) noexcept 782 | { 783 | if (this == &other) 784 | { 785 | return *this; 786 | } 787 | 788 | ClearData(); 789 | 790 | mVTable = other.mVTable; 791 | mUserFunctor = other.mUserFunctor; 792 | 793 | other.mVTable = nullptr; 794 | other.mUserFunctor = nullptr; 795 | 796 | return *this; 797 | } 798 | 799 | ImSearch::Callback::~Callback() 800 | { 801 | ClearData(); 802 | } 803 | 804 | bool ImSearch::Callback::operator()(const char* name) const 805 | { 806 | return InvokeAsPushSearchable(mVTable, mUserFunctor, name); 807 | } 808 | 809 | void ImSearch::Callback::operator()() const 810 | { 811 | InvokeAsPopSearchable(mVTable, mUserFunctor); 812 | } 813 | 814 | bool ImSearch::Callback::InvokeAsPushSearchable(ImSearch::Internal::VTable vTable, void* userFunctor, const char* name) 815 | { 816 | return vTable(VTableModes::Invoke, userFunctor, const_cast(name)); 817 | } 818 | 819 | void ImSearch::Callback::InvokeAsPopSearchable(ImSearch::Internal::VTable vTable, void* userFunctor) 820 | { 821 | vTable(VTableModes::Invoke, userFunctor, nullptr); 822 | } 823 | 824 | void ImSearch::Callback::ClearData() 825 | { 826 | if (mUserFunctor == nullptr) 827 | { 828 | return; 829 | } 830 | 831 | mVTable(VTableModes::Destruct, mUserFunctor, nullptr); 832 | 833 | ImGui::MemFree(mUserFunctor); 834 | mUserFunctor = nullptr; 835 | } 836 | 837 | bool ImSearch::operator==(const StrView& lhs, const StrView& rhs) 838 | { 839 | return lhs.mSize == rhs.mSize && 840 | ( 841 | (lhs.mData == nullptr && rhs.mData == nullptr) 842 | || memcmp(lhs.mData, rhs.mData, lhs.mSize) == 0 843 | ); 844 | } 845 | 846 | bool ImSearch::operator==(const Searchable& lhs, const Searchable& rhs) 847 | { 848 | return lhs.mText == rhs.mText 849 | && lhs.mIndexOfFirstChild == rhs.mIndexOfFirstChild 850 | && lhs.mIndexOfLastChild == rhs.mIndexOfLastChild 851 | && lhs.mIndexOfParent == rhs.mIndexOfParent 852 | && lhs.mIndexOfNextSibling == rhs.mIndexOfNextSibling; 853 | } 854 | 855 | bool ImSearch::operator==(const Input& lhs, const Input& rhs) 856 | { 857 | return lhs.mFlags == rhs.mFlags 858 | && lhs.mUserQuery == rhs.mUserQuery 859 | && lhs.mEntries == rhs.mEntries 860 | && lhs.mBonuses == rhs.mBonuses; 861 | } 862 | 863 | ImSearch::LocalContext& ImSearch::GetLocalContext() 864 | { 865 | ImSearch::ImSearchContext& ImSearchContext = GetImSearchContext(); 866 | IM_ASSERT(!ImSearchContext.mContextStack.empty() && "Not currently in between a ImSearch::BeginSearch and ImSearch::EndSearch"); 867 | return ImSearchContext.mContextStack.top(); 868 | } 869 | 870 | ImSearch::ImSearchContext& ImSearch::GetImSearchContext() 871 | { 872 | ImSearch::ImSearchContext* context = ImSearch::GetCurrentContext(); 873 | IM_ASSERT(sContext != nullptr && "An ImSearchContext has not been created, see ImSearch::CreateContext"); 874 | return *context; 875 | } 876 | 877 | ImSearch::IndexT ImSearch::GetCurrentItem(ImSearch::LocalContext& context) 878 | { 879 | IM_ASSERT(!context.mPushStack.empty() && "No active object; can only be called after PushSearchable"); 880 | IM_ASSERT(!context.mHasSubmitted && "No active object; Submit (or EndSearch) has already been called"); 881 | return context.mPushStack.top(); 882 | } 883 | 884 | bool ImSearch::CanCollectSubmissions() 885 | { 886 | return *ImSearch::GetUserQuery() != '\0'; 887 | } 888 | 889 | float ImSearch::GetScore(size_t index) 890 | { 891 | LocalContext& context = GetLocalContext(); 892 | auto& scores = context.mResult.mBuffers.mScores; 893 | 894 | if (index >= scores.size()) 895 | { 896 | return -1.0f; 897 | } 898 | 899 | return scores[index]; 900 | } 901 | 902 | size_t ImSearch::GetDisplayOrderEntry(size_t index) 903 | { 904 | LocalContext& context = GetLocalContext(); 905 | auto& displayOrder = context.mResult.mOutput.mDisplayOrder; 906 | 907 | if (index >= displayOrder.size()) 908 | { 909 | return std::numeric_limits::max(); 910 | } 911 | return displayOrder[index]; 912 | } 913 | 914 | int ImSearch::GetNumItemsFilteredOut() 915 | { 916 | const LocalContext& context = GetLocalContext(); 917 | const auto& displayOrder = context.mResult.mOutput.mDisplayOrder; 918 | 919 | int numInDisplayOrder{}; 920 | 921 | for (IndexT flagAndIndex : displayOrder) 922 | { 923 | numInDisplayOrder += (flagAndIndex & Output::sDisplayEndFlag) == 0; 924 | } 925 | const int numSubmitted = static_cast(context.mResult.mInput.mEntries.size()); 926 | IM_ASSERT(numInDisplayOrder <= numSubmitted); 927 | 928 | return numSubmitted - numInDisplayOrder; 929 | } 930 | 931 | float ImSearch::GetCutoffStrength() 932 | { 933 | ImSearchContext& context = GetImSearchContext(); 934 | return context.mCutoffStack.empty() ? sDefaultCutoffStrength : context.mCutoffStack.top(); 935 | } 936 | 937 | void ImSearch::SetPreviewText(const char* preview) 938 | { 939 | GetLocalContext().mResult.mOutput.mPreviewText = std::string{ preview }; 940 | } 941 | 942 | const char* ImSearch::GetPreviewText() 943 | { 944 | return GetLocalContext().mResult.mOutput.mPreviewText.c_str(); 945 | } 946 | 947 | void ImSearch::BeginHighlightZone(const char* textToHighlight) 948 | { 949 | ImGuiStorage* storage = ImGui::GetStateStorage(); 950 | 951 | std::string* str = new std::string(textToHighlight); 952 | storage->SetVoidPtr(ImGui::GetID("TextToHighlight"), str); 953 | 954 | ImDrawList* drawList = ImGui::GetWindowDrawList(); 955 | storage->SetInt(ImGui::GetID("StartIdxIdx"), drawList->IdxBuffer.size()); 956 | } 957 | 958 | void ImSearch::EndHighlightZone() 959 | { 960 | ImGuiStorage* storage = ImGui::GetStateStorage(); 961 | ImDrawList* drawList = ImGui::GetWindowDrawList(); 962 | 963 | std::string* str = static_cast(storage->GetVoidPtr(ImGui::GetID("TextToHighlight"))); 964 | const int prevIdxEnd = storage->GetInt(ImGui::GetID("StartIdxIdx")); 965 | 966 | HighlightSubstrings(str->c_str(), 967 | str->c_str() + str->size(), 968 | drawList, 969 | prevIdxEnd, 970 | drawList->IdxBuffer.size()); 971 | 972 | delete str; 973 | } 974 | 975 | void ImSearch::HighlightSubstrings(const char* substrStart, 976 | const char* substrEnd, 977 | ImDrawList* drawList, 978 | int startIdxIdx, 979 | int endIdxIdx) 980 | { 981 | const ImU32 textCol = ImSearch::GetColorU32(ImSearchCol_TextHighlighted); 982 | const ImU32 textBgCol = ImSearch::GetColorU32(ImSearchCol_TextHighlightedBg); 983 | 984 | ImDrawListSharedData* sharedData = ImGui::GetDrawListSharedData(); 985 | ImDrawList queryDrawList{ sharedData }; 986 | queryDrawList.AddDrawCmd(); 987 | 988 | queryDrawList.PushTextureID(ImGui::GetIO().Fonts->TexID); 989 | queryDrawList.PushClipRect({ -INFINITY, -INFINITY }, { INFINITY, INFINITY }); 990 | 991 | struct DrawnGlyph 992 | { 993 | DrawnGlyph(ImDrawList& l, char character) 994 | { 995 | mIdxIdxStart = l.IdxBuffer.size(); 996 | l.AddText( 997 | {}, 998 | 0xffffffff, 999 | &character, 1000 | &character + 1); 1001 | mIdxIdxEnd = l.IdxBuffer.size(); 1002 | } 1003 | int mIdxIdxStart{}; 1004 | int mIdxIdxEnd{}; 1005 | }; 1006 | ImVector glyphs{}; 1007 | 1008 | for (const char* ch = substrStart; ch < substrEnd; ch++) 1009 | { 1010 | const char lower = static_cast(std::tolower(static_cast(*ch))); 1011 | const char upper = static_cast(std::toupper(static_cast(*ch))); 1012 | 1013 | glyphs.push_back(DrawnGlyph{ queryDrawList, lower }); 1014 | glyphs.push_back(DrawnGlyph{ queryDrawList, upper }); 1015 | } 1016 | 1017 | queryDrawList.PopClipRect(); 1018 | queryDrawList.PopTextureID(); 1019 | 1020 | for (int matchStart = startIdxIdx; matchStart < endIdxIdx; matchStart++) 1021 | { 1022 | int matchEnd = matchStart; 1023 | 1024 | for (int chIdx = 0; chIdx < substrEnd - substrStart; chIdx++) 1025 | { 1026 | bool anyGlyphMatching = false; 1027 | 1028 | for (int i = 0; i < 2; i++) 1029 | { 1030 | const DrawnGlyph& glyph = glyphs[(chIdx * 2) + i]; 1031 | const int glyphLength = glyph.mIdxIdxEnd - glyph.mIdxIdxStart; 1032 | 1033 | const int nextMatchEnd = matchEnd + glyphLength; 1034 | 1035 | if (nextMatchEnd <= endIdxIdx 1036 | && DoDrawnGlyphsMatch( 1037 | queryDrawList, 1038 | queryDrawList.IdxBuffer.Data + glyph.mIdxIdxStart, 1039 | *drawList, 1040 | &drawList->IdxBuffer[matchEnd], 1041 | glyphLength)) 1042 | { 1043 | matchEnd = nextMatchEnd; 1044 | anyGlyphMatching = true; 1045 | break; 1046 | } 1047 | } 1048 | 1049 | if (!anyGlyphMatching) 1050 | { 1051 | matchEnd = matchStart; 1052 | break; 1053 | } 1054 | } 1055 | 1056 | if (matchStart == matchEnd) 1057 | { 1058 | continue; 1059 | } 1060 | 1061 | ImVec2 min{ INFINITY, INFINITY }; 1062 | ImVec2 max{ -INFINITY, -INFINITY }; 1063 | 1064 | for (int idxIdx = matchStart; idxIdx < matchEnd; idxIdx++) 1065 | { 1066 | ImDrawVert& vert = drawList->VtxBuffer[drawList->IdxBuffer[idxIdx]]; 1067 | vert.col = textCol; 1068 | 1069 | min.x = std::min(min.x, vert.pos.x); 1070 | min.y = std::min(min.y, vert.pos.y); 1071 | 1072 | max.x = std::max(max.x, vert.pos.x); 1073 | max.y = std::max(max.y, vert.pos.y); 1074 | } 1075 | 1076 | const ImVec2 padding = ImGui::GetStyle().FramePadding; 1077 | min.x -= padding.x * .5f; 1078 | min.y -= padding.y * .5f; 1079 | max.x += padding.x * .5f; 1080 | max.y += padding.y * .5f; 1081 | 1082 | drawList->AddRectFilled(min, max, textBgCol); 1083 | 1084 | const int count = matchEnd - matchStart; 1085 | drawList->PrimReserve(count, 0); 1086 | 1087 | for (int idxIdx = matchStart; idxIdx < matchEnd; idxIdx++) 1088 | { 1089 | drawList->PrimWriteIdx(drawList->IdxBuffer[idxIdx]); 1090 | } 1091 | } 1092 | } 1093 | 1094 | std::vector ImSearch::SplitTokens(StrView s) 1095 | { 1096 | std::vector tokens{}; 1097 | std::string current{}; 1098 | for (char c : s) 1099 | { 1100 | if (std::isalnum(static_cast(c))) 1101 | { 1102 | current += c; 1103 | continue; 1104 | } 1105 | 1106 | if (!current.empty()) 1107 | { 1108 | tokens.push_back(current); 1109 | current.clear(); 1110 | } 1111 | } 1112 | 1113 | if (!current.empty()) 1114 | { 1115 | tokens.push_back(current); 1116 | } 1117 | return tokens; 1118 | } 1119 | 1120 | std::string ImSearch::Join(const std::vector& tokens) 1121 | { 1122 | if (tokens.empty()) 1123 | { 1124 | return {}; 1125 | } 1126 | 1127 | std::string res = tokens[0]; 1128 | for (size_t i = 1; i < tokens.size(); i++) 1129 | { 1130 | res += ' ' + tokens[i]; 1131 | } 1132 | return res; 1133 | } 1134 | 1135 | ImSearch::StrView ImSearch::GetStringNeededToCompletePartial(StrView partial, StrView complete) 1136 | { 1137 | if (complete.size() <= partial.size()) 1138 | { 1139 | return {}; 1140 | } 1141 | 1142 | const StrView tokenSubStr = { complete.mData, partial.size() }; 1143 | 1144 | for (IndexT i = 0; i < partial.size(); i++) 1145 | { 1146 | if (std::tolower(static_cast(tokenSubStr[i])) != std::tolower(static_cast(partial[i]))) 1147 | { 1148 | return {}; 1149 | } 1150 | } 1151 | 1152 | return { complete.data() + partial.size(), complete.size() - partial.size() }; 1153 | } 1154 | 1155 | std::string ImSearch::MakeTokenisedString(StrView original) 1156 | { 1157 | std::vector targetTokens = SplitTokens(original); 1158 | std::sort(targetTokens.begin(), targetTokens.end()); 1159 | std::string processedTarget = Join(targetTokens); 1160 | 1161 | for (char& c : processedTarget) 1162 | { 1163 | c = static_cast(std::tolower(static_cast(c))); 1164 | } 1165 | 1166 | return processedTarget; 1167 | } 1168 | 1169 | ImSearch::StrView ImSearch::GetMemoizedTokenisedString(const std::string& original) 1170 | { 1171 | ImSearch::ImSearchContext& context = GetImSearchContext(); 1172 | auto& preprocessedStrings = context.mTokenisedStrings; 1173 | 1174 | auto it = context.mTokenisedStrings.find(original); 1175 | 1176 | if (it == preprocessedStrings.end()) 1177 | { 1178 | it = preprocessedStrings.emplace(original, MakeTokenisedString(original)).first; 1179 | } 1180 | 1181 | return it->second; 1182 | } 1183 | 1184 | // This file has been altered to better fit ImSearch. 1185 | // The original can be found here https://github.com/Tmplt/python-Levenshtein/blob/master/Levenshtein.c 1186 | ImSearch::IndexT ImSearch::LevenshteinDistance( 1187 | StrView s1, 1188 | StrView s2, 1189 | ReusableBuffers& buffers) 1190 | { 1191 | IndexT i; 1192 | IndexT* row; /* we only need to keep one row of costs */ 1193 | IndexT* end; 1194 | 1195 | /* strip common prefix */ 1196 | while (s1.mSize > 0 1197 | && s2.mSize > 0 1198 | && *s1.mData == *s2.mData) 1199 | { 1200 | s1.mSize--; 1201 | s2.mSize--; 1202 | s1.mData++; 1203 | s2.mData++; 1204 | } 1205 | 1206 | /* strip common suffix */ 1207 | while (s1.mSize > 0 && s2.mSize > 0 && s1.mData[s1.mSize - 1] == s2.mData[s2.mSize - 1]) 1208 | { 1209 | s1.mSize--; 1210 | s2.mSize--; 1211 | } 1212 | 1213 | /* catch trivial cases */ 1214 | if (s1.mSize == 0) 1215 | { 1216 | return s2.mSize; 1217 | } 1218 | if (s2.mSize == 0) 1219 | { 1220 | return s1.mSize; 1221 | } 1222 | 1223 | /* make the inner cycle (i.e. s2.mData) the longer one */ 1224 | if (s1.mSize > s2.mSize) 1225 | { 1226 | IndexT nx = s1.mSize; 1227 | const char* sx = s1.mData; 1228 | s1.mSize = s2.mSize; 1229 | s2.mSize = nx; 1230 | s1.mData = s2.mData; 1231 | s2.mData = sx; 1232 | } 1233 | /* check s1.mSize == 1 separately */ 1234 | if (s1.mSize == 1) 1235 | { 1236 | return s2.mSize + 1 - 2 * (memchr(s2.mData, *s1.mData, s2.mSize) != nullptr); 1237 | } 1238 | s1.mSize++; 1239 | s2.mSize++; 1240 | 1241 | /* initialize first row */ 1242 | buffers.mTempIndices.resize(s2.mSize); 1243 | row = buffers.mTempIndices.data(); 1244 | end = row + s2.mSize - 1; 1245 | for (i = 0; i < s2.mSize; i++) 1246 | { 1247 | row[i] = i; 1248 | } 1249 | 1250 | /* go through the matrix and compute the costs. yes, this is an extremely 1251 | * obfuscated version, but also extremely memory-conservative and relatively 1252 | * fast. */ 1253 | for (i = 1; i < s1.mSize; i++) 1254 | { 1255 | IndexT* p = row + 1; 1256 | const char char1 = s1.mData[i - 1]; 1257 | const char* char2p = s2.mData; 1258 | IndexT D = i; 1259 | IndexT x = i; 1260 | while (p <= end) 1261 | { 1262 | if (char1 == *(char2p++)) 1263 | { 1264 | x = --D; 1265 | } 1266 | else 1267 | { 1268 | x++; 1269 | } 1270 | D = *p; 1271 | D++; 1272 | if (x > D) 1273 | { 1274 | x = D; 1275 | } 1276 | *(p++) = x; 1277 | } 1278 | } 1279 | 1280 | i = *end; 1281 | return i; 1282 | } 1283 | 1284 | float ImSearch::Ratio(StrView s1, 1285 | StrView s2, 1286 | ReusableBuffers& buffers) 1287 | { 1288 | const IndexT distance = LevenshteinDistance(s1, 1289 | s2, 1290 | buffers); 1291 | const float ratio = 1.0f - static_cast(distance) / static_cast(s1.size() + s2.size()); 1292 | return ratio; 1293 | } 1294 | 1295 | float ImSearch::PartialRatio(StrView shorter, 1296 | StrView longer, 1297 | ReusableBuffers& buffers) 1298 | { 1299 | if (shorter.size() == 0 1300 | || longer.size() == 0) 1301 | { 1302 | return 0.0f; 1303 | } 1304 | 1305 | if (shorter.size() > longer.size()) 1306 | { 1307 | std::swap(shorter, longer); 1308 | } 1309 | 1310 | float maxRatio = 0.0f; 1311 | for (IndexT i = 0; i <= longer.size() - shorter.size(); i++) 1312 | { 1313 | const float ratio = Ratio(shorter, 1314 | { &longer[i], shorter.size() }, // Window into longer 1315 | buffers); 1316 | maxRatio = std::max(maxRatio, ratio); 1317 | 1318 | if (maxRatio >= 1.0f) 1319 | { 1320 | break; 1321 | } 1322 | } 1323 | return maxRatio; 1324 | } 1325 | 1326 | float ImSearch::WeightedRatio(StrView s1, 1327 | StrView s1Tokenised, 1328 | StrView s2, 1329 | StrView s2Tokenised, 1330 | ReusableBuffers& buffers) 1331 | { 1332 | float score = Ratio(s1, s2, buffers); 1333 | 1334 | const IndexT shorterSize = std::min(s1.size(), s2.size()); 1335 | const IndexT longerSize = std::max(s1.size(), s2.size()); 1336 | 1337 | if (longerSize <= shorterSize + shorterSize / 2) 1338 | { 1339 | score = std::max(score, 1340 | Ratio(s1Tokenised, s2Tokenised, buffers) * 0.95f); 1341 | } 1342 | else 1343 | { 1344 | const float weight = longerSize > shorterSize * 8 ? 0.5f : .8f; 1345 | 1346 | score = std::max(score, 1347 | PartialRatio(s1, s2, buffers) * weight); 1348 | 1349 | score = std::max(score, 1350 | PartialRatio(s1Tokenised, s2Tokenised, buffers) * 0.95f * weight); 1351 | } 1352 | 1353 | return score; 1354 | } 1355 | 1356 | #endif // #ifndef IMGUI_DISABLE 1357 | 1358 | 1359 | --------------------------------------------------------------------------------