Skip to main content

libsurfer/
hierarchy.rs

1//! Functions for drawing the left hand panel showing scopes and variables.
2use crate::SystemState;
3use crate::data_container::{DataContainer, VariableType as VarType};
4use crate::displayed_item_tree::VisibleItemIndex;
5use crate::message::Message;
6use crate::tooltips::{scope_tooltip_text, variable_tooltip_text};
7use crate::transaction_container::StreamScopeRef;
8use crate::transactions::{draw_transaction_root, draw_transaction_variable_list};
9use crate::variable_direction::get_direction_string;
10use crate::view::draw_true_name;
11use crate::wave_container::{
12    ScopeRef, ScopeRefExt, VariableMeta, VariableRef, VariableRefExt, WaveContainer,
13};
14use crate::wave_data::{ScopeType, WaveData};
15use derive_more::{Display, FromStr};
16use ecolor::Color32;
17use egui::text::LayoutJob;
18use egui::{CentralPanel, Frame, Layout, Panel, ScrollArea, TextStyle, Ui};
19use egui_remixicon::icons;
20use emath::Align;
21use enum_iterator::Sequence;
22use epaint::{
23    Margin,
24    text::{TextFormat, TextWrapMode},
25};
26use eyre::WrapErr as _;
27use itertools::Itertools;
28use num::BigUint;
29use serde::{Deserialize, Serialize};
30use std::ops::Range;
31use std::rc::Rc;
32use surfer_translation_types::translator::VariableNameInfo;
33use tracing::warn;
34#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Serialize, Sequence)]
35pub enum HierarchyStyle {
36    Separate,
37    Tree,
38    Variables,
39}
40
41#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Serialize, Sequence)]
42pub enum ParameterDisplayLocation {
43    Variables,
44    Scopes,
45    Tooltips,
46    None,
47}
48
49#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
50pub enum ScopeExpandType {
51    ExpandSpecific(ScopeRef),
52    ExpandAll,
53    CollapseAll,
54}
55
56use crate::variable_filter::VariableNameFilterType;
57use crate::wave_source::WaveSource;
58
59#[derive(Clone)]
60pub(crate) struct VariableListRow {
61    pub(crate) variable: VariableRef,
62    pub(crate) meta: Option<VariableMeta>,
63    pub(crate) name_info: Option<VariableNameInfo>,
64}
65
66/// Cache key for `draw_all_variables`. The cache is rebuilt whenever any field changes.
67#[derive(PartialEq)]
68pub(crate) struct AllVariableCacheKey {
69    cache_generation: u64,
70    translator_generation: u64,
71    wave_source: WaveSource,
72    filter_str: String,
73    filter_type: VariableNameFilterType,
74    case_insensitive: bool,
75    include_inputs: bool,
76    include_outputs: bool,
77    include_inouts: bool,
78    include_others: bool,
79    group_by_direction: bool,
80}
81
82impl SystemState {
83    /// Scopes and variables in two separate lists
84    pub fn separate(&mut self, ui: &mut Ui, msgs: &mut Vec<Message>) {
85        ui.visuals_mut().override_text_color =
86            Some(self.user.config.theme.primary_ui_color.foreground);
87
88        let total_space = ui.available_height();
89        Panel::top("scopes")
90            .resizable(true)
91            .default_size(total_space / 2.0)
92            .max_size(total_space - 64.0)
93            .frame(Frame::new().inner_margin(Margin::same(5)))
94            .show_inside(ui, |ui| {
95                ui.horizontal(|ui| {
96                    ui.heading("Scopes")
97                        .context_menu(|ui| self.hierarchy_menu(msgs, ui));
98                    if self.user.waves.is_some() {
99                        let default_padding = ui.spacing().button_padding;
100                        ui.spacing_mut().button_padding = egui::vec2(0.0, default_padding.y);
101                        ui.button(icons::MENU_UNFOLD_FILL)
102                            .on_hover_text("Expand all scopes")
103                            .clicked()
104                            .then(|| msgs.push(Message::ExpandScope(ScopeExpandType::ExpandAll)));
105                        ui.button(icons::MENU_FOLD_FILL)
106                            .on_hover_text("Collapse all scopes")
107                            .clicked()
108                            .then(|| msgs.push(Message::ExpandScope(ScopeExpandType::CollapseAll)));
109                        ui.spacing_mut().button_padding = default_padding;
110                    }
111                });
112                ui.add_space(3.0);
113
114                ScrollArea::both()
115                    .id_salt("scopes")
116                    .auto_shrink([false; 2])
117                    .show(ui, |ui| {
118                        ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
119                        if let Some(waves) = &self.user.waves {
120                            self.draw_all_scopes(msgs, waves, false, ui);
121                        }
122                    });
123            });
124        CentralPanel::default()
125            .frame(Frame::new().inner_margin(Margin::same(5)))
126            .show_inside(ui, |ui| {
127                ui.with_layout(Layout::left_to_right(Align::TOP), |ui| {
128                    ui.heading("Variables")
129                        .context_menu(|ui| self.hierarchy_menu(msgs, ui));
130                    ui.add_space(3.0);
131                    self.draw_variable_filter_edit(ui, msgs, false);
132                });
133                ui.add_space(3.0);
134
135                self.draw_variables(msgs, ui);
136            });
137        *self.scope_ref_to_expand.borrow_mut() = None;
138    }
139
140    fn draw_variable_list_header(&self, ui: &mut Ui) {
141        let show_icons = self.show_hierarchy_icons();
142        let show_direction = self.show_variable_direction();
143
144        // Only show header if icons or direction are enabled
145        if !show_icons && !show_direction {
146            return;
147        }
148
149        ui.with_layout(
150            Layout::top_down(Align::LEFT).with_cross_justify(true),
151            |ui| {
152                let monospace_font = ui
153                    .style()
154                    .text_styles
155                    .get(&TextStyle::Monospace)
156                    .cloned()
157                    .unwrap();
158
159                let text_format = TextFormat {
160                    font_id: monospace_font,
161                    color: self.user.config.theme.foreground,
162                    ..Default::default()
163                };
164
165                let mut label = LayoutJob::default();
166                // Type column - "T " to match "icon " (only if shown)
167                if show_icons {
168                    label.append("T ", 0.0, text_format.clone());
169                }
170                // Direction column - "D " to match "icon " (only if shown)
171                if show_direction {
172                    label.append("D ", 0.0, text_format.clone());
173                }
174                // Name column
175                label.append("Name", 0.0, text_format);
176
177                ui.add(egui::Button::selectable(false, label));
178            },
179        );
180        ui.separator();
181    }
182
183    fn draw_variables(&mut self, msgs: &mut Vec<Message>, ui: &mut Ui) {
184        if let Some(waves) = &self.user.waves {
185            let empty_scope = if waves.inner.is_waves() {
186                ScopeType::WaveScope(ScopeRef::empty())
187            } else {
188                ScopeType::StreamScope(StreamScopeRef::Empty(String::default()))
189            };
190            let active_scope = waves.active_scope.as_ref().unwrap_or(&empty_scope);
191            match active_scope {
192                ScopeType::WaveScope(scope) => {
193                    let Some(wave_container) = waves.inner.as_waves() else {
194                        return;
195                    };
196                    let variables = self.filtered_variables_unsorted(
197                        &wave_container.variables_in_scope(scope),
198                        false,
199                    );
200                    let variable_rows = self.build_variable_rows(wave_container, &variables);
201                    // Draw header before scroll area
202                    self.draw_variable_list_header(ui);
203                    // Parameters shown in variable list
204                    if self.parameter_display_location() == ParameterDisplayLocation::Variables {
205                        let parameters = wave_container.parameters_in_scope(scope);
206                        if !parameters.is_empty() {
207                            ScrollArea::both()
208                                .auto_shrink([false; 2])
209                                .id_salt("variables")
210                                .show(ui, |ui| {
211                                    ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
212                                    self.draw_parameters(msgs, wave_container, &parameters, ui);
213                                    self.draw_variable_rows(
214                                        msgs,
215                                        wave_container,
216                                        ui,
217                                        &variable_rows,
218                                        None,
219                                        false,
220                                    );
221                                });
222                            return; // Early exit
223                        }
224                    }
225                    // Parameters not shown here or no parameters: use fast approach only drawing visible rows
226                    let row_height = ui
227                        .text_style_height(&TextStyle::Monospace)
228                        .max(ui.text_style_height(&TextStyle::Body));
229                    ScrollArea::both()
230                        .auto_shrink([false; 2])
231                        .id_salt("variables")
232                        .show_rows(ui, row_height, variable_rows.len(), |ui, row_range| {
233                            ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
234
235                            self.draw_variable_rows(
236                                msgs,
237                                wave_container,
238                                ui,
239                                &variable_rows,
240                                Some(&row_range),
241                                false,
242                            );
243                        });
244                }
245                ScopeType::StreamScope(s) => {
246                    ScrollArea::both()
247                        .auto_shrink([false; 2])
248                        .id_salt("variables")
249                        .show(ui, |ui| {
250                            ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
251
252                            draw_transaction_variable_list(msgs, waves, ui, s);
253                        });
254                }
255            }
256        }
257    }
258
259    fn draw_parameters(
260        &self,
261        msgs: &mut Vec<Message>,
262        wave_container: &WaveContainer,
263        parameters: &[VariableRef],
264        ui: &mut Ui,
265    ) {
266        egui::collapsing_header::CollapsingState::load_with_default_open(
267            ui.ctx(),
268            egui::Id::new(parameters),
269            self.expand_parameter_section,
270        )
271        .show_header(ui, |ui| {
272            ui.with_layout(
273                Layout::top_down(Align::LEFT).with_cross_justify(true),
274                |ui| {
275                    ui.label("Parameters");
276                },
277            );
278        })
279        .body(|ui| {
280            self.filter_and_draw_variable_list(msgs, wave_container, ui, parameters, None);
281        });
282    }
283
284    /// Scopes and variables in a joint tree.
285    pub fn tree(&mut self, ui: &mut Ui, msgs: &mut Vec<Message>) {
286        ui.visuals_mut().override_text_color =
287            Some(self.user.config.theme.primary_ui_color.foreground);
288
289        ui.with_layout(
290            Layout::top_down(Align::LEFT).with_cross_justify(true),
291            |ui| {
292                Frame::new().inner_margin(Margin::same(5)).show(ui, |ui| {
293                    ui.with_layout(Layout::left_to_right(Align::TOP), |ui| {
294                        ui.heading("Hierarchy")
295                            .context_menu(|ui| self.hierarchy_menu(msgs, ui));
296                        ui.add_space(3.0);
297                        self.draw_variable_filter_edit(ui, msgs, false);
298                    });
299                    ui.add_space(3.0);
300
301                    ScrollArea::both().id_salt("hierarchy").show(ui, |ui| {
302                        ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
303                        if let Some(waves) = &self.user.waves {
304                            ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
305                            self.draw_all_scopes(msgs, waves, true, ui);
306                        }
307                    });
308                });
309            },
310        );
311    }
312
313    /// List with all variables.
314    pub fn variable_list(&mut self, ui: &mut Ui, msgs: &mut Vec<Message>) {
315        ui.visuals_mut().override_text_color =
316            Some(self.user.config.theme.primary_ui_color.foreground);
317
318        ui.with_layout(
319            Layout::top_down(Align::LEFT).with_cross_justify(true),
320            |ui| {
321                Frame::new().inner_margin(Margin::same(5)).show(ui, |ui| {
322                    ui.with_layout(Layout::left_to_right(Align::TOP), |ui| {
323                        ui.heading("Variables")
324                            .context_menu(|ui| self.hierarchy_menu(msgs, ui));
325                        ui.add_space(3.0);
326                        self.draw_variable_filter_edit(ui, msgs, true);
327                    });
328                    ui.add_space(3.0);
329                    self.draw_all_variables(msgs, ui);
330                });
331            },
332        );
333    }
334
335    fn make_all_variable_cache_key(
336        &self,
337        cache_generation: u64,
338        wave_source: &WaveSource,
339    ) -> AllVariableCacheKey {
340        let f = &self.user.variable_filter;
341        AllVariableCacheKey {
342            cache_generation,
343            translator_generation: self.translator_generation,
344            wave_source: wave_source.clone(),
345            filter_str: f.name_filter_str.clone(),
346            filter_type: f.name_filter_type.clone(),
347            case_insensitive: f.name_filter_case_insensitive,
348            include_inputs: f.include_inputs,
349            include_outputs: f.include_outputs,
350            include_inouts: f.include_inouts,
351            include_others: f.include_others,
352            group_by_direction: f.group_by_direction,
353        }
354    }
355
356    fn draw_all_variables(&mut self, msgs: &mut Vec<Message>, ui: &mut Ui) {
357        // Phase 1: Rebuild the row cache only when the key changes.
358        // wave_container borrows self.user.waves; all_variable_rows_cache is a disjoint field.
359        if let Some(waves) = &self.user.waves
360            && let DataContainer::Waves(wave_container) = &waves.inner
361        {
362            let key = self.make_all_variable_cache_key(waves.cache_generation, &waves.source);
363            let is_stale = self
364                .all_variable_rows_cache
365                .as_ref()
366                .is_none_or(|(k, _)| k != &key);
367            if is_stale {
368                let variables = self.filtered_variables_unsorted(&wave_container.variables(), true);
369                let rows = self.build_variable_rows(wave_container, &variables);
370                self.all_variable_rows_cache = Some((key, Rc::new(rows)));
371            }
372        }
373
374        // Phase 2: Draw using the cached rows.
375        if let Some(waves) = &self.user.waves {
376            match &waves.inner {
377                DataContainer::Waves(wave_container) => {
378                    // Clone the Rc (O(1)) to get an owned handle usable inside the closure.
379                    let variable_rows = self
380                        .all_variable_rows_cache
381                        .as_ref()
382                        .map_or_else(|| Rc::new(Vec::new()), |(_, rows)| Rc::clone(rows));
383                    let row_height = ui
384                        .text_style_height(&TextStyle::Monospace)
385                        .max(ui.text_style_height(&TextStyle::Body));
386                    // Draw header before scroll area
387                    self.draw_variable_list_header(ui);
388                    ScrollArea::both()
389                        .auto_shrink([false; 2])
390                        .id_salt("variables")
391                        .show_rows(ui, row_height, variable_rows.len(), |ui, row_range| {
392                            ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
393                            self.draw_variable_rows(
394                                msgs,
395                                wave_container,
396                                ui,
397                                &variable_rows,
398                                Some(&row_range),
399                                true,
400                            );
401                        });
402                }
403                DataContainer::Transactions(_) => {
404                    // No support for Streams yet
405                    ui.with_layout(
406                        Layout::top_down(Align::LEFT).with_cross_justify(true),
407                        |ui| {
408                            ui.label("Streams are not yet supported.");
409                            ui.label("Select another view.");
410                        },
411                    );
412                }
413                DataContainer::Empty => {}
414            }
415        }
416    }
417
418    fn draw_all_scopes(
419        &self,
420        msgs: &mut Vec<Message>,
421        wave: &WaveData,
422        draw_variables: bool,
423        ui: &mut Ui,
424    ) {
425        for scope in wave.inner.root_scopes() {
426            match scope {
427                ScopeType::WaveScope(scope) => {
428                    self.draw_selectable_child_or_orphan_scope(
429                        msgs,
430                        wave,
431                        &scope,
432                        draw_variables,
433                        ui,
434                    );
435                }
436                ScopeType::StreamScope(_) => {
437                    draw_transaction_root(msgs, wave, ui);
438                }
439            }
440        }
441        if draw_variables && let Some(wave_container) = wave.inner.as_waves() {
442            let scope = ScopeRef::empty();
443            let variables = wave_container.variables_in_scope(&scope);
444            self.filter_and_draw_variable_list(msgs, wave_container, ui, &variables, None);
445        }
446    }
447
448    fn add_scope_selectable_label(
449        &self,
450        msgs: &mut Vec<Message>,
451        wave: &WaveData,
452        scope: &ScopeRef,
453        ui: &mut Ui,
454        scroll_to_label: bool,
455    ) {
456        let name = scope.name();
457        let is_selected = wave.active_scope == Some(ScopeType::WaveScope(scope.clone()));
458        let mut response = if self.show_hierarchy_icons() {
459            let scope_type = wave
460                .inner
461                .as_waves()
462                .and_then(|wc| wc.get_scope_type(scope));
463            let (icon, icon_color) = self.user.config.theme.scope_icons.get_icon(scope_type);
464
465            let body_font = ui
466                .style()
467                .text_styles
468                .get(&TextStyle::Body)
469                .cloned()
470                .unwrap_or_default();
471            let icon_format = TextFormat {
472                font_id: body_font.clone(),
473                color: icon_color,
474                ..Default::default()
475            };
476            let name_format = TextFormat {
477                font_id: body_font,
478                color: self.user.config.theme.foreground,
479                ..Default::default()
480            };
481            let mut label = LayoutJob::default();
482            label.append(icon, 0.0, icon_format);
483            label.append(" ", 0.0, name_format.clone());
484            label.append(&name, 0.0, name_format);
485
486            ui.add(egui::Button::selectable(is_selected, label))
487        } else {
488            ui.add(egui::Button::selectable(is_selected, name))
489        };
490        let _ = response.interact(egui::Sense::click_and_drag());
491        response.drag_started().then(|| {
492            msgs.push(Message::VariableDragStarted(VisibleItemIndex(
493                wave.display_item_ref_counter,
494            )));
495        });
496
497        if scroll_to_label {
498            response.scroll_to_me(Some(Align::Center));
499        }
500
501        response.drag_stopped().then(|| {
502            if ui.input(|i| i.pointer.hover_pos().unwrap_or_default().x)
503                > self.user.sidepanel_width.unwrap_or_default()
504            {
505                let scope_t = ScopeType::WaveScope(scope.clone());
506                let variables = wave
507                    .inner
508                    .variables_in_scope(&scope_t)
509                    .iter()
510                    .filter_map(|var| match var {
511                        VarType::Variable(var) => Some(var.clone()),
512                        VarType::Generator(_) => None,
513                    })
514                    .collect_vec();
515
516                msgs.push(Message::AddDraggedVariables(
517                    self.filtered_variables(variables.as_slice(), false),
518                ));
519            }
520        });
521        if self.show_scope_tooltip() {
522            response = response.on_hover_ui(|ui| {
523                ui.set_max_width(ui.spacing().tooltip_width);
524                ui.add(egui::Label::new(scope_tooltip_text(
525                    wave,
526                    scope,
527                    self.parameter_display_location() == ParameterDisplayLocation::Tooltips,
528                )));
529            });
530        }
531        response.context_menu(|ui| {
532            if ui.button("Add scope").clicked() {
533                msgs.push(Message::AddScope(scope.clone(), false));
534            }
535            if ui.button("Add scope recursively").clicked() {
536                msgs.push(Message::AddScope(scope.clone(), true));
537            }
538            if ui.button("Add scope as group").clicked() {
539                msgs.push(Message::AddScopeAsGroup(scope.clone(), false));
540            }
541            if ui.button("Add scope as group recursively").clicked() {
542                msgs.push(Message::AddScopeAsGroup(scope.clone(), true));
543            }
544            if wave
545                .inner
546                .as_waves()
547                .is_some_and(|wc| wc.scope_is_array(scope))
548                && ui.button("Show frame buffer").clicked()
549            {
550                msgs.push(Message::SetFrameBufferArray(scope.clone()));
551            }
552        });
553        response.clicked().then(|| {
554            msgs.push(Message::SetActiveScope(if is_selected {
555                None
556            } else {
557                Some(ScopeType::WaveScope(scope.clone()))
558            }));
559        });
560    }
561
562    fn draw_selectable_child_or_orphan_scope(
563        &self,
564        msgs: &mut Vec<Message>,
565        wave: &WaveData,
566        scope: &ScopeRef,
567        draw_variables: bool,
568        ui: &mut Ui,
569    ) {
570        // Extract wave container once to avoid repeated as_waves().unwrap() calls
571        let Some(wave_container) = wave.inner.as_waves() else {
572            return;
573        };
574
575        let Some(child_scopes) = wave_container
576            .child_scopes(scope)
577            .context("Failed to get child scopes")
578            .map_err(|e| warn!("{e:#?}"))
579            .ok()
580        else {
581            return;
582        };
583
584        let no_variables_in_scope = wave_container.no_variables_in_scope(scope);
585        if child_scopes.is_empty() && no_variables_in_scope && !self.show_empty_scopes() {
586            return;
587        }
588
589        if child_scopes.is_empty() && (!draw_variables || no_variables_in_scope) {
590            // Indent our label by both icon width and icon spacing to
591            // match the other headers that actually have an icon.
592            ui.horizontal(|ui| {
593                ui.add_space(ui.spacing().icon_width + ui.spacing().icon_spacing);
594                self.add_scope_selectable_label(msgs, wave, scope, ui, false);
595            });
596        } else {
597            let should_open_header = self.should_open_header_and_scroll_to(scope);
598            let mut collapsing_header =
599                egui::collapsing_header::CollapsingState::load_with_default_open(
600                    ui.ctx(),
601                    egui::Id::new(scope),
602                    false,
603                );
604            if let Some((header_state, _)) = should_open_header {
605                collapsing_header.set_open(header_state);
606            }
607            collapsing_header
608                .show_header(ui, |ui| {
609                    ui.with_layout(
610                        Layout::top_down(Align::LEFT).with_cross_justify(true),
611                        |ui| {
612                            self.add_scope_selectable_label(
613                                msgs,
614                                wave,
615                                scope,
616                                ui,
617                                should_open_header.is_some_and(|(_, scroll)| scroll),
618                            );
619                        },
620                    );
621                })
622                .body(|ui| {
623                    if (draw_variables
624                        && !(matches!(
625                            self.parameter_display_location(),
626                            ParameterDisplayLocation::Tooltips | ParameterDisplayLocation::None
627                        )))
628                        || self.parameter_display_location() == ParameterDisplayLocation::Scopes
629                    {
630                        let parameters = wave_container.parameters_in_scope(scope);
631                        if !parameters.is_empty() {
632                            self.draw_parameters(msgs, wave_container, &parameters, ui);
633                        }
634                    }
635                    self.draw_root_scope_view(msgs, wave, scope, draw_variables, ui);
636                    if draw_variables {
637                        let variables = wave_container.variables_in_scope(scope);
638                        self.filter_and_draw_variable_list(
639                            msgs,
640                            wave_container,
641                            ui,
642                            &variables,
643                            None,
644                        );
645                    }
646                });
647        }
648    }
649
650    fn draw_root_scope_view(
651        &self,
652        msgs: &mut Vec<Message>,
653        wave: &WaveData,
654        root_scope: &ScopeRef,
655        draw_variables: bool,
656        ui: &mut Ui,
657    ) {
658        // Extract wave container once to avoid unwrap
659        let Some(wave_container) = wave.inner.as_waves() else {
660            return;
661        };
662
663        wave_container
664            .child_scopes(root_scope)
665            .context("Failed to get child scopes")
666            .map_err(|e| warn!("{e:#?}"))
667            .ok()
668            .into_iter()
669            .flatten()
670            .sorted_by(|a, b| numeric_sort::cmp(&a.name(), &b.name()))
671            .for_each(|child_scope| {
672                self.draw_selectable_child_or_orphan_scope(
673                    msgs,
674                    wave,
675                    &child_scope,
676                    draw_variables,
677                    ui,
678                );
679            });
680    }
681
682    fn filter_and_draw_variable_list(
683        &self,
684        msgs: &mut Vec<Message>,
685        wave_container: &WaveContainer,
686        ui: &mut Ui,
687        variables: &[VariableRef],
688        row_range: Option<&Range<usize>>,
689    ) {
690        let filtered_variables = self.filtered_variables_unsorted(variables, false);
691        self.draw_variable_list(
692            msgs,
693            wave_container,
694            ui,
695            &filtered_variables,
696            row_range,
697            false,
698        );
699    }
700
701    fn draw_variable_list(
702        &self,
703        msgs: &mut Vec<Message>,
704        wave_container: &WaveContainer,
705        ui: &mut Ui,
706        variables: &[VariableRef],
707        row_range: Option<&Range<usize>>,
708        display_full_path: bool,
709    ) {
710        let variable_rows = self.build_variable_rows(wave_container, variables);
711        self.draw_variable_rows(
712            msgs,
713            wave_container,
714            ui,
715            &variable_rows,
716            row_range,
717            display_full_path,
718        );
719    }
720
721    fn build_variable_rows(
722        &self,
723        wave_container: &WaveContainer,
724        variables: &[VariableRef],
725    ) -> Vec<VariableListRow> {
726        let mut rows = variables
727            .iter()
728            .map(|var| {
729                let meta = wave_container.variable_meta(var).ok();
730                let name_info = self.get_variable_name_info(var, meta.as_ref());
731                VariableListRow {
732                    variable: var.clone(),
733                    meta,
734                    name_info,
735                }
736            })
737            .collect::<Vec<_>>();
738
739        rows.sort_by(|a, b| {
740            let a_priority = a
741                .name_info
742                .as_ref()
743                .and_then(|info| info.priority)
744                .unwrap_or_default();
745            let b_priority = b
746                .name_info
747                .as_ref()
748                .and_then(|info| info.priority)
749                .unwrap_or_default();
750            b_priority
751                .cmp(&a_priority)
752                .then_with(|| self.variable_cmp(&a.variable, &b.variable, Some(wave_container)))
753        });
754        rows
755    }
756
757    fn draw_variable_rows(
758        &self,
759        msgs: &mut Vec<Message>,
760        wave_container: &WaveContainer,
761        ui: &mut Ui,
762        variable_rows: &[VariableListRow],
763        row_range: Option<&Range<usize>>,
764        display_full_path: bool,
765    ) {
766        let variable_rows = if let Some(range) = row_range {
767            let start = range.start.min(variable_rows.len());
768            let end = range.end.min(variable_rows.len());
769            &variable_rows[start..end]
770        } else {
771            variable_rows
772        };
773
774        // Precompute common font metrics once per frame to avoid expensive per-row work.
775        // NOTE: Safe unwrap, we know that egui has its own built-in font.
776        // Use precomputed font and char width where available to reduce work.
777        let monospace_font = ui
778            .style()
779            .text_styles
780            .get(&TextStyle::Monospace)
781            .cloned()
782            .unwrap();
783        let body_font = ui
784            .style()
785            .text_styles
786            .get(&TextStyle::Body)
787            .cloned()
788            .unwrap();
789        let char_width_mono = ui.fonts_mut(|fonts| {
790            fonts
791                .layout_no_wrap(" ".to_string(), monospace_font.clone(), Color32::BLACK)
792                .size()
793                .x
794        });
795        // The button padding is added by egui on selectable labels
796        let available_space = ui.available_width() - ui.spacing().button_padding.x * 2.;
797
798        // Draw variables
799        for row in variable_rows {
800            let variable = &row.variable;
801            let meta = row.meta.as_ref();
802            let name_info = row.name_info.clone();
803
804            // Get index string
805            let index = meta
806                .and_then(|meta| meta.index)
807                .map(|index| {
808                    if self.show_variable_indices() {
809                        format!(" {index}")
810                    } else {
811                        String::new()
812                    }
813                })
814                .unwrap_or_default();
815
816            // Get type icon with color
817            let (type_icon, icon_color) = if self.show_hierarchy_icons() {
818                let (icon, color) = self.user.config.theme.variable_icons.get_icon(meta);
819                (format!("{icon} "), color)
820            } else {
821                (String::new(), self.user.config.theme.foreground)
822            };
823
824            // Get direction icon
825            let direction = self
826                .show_variable_direction()
827                .then(|| get_direction_string(meta, name_info.as_ref()))
828                .flatten()
829                .unwrap_or_default();
830            // Get value in case of parameter
831            let value = if meta.is_some_and(surfer_translation_types::VariableMeta::is_parameter) {
832                let res = wave_container.query_variable(variable, &BigUint::ZERO).ok();
833                res.and_then(|o| o.and_then(|q| q.current.map(|v| format!(": {}", v.1))))
834                    .unwrap_or_else(|| ": Undefined".to_string())
835            } else {
836                String::new()
837            };
838
839            ui.with_layout(
840                Layout::top_down(Align::LEFT).with_cross_justify(true),
841                |ui| {
842                    let mut label = LayoutJob::default();
843                    let true_name = name_info.and_then(|info| info.true_name);
844
845                    let font = if true_name.is_some() {
846                        monospace_font.clone()
847                    } else {
848                        body_font.clone()
849                    };
850                    let icon_format = TextFormat {
851                        font_id: font.clone(),
852                        color: icon_color,
853                        ..Default::default()
854                    };
855                    let text_format = TextFormat {
856                        font_id: font,
857                        color: self.user.config.theme.foreground,
858                        ..Default::default()
859                    };
860
861                    if let Some(name) = true_name {
862                        let type_icon_size = type_icon.chars().count();
863                        let direction_size = direction.chars().count();
864                        let index_size = index.chars().count();
865                        let value_size = value.chars().count();
866                        let used_space = (type_icon_size + direction_size + index_size + value_size)
867                            as f32
868                            * char_width_mono;
869                        let space_for_name = available_space - used_space;
870
871                        label.append(&type_icon, 0.0, icon_format);
872                        label.append(&direction, 0.0, text_format.clone());
873
874                        draw_true_name(
875                            &name,
876                            &mut label,
877                            monospace_font.clone(),
878                            self.user.config.theme.foreground,
879                            char_width_mono,
880                            space_for_name,
881                            self.user.config.layout.waveforms_line_height,
882                        );
883
884                        label.append(&index, 0.0, text_format.clone());
885                        label.append(&value, 0.0, text_format);
886                    } else {
887                        let name = if display_full_path {
888                            variable.full_path_string()
889                        } else {
890                            variable.name.clone()
891                        };
892                        label.append(&type_icon, 0.0, icon_format);
893                        label.append(&direction, 0.0, text_format.clone());
894                        label.append(&name, 0.0, text_format.clone());
895                        label.append(&index, 0.0, text_format.clone());
896                        label.append(&value, 0.0, text_format);
897                    }
898
899                    let mut response = ui.add(egui::Button::selectable(false, label));
900
901                    let _ = response.interact(egui::Sense::click_and_drag());
902
903                    if self.show_tooltip() {
904                        // Reuse the already-obtained `meta` and pass a clone of the variable
905                        // reference into the closure so we don't call `variable_meta` again.
906                        let tooltip_meta = meta;
907                        let tooltip_var = variable.clone();
908                        response = response.on_hover_ui(move |ui| {
909                            ui.set_max_width(ui.spacing().tooltip_width);
910                            ui.add(egui::Label::new(variable_tooltip_text(
911                                tooltip_meta,
912                                &tooltip_var,
913                            )));
914                        });
915                    }
916                    response.drag_started().then(|| {
917                        msgs.push(Message::VariableDragStarted(VisibleItemIndex(
918                            self.user.waves.as_ref().unwrap().display_item_ref_counter,
919                        )));
920                    });
921                    response.drag_stopped().then(|| {
922                        if ui.input(|i| i.pointer.hover_pos().unwrap_or_default().x)
923                            > self.user.sidepanel_width.unwrap_or_default()
924                        {
925                            msgs.push(Message::AddDraggedVariables(vec![variable.clone()]));
926                        }
927                    });
928                    response
929                        .clicked()
930                        .then(|| msgs.push(Message::AddVariables(vec![variable.clone()])));
931                },
932            );
933        }
934    }
935
936    fn should_open_header_and_scroll_to(&self, scope: &ScopeRef) -> Option<(bool, bool)> {
937        let mut scope_ref_cell = self.scope_ref_to_expand.borrow_mut();
938        if let Some(state) = scope_ref_cell.as_mut() {
939            match state {
940                ScopeExpandType::ExpandAll => return Some((true, false)),
941                ScopeExpandType::CollapseAll => return Some((false, false)),
942                ScopeExpandType::ExpandSpecific(state) => {
943                    if state.strs.starts_with(&scope.strs) {
944                        if (state.strs.len() - 1) == scope.strs.len() {
945                            // need to compare vs. parent of signal
946                            *scope_ref_cell = None;
947                        }
948                        return Some((true, true));
949                    }
950                }
951            }
952        }
953        None
954    }
955}