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}