Skip to main content

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
71            .unwrap_or_else(|| Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart));
72
73        let mut buf = Vec::new();
74        let mut row_data: Vec<SelectableRow<Row, F>> = Vec::new();
75
76        for val in self.rows.values() {
77            let mut string_val = String::new();
78
79            for column in column_list {
80                let value = column.column_text(&val.row_data);
81                string_val.push_str(&value);
82                string_val.push(' ');
83            }
84
85            if pattern
86                .score(Utf32Str::new(&string_val, &mut buf), &mut self.matcher)
87                .is_some()
88            {
89                row_data.push(val.clone());
90
91                if let Some(max) = limit
92                    && row_data.len() >= max
93                {
94                    break;
95                }
96            }
97        }
98
99        self.formatted_rows.clear();
100        self.active_rows.clear();
101        self.active_columns.clear();
102
103        row_data.par_sort_by(|a, b| {
104            let ordering = self.sorted_by.order_by(&a.row_data, &b.row_data);
105            match self.sort_order {
106                SortOrder::Ascending => ordering,
107                SortOrder::Descending => ordering.reverse(),
108            }
109        });
110
111        self.indexed_ids = row_data
112            .par_iter()
113            .enumerate()
114            .map(|(index, row)| (row.id, index))
115            .collect();
116
117        self.formatted_rows = row_data;
118    }
119
120    /// Sets a custom matcher to use for fuzzy searching rows
121    ///
122    /// This allows the table to use a custom `Matcher` from `nucleo-matcher` crate
123    /// for searching/filtering through rows based on the input text. Use this to change
124    /// the search algorithm or tweak scoring behavior.
125    ///
126    /// # Parameters:
127    /// - `matcher`: The matcher instance to use for row filtering.
128    ///
129    /// # Returns:
130    /// - `Self`: The modified table with the specified matcher applied.
131    ///
132    /// # Example:
133    /// ```rust,ignore
134    /// let matcher = Matcher::default();
135    /// let table = SelectableTable::new(columns)
136    ///     .matcher(matcher);
137    /// ```
138    #[must_use]
139    pub fn matcher(mut self, matcher: Matcher) -> Self {
140        self.matcher = matcher;
141        self
142    }
143
144    /// Replaces the current matcher with a new one.
145    ///
146    /// This method allows updating the fuzzy search matcher dynamically.
147    ///
148    /// # Parameters:
149    /// - `matcher`: The new matcher instance to set.
150    ///
151    /// # Example:
152    /// ```rust,ignore
153    /// table.set_matcher(new_matcher);
154    /// ```
155    pub fn set_matcher(&mut self, matcher: Matcher) {
156        self.matcher = matcher;
157    }
158}