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