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 * 0.5)
92            .max_size(total_space - 64.0)
93            .frame(Frame::new().inner_margin(Margin::same(5)))
94            .show(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(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
545            let is_array_scope = wave
546                .inner
547                .as_waves()
548                .is_some_and(|wc| wc.scope_is_array(scope));
549
550            if is_array_scope && ui.button("Show frame buffer").clicked() {
551                msgs.push(Message::SetFrameBufferArray(scope.clone()));
552            }
553
554            if is_array_scope && ui.button("Show Memory Viewer").clicked() {
555                msgs.push(Message::OpenMemoryViewer {
556                    scope: scope.clone(),
557                    name: Some(scope.name()),
558                });
559            }
560        });
561        response.clicked().then(|| {
562            msgs.push(Message::SetActiveScope(if is_selected {
563                None
564            } else {
565                Some(ScopeType::WaveScope(scope.clone()))
566            }));
567        });
568    }
569
570    fn draw_selectable_child_or_orphan_scope(
571        &self,
572        msgs: &mut Vec<Message>,
573        wave: &WaveData,
574        scope: &ScopeRef,
575        draw_variables: bool,
576        ui: &mut Ui,
577    ) {
578        // Extract wave container once to avoid repeated as_waves().unwrap() calls
579        let Some(wave_container) = wave.inner.as_waves() else {
580            return;
581        };
582
583        let Some(child_scopes) = wave_container
584            .child_scopes(scope)
585            .context("Failed to get child scopes")
586            .map_err(|e| warn!("{e:#?}"))
587            .ok()
588        else {
589            return;
590        };
591
592        let no_variables_in_scope = wave_container.no_variables_in_scope(scope);
593        if child_scopes.is_empty() && no_variables_in_scope && !self.show_empty_scopes() {
594            return;
595        }
596
597        if child_scopes.is_empty() && (!draw_variables || no_variables_in_scope) {
598            // Indent our label by both icon width and icon spacing to
599            // match the other headers that actually have an icon.
600            ui.horizontal(|ui| {
601                ui.add_space(ui.spacing().icon_width + ui.spacing().icon_spacing);
602                self.add_scope_selectable_label(msgs, wave, scope, ui, false);
603            });
604        } else {
605            let should_open_header = self.should_open_header_and_scroll_to(scope);
606            let mut collapsing_header =
607                egui::collapsing_header::CollapsingState::load_with_default_open(
608                    ui.ctx(),
609                    egui::Id::new(scope),
610                    false,
611                );
612            if let Some((header_state, _)) = should_open_header {
613                collapsing_header.set_open(header_state);
614            }
615            collapsing_header
616                .show_header(ui, |ui| {
617                    ui.with_layout(
618                        Layout::top_down(Align::LEFT).with_cross_justify(true),
619                        |ui| {
620                            self.add_scope_selectable_label(
621                                msgs,
622                                wave,
623                                scope,
624                                ui,
625                                should_open_header.is_some_and(|(_, scroll)| scroll),
626                            );
627                        },
628                    );
629                })
630                .body(|ui| {
631                    if (draw_variables
632                        && !(matches!(
633                            self.parameter_display_location(),
634                            ParameterDisplayLocation::Tooltips | ParameterDisplayLocation::None
635                        )))
636                        || self.parameter_display_location() == ParameterDisplayLocation::Scopes
637                    {
638                        let parameters = wave_container.parameters_in_scope(scope);
639                        if !parameters.is_empty() {
640                            self.draw_parameters(msgs, wave_container, &parameters, ui);
641                        }
642                    }
643                    self.draw_root_scope_view(msgs, wave, scope, draw_variables, ui);
644                    if draw_variables {
645                        let variables = wave_container.variables_in_scope(scope);
646                        self.filter_and_draw_variable_list(
647                            msgs,
648                            wave_container,
649                            ui,
650                            &variables,
651                            None,
652                        );
653                    }
654                });
655        }
656    }
657
658    fn draw_root_scope_view(
659        &self,
660        msgs: &mut Vec<Message>,
661        wave: &WaveData,
662        root_scope: &ScopeRef,
663        draw_variables: bool,
664        ui: &mut Ui,
665    ) {
666        // Extract wave container once to avoid unwrap
667        let Some(wave_container) = wave.inner.as_waves() else {
668            return;
669        };
670
671        wave_container
672            .child_scopes(root_scope)
673            .context("Failed to get child scopes")
674            .map_err(|e| warn!("{e:#?}"))
675            .ok()
676            .into_iter()
677            .flatten()
678            .sorted_by(|a, b| numeric_sort::cmp(&a.name(), &b.name()))
679            .for_each(|child_scope| {
680                self.draw_selectable_child_or_orphan_scope(
681                    msgs,
682                    wave,
683                    &child_scope,
684                    draw_variables,
685                    ui,
686                );
687            });
688    }
689
690    fn filter_and_draw_variable_list(
691        &self,
692        msgs: &mut Vec<Message>,
693        wave_container: &WaveContainer,
694        ui: &mut Ui,
695        variables: &[VariableRef],
696        row_range: Option<&Range<usize>>,
697    ) {
698        let filtered_variables = self.filtered_variables_unsorted(variables, false);
699        self.draw_variable_list(
700            msgs,
701            wave_container,
702            ui,
703            &filtered_variables,
704            row_range,
705            false,
706        );
707    }
708
709    fn draw_variable_list(
710        &self,
711        msgs: &mut Vec<Message>,
712        wave_container: &WaveContainer,
713        ui: &mut Ui,
714        variables: &[VariableRef],
715        row_range: Option<&Range<usize>>,
716        display_full_path: bool,
717    ) {
718        let variable_rows = self.build_variable_rows(wave_container, variables);
719        self.draw_variable_rows(
720            msgs,
721            wave_container,
722            ui,
723            &variable_rows,
724            row_range,
725            display_full_path,
726        );
727    }
728
729    fn build_variable_rows(
730        &self,
731        wave_container: &WaveContainer,
732        variables: &[VariableRef],
733    ) -> Vec<VariableListRow> {
734        let mut rows = variables
735            .iter()
736            .map(|var| {
737                let meta = wave_container.variable_meta(var).ok();
738                let name_info = self.get_variable_name_info(var, meta.as_ref());
739                VariableListRow {
740                    variable: var.clone(),
741                    meta,
742                    name_info,
743                }
744            })
745            .collect::<Vec<_>>();
746
747        rows.sort_by(|a, b| {
748            let a_priority = a
749                .name_info
750                .as_ref()
751                .and_then(|info| info.priority)
752                .unwrap_or_default();
753            let b_priority = b
754                .name_info
755                .as_ref()
756                .and_then(|info| info.priority)
757                .unwrap_or_default();
758            b_priority
759                .cmp(&a_priority)
760                .then_with(|| self.variable_cmp(&a.variable, &b.variable, Some(wave_container)))
761        });
762        rows
763    }
764
765    fn draw_variable_rows(
766        &self,
767        msgs: &mut Vec<Message>,
768        wave_container: &WaveContainer,
769        ui: &mut Ui,
770        variable_rows: &[VariableListRow],
771        row_range: Option<&Range<usize>>,
772        display_full_path: bool,
773    ) {
774        let variable_rows = if let Some(range) = row_range {
775            let start = range.start.min(variable_rows.len());
776            let end = range.end.min(variable_rows.len());
777            &variable_rows[start..end]
778        } else {
779            variable_rows
780        };
781
782        // Precompute common font metrics once per frame to avoid expensive per-row work.
783        // NOTE: Safe unwrap, we know that egui has its own built-in font.
784        // Use precomputed font and char width where available to reduce work.
785        let monospace_font = ui
786            .style()
787            .text_styles
788            .get(&TextStyle::Monospace)
789            .cloned()
790            .unwrap();
791        let body_font = ui
792            .style()
793            .text_styles
794            .get(&TextStyle::Body)
795            .cloned()
796            .unwrap();
797        let char_width_mono = ui.fonts_mut(|fonts| {
798            fonts
799                .layout_no_wrap(" ".to_string(), monospace_font.clone(), Color32::BLACK)
800                .size()
801                .x
802        });
803        // The button padding is added by egui on selectable labels
804        let available_space = ui.available_width() - ui.spacing().button_padding.x * 2.;
805
806        // Draw variables
807        for row in variable_rows {
808            let variable = &row.variable;
809            let meta = row.meta.as_ref();
810            let name_info = row.name_info.clone();
811
812            // Get index string
813            let index = meta
814                .and_then(|meta| meta.index)
815                .map(|index| {
816                    if self.show_variable_indices() {
817                        format!(" {index}")
818                    } else {
819                        String::new()
820                    }
821                })
822                .unwrap_or_default();
823
824            // Get type icon with color
825            let (type_icon, icon_color) = if self.show_hierarchy_icons() {
826                let (icon, color) = self.user.config.theme.variable_icons.get_icon(meta);
827                (format!("{icon} "), color)
828            } else {
829                (String::new(), self.user.config.theme.foreground)
830            };
831
832            // Get direction icon
833            let direction = self
834                .show_variable_direction()
835                .then(|| get_direction_string(meta, name_info.as_ref()))
836                .flatten()
837                .unwrap_or_default();
838            // Get value in case of parameter
839            let value = if meta.is_some_and(surfer_translation_types::VariableMeta::is_parameter) {
840                let res = wave_container.query_variable(variable, &BigUint::ZERO).ok();
841                res.and_then(|o| o.and_then(|q| q.current.map(|v| format!(": {}", v.1))))
842                    .unwrap_or_else(|| ": Undefined".to_string())
843            } else {
844                String::new()
845            };
846
847            ui.with_layout(
848                Layout::top_down(Align::LEFT).with_cross_justify(true),
849                |ui| {
850                    let mut label = LayoutJob::default();
851                    let true_name = name_info.and_then(|info| info.true_name);
852
853                    let font = if true_name.is_some() {
854                        monospace_font.clone()
855                    } else {
856                        body_font.clone()
857                    };
858                    let icon_format = TextFormat {
859                        font_id: font.clone(),
860                        color: icon_color,
861                        ..Default::default()
862                    };
863                    let text_format = TextFormat {
864                        font_id: font,
865                        color: self.user.config.theme.foreground,
866                        ..Default::default()
867                    };
868
869                    if let Some(name) = true_name {
870                        let type_icon_size = type_icon.chars().count();
871                        let direction_size = direction.chars().count();
872                        let index_size = index.chars().count();
873                        let value_size = value.chars().count();
874                        let used_space = (type_icon_size + direction_size + index_size + value_size)
875                            as f32
876                            * char_width_mono;
877                        let space_for_name = available_space - used_space;
878
879                        label.append(&type_icon, 0.0, icon_format);
880                        label.append(&direction, 0.0, text_format.clone());
881
882                        draw_true_name(
883                            &name,
884                            &mut label,
885                            &monospace_font,
886                            self.user.config.theme.foreground,
887                            char_width_mono,
888                            space_for_name,
889                            self.user.config.layout.waveforms_line_height,
890                        );
891
892                        label.append(&index, 0.0, text_format.clone());
893                        label.append(&value, 0.0, text_format);
894                    } else {
895                        let name = if display_full_path {
896                            variable.full_path_string()
897                        } else {
898                            variable.name.clone()
899                        };
900                        label.append(&type_icon, 0.0, icon_format);
901                        label.append(&direction, 0.0, text_format.clone());
902                        label.append(&name, 0.0, text_format.clone());
903                        label.append(&index, 0.0, text_format.clone());
904                        label.append(&value, 0.0, text_format);
905                    }
906
907                    let mut response = ui.add(egui::Button::selectable(false, label));
908
909                    let _ = response.interact(egui::Sense::click_and_drag());
910
911                    if self.show_tooltip() {
912                        // Reuse the already-obtained `meta` and pass a clone of the variable
913                        // reference into the closure so we don't call `variable_meta` again.
914                        let tooltip_meta = meta;
915                        let tooltip_var = variable.clone();
916                        response = response.on_hover_ui(move |ui| {
917                            ui.set_max_width(ui.spacing().tooltip_width);
918                            ui.add(egui::Label::new(variable_tooltip_text(
919                                tooltip_meta,
920                                &tooltip_var,
921                            )));
922                        });
923                    }
924                    response.drag_started().then(|| {
925                        msgs.push(Message::VariableDragStarted(VisibleItemIndex(
926                            self.user.waves.as_ref().unwrap().display_item_ref_counter,
927                        )));
928                    });
929                    response.drag_stopped().then(|| {
930                        if ui.input(|i| i.pointer.hover_pos().unwrap_or_default().x)
931                            > self.user.sidepanel_width.unwrap_or_default()
932                        {
933                            msgs.push(Message::AddDraggedVariables(vec![variable.clone()]));
934                        }
935                    });
936                    response
937                        .clicked()
938                        .then(|| msgs.push(Message::AddVariables(vec![variable.clone()])));
939                },
940            );
941        }
942    }
943
944    fn should_open_header_and_scroll_to(&self, scope: &ScopeRef) -> Option<(bool, bool)> {
945        let mut scope_ref_cell = self.scope_ref_to_expand.borrow_mut();
946        if let Some(state) = scope_ref_cell.as_mut() {
947            match state {
948                ScopeExpandType::ExpandAll => return Some((true, false)),
949                ScopeExpandType::CollapseAll => return Some((false, false)),
950                ScopeExpandType::ExpandSpecific(state) => {
951                    if state.strs.starts_with(&scope.strs) {
952                        if (state.strs.len() - 1) == scope.strs.len() {
953                            // need to compare vs. parent of signal
954                            *scope_ref_cell = None;
955                        }
956                        return Some((true, true));
957                    }
958                }
959            }
960        }
961        None
962    }
963}