egui_selectable_table/fuzzy_matcher.rs
1use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
2use nucleo_matcher::{Matcher, Utf32Str};
3use rayon::prelude::*;
4use std::hash::Hash;
5
6use crate::{ColumnOperations, ColumnOrdering, SelectableRow, SelectableTable, SortOrder};
7
8impl<Row, F, Conf> SelectableTable<Row, F, Conf>
9where
10 Row: Clone + Send + Sync,
11 F: Eq
12 + Hash
13 + Clone
14 + Ord
15 + Send
16 + Sync
17 + Default
18 + ColumnOperations<Row, F, Conf>
19 + ColumnOrdering<Row>,
20 Conf: Default,
21{
22 /// Performs a fuzzy search using specified columns across all rows and updates the displayed rows.
23 ///
24 /// This function filters the table rows based on a search `query` using `nucleo-matcher`
25 /// crate.
26 /// It checks the specified `column_list` for each row, concatenates their string representations,
27 /// and scores them using the provided or generated `Pattern`. Only rows with a non-`None` score
28 /// are retained.
29 ///
30 /// If a `limit` is provided, it will result at most `limit` rows.
31 ///
32 /// # Parameters:
33 /// - `column_list`: A list of columns to search across. Does nothing if empty.
34 /// - `query`: The search string. Does nothing if empty.
35 /// - `limit`: Optional limit on the number of results returned. Does nothing if `0`. Defaults
36 /// to no limit
37 /// - `pattern`: Optional precomputed fuzzy `Pattern`. Default pattern is created from the query using
38 /// case-insensitive matching and smart normalization.
39 ///
40 /// The search is relatively fast even with a million rows but it should not be called every
41 /// frame and be used sparingly.
42 ///
43 /// To reset search results, call [`recreate_rows`](SelectableTable::recreate_rows).
44 ///
45 /// # Example:
46 /// ```rust,ignore
47 /// table.search_and_show(&vec![Column::Name, Column::Username], "john", Some(10), None);
48 /// ```
49 pub fn search_and_show(
50 &mut self,
51 column_list: &Vec<F>,
52 query: &str,
53 limit: Option<usize>,
54 pattern: Option<Pattern>,
55 ) {
56 if query.is_empty() {
57 return;
58 }
59
60 if column_list.is_empty() {
61 return;
62 }
63
64 if let Some(limit) = limit
65 && limit == 0
66 {
67 return;
68 }
69
70 let pattern = pattern.map_or_else(
71 || Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart),
72 |pattern| pattern,
73 );
74
75 let mut buf = Vec::new();
76 let mut row_data: Vec<SelectableRow<Row, F>> = Vec::new();
77
78 for val in self.rows.values() {
79 let mut string_val = String::new();
80
81 for column in column_list {
82 let value = column.column_text(&val.row_data);
83 string_val.push_str(&value);
84 string_val.push(' ');
85 }
86
87 if pattern
88 .score(Utf32Str::new(&string_val, &mut buf), &mut self.matcher)
89 .is_some()
90 {
91 row_data.push(val.clone());
92
93 if let Some(max) = limit {
94 if row_data.len() >= max {
95 break;
96 }
97 }
98 }
99 }
100
101 self.formatted_rows.clear();
102 self.active_rows.clear();
103 self.active_columns.clear();
104
105 row_data.par_sort_by(|a, b| {
106 let ordering = self.sorted_by.order_by(&a.row_data, &b.row_data);
107 match self.sort_order {
108 SortOrder::Ascending => ordering,
109 SortOrder::Descending => ordering.reverse(),
110 }
111 });
112
113 self.indexed_ids = row_data
114 .par_iter()
115 .enumerate()
116 .map(|(index, row)| (row.id, index))
117 .collect();
118
119 self.formatted_rows = row_data;
120 }
121
122 /// Sets a custom matcher to use for fuzzy searching rows
123 ///
124 /// This allows the table to use a custom `Matcher` from `nucleo-matcher` crate
125 /// for searching/filtering through rows based on the input text. Use this to change
126 /// the search algorithm or tweak scoring behavior.
127 ///
128 /// # Parameters:
129 /// - `matcher`: The matcher instance to use for row filtering.
130 ///
131 /// # Returns:
132 /// - `Self`: The modified table with the specified matcher applied.
133 ///
134 /// # Example:
135 /// ```rust,ignore
136 /// let matcher = Matcher::default();
137 /// let table = SelectableTable::new(columns)
138 /// .matcher(matcher);
139 /// ```
140 #[must_use]
141 pub fn matcher(mut self, matcher: Matcher) -> Self {
142 self.matcher = matcher;
143 self
144 }
145
146 /// Replaces the current matcher with a new one.
147 ///
148 /// This method allows updating the fuzzy search matcher dynamically.
149 ///
150 /// # Parameters:
151 /// - `matcher`: The new matcher instance to set.
152 ///
153 /// # Example:
154 /// ```rust,ignore
155 /// table.set_matcher(new_matcher);
156 /// ```
157 pub fn set_matcher(&mut self, matcher: Matcher) {
158 self.matcher = matcher;
159 }
160}