17.5 KB、471行 生 Blame 履歴
1#include "imgui_internal.h" //todo move stuff to cpps
2#include <cstdint>
3#include <cctype>
4#include <utility>
5#include <vector>
6#include <string>
7#include <algorithm>
8
9//https://github.com/ocornut/imgui/issues/1658#issuecomment-886171438
10
11namespace ImGui
12{
13
14 // https://github.com/forrestthewoods/lib_fts
15
16 // Forward declarations for "private" implementation
17 namespace fuzzy_internal
18 {
19 static bool fuzzy_match_recursive(const char *pattern, const char *str, int &outScore, const char *strBegin,
20 uint8_t const *srcMatches, uint8_t *newMatches, int maxMatches, int nextMatch,
21 int &recursionCount, int recursionLimit);
22 }
23 // Private implementation
24 static bool fuzzy_internal::fuzzy_match_recursive(const char *pattern, const char *str, int &outScore,
25 const char *strBegin, uint8_t const *srcMatches, uint8_t *matches, int maxMatches,
26 int nextMatch, int &recursionCount, int recursionLimit)
27 {
28 // Count recursions
29 ++recursionCount;
30 if (recursionCount >= recursionLimit)
31 return false;
32
33 // Detect end of strings
34 if (*pattern == '\0' || *str == '\0')
35 return false;
36
37 // Recursion params
38 bool recursiveMatch = false;
39 uint8_t bestRecursiveMatches[256];
40 int bestRecursiveScore = 0;
41
42 // Loop through pattern and str looking for a match
43 bool first_match = true;
44 while (*pattern != '\0' && *str != '\0')
45 {
46
47 // Found match
48 if (tolower(*pattern) == tolower(*str))
49 {
50
51 // Supplied matches buffer was too short
52 if (nextMatch >= maxMatches)
53 return false;
54
55 // "Copy-on-Write" srcMatches into matches
56 if (first_match && srcMatches)
57 {
58 memcpy(matches, srcMatches, nextMatch);
59 first_match = false;
60 }
61
62 // Recursive call that "skips" this match
63 uint8_t recursiveMatches[256];
64 int recursiveScore;
65 if (fuzzy_match_recursive(pattern, str + 1, recursiveScore, strBegin, matches, recursiveMatches, sizeof(recursiveMatches), nextMatch, recursionCount, recursionLimit))
66 {
67
68 // Pick best recursive score
69 if (!recursiveMatch || recursiveScore > bestRecursiveScore)
70 {
71 memcpy(bestRecursiveMatches, recursiveMatches, 256);
72 bestRecursiveScore = recursiveScore;
73 }
74 recursiveMatch = true;
75 }
76
77 // Advance
78 matches[nextMatch++] = (uint8_t)(str - strBegin);
79 ++pattern;
80 }
81 ++str;
82 }
83
84 // Determine if full pattern was matched
85 bool matched = *pattern == '\0' ? true : false;
86
87 // Calculate score
88 if (matched)
89 {
90 const int sequential_bonus = 15; // bonus for adjacent matches
91 const int separator_bonus = 30; // bonus if match occurs after a separator
92 const int camel_bonus = 30; // bonus if match is uppercase and prev is lower
93 const int first_letter_bonus = 15; // bonus if the first letter is matched
94
95 const int leading_letter_penalty = -5; // penalty applied for every letter in str before the first match
96 const int max_leading_letter_penalty = -15; // maximum penalty for leading letters
97 const int unmatched_letter_penalty = -1; // penalty for every letter that doesn't matter
98
99 // Iterate str to end
100 while (*str != '\0')
101 ++str;
102
103 // Initialize score
104 outScore = 100;
105
106 // Apply leading letter penalty
107 int penalty = leading_letter_penalty * matches[0];
108 if (penalty < max_leading_letter_penalty)
109 penalty = max_leading_letter_penalty;
110 outScore += penalty;
111
112 // Apply unmatched penalty
113 int unmatched = (int)(str - strBegin) - nextMatch;
114 outScore += unmatched_letter_penalty * unmatched;
115
116 // Apply ordering bonuses
117 for (int i = 0; i < nextMatch; ++i)
118 {
119 uint8_t currIdx = matches[i];
120
121 if (i > 0)
122 {
123 uint8_t prevIdx = matches[i - 1];
124
125 // Sequential
126 if (currIdx == (prevIdx + 1))
127 outScore += sequential_bonus;
128 }
129
130 // Check for bonuses based on neighbor character value
131 if (currIdx > 0)
132 {
133 // Camel case
134 char neighbor = strBegin[currIdx - 1];
135 char curr = strBegin[currIdx];
136 if (::islower(neighbor) && ::isupper(curr))
137 outScore += camel_bonus;
138
139 // Separator
140 bool neighborSeparator = neighbor == '_' || neighbor == ' ';
141 if (neighborSeparator)
142 outScore += separator_bonus;
143 }
144 else
145 {
146 // First letter
147 outScore += first_letter_bonus;
148 }
149 }
150 }
151
152 // Return best result
153 if (recursiveMatch && (!matched || bestRecursiveScore > outScore))
154 {
155 // Recursive score is better than "this"
156 memcpy(matches, bestRecursiveMatches, maxMatches);
157 outScore = bestRecursiveScore;
158 return true;
159 }
160 else if (matched)
161 {
162 // "this" score is better than recursive
163 return true;
164 }
165 else
166 {
167 // no match
168 return false;
169 }
170 }
171
172
173
174 static bool fuzzy_match(char const *pattern, char const *str, int &outScore, uint8_t *matches, int maxMatches)
175 {
176 int recursionCount = 0;
177 int recursionLimit = 10;
178
179 return fuzzy_internal::fuzzy_match_recursive(pattern, str, outScore, str, nullptr, matches, maxMatches, 0, recursionCount, recursionLimit);
180 }
181
182 // Public interface
183 bool fuzzy_match_simple(char const *pattern, char const *str)
184 {
185 while (*pattern != '\0' && *str != '\0')
186 {
187 if (tolower(*pattern) == tolower(*str))
188 ++pattern;
189 ++str;
190 }
191
192 return *pattern == '\0' ? true : false;
193 }
194
195 bool fuzzy_match(char const *pattern, char const *str, int &outScore)
196 {
197
198 uint8_t matches[256];
199 return fuzzy_match(pattern, str, outScore, matches, sizeof(matches));
200 }
201
202 static bool sortbysec_desc(const std::pair<int, int> &a, const std::pair<int, int> &b)
203 {
204 return (b.second < a.second);
205 }
206
207 bool ComboWithFilter(const char *label, int *current_item,const std::vector<char*> &items)
208 {
209 ImGuiContext &g = *GImGui;
210
211 ImGuiWindow *window = GetCurrentWindow();
212 if (window->SkipItems)
213 return false;
214
215 const ImGuiStyle &style = g.Style;
216 int items_count = items.size();
217
218 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
219 const char *preview_value = NULL;
220 if (*current_item >= 0 && *current_item < items_count)
221 preview_value = items[*current_item];
222
223 static char pattern_buffer[256] = {0};
224 bool isNeedFilter = false;
225
226 char comboButtonName[512] = {0};
227 ImFormatString(comboButtonName, IM_ARRAYSIZE(comboButtonName), "%s##name_ComboWithFilter_button_%s", preview_value ? preview_value : "", label);
228
229 char name_popup[256 + 10];
230 ImFormatString(name_popup, IM_ARRAYSIZE(name_popup), "##name_popup_%s", label);
231
232 // Display items
233 // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed)
234 bool value_changed = false;
235
236 const float expected_w = CalcItemWidth();
237 ImVec2 item_min = GetItemRectMin();
238 bool isNewOpen = false;
239 float sz = GetFrameHeight();
240 ImVec2 size(sz, sz);
241 ImVec2 CursorPos = window->DC.CursorPos;
242 ImVec2 pos = ImVec2{CursorPos.x + expected_w - sz, CursorPos.y};
243 const ImRect bb(pos, ImVec2{pos.x + size.x , pos.y + size.y});
244
245 float ButtonTextAlignX = g.Style.ButtonTextAlign.x;
246 g.Style.ButtonTextAlign.x = 0;
247 if (ImGui::Button(comboButtonName, ImVec2(expected_w, 0)))
248 {
249 ImGui::OpenPopup(name_popup);
250 isNewOpen = true;
251 }
252 g.Style.ButtonTextAlign.x = ButtonTextAlignX;
253 bool hovered = IsItemHovered();
254 bool active = IsItemActivated();
255 bool pressed = IsItemClicked();
256
257 // Render
258 //const ImU32 bg_col = GetColorU32((active && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
259 //RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
260 const ImU32 text_col = GetColorU32(ImGuiCol_Text);
261 RenderArrow(window->DrawList, ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f)+ bb.Min.x,
262 ImMax(0.0f, (size.y - g.FontSize) * 0.5f) + bb.Min.y), text_col, ImGuiDir_Down);
263
264 if (isNewOpen)
265 {
266 memset(pattern_buffer, 0, IM_ARRAYSIZE(pattern_buffer));
267 }
268 ImVec2 item_max = GetItemRectMax();
269 SetNextWindowPos({CursorPos.x, item_max.y});
270 ImGui::SetNextWindowSize({ImGui::GetItemRectSize().x, 0});
271 if (ImGui::BeginPopup(name_popup))
272 {
273 ImGui::PushStyleColor(ImGuiCol_FrameBg, (ImVec4)ImColor(240, 240, 240, 255));
274 ImGui::PushStyleColor(ImGuiCol_Text, (ImVec4)ImColor(0, 0, 0, 255));
275 ImGui::PushItemWidth(-FLT_MIN);
276 // Filter input
277 if (isNewOpen)
278 ImGui::SetKeyboardFocusHere();
279 InputText("##ComboWithFilter_inputText", pattern_buffer, 256);
280
281 // Search Icon, you can use it if you load IconsFontAwesome5 https://github.com/juliettef/IconFontCppHeaders
282 //const ImVec2 label_size = CalcTextSize(ICON_FA_SEARCH, NULL, true);
283 //const ImVec2 search_icon_pos(ImGui::GetItemRectMax().x - label_size.x - style.ItemInnerSpacing.x * 2, window->DC.CursorPos.y + style.FramePadding.y + g.FontSize * 0.1f);
284 //RenderText(search_icon_pos, ICON_FA_SEARCH);
285
286 ImGui::PopStyleColor(2);
287 if (pattern_buffer[0] != '\0')
288 {
289 isNeedFilter = true;
290 }
291
292 std::vector<std::pair<int, int> > itemScoreVector;
293 if (isNeedFilter)
294 {
295 for (int i = 0; i < items_count; i++)
296 {
297 int score = 0;
298 bool matched = fuzzy_match(pattern_buffer, items[i], score);
299 if (matched)
300 itemScoreVector.push_back(std::make_pair(i, score));
301 }
302 std::sort(itemScoreVector.begin(), itemScoreVector.end(), sortbysec_desc);
303 }
304
305 int show_count = isNeedFilter ? itemScoreVector.size() : items_count;
306 if (ImGui::ListBoxHeader("##ComboWithFilter_itemList", show_count))
307 {
308 for (int i = 0; i < show_count; i++)
309 {
310 int idx = isNeedFilter ? itemScoreVector[i].first : i;
311 PushID((void *)(intptr_t)idx);
312 const bool item_selected = (idx == *current_item);
313 const char *item_text = items[idx];
314 if (Selectable(item_text, item_selected))
315 {
316 value_changed = true;
317 *current_item = idx;
318 CloseCurrentPopup();
319 }
320 if (item_selected)
321 SetItemDefaultFocus();
322 PopID();
323 }
324 ImGui::ListBoxFooter();
325 }
326 ImGui::PopItemWidth();
327 ImGui::EndPopup();
328 }
329
330
331 if (value_changed)
332 {
333 MarkItemEdited(g.LastItemData.ID);
334 }
335
336 return value_changed;
337 }
338
339 bool ComboWithFilter(const char *label, int *current_item, std::vector<const char *> &items)
340 {
341 ImGuiContext &g = *GImGui;
342
343 ImGuiWindow *window = GetCurrentWindow();
344 if (window->SkipItems)
345 return false;
346
347 const ImGuiStyle &style = g.Style;
348 int items_count = items.size();
349
350 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
351 const char *preview_value = NULL;
352 if (*current_item >= 0 && *current_item < items_count)
353 preview_value = items[*current_item];
354
355 static char pattern_buffer[256] = {0};
356 bool isNeedFilter = false;
357
358 char comboButtonName[512] = {0};
359 ImFormatString(comboButtonName, IM_ARRAYSIZE(comboButtonName), "%s##name_ComboWithFilter_button_%s", preview_value ? preview_value : "", label);
360
361 char name_popup[256 + 10];
362 ImFormatString(name_popup, IM_ARRAYSIZE(name_popup), "##name_popup_%s", label);
363
364 // Display items
365 // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed)
366 bool value_changed = false;
367
368 const float expected_w = CalcItemWidth();
369 ImVec2 item_min = GetItemRectMin();
370 bool isNewOpen = false;
371 float sz = GetFrameHeight();
372 ImVec2 size(sz, sz);
373 ImVec2 CursorPos = window->DC.CursorPos;
374 ImVec2 pos = ImVec2{CursorPos.x + expected_w - sz, CursorPos.y};
375 const ImRect bb(pos, ImVec2{pos.x + size.x , pos.y + size.y});
376
377 float ButtonTextAlignX = g.Style.ButtonTextAlign.x;
378 g.Style.ButtonTextAlign.x = 0;
379 if (ImGui::Button(comboButtonName, ImVec2(expected_w, 0)))
380 {
381 ImGui::OpenPopup(name_popup);
382 isNewOpen = true;
383 }
384 g.Style.ButtonTextAlign.x = ButtonTextAlignX;
385 bool hovered = IsItemHovered();
386 bool active = IsItemActivated();
387 bool pressed = IsItemClicked();
388
389 // Render
390 //const ImU32 bg_col = GetColorU32((active && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
391 //RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
392 const ImU32 text_col = GetColorU32(ImGuiCol_Text);
393 RenderArrow(window->DrawList, ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f) + bb.Min.x,
394 ImMax(0.0f, (size.y - g.FontSize) * 0.5f) + bb.Min.y), text_col, ImGuiDir_Down);
395
396 if (isNewOpen)
397 {
398 memset(pattern_buffer, 0, IM_ARRAYSIZE(pattern_buffer));
399 }
400 ImVec2 item_max = GetItemRectMax();
401 SetNextWindowPos({CursorPos.x, item_max.y});
402 ImGui::SetNextWindowSize({ImGui::GetItemRectSize().x, 0});
403 if (ImGui::BeginPopup(name_popup))
404 {
405 ImGui::PushStyleColor(ImGuiCol_FrameBg, (ImVec4)ImColor(240, 240, 240, 255));
406 ImGui::PushStyleColor(ImGuiCol_Text, (ImVec4)ImColor(0, 0, 0, 255));
407 ImGui::PushItemWidth(-FLT_MIN);
408 // Filter input
409 if (isNewOpen)
410 ImGui::SetKeyboardFocusHere();
411 InputText("##ComboWithFilter_inputText", pattern_buffer, 256);
412
413 // Search Icon, you can use it if you load IconsFontAwesome5 https://github.com/juliettef/IconFontCppHeaders
414 //const ImVec2 label_size = CalcTextSize(ICON_FA_SEARCH, NULL, true);
415 //const ImVec2 search_icon_pos(ImGui::GetItemRectMax().x - label_size.x - style.ItemInnerSpacing.x * 2, window->DC.CursorPos.y + style.FramePadding.y + g.FontSize * 0.1f);
416 //RenderText(search_icon_pos, ICON_FA_SEARCH);
417
418 ImGui::PopStyleColor(2);
419 if (pattern_buffer[0] != '\0')
420 {
421 isNeedFilter = true;
422 }
423
424 std::vector<std::pair<int, int> > itemScoreVector;
425 if (isNeedFilter)
426 {
427 for (int i = 0; i < items_count; i++)
428 {
429 int score = 0;
430 bool matched = fuzzy_match(pattern_buffer, items[i], score);
431 if (matched)
432 itemScoreVector.push_back(std::make_pair(i, score));
433 }
434 std::sort(itemScoreVector.begin(), itemScoreVector.end(), sortbysec_desc);
435 }
436
437 int show_count = isNeedFilter ? itemScoreVector.size() : items_count;
438 if (ImGui::ListBoxHeader("##ComboWithFilter_itemList", show_count))
439 {
440 for (int i = 0; i < show_count; i++)
441 {
442 int idx = isNeedFilter ? itemScoreVector[i].first : i;
443 PushID((void *)(intptr_t)idx);
444 const bool item_selected = (idx == *current_item);
445 const char *item_text = items[idx];
446 if (Selectable(item_text, item_selected))
447 {
448 value_changed = true;
449 *current_item = idx;
450 CloseCurrentPopup();
451 }
452 if (item_selected)
453 SetItemDefaultFocus();
454 PopID();
455 }
456 ImGui::ListBoxFooter();
457 }
458 ImGui::PopItemWidth();
459 ImGui::EndPopup();
460 }
461
462
463 if (value_changed)
464 {
465 MarkItemEdited(g.LastItemData.ID);
466 }
467
468 return value_changed;
469 }
470
471}