libsurfer/
variable_filter.rs

1//! Filtering of the variable list.
2use derive_more::Display;
3use egui::{Button, Layout, TextEdit, Ui};
4use egui_remixicon::icons;
5use emath::{Align, Vec2};
6use enum_iterator::Sequence;
7use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
8use itertools::Itertools;
9use regex::{escape, Regex, RegexBuilder};
10use serde::{Deserialize, Serialize};
11
12use crate::data_container::DataContainer::Transactions;
13use crate::transaction_container::{StreamScopeRef, TransactionStreamRef};
14use crate::variable_direction::VariableDirectionExt;
15use crate::wave_container::WaveContainer;
16use crate::wave_data::ScopeType;
17use crate::{message::Message, wave_container::VariableRef, SystemState};
18use surfer_translation_types::VariableDirection;
19
20use std::cmp::Ordering;
21
22#[derive(Debug, Display, PartialEq, Serialize, Deserialize, Sequence)]
23pub enum VariableNameFilterType {
24    #[display("Fuzzy")]
25    Fuzzy,
26
27    #[display("Regular expression")]
28    Regex,
29
30    #[display("Variable starts with")]
31    Start,
32
33    #[display("Variable contains")]
34    Contain,
35}
36
37#[derive(Debug, Serialize, Deserialize)]
38pub struct VariableFilter {
39    pub(crate) name_filter_type: VariableNameFilterType,
40    pub(crate) name_filter_str: String,
41    pub(crate) name_filter_case_insensitive: bool,
42
43    pub(crate) include_inputs: bool,
44    pub(crate) include_outputs: bool,
45    pub(crate) include_inouts: bool,
46    pub(crate) include_others: bool,
47
48    pub(crate) group_by_direction: bool,
49}
50
51#[derive(Debug, Deserialize)]
52pub enum VariableIOFilterType {
53    Input,
54    Output,
55    InOut,
56    Other,
57}
58
59impl Default for VariableFilter {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl VariableFilter {
66    pub fn new() -> VariableFilter {
67        VariableFilter {
68            name_filter_type: VariableNameFilterType::Contain,
69            name_filter_str: String::from(""),
70            name_filter_case_insensitive: true,
71
72            include_inputs: true,
73            include_outputs: true,
74            include_inouts: true,
75            include_others: true,
76
77            group_by_direction: false,
78        }
79    }
80
81    fn name_filter_fn(&self) -> Box<dyn FnMut(&str) -> bool> {
82        if self.name_filter_str.is_empty() {
83            return Box::new(|_var_name| true);
84        }
85
86        match self.name_filter_type {
87            VariableNameFilterType::Fuzzy => {
88                let matcher = if self.name_filter_case_insensitive {
89                    SkimMatcherV2::default().ignore_case()
90                } else {
91                    SkimMatcherV2::default().respect_case()
92                };
93
94                // Make a copy of the filter string to move into the closure below
95                let filter_str_clone = self.name_filter_str.clone();
96
97                Box::new(move |var_name| matcher.fuzzy_match(var_name, &filter_str_clone).is_some())
98            }
99            VariableNameFilterType::Regex => {
100                if let Ok(regex) = RegexBuilder::new(&self.name_filter_str)
101                    .case_insensitive(self.name_filter_case_insensitive)
102                    .build()
103                {
104                    Box::new(move |var_name| regex.is_match(var_name))
105                } else {
106                    Box::new(|_var_name| false)
107                }
108            }
109            VariableNameFilterType::Start => {
110                if let Ok(regex) = RegexBuilder::new(&format!("^{}", escape(&self.name_filter_str)))
111                    .case_insensitive(self.name_filter_case_insensitive)
112                    .build()
113                {
114                    Box::new(move |var_name| regex.is_match(var_name))
115                } else {
116                    Box::new(|_var_name| false)
117                }
118            }
119            VariableNameFilterType::Contain => {
120                if let Ok(regex) = RegexBuilder::new(&escape(&self.name_filter_str))
121                    .case_insensitive(self.name_filter_case_insensitive)
122                    .build()
123                {
124                    Box::new(move |var_name| regex.is_match(var_name))
125                } else {
126                    Box::new(|_var_name| false)
127                }
128            }
129        }
130    }
131
132    fn kind_filter(&self, vr: &VariableRef, wave_container_opt: Option<&WaveContainer>) -> bool {
133        match get_variable_direction(vr, wave_container_opt) {
134            VariableDirection::Input => self.include_inputs,
135            VariableDirection::Output => self.include_outputs,
136            VariableDirection::InOut => self.include_inouts,
137            _ => self.include_others,
138        }
139    }
140
141    pub fn matching_variables(
142        &self,
143        variables: &[VariableRef],
144        wave_container_opt: Option<&WaveContainer>,
145    ) -> Vec<VariableRef> {
146        let mut name_filter = self.name_filter_fn();
147
148        variables
149            .iter()
150            .filter(|&vr| name_filter(&vr.name))
151            .filter(|&vr| self.kind_filter(vr, wave_container_opt))
152            .cloned()
153            .collect_vec()
154    }
155}
156
157impl SystemState {
158    pub fn draw_variable_filter_edit(&mut self, ui: &mut Ui, msgs: &mut Vec<Message>) {
159        ui.with_layout(Layout::right_to_left(Align::TOP), |ui| {
160            let default_padding = ui.spacing().button_padding;
161            ui.spacing_mut().button_padding = Vec2 {
162                x: 0.,
163                y: default_padding.y,
164            };
165            ui.button(icons::ADD_FILL)
166                .on_hover_text("Add all variables from active Scope")
167                .clicked()
168                .then(|| {
169                    if let Some(waves) = self.user.waves.as_ref() {
170                        // Iterate over the reversed list to get
171                        // waves in the same order as the variable
172                        // list
173                        if let Some(active_scope) = waves.active_scope.as_ref() {
174                            match active_scope {
175                                ScopeType::WaveScope(active_scope) => {
176                                    let variables = waves
177                                        .inner
178                                        .as_waves()
179                                        .unwrap()
180                                        .variables_in_scope(active_scope);
181                                    msgs.push(Message::AddVariables(self.filtered_variables(
182                                        &variables,
183                                        &self.user.variable_filter,
184                                    )));
185                                }
186                                ScopeType::StreamScope(active_scope) => {
187                                    let Transactions(inner) = &waves.inner else {
188                                        return;
189                                    };
190                                    match active_scope {
191                                        StreamScopeRef::Root => {
192                                            for stream in inner.get_streams() {
193                                                msgs.push(Message::AddStreamOrGenerator(
194                                                    TransactionStreamRef::new_stream(
195                                                        stream.id,
196                                                        stream.name.clone(),
197                                                    ),
198                                                ));
199                                            }
200                                        }
201                                        StreamScopeRef::Stream(s) => {
202                                            for gen_id in
203                                                &inner.get_stream(s.stream_id).unwrap().generators
204                                            {
205                                                let gen = inner.get_generator(*gen_id).unwrap();
206
207                                                msgs.push(Message::AddStreamOrGenerator(
208                                                    TransactionStreamRef::new_gen(
209                                                        gen.stream_id,
210                                                        gen.id,
211                                                        gen.name.clone(),
212                                                    ),
213                                                ));
214                                            }
215                                        }
216                                        StreamScopeRef::Empty(_) => {}
217                                    }
218                                }
219                            }
220                        }
221                    }
222                });
223            ui.add(
224                Button::new(icons::FONT_SIZE)
225                    .selected(!self.user.variable_filter.name_filter_case_insensitive),
226            )
227            .on_hover_text("Case sensitive filter")
228            .clicked()
229            .then(|| {
230                msgs.push(Message::SetVariableNameFilterCaseInsensitive(
231                    !self.user.variable_filter.name_filter_case_insensitive,
232                ));
233            });
234            ui.menu_button(icons::FILTER_FILL, |ui| {
235                self.variable_filter_type_menu(ui, msgs);
236            });
237            ui.add_enabled(
238                !self.user.variable_filter.name_filter_str.is_empty(),
239                Button::new(icons::CLOSE_FILL),
240            )
241            .on_hover_text("Clear filter")
242            .clicked()
243            .then(|| self.user.variable_filter.name_filter_str.clear());
244
245            // Check if regex and if an incorrect regex, change background color
246            if self.user.variable_filter.name_filter_type == VariableNameFilterType::Regex
247                && Regex::new(&self.user.variable_filter.name_filter_str).is_err()
248            {
249                ui.style_mut().visuals.extreme_bg_color =
250                    self.user.config.theme.accent_error.background;
251            }
252            // Create text edit
253            let response = ui.add(
254                TextEdit::singleline(&mut self.user.variable_filter.name_filter_str)
255                    .hint_text("Filter (context menu for type)"),
256            );
257            response.context_menu(|ui| {
258                self.variable_filter_type_menu(ui, msgs);
259            });
260            // Handle focus
261            if response.gained_focus() {
262                msgs.push(Message::SetFilterFocused(true));
263            }
264            if response.lost_focus() {
265                msgs.push(Message::SetFilterFocused(false));
266            }
267            ui.spacing_mut().button_padding = default_padding;
268        });
269    }
270
271    pub fn variable_filter_type_menu(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
272        for filter_type in enum_iterator::all::<VariableNameFilterType>() {
273            ui.radio(
274                self.user.variable_filter.name_filter_type == filter_type,
275                filter_type.to_string(),
276            )
277            .clicked()
278            .then(|| {
279                ui.close_menu();
280                msgs.push(Message::SetVariableNameFilterType(filter_type));
281            });
282        }
283
284        ui.separator();
285
286        // Checkbox wants a mutable bool reference but we don't have mutable self to give it a
287        // mutable 'group_by_direction' directly. Plus we want to update things via a message. So
288        // make a copy of the flag here that can be mutable and just ensure we update the actual
289        // flag on a click.
290        let mut group_by_direction = self.user.variable_filter.group_by_direction;
291
292        ui.checkbox(&mut group_by_direction, "Group by direction")
293            .clicked()
294            .then(|| {
295                msgs.push(Message::SetVariableGroupByDirection(
296                    !self.user.variable_filter.group_by_direction,
297                ))
298            });
299
300        ui.separator();
301
302        ui.horizontal(|ui| {
303            let input = VariableDirection::Input;
304            let output = VariableDirection::Output;
305            let inout = VariableDirection::InOut;
306
307            ui.add(
308                Button::new(input.get_icon().unwrap())
309                    .selected(self.user.variable_filter.include_inputs),
310            )
311            .on_hover_text("Show inputs")
312            .clicked()
313            .then(|| {
314                msgs.push(Message::SetVariableIOFilter(
315                    VariableIOFilterType::Input,
316                    !self.user.variable_filter.include_inputs,
317                ));
318            });
319
320            ui.add(
321                Button::new(output.get_icon().unwrap())
322                    .selected(self.user.variable_filter.include_outputs),
323            )
324            .on_hover_text("Show outputs")
325            .clicked()
326            .then(|| {
327                msgs.push(Message::SetVariableIOFilter(
328                    VariableIOFilterType::Output,
329                    !self.user.variable_filter.include_outputs,
330                ));
331            });
332
333            ui.add(
334                Button::new(inout.get_icon().unwrap())
335                    .selected(self.user.variable_filter.include_inouts),
336            )
337            .on_hover_text("Show inouts")
338            .clicked()
339            .then(|| {
340                msgs.push(Message::SetVariableIOFilter(
341                    VariableIOFilterType::InOut,
342                    !self.user.variable_filter.include_inouts,
343                ));
344            });
345
346            ui.add(
347                Button::new(icons::GLOBAL_LINE).selected(self.user.variable_filter.include_others),
348            )
349            .on_hover_text("Show others")
350            .clicked()
351            .then(|| {
352                msgs.push(Message::SetVariableIOFilter(
353                    VariableIOFilterType::Other,
354                    !self.user.variable_filter.include_others,
355                ));
356            });
357        });
358    }
359
360    pub fn variable_cmp(
361        &self,
362        a: &VariableRef,
363        b: &VariableRef,
364        wave_container: Option<&WaveContainer>,
365    ) -> Ordering {
366        let a_direction = get_variable_direction(a, wave_container);
367        let b_direction = get_variable_direction(b, wave_container);
368
369        if !self.user.variable_filter.group_by_direction || a_direction == b_direction {
370            numeric_sort::cmp(&a.name, &b.name)
371        } else if a_direction < b_direction {
372            Ordering::Less
373        } else {
374            Ordering::Greater
375        }
376    }
377
378    pub fn filtered_variables(
379        &self,
380        variables: &[VariableRef],
381        variable_filter: &VariableFilter,
382    ) -> Vec<VariableRef> {
383        let wave_container = match &self.user.waves {
384            Some(wd) => wd.inner.as_waves(),
385            None => None,
386        };
387
388        variable_filter
389            .matching_variables(variables, wave_container)
390            .iter()
391            .sorted_by(|a, b| self.variable_cmp(a, b, wave_container))
392            .cloned()
393            .collect_vec()
394    }
395}
396
397fn get_variable_direction(
398    vr: &VariableRef,
399    wave_container_opt: Option<&WaveContainer>,
400) -> VariableDirection {
401    match wave_container_opt {
402        Some(wave_container) => wave_container
403            .variable_meta(vr)
404            .map_or(VariableDirection::Unknown, |m| {
405                m.direction.unwrap_or(VariableDirection::Unknown)
406            }),
407        None => VariableDirection::Unknown,
408    }
409}