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