Skip to main content

libsurfer/
view.rs

1use crate::{
2    config::{FocusHighlight, ThemeColorPair, TransitionValue},
3    dialog::{draw_open_sibling_state_file_dialog, draw_reload_waveform_dialog},
4    displayed_item::DisplayedVariable,
5    fzcmd::expand_command,
6    item_drawing_info::{
7        DividerDrawingInfo, GroupDrawingInfo, ItemDrawingInfo, MarkerDrawingInfo,
8        PlaceholderDrawingInfo, StreamDrawingInfo, TimeLineDrawingInfo, VariableDrawingInfo,
9    },
10    menus::generic_context_menu,
11    time::TimeFormatter,
12    tooltips::variable_tooltip_text,
13    wave_container::{ScopeId, VarId, VariableMeta},
14};
15use ahash::{AHashSet, AHasher};
16use ecolor::Color32;
17#[cfg(not(target_arch = "wasm32"))]
18use egui::ViewportCommand;
19use egui::{
20    CentralPanel, FontSelection, Frame, Layout, Painter, Panel, RichText, ScrollArea, Sense,
21    TextStyle, Ui, UiBuilder, WidgetText,
22};
23use emath::{Align, GuiRounding, Pos2, Rect, RectTransform, Vec2};
24use epaint::{
25    CornerRadius, Margin, Shape, Stroke,
26    text::{FontId, LayoutJob, TextFormat, TextWrapMode},
27};
28use itertools::Itertools;
29use num::{BigUint, One, Zero};
30use std::hash::{Hash, Hasher};
31use std::rc::Rc;
32use tracing::info;
33
34use surfer_translation_types::{
35    TranslatedValue, Translator, VariableInfo, VariableValue,
36    translator::{TrueName, VariableNameInfo},
37};
38
39use crate::OUTSTANDING_TRANSACTIONS;
40#[cfg(feature = "performance_plot")]
41use crate::benchmark::NUM_PERF_SAMPLES;
42use crate::command_parser::get_parser;
43use crate::config::SurferTheme;
44use crate::displayed_item::{DisplayedFieldRef, DisplayedItem, DisplayedItemRef};
45use crate::displayed_item_tree::{ItemIndex, VisibleItemIndex};
46use crate::help::{
47    draw_about_window, draw_control_help_window, draw_license_window, draw_quickstart_help_window,
48};
49use crate::time::time_string;
50use crate::translation::TranslationResultExt;
51use crate::util::get_alpha_focus_id;
52use crate::wave_container::{FieldRef, FieldRefExt, VariableRef};
53use crate::{
54    Message, MoveDir, SystemState, command_prompt::show_command_prompt, hierarchy::HierarchyStyle,
55    wave_data::WaveData,
56};
57
58pub struct DrawingContext<'a> {
59    pub painter: &'a mut Painter,
60    pub cfg: &'a DrawConfig,
61    pub to_screen: &'a dyn Fn(f32, f32) -> Pos2,
62    pub theme: &'a SurferTheme,
63}
64
65#[derive(Debug)]
66pub struct DrawConfig {
67    pub canvas_size: Vec2,
68    pub line_height: f32,
69    pub text_size: f32,
70    pub extra_draw_width: i32,
71}
72
73impl DrawConfig {
74    #[must_use]
75    pub fn new(canvas_size: Vec2, line_height: f32, text_size: f32) -> Self {
76        Self {
77            canvas_size,
78            line_height,
79            text_size,
80            extra_draw_width: 6,
81        }
82    }
83}
84
85impl eframe::App for SystemState {
86    fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
87        #[cfg(feature = "performance_plot")]
88        self.timing.borrow_mut().start_frame();
89
90        if self.continuous_redraw {
91            self.invalidate_draw_commands();
92        }
93
94        let (fullscreen, window_size) = ui.input(|i| {
95            (
96                i.viewport().fullscreen.unwrap_or_default(),
97                Some(i.viewport_rect().size()),
98            )
99        });
100        #[cfg(target_arch = "wasm32")]
101        let _ = fullscreen;
102
103        #[cfg(feature = "performance_plot")]
104        self.timing.borrow_mut().start("draw");
105        let mut msgs = self.draw(ui, window_size);
106        #[cfg(feature = "performance_plot")]
107        self.timing.borrow_mut().end("draw");
108
109        #[cfg(feature = "performance_plot")]
110        self.timing.borrow_mut().start("push_async_messages");
111        self.push_async_messages(&mut msgs);
112        #[cfg(feature = "performance_plot")]
113        self.timing.borrow_mut().end("push_async_messages");
114
115        #[cfg(feature = "performance_plot")]
116        self.timing.borrow_mut().start("update");
117        let ui_zoom_factor = self.ui_zoom_factor();
118        if ui.zoom_factor() != ui_zoom_factor {
119            ui.set_zoom_factor(ui_zoom_factor);
120        }
121
122        while let Some(msg) = msgs.pop() {
123            #[cfg(not(target_arch = "wasm32"))]
124            if let Message::Exit = msg {
125                ui.send_viewport_cmd(ViewportCommand::Close);
126            }
127            #[cfg(not(target_arch = "wasm32"))]
128            if let Message::ToggleFullscreen = msg {
129                ui.send_viewport_cmd(ViewportCommand::Fullscreen(!fullscreen));
130            }
131            self.update(msg);
132        }
133        #[cfg(feature = "performance_plot")]
134        self.timing.borrow_mut().end("update");
135
136        self.handle_batch_commands();
137        #[cfg(target_arch = "wasm32")]
138        self.handle_wasm_external_messages();
139
140        let viewport_is_moving = if let Some(waves) = &mut self.user.waves {
141            let mut is_moving = false;
142            for vp in &mut waves.viewports {
143                if vp.is_moving() {
144                    vp.move_viewport(ui.input(|i| i.stable_dt));
145                    is_moving = true;
146                }
147            }
148            is_moving
149        } else {
150            false
151        };
152
153        if let Some(waves) = self.user.waves.as_ref().and_then(|w| w.inner.as_waves()) {
154            waves.tick();
155        }
156
157        if viewport_is_moving {
158            self.invalidate_draw_commands();
159            ui.request_repaint();
160        }
161
162        #[cfg(feature = "performance_plot")]
163        self.timing.borrow_mut().start("handle_wcp_commands");
164        self.handle_wcp_commands();
165        #[cfg(feature = "performance_plot")]
166        self.timing.borrow_mut().end("handle_wcp_commands");
167
168        // We can save some user battery life by not redrawing unless needed. At the moment,
169        // we only need to continuously redraw to make surfer interactive during loading, otherwise
170        // we'll let egui manage repainting. In practice
171        if self.continuous_redraw
172            || self.progress_tracker.is_some()
173            || self.user.show_performance
174            || OUTSTANDING_TRANSACTIONS.load(std::sync::atomic::Ordering::SeqCst) != 0
175        {
176            ui.request_repaint();
177        }
178
179        #[cfg(feature = "performance_plot")]
180        if let Some(prev_cpu) = frame.info().cpu_usage {
181            self.rendering_cpu_times.push_back(prev_cpu);
182            if self.rendering_cpu_times.len() > NUM_PERF_SAMPLES {
183                self.rendering_cpu_times.pop_front();
184            }
185        }
186
187        #[cfg(feature = "performance_plot")]
188        self.timing.borrow_mut().end_frame();
189
190        // Must be last: override cursor after all rendering so nothing can overwrite it.
191        if self.toolbar_dragging_group.is_some() {
192            ui.ctx()
193                .output_mut(|o| o.cursor_icon = egui::CursorIcon::Grabbing);
194        }
195    }
196}
197
198/// A single row produced by flattening a (possibly compound/nested) variable's expanded
199/// fields; see `SystemState::flatten_variable_rows`.
200pub(crate) struct VariableFieldRow {
201    field: FieldRef,
202    depth: u32,
203    is_compound: bool,
204    has_children: bool,
205    unfolded: bool,
206}
207
208impl SystemState {
209    pub(crate) fn draw(&mut self, ui: &mut Ui, window_size: Option<Vec2>) -> Vec<Message> {
210        if crate::logs::take_error_notification() {
211            self.user.show_logs = true;
212        }
213
214        let max_width = ui.available_size().x;
215        let max_height = ui.available_size().y;
216
217        let mut msgs = vec![];
218
219        if self.user.show_about {
220            draw_about_window(ui, &mut msgs);
221        }
222
223        if self.user.show_license {
224            draw_license_window(ui, &mut msgs);
225        }
226
227        if self.user.show_keys {
228            draw_control_help_window(ui, &mut msgs, &self.user.config.shortcuts);
229        }
230
231        if self.user.show_quick_start {
232            draw_quickstart_help_window(ui, &mut msgs, &self.user.config.shortcuts);
233        }
234
235        if self.user.show_gestures {
236            self.mouse_gesture_help(ui, &mut msgs);
237        }
238
239        if self.user.show_logs {
240            self.draw_log_window(ui, &mut msgs);
241        }
242
243        if self.frame_buffer_content.is_some() {
244            self.draw_frame_buffer_window(ui.ctx(), &mut msgs);
245        }
246
247        self.draw_memory_viewer_window(ui.ctx(), &mut msgs);
248
249        if let Some(dialog) = self.user.show_reload_suggestion {
250            draw_reload_waveform_dialog(ui, dialog, &mut msgs);
251        }
252
253        if let Some(dialog) = self.user.show_open_sibling_state_file_suggestion {
254            draw_open_sibling_state_file_dialog(ui, dialog, &mut msgs);
255        }
256
257        if self.user.show_performance {
258            #[cfg(feature = "performance_plot")]
259            self.draw_performance_graph(ui, &mut msgs);
260        }
261
262        if self.user.show_cursor_window
263            && let Some(waves) = &self.user.waves
264        {
265            self.draw_marker_window(waves, ui, &mut msgs);
266        }
267
268        if self
269            .user
270            .show_menu
271            .unwrap_or_else(|| self.user.config.layout.show_menu())
272        {
273            self.add_menu_panel(ui, &mut msgs);
274        }
275
276        if self.show_toolbar() {
277            self.add_toolbar_panel(ui, &mut msgs);
278        }
279
280        if self.user.show_url_entry {
281            self.draw_load_url(ui, &mut msgs);
282        }
283
284        if self.user.show_server_file_window {
285            self.draw_surver_file_window(ui, &mut msgs);
286        }
287
288        if self.show_statusbar() {
289            self.add_statusbar_panel(ui, self.user.waves.as_ref(), &mut msgs);
290        }
291
292        if let Some(waves) = &self.user.waves
293            && self.show_overview()
294            && !waves.items_tree.is_empty()
295        {
296            self.add_overview_panel(ui, waves, &mut msgs);
297        }
298
299        if self.show_hierarchy() {
300            Panel::left("variable select left panel")
301                .default_size(300.)
302                .size_range(100.0..=max_width)
303                .frame(Frame {
304                    fill: self.user.config.theme.primary_ui_color.background,
305                    ..Default::default()
306                })
307                .show(ui, |ui| {
308                    self.user.sidepanel_width = Some(ui.clip_rect().width());
309                    match self.hierarchy_style() {
310                        HierarchyStyle::Separate => self.separate(ui, &mut msgs),
311                        HierarchyStyle::Tree => self.tree(ui, &mut msgs),
312                        HierarchyStyle::Variables => self.variable_list(ui, &mut msgs),
313                    }
314                });
315        }
316
317        if self.command_prompt.visible {
318            show_command_prompt(self, ui, window_size, &mut msgs);
319            if let Some(new_idx) = self.command_prompt.new_selection {
320                self.command_prompt.selected = new_idx;
321                self.command_prompt.new_selection = None;
322            }
323        }
324
325        if let Some(user_waves) = &self.user.waves {
326            let scroll_offset = user_waves.scroll_offset;
327            if user_waves.any_displayed() {
328                let draw_focus_ids = self.command_prompt.visible
329                    && expand_command(&self.command_prompt_text.borrow(), get_parser(self))
330                        .expanded
331                        .starts_with("item_focus");
332                if draw_focus_ids {
333                    Panel::left("focus id list")
334                        .default_size(40.)
335                        .size_range(40.0..=max_width)
336                        .show(ui, |ui| {
337                            if self.show_default_timeline() {
338                                ui.add_space(self.default_timeline_offset());
339                            }
340                            let response = ScrollArea::both()
341                                .vertical_scroll_offset(scroll_offset)
342                                .show(ui, |ui| {
343                                    self.draw_item_focus_list(ui);
344                                });
345                            self.user.waves.as_mut().unwrap().total_height =
346                                response.inner_rect.height();
347                            if (scroll_offset - response.state.offset.y).abs() > 5. {
348                                msgs.push(Message::SetScrollOffset(response.state.offset.y));
349                            }
350                        });
351                }
352
353                Panel::left("variable list")
354                    .frame(
355                        Frame::default()
356                            .inner_margin(0)
357                            .outer_margin(0)
358                            .fill(self.user.config.theme.secondary_ui_color.background)
359                            .stroke(Stroke::NONE),
360                    )
361                    .default_size(100.)
362                    .size_range(100.0..=max_width)
363                    .show(ui, |ui| {
364                        ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
365                        let text_margin = Self::item_text_margin(ui);
366                        // Zeroed so the gap between this header and the `ScrollArea` below
367                        // is exactly the reserved space added below, matching the canvas
368                        // (see `SystemState::default_timeline_offset`).
369                        ui.spacing_mut().item_spacing.y = 0.0;
370                        if self.show_default_timeline() {
371                            let header_top = ui.cursor().top();
372                            ui.allocate_ui_with_layout(
373                                Vec2::new(
374                                    ui.available_width(),
375                                    self.user.config.layout.waveforms_text_size,
376                                ),
377                                Layout::top_down(Align::LEFT),
378                                |ui| {
379                                    ui.horizontal(|ui| {
380                                        ui.add_space(text_margin.x);
381                                        ui.label(RichText::new("Time").italics());
382                                    });
383                                },
384                            );
385                            // The label's rendered height can exceed the requested size (e.g.
386                            // due to font metrics), so pad up to the exact reserved offset
387                            // instead of assuming it consumed `waveforms_text_size`.
388                            let target_bottom = header_top + self.default_timeline_offset();
389                            let next_y = ui.cursor().top();
390                            if next_y < target_bottom {
391                                ui.add_space(target_bottom - next_y);
392                            }
393                        }
394
395                        let response = ScrollArea::both()
396                            .auto_shrink([false; 2])
397                            .vertical_scroll_offset(scroll_offset)
398                            .show(ui, |ui| {
399                                self.draw_item_list(&mut msgs, ui);
400                            });
401                        self.user.waves.as_mut().unwrap().total_height =
402                            response.inner_rect.height();
403                        if (scroll_offset - response.state.offset.y).abs() > 5. {
404                            msgs.push(Message::SetScrollOffset(response.state.offset.y));
405                        }
406                    });
407
408                // Will only draw if a transaction is focused
409                self.draw_transaction_detail_panel(ui, max_width, &mut msgs);
410
411                Panel::left("variable values")
412                    .frame(
413                        Frame::default()
414                            .inner_margin(0)
415                            .outer_margin(0)
416                            .fill(self.user.config.theme.secondary_ui_color.background)
417                            .stroke(Stroke::NONE),
418                    )
419                    .default_size(100.)
420                    .size_range(10.0..=max_width)
421                    .show(ui, |ui| {
422                        ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
423                        if self.show_default_timeline() {
424                            ui.add_space(self.default_timeline_offset());
425                        }
426                        let response = ScrollArea::both()
427                            .auto_shrink([false; 2])
428                            .vertical_scroll_offset(scroll_offset)
429                            .show(ui, |ui| self.draw_var_values(ui, &mut msgs));
430                        if (scroll_offset - response.state.offset.y).abs() > 5. {
431                            msgs.push(Message::SetScrollOffset(response.state.offset.y));
432                        }
433                    });
434                let std_stroke = ui.style().visuals.widgets.noninteractive.bg_stroke;
435                ui.style_mut().visuals.widgets.noninteractive.bg_stroke =
436                    Stroke::from(&self.user.config.theme.viewport_separator);
437
438                if self.user.show_annotation_list {
439                    let Some(waves) = self.user.waves.as_ref() else {
440                        return msgs;
441                    };
442
443                    let time_formatter = TimeFormatter::new(
444                        &waves.inner.metadata().timescale,
445                        &self.user.wanted_timeunit,
446                        &self.get_time_format(),
447                    );
448
449                    let Some(waves) = self.user.waves.as_mut() else {
450                        return msgs;
451                    };
452                    let annotation_groups = &mut waves.annotation_groups.clone();
453
454                    Panel::right("Annotation list")
455                        .default_size(290.)
456                        .size_range(100.0..=max_width)
457                        .show_separator_line(false)
458                        .frame(
459                            Frame::default()
460                                .inner_margin(0)
461                                .outer_margin(0)
462                                .fill(self.user.config.theme.secondary_ui_color.background)
463                                .stroke(std_stroke),
464                        )
465                        .show(ui, |ui| {
466                            waves.draw_annotation_list(
467                                ui,
468                                &mut msgs,
469                                &time_formatter,
470                                annotation_groups,
471                            );
472                        });
473
474                    waves.annotation_groups = annotation_groups.clone();
475                }
476
477                self.click_handled = false;
478
479                let number_of_viewports = self.user.waves.as_ref().unwrap().viewports.len();
480                if number_of_viewports > 1 {
481                    // Draw additional viewports
482                    let max_width = ui.available_width();
483                    let default_size = max_width / (number_of_viewports as f32);
484                    for viewport_idx in 1..number_of_viewports {
485                        Panel::right(format!("view port {viewport_idx}"))
486                            .default_size(default_size)
487                            .size_range(30.0..=max_width)
488                            .frame(Frame {
489                                inner_margin: Margin::ZERO,
490                                outer_margin: Margin::ZERO,
491                                ..Default::default()
492                            })
493                            .show(ui, |ui| self.draw_items(ui, &mut msgs, viewport_idx));
494                    }
495                }
496
497                CentralPanel::default()
498                    .frame(Frame {
499                        inner_margin: Margin::ZERO,
500                        outer_margin: Margin::ZERO,
501                        ..Default::default()
502                    })
503                    .show(ui, |ui| {
504                        self.draw_items(ui, &mut msgs, 0);
505                        if ui.input(|i| i.pointer.primary_clicked()) && !self.click_handled {
506                            msgs.push(Message::AnnotationClicked(None, None, None, None, None));
507                        }
508                    });
509                ui.style_mut().visuals.widgets.noninteractive.bg_stroke = std_stroke;
510            }
511        }
512
513        if self.user.waves.is_none()
514            || self
515                .user
516                .waves
517                .as_ref()
518                .is_some_and(|waves| !waves.any_displayed())
519        {
520            CentralPanel::default()
521                .frame(Frame::NONE.fill(self.user.config.theme.canvas_colors.background))
522                .show(ui, |ui| {
523                    ui.add_space(max_height * 0.1);
524                    ui.vertical_centered(|ui| {
525                        ui.label(RichText::new("πŸ„ Surfer").monospace().size(24.));
526                        ui.add_space(20.);
527                        let layout = Layout::top_down(Align::LEFT);
528                        ui.allocate_ui_with_layout(
529                            Vec2 {
530                                x: max_width * 0.35,
531                                y: max_height * 0.5,
532                            },
533                            layout,
534                            |ui| self.help_message(ui),
535                        );
536                    });
537                });
538        }
539
540        ui.input(|i| {
541            i.raw.dropped_files.iter().for_each(|file| {
542                info!("Got dropped file");
543                msgs.push(Message::FileDropped(file.clone()));
544            });
545        });
546
547        // If some dialogs are open, skip decoding keypresses
548        // if !self.user.show_url_entry && self.user.show_reload_suggestion.is_none() {
549        //     self.handle_pressed_keys(ctx, &mut msgs);
550        // }
551
552        // If egui want keyboard inputs, skip decoding keypresses
553        if !self.user.show_url_entry
554            && self.user.show_reload_suggestion.is_none()
555            && !ui.egui_wants_keyboard_input()
556        {
557            self.handle_pressed_keys(ui, &mut msgs);
558        }
559
560        msgs
561    }
562
563    fn draw_load_url(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
564        let mut open = true;
565        egui::Window::new("Load URL")
566            .open(&mut open)
567            .collapsible(false)
568            .resizable(true)
569            .show(ui, |ui| {
570                ui.vertical_centered(|ui| {
571                    let url = &mut *self.url.borrow_mut();
572                    let response = ui.text_edit_singleline(url);
573                    ui.horizontal(|ui| {
574                        if ui.button("Load URL").clicked()
575                            || (response.lost_focus()
576                                && ui.input(|i| i.key_pressed(egui::Key::Enter)))
577                        {
578                            if let Some(callback) = &self.url_callback {
579                                msgs.push(callback(url.clone()));
580                            }
581                            msgs.push(Message::SetUrlEntryVisible(false, None));
582                        }
583                        if ui.button("Cancel").clicked() {
584                            msgs.push(Message::SetUrlEntryVisible(false, None));
585                        }
586                    });
587                });
588            });
589        if !open {
590            msgs.push(Message::SetUrlEntryVisible(false, None));
591        }
592    }
593
594    pub fn handle_pointer_in_ui(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
595        if ui.ui_contains_pointer() {
596            let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
597            if scroll_delta.y > 0.0 {
598                msgs.push(Message::InvalidateCount);
599                msgs.push(Message::VerticalScroll(MoveDir::Up, self.get_count()));
600            } else if scroll_delta.y < 0.0 {
601                msgs.push(Message::InvalidateCount);
602                msgs.push(Message::VerticalScroll(MoveDir::Down, self.get_count()));
603            }
604        }
605    }
606
607    /// Add bottom padding so the last item isn’t clipped or covered by the scrollbar.
608    /// `last_bottom` must already be in this `ui`'s coordinate space (see
609    /// `ItemDrawingInfo::bottom_at`).
610    fn add_padding_for_last_bottom(ui: &mut Ui, last_bottom: Option<f32>, line_height: f32) {
611        if let Some(bottom) = last_bottom {
612            let target_bottom = bottom + line_height;
613            let next_y = ui.cursor().top();
614            if next_y < target_bottom {
615                ui.add_space(target_bottom - next_y);
616            }
617        }
618    }
619
620    fn item_text_margin(ui: &Ui) -> Vec2 {
621        ui.spacing().item_spacing
622    }
623
624    fn enforce_stable_row_widget_expansion(ui: &mut Ui) {
625        let visuals = &mut ui.style_mut().visuals.widgets;
626        visuals.inactive.expansion = 0.0;
627        visuals.hovered.expansion = 0.0;
628        visuals.active.expansion = 0.0;
629        visuals.open.expansion = 0.0;
630    }
631
632    fn desired_item_row_height(&self, displayed_item: &DisplayedItem) -> f32 {
633        let base_row_height = self.user.config.layout.waveforms_line_height
634            + 2.0 * self.user.config.layout.waveforms_gap;
635        match displayed_item {
636            DisplayedItem::Variable(_) | DisplayedItem::Placeholder(_) => {
637                self.user.config.layout.waveforms_line_height
638                    * displayed_item.height_scaling_factor()
639                    + 2.0 * self.user.config.layout.waveforms_gap
640            }
641            DisplayedItem::Stream(stream) => {
642                self.user.config.layout.transactions_line_height * stream.rows as f32
643            }
644            DisplayedItem::Divider(_)
645            | DisplayedItem::Marker(_)
646            | DisplayedItem::TimeLine(_)
647            | DisplayedItem::Group(_) => base_row_height,
648        }
649    }
650
651    /// Lists the rows a variable (and its expanded subfields, if any) occupies, in
652    /// top-to-bottom order. Pure function of the variable's shape and its persisted fold
653    /// state (`unfolded_fields`); see `flattened_variable_rows` for the cached wrapper used
654    /// during drawing.
655    fn flatten_variable_rows(
656        unfolded_fields: &AHashSet<Vec<String>>,
657        field: &FieldRef,
658        info: &VariableInfo,
659        depth: u32,
660        out: &mut Vec<VariableFieldRow>,
661    ) {
662        match info {
663            VariableInfo::Compound { subfields } => {
664                let unfolded = unfolded_fields.contains(&field.field);
665                out.push(VariableFieldRow {
666                    field: field.clone(),
667                    depth,
668                    is_compound: true,
669                    has_children: !subfields.is_empty(),
670                    unfolded,
671                });
672                if unfolded {
673                    for (name, child_info) in subfields {
674                        let mut child_field = field.clone();
675                        child_field.field.push(name.clone());
676                        Self::flatten_variable_rows(
677                            unfolded_fields,
678                            &child_field,
679                            child_info,
680                            depth + 1,
681                            out,
682                        );
683                    }
684                }
685            }
686            VariableInfo::Bool
687            | VariableInfo::Bits
688            | VariableInfo::Clock
689            | VariableInfo::String
690            | VariableInfo::Event
691            | VariableInfo::Real => out.push(VariableFieldRow {
692                field: field.clone(),
693                depth,
694                is_compound: false,
695                has_children: false,
696                unfolded: false,
697            }),
698        }
699    }
700
701    /// Recursively unfolds compound fields up to `levels` deep (0 = fold everything), for
702    /// `Message::ExpandDrawnItem`.
703    pub(crate) fn expand_levels_to_unfolded_fields(
704        info: &VariableInfo,
705        levels: usize,
706    ) -> AHashSet<Vec<String>> {
707        let mut out = AHashSet::new();
708        Self::collect_unfolded_fields(info, &mut Vec::new(), levels, &mut out);
709        out
710    }
711
712    fn collect_unfolded_fields(
713        info: &VariableInfo,
714        field: &mut Vec<String>,
715        levels: usize,
716        out: &mut AHashSet<Vec<String>>,
717    ) {
718        if levels == 0 {
719            return;
720        }
721        if let VariableInfo::Compound { subfields } = info {
722            out.insert(field.clone());
723            for (name, child_info) in subfields {
724                field.push(name.clone());
725                Self::collect_unfolded_fields(child_info, field, levels - 1, out);
726                field.pop();
727            }
728        }
729    }
730
731    fn unfolded_fields_signature(unfolded_fields: &AHashSet<Vec<String>>) -> u64 {
732        let mut items: Vec<&Vec<String>> = unfolded_fields.iter().collect();
733        items.sort();
734        let mut hasher = AHasher::default();
735        items.hash(&mut hasher);
736        hasher.finish()
737    }
738
739    /// Cached wrapper around `flatten_variable_rows`: only re-walks the (possibly large)
740    /// compound field tree when this variable's fold state has changed since last frame.
741    fn flattened_variable_rows(
742        &self,
743        item_ref: DisplayedItemRef,
744        field: &FieldRef,
745        info: &VariableInfo,
746        unfolded_fields: &AHashSet<Vec<String>>,
747    ) -> Rc<Vec<VariableFieldRow>> {
748        let signature = Self::unfolded_fields_signature(unfolded_fields);
749        if let Some((cached_signature, rows)) = self.flattened_rows_cache.borrow().get(&item_ref)
750            && *cached_signature == signature
751        {
752            return Rc::clone(rows);
753        }
754        let mut rows = Vec::new();
755        Self::flatten_variable_rows(unfolded_fields, field, info, 0, &mut rows);
756        let rows = Rc::new(rows);
757        self.flattened_rows_cache
758            .borrow_mut()
759            .insert(item_ref, (signature, Rc::clone(&rows)));
760        rows
761    }
762
763    /// Computes top/bottom for every row (including compound-variable subfields), as if the
764    /// first row started at y = 0. Pure function of Surfer state (item tree, displayed
765    /// items, fold state) and layout config - no `ui`/egui state involved - so the result
766    /// only needs to change when items are added/removed/reordered or a group/compound is
767    /// folded/unfolded; see `item_layout_signature` and its use in `draw_item_list`.
768    fn compute_item_drawing_infos(&self, waves: &WaveData) -> Vec<ItemDrawingInfo> {
769        let mut out = Vec::new();
770        let mut y = 0.0f32;
771        for info in waves.items_tree.iter_visible_extra() {
772            let item_ref = info.node.item_ref;
773            let vidx = info.vidx;
774            let Some(displayed_item) = waves.displayed_items.get(&item_ref) else {
775                continue;
776            };
777            let row_height = self.desired_item_row_height(displayed_item);
778            match displayed_item {
779                DisplayedItem::Variable(displayed_variable) => {
780                    let field = FieldRef::without_fields(displayed_variable.variable_ref.clone());
781                    let rows = self.flattened_variable_rows(
782                        item_ref,
783                        &field,
784                        &displayed_variable.info,
785                        &displayed_variable.unfolded_fields,
786                    );
787                    for row in rows.iter() {
788                        let top = y;
789                        let bottom = y + row_height;
790                        out.push(ItemDrawingInfo::Variable(VariableDrawingInfo {
791                            displayed_field_ref: DisplayedFieldRef {
792                                item: item_ref,
793                                field: row.field.field.clone(),
794                            },
795                            field_ref: row.field.clone(),
796                            vidx,
797                            top,
798                            bottom,
799                        }));
800                        y = bottom;
801                    }
802                }
803                DisplayedItem::Divider(_) => {
804                    out.push(ItemDrawingInfo::Divider(DividerDrawingInfo {
805                        vidx,
806                        top: y,
807                        bottom: y + row_height,
808                    }));
809                    y += row_height;
810                }
811                DisplayedItem::Marker(cursor) => {
812                    out.push(ItemDrawingInfo::Marker(MarkerDrawingInfo {
813                        vidx,
814                        top: y,
815                        bottom: y + row_height,
816                        idx: cursor.idx,
817                    }));
818                    y += row_height;
819                }
820                DisplayedItem::TimeLine(_) => {
821                    out.push(ItemDrawingInfo::TimeLine(TimeLineDrawingInfo {
822                        vidx,
823                        top: y,
824                        bottom: y + row_height,
825                    }));
826                    y += row_height;
827                }
828                DisplayedItem::Stream(stream) => {
829                    out.push(ItemDrawingInfo::Stream(StreamDrawingInfo {
830                        transaction_stream_ref: stream.transaction_stream_ref.clone(),
831                        vidx,
832                        top: y,
833                        bottom: y + row_height,
834                    }));
835                    y += row_height;
836                }
837                DisplayedItem::Group(_) => {
838                    out.push(ItemDrawingInfo::Group(GroupDrawingInfo {
839                        vidx,
840                        top: y,
841                        bottom: y + row_height,
842                    }));
843                    y += row_height;
844                }
845                DisplayedItem::Placeholder(_) => {
846                    out.push(ItemDrawingInfo::Placeholder(PlaceholderDrawingInfo {
847                        vidx,
848                        top: y,
849                        bottom: y + row_height,
850                    }));
851                    y += row_height;
852                }
853            }
854        }
855        out
856    }
857
858    /// Cheap structural signature of everything `compute_item_drawing_infos` depends on;
859    /// `draw_item_list` only rebuilds the cached layout when this changes.
860    fn item_layout_signature(&self, waves: &WaveData) -> u64 {
861        let mut hasher = AHasher::default();
862        let layout = &self.user.config.layout;
863        layout.waveforms_line_height.to_bits().hash(&mut hasher);
864        layout.waveforms_gap.to_bits().hash(&mut hasher);
865        layout.transactions_line_height.to_bits().hash(&mut hasher);
866        for info in waves.items_tree.iter_visible_extra() {
867            info.node.item_ref.0.hash(&mut hasher);
868            info.node.level.hash(&mut hasher);
869            info.node.unfolded.hash(&mut hasher);
870            match waves.displayed_items.get(&info.node.item_ref) {
871                Some(DisplayedItem::Variable(v)) => {
872                    v.height_scaling_factor.map(f32::to_bits).hash(&mut hasher);
873                    Self::unfolded_fields_signature(&v.unfolded_fields).hash(&mut hasher);
874                }
875                Some(DisplayedItem::Placeholder(p)) => {
876                    p.height_scaling_factor.map(f32::to_bits).hash(&mut hasher);
877                }
878                Some(DisplayedItem::Stream(s)) => {
879                    s.rows.hash(&mut hasher);
880                }
881                _ => {}
882            }
883        }
884        hasher.finish()
885    }
886
887    /// Refreshes `waves.drawing_infos` if the structural
888    /// signature changed (items added/removed/reordered/dragged, or a group/compound
889    /// folded/unfolded).
890    fn ensure_drawing_infos_cached(&mut self) {
891        let Some(waves) = self.user.waves.as_ref() else {
892            return;
893        };
894        let signature = self.item_layout_signature(waves);
895        if waves.drawing_infos_signature == Some(signature) {
896            return;
897        }
898        let infos = self.compute_item_drawing_infos(waves);
899        let waves = self.user.waves.as_mut().unwrap();
900        waves.drawing_infos = infos;
901        waves.drawing_infos_signature = Some(signature);
902    }
903
904    fn draw_item_focus_list(&self, ui: &mut Ui) {
905        let Some(waves) = self.user.waves.as_ref() else {
906            return;
907        };
908        let alignment = self.get_name_alignment();
909        ui.with_layout(
910            Layout::top_down(alignment).with_cross_justify(false),
911            |ui| {
912                let start_y = ui.cursor().top();
913                let row_layout = if alignment == Align::LEFT {
914                    Layout::left_to_right(Align::Center)
915                } else {
916                    Layout::right_to_left(Align::Center)
917                };
918                let clip = ui.clip_rect();
919                let visible_top = clip.top() - start_y;
920                let visible_bottom = clip.bottom() - start_y;
921                // drawing_infos accounts for height_scaling_factor
922                for drawing_info in waves.visible_drawing_infos(visible_top, visible_bottom) {
923                    let next_y = ui.cursor().top();
924                    // Align with the corresponding row in other panels
925                    if next_y < drawing_info.top_at(start_y) {
926                        ui.add_space(drawing_info.top_at(start_y) - next_y);
927                    }
928                    let vidx = drawing_info.vidx();
929                    let row_rect = Rect::from_min_max(
930                        Pos2::new(ui.max_rect().left(), drawing_info.top_at(start_y)),
931                        Pos2::new(ui.max_rect().right(), drawing_info.bottom_at(start_y)),
932                    );
933                    let mut row_ui =
934                        ui.new_child(UiBuilder::new().max_rect(row_rect).layout(row_layout));
935                    Self::enforce_stable_row_widget_expansion(&mut row_ui);
936                    row_ui.style_mut().visuals.selection.bg_fill =
937                        self.user.config.theme.accent_warn.background;
938                    row_ui.style_mut().visuals.override_text_color =
939                        Some(self.user.config.theme.accent_warn.foreground);
940                    let _ = row_ui.selectable_label(true, get_alpha_focus_id(vidx, waves));
941                }
942                Self::add_padding_for_last_bottom(
943                    ui,
944                    waves.drawing_bottom_at(start_y),
945                    self.user.config.layout.waveforms_line_height,
946                );
947            },
948        );
949    }
950
951    fn hierarchy_icon(
952        &self,
953        ui: &mut Ui,
954        has_children: bool,
955        unfolded: bool,
956        alignment: Align,
957    ) -> egui::Response {
958        let (rect, response) = ui.allocate_exact_size(
959            Vec2::splat(self.user.config.layout.waveforms_text_size),
960            Sense::click(),
961        );
962        if !has_children {
963            return response;
964        }
965
966        // fixme: use the much nicer remixicon arrow? do a layout here and paint the galley into the rect?
967        // or alternatively: change how the tree iterator works and use the egui facilities (cross widget?)
968        let icon_rect = Rect::from_center_size(
969            rect.center(),
970            emath::vec2(rect.width(), rect.height()) * 0.75,
971        );
972        let mut points = vec![
973            icon_rect.left_top(),
974            icon_rect.right_top(),
975            icon_rect.center_bottom(),
976        ];
977        let rotation = emath::Rot2::from_angle(if unfolded {
978            0.0
979        } else if alignment == Align::LEFT {
980            -std::f32::consts::PI * 0.5
981        } else {
982            std::f32::consts::PI * 0.5
983        });
984        for p in &mut points {
985            *p = icon_rect.center() + rotation * (*p - icon_rect.center());
986        }
987
988        let style = ui.style().interact(&response);
989        ui.painter().add(Shape::convex_polygon(
990            points,
991            style.fg_stroke.color,
992            Stroke::NONE,
993        ));
994        response
995    }
996
997    fn draw_item_list(&mut self, msgs: &mut Vec<Message>, ui: &mut Ui) {
998        let (any_groups, alignment) = {
999            let Some(waves) = self.user.waves.as_ref() else {
1000                return;
1001            };
1002            (
1003                waves.items_tree.iter().any(|node| node.level > 0),
1004                self.get_name_alignment(),
1005            )
1006        };
1007        let text_margin = Self::item_text_margin(ui);
1008
1009        // Refresh the cache purely based on Surfer state (items/fold/drag); ui-derived
1010        // values like the panel's current position never influence this decision.
1011        self.ensure_drawing_infos_cached();
1012
1013        ui.with_layout(Layout::top_down(alignment).with_cross_justify(true), |ui| {
1014            let background_rect = ui.max_rect();
1015            let painter = ui.painter().clone();
1016
1017            // Add default horizontal margin for text while keeping the vertical start
1018            // exactly at `background_rect.min.y`, matching the value column and canvas.
1019            let rect_with_margin = Rect {
1020                min: background_rect.min + Vec2::new(text_margin.x, 0.0),
1021                max: background_rect.max + Vec2::new(0.0, 40.0),
1022            };
1023
1024            let builder = UiBuilder::new().max_rect(rect_with_margin);
1025            ui.scope_builder(builder, |ui| {
1026                // No item_spacing between rows: gaps come from the explicit wave padding below.
1027                ui.spacing_mut().item_spacing.y = 0.0;
1028                let content_rect = ui.available_rect_before_wrap();
1029                // `waves.drawing_infos` is offset-free (canonical); translate the clip rect
1030                // into that same space instead, so visibility can be checked against the raw
1031                // cached values, and `start_y` only gets added when a row is actually drawn.
1032                let start_y = ui.cursor().top();
1033                let clip = ui.clip_rect();
1034                let clip_top = clip.top() - start_y;
1035                let clip_bottom = clip.bottom() - start_y;
1036
1037                let Some(waves) = self.user.waves.as_ref() else {
1038                    return;
1039                };
1040
1041                let total_bottom = waves.drawing_bottom().unwrap_or(0.0);
1042                let mut row_iter = waves.drawing_infos.iter().peekable();
1043
1044                for (item_count, info) in waves.items_tree.iter_visible_extra().enumerate() {
1045                    let item_ref = info.node.item_ref;
1046                    let vidx = info.vidx;
1047                    let Some(displayed_item) = waves.displayed_items.get(&item_ref) else {
1048                        continue;
1049                    };
1050
1051                    // Pull this item's precomputed rows out of the cache.
1052                    let mut item_rows: Vec<&ItemDrawingInfo> = Vec::new();
1053                    while row_iter.peek().is_some_and(|r| r.vidx() == vidx) {
1054                        item_rows.push(row_iter.next().unwrap());
1055                    }
1056                    let Some(&first_row) = item_rows.first() else {
1057                        continue;
1058                    };
1059                    let last_row = item_rows.last().unwrap();
1060
1061                    let background_color = self.get_background_color(waves, vidx, item_count);
1062
1063                    let is_visible =
1064                        last_row.bottom() >= clip_top && first_row.top() <= clip_bottom;
1065                    if !is_visible {
1066                        // Position is already known from the cache; nothing else to do.
1067                        continue;
1068                    }
1069
1070                    let row_top = first_row.top_at(start_y);
1071                    let row_bottom = last_row.bottom_at(start_y);
1072
1073                    let min = Pos2::new(background_rect.left(), row_top);
1074                    let max = Pos2::new(background_rect.right(), row_bottom);
1075                    painter.rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
1076
1077                    // Center-align cross-axis so the (smaller) group/fold triangle icon is
1078                    // vertically centered in the row instead of stuck to its top.
1079                    let row_layout = if alignment == Align::LEFT {
1080                        Layout::left_to_right(Align::Center)
1081                    } else {
1082                        Layout::right_to_left(Align::Center)
1083                    };
1084                    // Content starts at `content_rect.left()` (margin-adjusted), unlike the
1085                    // background fill above which spans the full un-margined row width.
1086                    let row_rect = Rect {
1087                        min: Pos2::new(content_rect.left(), row_top),
1088                        max,
1089                    };
1090                    let mut row_ui =
1091                        ui.new_child(UiBuilder::new().max_rect(row_rect).layout(row_layout));
1092                    let row_ui = &mut row_ui;
1093
1094                    row_ui.add_space(10.0 * f32::from(info.node.level));
1095                    if any_groups {
1096                        let response = self.hierarchy_icon(
1097                            row_ui,
1098                            info.has_children,
1099                            info.node.unfolded,
1100                            alignment,
1101                        );
1102                        if response.clicked() {
1103                            if info.node.unfolded {
1104                                msgs.push(Message::GroupFold(Some(item_ref)));
1105                            } else {
1106                                msgs.push(Message::GroupUnfold(Some(item_ref)));
1107                            }
1108                        }
1109                    }
1110
1111                    match displayed_item {
1112                        DisplayedItem::Variable(displayed_variable) => self.draw_variable(
1113                            msgs,
1114                            vidx,
1115                            displayed_item,
1116                            item_ref,
1117                            &FieldRef::without_fields(displayed_variable.variable_ref.clone()),
1118                            &displayed_variable.info,
1119                            &displayed_variable.unfolded_fields,
1120                            &item_rows,
1121                            start_y,
1122                            row_ui,
1123                            alignment,
1124                            background_color,
1125                        ),
1126                        DisplayedItem::Divider(_)
1127                        | DisplayedItem::Marker(_)
1128                        | DisplayedItem::Placeholder(_)
1129                        | DisplayedItem::TimeLine(_)
1130                        | DisplayedItem::Stream(_)
1131                        | DisplayedItem::Group(_) => {
1132                            row_ui.with_layout(
1133                                row_ui
1134                                    .layout()
1135                                    .with_main_justify(true)
1136                                    .with_main_align(alignment),
1137                                |ui| {
1138                                    self.draw_plain_item(
1139                                        msgs,
1140                                        vidx,
1141                                        item_ref,
1142                                        displayed_item,
1143                                        ui,
1144                                        background_color,
1145                                    );
1146                                },
1147                            );
1148                        }
1149                    }
1150
1151                    // expand to the left, but not over the icon size
1152                    let mut expanded_rect = Rect::from_min_max(
1153                        Pos2::new(row_rect.min.x, row_top),
1154                        Pos2::new(row_rect.max.x, first_row.bottom_at(start_y)),
1155                    );
1156                    expanded_rect.set_left(
1157                        content_rect.left()
1158                            + self.user.config.layout.waveforms_text_size
1159                            + text_margin.x,
1160                    );
1161                    expanded_rect.set_right(content_rect.right());
1162                    self.draw_drag_target(
1163                        msgs,
1164                        vidx,
1165                        expanded_rect,
1166                        content_rect,
1167                        row_ui,
1168                        info.last,
1169                    );
1170                }
1171
1172                // Reserve the full content height once, instead of accumulating per-row space.
1173                let target_bottom =
1174                    total_bottom + start_y + self.user.config.layout.waveforms_line_height;
1175                let next_y = ui.cursor().top();
1176                if next_y < target_bottom {
1177                    ui.add_space(target_bottom - next_y);
1178                }
1179            });
1180        });
1181
1182        // Context menu for the unused part
1183        let response = ui.allocate_response(ui.available_size(), Sense::click());
1184        generic_context_menu(msgs, &response);
1185    }
1186
1187    fn get_name_alignment(&self) -> Align {
1188        if self.align_names_right() {
1189            Align::RIGHT
1190        } else {
1191            Align::LEFT
1192        }
1193    }
1194
1195    fn draw_drag_source(
1196        &self,
1197        msgs: &mut Vec<Message>,
1198        vidx: VisibleItemIndex,
1199        item_response: &egui::Response,
1200        modifiers: egui::Modifiers,
1201    ) {
1202        if item_response.dragged_by(egui::PointerButton::Primary)
1203            && item_response.drag_delta().length() > self.user.config.theme.drag_threshold
1204        {
1205            if !modifiers.ctrl
1206                && !(self.user.waves.as_ref())
1207                    .and_then(|w| w.items_tree.get_visible(vidx))
1208                    .is_some_and(|i| i.selected)
1209            {
1210                msgs.push(Message::FocusItem(vidx));
1211                msgs.push(Message::ItemSelectionClear);
1212            }
1213            msgs.push(Message::SetItemSelected(vidx, true));
1214            msgs.push(Message::VariableDragStarted(vidx));
1215        }
1216
1217        if item_response.drag_stopped()
1218            && self
1219                .user
1220                .drag_source_idx
1221                .is_some_and(|source_idx| source_idx == vidx)
1222        {
1223            msgs.push(Message::VariableDragFinished);
1224        }
1225    }
1226
1227    #[allow(clippy::too_many_arguments)]
1228    fn draw_variable_label(
1229        &self,
1230        vidx: VisibleItemIndex,
1231        displayed_item: &DisplayedItem,
1232        displayed_id: DisplayedItemRef,
1233        field: &FieldRef,
1234        msgs: &mut Vec<Message>,
1235        ui: &mut Ui,
1236        meta: Option<&VariableMeta>,
1237        background_color: Color32,
1238    ) -> egui::Response {
1239        let mut variable_label = self.draw_item_label(
1240            vidx,
1241            displayed_id,
1242            displayed_item,
1243            Some(field),
1244            msgs,
1245            ui,
1246            meta,
1247            background_color,
1248        );
1249
1250        if self.show_tooltip() {
1251            variable_label = variable_label.on_hover_ui(|ui| {
1252                let tooltip = if let Some(user_waves) = &self.user.waves {
1253                    if field.field.is_empty() {
1254                        if meta.is_some() {
1255                            variable_tooltip_text(meta, &field.root)
1256                        } else {
1257                            let wave_container = user_waves.inner.as_waves().unwrap();
1258                            let meta = wave_container.variable_meta(&field.root).ok();
1259                            variable_tooltip_text(meta.as_ref(), &field.root)
1260                        }
1261                    } else {
1262                        "From translator".to_string()
1263                    }
1264                } else {
1265                    "No waveform loaded".to_string()
1266                };
1267                ui.set_max_width(ui.spacing().tooltip_width);
1268                ui.add(egui::Label::new(tooltip));
1269            });
1270        }
1271
1272        variable_label
1273    }
1274
1275    #[allow(clippy::too_many_arguments)]
1276    fn draw_variable(
1277        &self,
1278        msgs: &mut Vec<Message>,
1279        vidx: VisibleItemIndex,
1280        displayed_item: &DisplayedItem,
1281        displayed_id: DisplayedItemRef,
1282        field: &FieldRef,
1283        info: &VariableInfo,
1284        unfolded_fields: &AHashSet<Vec<String>>,
1285        item_rows: &[&ItemDrawingInfo],
1286        start_y: f32,
1287        ui: &mut Ui,
1288        alignment: Align,
1289        background_color: Color32,
1290    ) {
1291        let wave_top_padding = self.user.config.layout.waveforms_gap;
1292        // Row positions are already known (see `compute_item_drawing_infos`); only the
1293        // per-row shape (depth/is_compound/...) is looked up here, from the same cache.
1294        let rows = self.flattened_variable_rows(displayed_id, field, info, unfolded_fields);
1295        // `item_rows` holds offset-free positions; translate the clip rect into that same
1296        // space instead, so visibility is checked against the raw cached values.
1297        let clip = ui.clip_rect();
1298        let clip_top = clip.top() - start_y;
1299        let clip_bottom = clip.bottom() - start_y;
1300
1301        ui.with_layout(Layout::top_down(alignment).with_cross_justify(true), |ui| {
1302            for (row, position) in rows.iter().zip(item_rows.iter()) {
1303                // Sub-fields deep inside a large expanded compound can be scrolled out of
1304                // view; their position is already known, so skip the widget/text layout.
1305                if !position.overlaps(clip_top, clip_bottom) {
1306                    continue;
1307                }
1308
1309                let row_top = position.top_at(start_y);
1310                let row_bottom = position.bottom_at(start_y);
1311
1312                let row_layout = if alignment == Align::LEFT {
1313                    Layout::left_to_right(Align::Center)
1314                } else {
1315                    Layout::right_to_left(Align::Center)
1316                };
1317                let row_rect = Rect::from_min_max(
1318                    Pos2::new(ui.max_rect().left(), row_top),
1319                    Pos2::new(ui.max_rect().right(), row_bottom),
1320                );
1321                let mut row_ui =
1322                    ui.new_child(UiBuilder::new().max_rect(row_rect).layout(row_layout));
1323                Self::enforce_stable_row_widget_expansion(&mut row_ui);
1324                row_ui.add_space(10.0 * row.depth as f32);
1325
1326                if row.is_compound {
1327                    let icon_response =
1328                        self.hierarchy_icon(&mut row_ui, row.has_children, row.unfolded, alignment);
1329                    if icon_response.clicked() {
1330                        msgs.push(Message::ToggleVariableFieldFold(
1331                            displayed_id,
1332                            row.field.field.clone(),
1333                        ));
1334                    }
1335                }
1336
1337                let label_response = row_ui
1338                    .with_layout(Layout::top_down(alignment).with_cross_justify(true), |ui| {
1339                        ui.add_space(wave_top_padding);
1340                        self.draw_variable_label(
1341                            vidx,
1342                            displayed_item,
1343                            displayed_id,
1344                            &row.field,
1345                            msgs,
1346                            ui,
1347                            None,
1348                            background_color,
1349                        )
1350                    })
1351                    .inner;
1352
1353                // Compound header rows aren't draggable variables themselves.
1354                if !row.is_compound {
1355                    self.draw_drag_source(msgs, vidx, &label_response, ui.input(|e| e.modifiers));
1356                }
1357            }
1358        });
1359    }
1360
1361    fn draw_drag_target(
1362        &self,
1363        msgs: &mut Vec<Message>,
1364        vidx: VisibleItemIndex,
1365        expanded_rect: Rect,
1366        content_rect: Rect,
1367        ui: &mut Ui,
1368        last: bool,
1369    ) {
1370        if !self.user.drag_started || self.user.drag_source_idx.is_none() {
1371            return;
1372        }
1373
1374        let waves = self
1375            .user
1376            .waves
1377            .as_ref()
1378            .expect("waves not available, but expected");
1379
1380        // expanded_rect is just for the label, leaving us with gaps between lines
1381        // expand to counter that
1382        let rect_with_margin = expanded_rect.expand2(ui.spacing().item_spacing / 2f32);
1383
1384        // collision check rect need to be
1385        // - limited to half the height of the item text
1386        // - extended to cover the empty space to the left
1387        // - for the last element, expanded till the bottom
1388        let before_rect = rect_with_margin
1389            .with_max_y(rect_with_margin.left_center().y)
1390            .with_min_x(content_rect.left())
1391            .round_to_pixels(ui.painter().pixels_per_point());
1392        let after_rect = if last {
1393            rect_with_margin.with_max_y(ui.max_rect().max.y)
1394        } else {
1395            rect_with_margin
1396        }
1397        .with_min_y(rect_with_margin.left_center().y)
1398        .with_min_x(content_rect.left())
1399        .round_to_pixels(ui.painter().pixels_per_point());
1400
1401        let (insert_vidx, line_y) = if ui.rect_contains_pointer(before_rect) {
1402            (vidx, rect_with_margin.top())
1403        } else if ui.rect_contains_pointer(after_rect) {
1404            (VisibleItemIndex(vidx.0 + 1), rect_with_margin.bottom())
1405        } else {
1406            return;
1407        };
1408
1409        let level_range = waves.items_tree.valid_levels_visible(insert_vidx, |node| {
1410            matches!(
1411                waves.displayed_items.get(&node.item_ref),
1412                Some(DisplayedItem::Group(..))
1413            )
1414        });
1415
1416        let left_x = |level: u8| -> f32 { rect_with_margin.left() + f32::from(level) * 10.0 };
1417        let Some(insert_level) = level_range.find_or_last(|&level| {
1418            let mut rect = expanded_rect.with_min_x(left_x(level));
1419            rect.set_width(10.0);
1420            if level == 0 {
1421                rect.set_left(content_rect.left());
1422            }
1423            ui.rect_contains_pointer(rect)
1424        }) else {
1425            return;
1426        };
1427
1428        ui.painter().line_segment(
1429            [
1430                Pos2::new(left_x(insert_level), line_y),
1431                Pos2::new(rect_with_margin.right(), line_y),
1432            ],
1433            Stroke::new(
1434                self.user.config.theme.linewidth,
1435                self.user.config.theme.drag_hint_color,
1436            ),
1437        );
1438        msgs.push(Message::VariableDragTargetChanged(
1439            crate::displayed_item_tree::TargetPosition {
1440                before: ItemIndex(
1441                    waves
1442                        .items_tree
1443                        .to_displayed(insert_vidx)
1444                        .map_or_else(|| waves.items_tree.len(), |index| index.0),
1445                ),
1446                level: insert_level,
1447            },
1448        ));
1449    }
1450
1451    #[allow(clippy::too_many_arguments)]
1452    fn draw_item_label(
1453        &self,
1454        vidx: VisibleItemIndex,
1455        displayed_id: DisplayedItemRef,
1456        displayed_item: &DisplayedItem,
1457        field: Option<&FieldRef>,
1458        msgs: &mut Vec<Message>,
1459        ui: &mut Ui,
1460        meta: Option<&VariableMeta>,
1461        background_color: Color32,
1462    ) -> egui::Response {
1463        let color_pair = {
1464            if self.item_is_focused(vidx) {
1465                &self.user.config.theme.accent_info
1466            } else if self.item_is_selected(displayed_id) {
1467                &self.user.config.theme.selected_elements_colors
1468            } else if matches!(
1469                displayed_item,
1470                DisplayedItem::Variable(_) | DisplayedItem::Placeholder(_)
1471            ) {
1472                &ThemeColorPair {
1473                    background: background_color,
1474                    foreground: self.user.config.theme.get_best_text_color(background_color),
1475                }
1476            } else {
1477                &ThemeColorPair {
1478                    background: self.user.config.theme.primary_ui_color.background,
1479                    foreground: self.get_item_text_color(displayed_item),
1480                }
1481            }
1482        };
1483        {
1484            let style = ui.style_mut();
1485            style.visuals.selection.bg_fill = color_pair.background;
1486        }
1487
1488        let mut layout_job = LayoutJob::default();
1489        match displayed_item {
1490            DisplayedItem::Variable(var) if field.is_some() => {
1491                let field = field.unwrap();
1492                let base_line_height = self.user.config.layout.waveforms_line_height;
1493                layout_job.first_row_min_height =
1494                    base_line_height * displayed_item.height_scaling_factor();
1495                if field.field.is_empty() {
1496                    let name_info = self.get_variable_name_info(&var.variable_ref, meta);
1497
1498                    if let Some(true_name) = name_info.and_then(|info| info.true_name) {
1499                        let monospace_font =
1500                            ui.style().text_styles.get(&TextStyle::Monospace).unwrap();
1501                        let monospace_width = {
1502                            ui.fonts_mut(|fonts| {
1503                                fonts
1504                                    .layout_no_wrap(
1505                                        " ".to_string(),
1506                                        monospace_font.clone(),
1507                                        Color32::BLACK,
1508                                    )
1509                                    .size()
1510                                    .x
1511                            })
1512                        };
1513                        let available_width = ui.available_width();
1514
1515                        draw_true_name(
1516                            &true_name,
1517                            &mut layout_job,
1518                            monospace_font,
1519                            color_pair.foreground,
1520                            monospace_width,
1521                            available_width,
1522                            base_line_height,
1523                        );
1524                    } else {
1525                        displayed_item.add_to_layout_job(
1526                            color_pair.foreground,
1527                            ui.style(),
1528                            &mut layout_job,
1529                            Some(field),
1530                            &self.user.config,
1531                        );
1532                    }
1533                } else {
1534                    RichText::new(field.field.last().unwrap().clone())
1535                        .color(color_pair.foreground)
1536                        .line_height(Some(base_line_height))
1537                        .append_to(
1538                            &mut layout_job,
1539                            ui.style(),
1540                            FontSelection::Default,
1541                            Align::Center,
1542                        );
1543                }
1544            }
1545            _ => displayed_item.add_to_layout_job(
1546                color_pair.foreground,
1547                ui.style(),
1548                &mut layout_job,
1549                field,
1550                &self.user.config,
1551            ),
1552        }
1553
1554        let item_label = ui
1555            .scope(|ui| {
1556                // Keep row geometry stable across interaction states so hover does not
1557                // change vertical spacing when custom line-height multipliers are used.
1558                Self::enforce_stable_row_widget_expansion(ui);
1559                ui.selectable_label(
1560                    self.item_is_selected(displayed_id) || self.item_is_focused(vidx),
1561                    WidgetText::LayoutJob(layout_job.into()),
1562                )
1563                .interact(Sense::drag())
1564            })
1565            .inner;
1566
1567        // click can select and deselect, depending on previous selection state & modifiers
1568        // with the rules:
1569        // - a primary click on the single selected item will deselect it (so that there is a
1570        //   way to deselect and get rid of the selection highlight)
1571        // - a primary/secondary click otherwise will select just the clicked item
1572        // - a secondary click on the selection will not change the selection
1573        // - a click with shift added will select all items between focused and clicked
1574        // - a click with control added will toggle the selection of the item
1575        // - shift + control does not have special meaning
1576        //
1577        // We do not implement more complex behavior like the selection toggling
1578        // that the windows explorer had in the past (with combined ctrl+shift)
1579        if item_label.clicked() || item_label.secondary_clicked() {
1580            let focused_item = self.user.waves.as_ref().and_then(|w| w.focused_item);
1581            let is_focused = focused_item == Some(vidx);
1582            let is_selected = self.item_is_selected(displayed_id);
1583            let single_selected = self
1584                .user
1585                .waves
1586                .as_ref()
1587                .map(|w| {
1588                    // FIXME check if this is fast
1589                    let it = w.items_tree.iter_visible_selected();
1590                    it.count() == 1
1591                })
1592                .unwrap();
1593
1594            let modifiers = ui.input(|i| i.modifiers);
1595            tracing::trace!(focused_item=?focused_item, is_focused=?is_focused, is_selected=?is_selected, single_selected=?single_selected, modifiers=?modifiers);
1596
1597            // allow us to deselect, but only do so if this is the only selected item
1598            if item_label.clicked() && is_selected && single_selected {
1599                msgs.push(Message::Batch(vec![
1600                    Message::ItemSelectionClear,
1601                    Message::UnfocusItem,
1602                ]));
1603                return item_label;
1604            }
1605
1606            match (item_label.clicked(), modifiers.command, modifiers.shift) {
1607                (false, false, false) if is_selected => {}
1608                (_, false, false) => {
1609                    msgs.push(Message::Batch(vec![
1610                        Message::ItemSelectionClear,
1611                        Message::SetItemSelected(vidx, true),
1612                        Message::FocusItem(vidx),
1613                    ]));
1614                }
1615                (_, _, true) => msgs.push(Message::Batch(vec![
1616                    Message::ItemSelectRange(vidx),
1617                    Message::FocusItem(vidx),
1618                ])),
1619                (_, true, false) => {
1620                    if !is_selected {
1621                        msgs.push(Message::Batch(vec![
1622                            Message::SetItemSelected(vidx, true),
1623                            Message::FocusItem(vidx),
1624                        ]));
1625                    } else if item_label.clicked() {
1626                        msgs.push(Message::Batch(vec![
1627                            Message::SetItemSelected(vidx, false),
1628                            Message::UnfocusItem,
1629                        ]));
1630                    }
1631                }
1632            }
1633        }
1634
1635        item_label.context_menu(|ui| {
1636            self.item_context_menu(
1637                field,
1638                msgs,
1639                ui,
1640                vidx,
1641                true,
1642                crate::message::MessageTarget::CurrentSelection,
1643            );
1644        });
1645
1646        item_label
1647    }
1648
1649    fn draw_plain_item(
1650        &self,
1651        msgs: &mut Vec<Message>,
1652        vidx: VisibleItemIndex,
1653        displayed_id: DisplayedItemRef,
1654        displayed_item: &DisplayedItem,
1655        ui: &mut Ui,
1656        background_color: Color32,
1657    ) {
1658        let wave_top_padding = self.user.config.layout.waveforms_gap;
1659        let row = ui.allocate_ui_with_layout(
1660            ui.available_size(),
1661            Layout::top_down(self.get_name_alignment()).with_cross_justify(true),
1662            |ui| {
1663                ui.add_space(wave_top_padding);
1664                self.draw_item_label(
1665                    vidx,
1666                    displayed_id,
1667                    displayed_item,
1668                    None,
1669                    msgs,
1670                    ui,
1671                    None,
1672                    background_color,
1673                )
1674            },
1675        );
1676
1677        self.draw_drag_source(msgs, vidx, &row.inner, ui.input(|e| e.modifiers));
1678    }
1679
1680    fn item_is_focused(&self, vidx: VisibleItemIndex) -> bool {
1681        if let Some(waves) = &self.user.waves {
1682            waves.focused_item == Some(vidx)
1683        } else {
1684            false
1685        }
1686    }
1687
1688    fn item_is_selected(&self, id: DisplayedItemRef) -> bool {
1689        if let Some(waves) = &self.user.waves {
1690            waves
1691                .items_tree
1692                .iter_visible_selected()
1693                .any(|node| node.item_ref == id)
1694        } else {
1695            false
1696        }
1697    }
1698
1699    fn draw_var_values(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
1700        let Some(waves) = &self.user.waves else {
1701            return;
1702        };
1703        let response = ui.allocate_response(ui.available_size(), Sense::click());
1704        generic_context_menu(msgs, &response);
1705
1706        let mut painter = ui.painter().clone();
1707        let rect = response.rect;
1708        let container_rect = Rect::from_min_size(Pos2::ZERO, rect.size());
1709        let to_screen = RectTransform::from_to(container_rect, rect);
1710        let cfg = DrawConfig::new(
1711            rect.size(),
1712            self.user.config.layout.waveforms_line_height,
1713            self.user.config.layout.waveforms_text_size,
1714        );
1715
1716        let ctx = DrawingContext {
1717            painter: &mut painter,
1718            cfg: &cfg,
1719            to_screen: &|x, y| to_screen.transform_pos(Pos2::new(x, y)),
1720            theme: &self.user.config.theme,
1721        };
1722
1723        let ucursor = waves.cursor.as_ref().and_then(num::BigInt::to_biguint);
1724
1725        // Add default horizontal margin as it was removed when creating the frame; keep the
1726        // vertical start exactly at `rect.min.y`, matching the name column and canvas.
1727        let rect_with_margin = Rect {
1728            min: rect.min + Vec2::new(ui.spacing().item_spacing.x, 0.0),
1729            max: rect.max + Vec2::new(0.0, 40.0),
1730        };
1731
1732        let builder = UiBuilder::new().max_rect(rect_with_margin);
1733        ui.scope_builder(builder, |ui| {
1734            let text_style = TextStyle::Monospace;
1735            ui.style_mut().override_text_style = Some(text_style);
1736            ui.spacing_mut().item_spacing.y = 0.0;
1737            let start_y = ui.cursor().top();
1738            let clip = ui.clip_rect();
1739            let clip_top = clip.top() - start_y;
1740            let clip_bottom = clip.bottom() - start_y;
1741            // `drawing_infos` is offset-free and already top-to-bottom sorted by
1742            // construction (see `SystemState::compute_item_drawing_infos`), so no re-sort
1743            // is needed here; `start_y` is only added when a row is actually drawn.
1744            for drawing_info in waves.visible_drawing_infos(clip_top, clip_bottom) {
1745                let next_y = ui.cursor().top();
1746                // In order to align the text in this view with the variable tree,
1747                // we need to keep track of how far away from the expected offset we are,
1748                // and compensate for it
1749                if next_y < drawing_info.top_at(start_y) {
1750                    ui.add_space(drawing_info.top_at(start_y) - next_y);
1751                }
1752
1753                let backgroundcolor =
1754                    self.get_background_color(waves, drawing_info.vidx(), drawing_info.vidx().0);
1755                self.draw_background(drawing_info, start_y, &ctx, backgroundcolor);
1756                match drawing_info {
1757                    ItemDrawingInfo::Variable(variable_info) => {
1758                        if ucursor.as_ref().is_none() {
1759                            ui.label("");
1760                            continue;
1761                        }
1762
1763                        let v = self.get_variable_value(
1764                            waves,
1765                            &variable_info.displayed_field_ref,
1766                            ucursor.as_ref(),
1767                        );
1768                        if let Some(v) = v {
1769                            let waveforms_gap = self.user.config.layout.waveforms_gap;
1770                            let waveform_height =
1771                                (drawing_info.height() - 2.0 * waveforms_gap).max(1.0);
1772
1773                            ui.add_space(waveforms_gap);
1774                            // Reserve the full row height on the job but keep the glyph's own
1775                            // line_height natural, so `Align::Center` valign can center the text.
1776                            let mut value_layout_job = LayoutJob {
1777                                first_row_min_height: waveform_height,
1778                                ..Default::default()
1779                            };
1780                            RichText::new(v)
1781                                .color(self.user.config.theme.get_best_text_color(backgroundcolor))
1782                                .line_height(Some(self.user.config.layout.waveforms_line_height))
1783                                .append_to(
1784                                    &mut value_layout_job,
1785                                    ui.style(),
1786                                    FontSelection::Default,
1787                                    Align::Center,
1788                                );
1789                            ui.label(WidgetText::LayoutJob(value_layout_job.into()))
1790                                .context_menu(|ui| {
1791                                    self.item_context_menu(
1792                                        Some(&variable_info.field_ref),
1793                                        msgs,
1794                                        ui,
1795                                        variable_info.vidx,
1796                                        true,
1797                                        crate::message::MessageTarget::CurrentSelection,
1798                                    );
1799                                });
1800                        }
1801                    }
1802
1803                    ItemDrawingInfo::Marker(numbered_cursor) => {
1804                        if let Some(cursor) = &waves.cursor {
1805                            let delta = time_string(
1806                                &(waves.numbered_marker_time(numbered_cursor.idx) - cursor),
1807                                &waves.inner.metadata().timescale,
1808                                &self.user.wanted_timeunit,
1809                                &self.get_time_format(),
1810                            );
1811
1812                            ui.add_space(self.user.config.layout.waveforms_gap);
1813                            ui.label(
1814                                RichText::new(format!("Ξ”: {delta}"))
1815                                    .color(
1816                                        self.user.config.theme.get_best_text_color(backgroundcolor),
1817                                    )
1818                                    .line_height(Some(
1819                                        self.user.config.layout.waveforms_line_height.max(1.0),
1820                                    )),
1821                            )
1822                            .context_menu(|ui| {
1823                                self.item_context_menu(
1824                                    None,
1825                                    msgs,
1826                                    ui,
1827                                    drawing_info.vidx(),
1828                                    true,
1829                                    crate::message::MessageTarget::CurrentSelection,
1830                                );
1831                            });
1832                        } else {
1833                            ui.label("");
1834                        }
1835                    }
1836                    ItemDrawingInfo::Divider(_)
1837                    | ItemDrawingInfo::TimeLine(_)
1838                    | ItemDrawingInfo::Stream(_)
1839                    | ItemDrawingInfo::Group(_)
1840                    | ItemDrawingInfo::Placeholder(_) => {
1841                        ui.label("");
1842                    }
1843                }
1844            }
1845            Self::add_padding_for_last_bottom(
1846                ui,
1847                waves.drawing_bottom_at(start_y),
1848                self.user.config.layout.waveforms_line_height
1849                    + 2.0 * self.user.config.layout.waveforms_gap,
1850            );
1851        });
1852    }
1853
1854    pub fn get_variable_value(
1855        &self,
1856        waves: &WaveData,
1857        displayed_field_ref: &DisplayedFieldRef,
1858        ucursor: Option<&num::BigUint>,
1859    ) -> Option<String> {
1860        let ucursor = ucursor?;
1861
1862        let DisplayedItem::Variable(displayed_variable) =
1863            waves.displayed_items.get(&displayed_field_ref.item)?
1864        else {
1865            return None;
1866        };
1867
1868        let variable = &displayed_variable.variable_ref;
1869        let meta = waves
1870            .inner
1871            .as_waves()
1872            .unwrap()
1873            .variable_meta(variable)
1874            .ok()?;
1875        let translator = waves.variable_translator_with_meta(
1876            &displayed_field_ref.without_field(),
1877            &self.translators,
1878            &meta,
1879        );
1880
1881        let wave_container = waves.inner.as_waves().unwrap();
1882        let query_result = wave_container
1883            .query_variable(variable, ucursor)
1884            .ok()
1885            .flatten()?;
1886
1887        let (time, val) = query_result.current?;
1888        let curr = self.translate_query_result(
1889            displayed_field_ref,
1890            displayed_variable,
1891            translator,
1892            &meta,
1893            &val,
1894        );
1895
1896        // If time doesn't match cursor, i.e., we are not at a transition or the cursor is at zero
1897        // or we want the next value after the transition, return current
1898        if time != *ucursor
1899            || (*ucursor).is_zero()
1900            || self.transition_value() == TransitionValue::Next
1901        {
1902            return curr;
1903        }
1904
1905        // Otherwise, we need to check the previous value for transition display
1906        let prev_query_result = wave_container
1907            .query_variable(variable, &(ucursor - BigUint::one()))
1908            .ok()
1909            .flatten()?;
1910
1911        let (_, prev_val) = prev_query_result.current?;
1912        let prev = self.translate_query_result(
1913            displayed_field_ref,
1914            displayed_variable,
1915            translator,
1916            &meta,
1917            &prev_val,
1918        );
1919
1920        match self.transition_value() {
1921            TransitionValue::Previous => Some(format!("←{}", prev.unwrap_or_default())),
1922            TransitionValue::Both => match (curr, prev) {
1923                (Some(curr_val), Some(prev_val)) => Some(format!("{prev_val} β†’ {curr_val}")),
1924                (None, Some(prev_val)) => Some(format!("{prev_val} β†’")),
1925                (Some(curr_val), None) => Some(format!("β†’ {curr_val}")),
1926                _ => None,
1927            },
1928            TransitionValue::Next => curr, // This will never happen due to the earlier check
1929        }
1930    }
1931
1932    fn translate_query_result(
1933        &self,
1934        displayed_field_ref: &DisplayedFieldRef,
1935        displayed_variable: &DisplayedVariable,
1936        translator: &dyn Translator<VarId, ScopeId, Message>,
1937        meta: &VariableMeta,
1938        val: &VariableValue,
1939    ) -> Option<String> {
1940        let translated = translator.translate(meta, val).ok()?;
1941        let fields = translated.format_flat(
1942            &displayed_variable.format,
1943            &displayed_variable.field_formats,
1944            &self.translators,
1945        );
1946
1947        let subfield = fields
1948            .iter()
1949            .find(|res| res.names == displayed_field_ref.field)?;
1950
1951        match &subfield.value {
1952            Some(TranslatedValue { value, .. }) => Some(value.clone()),
1953            None => Some("-".to_string()),
1954        }
1955    }
1956
1957    pub fn get_variable_name_info(
1958        &self,
1959        var: &VariableRef,
1960        meta: Option<&VariableMeta>,
1961    ) -> Option<VariableNameInfo> {
1962        self.variable_name_info_cache
1963            .borrow_mut()
1964            .entry(var.clone())
1965            .or_insert_with(|| {
1966                meta.as_ref().and_then(|meta| {
1967                    self.translators
1968                        .all_translators()
1969                        .iter()
1970                        .find_map(|t| t.variable_name_info(meta))
1971                })
1972            })
1973            .clone()
1974    }
1975
1976    pub fn draw_background(
1977        &self,
1978        drawing_info: &ItemDrawingInfo,
1979        y_offset: f32,
1980        ctx: &DrawingContext<'_>,
1981        background_color: Color32,
1982    ) {
1983        let row_top = drawing_info.top_at(y_offset);
1984        let row_bottom = drawing_info.bottom_at(y_offset);
1985        let left = (ctx.to_screen)(0.0, 0.0).x;
1986        let right = (ctx.to_screen)(ctx.cfg.canvas_size.x, 0.0).x;
1987        let min = Pos2::new(left, row_top);
1988        let max = Pos2::new(right, row_bottom);
1989        ctx.painter
1990            .rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
1991    }
1992
1993    pub fn get_background_color(
1994        &self,
1995        waves: &WaveData,
1996        vidx: VisibleItemIndex,
1997        item_count: usize,
1998    ) -> Color32 {
1999        if let Some(focused) = waves.focused_item
2000            && matches!(self.focus_highlight(), FocusHighlight::Background)
2001            && focused == vidx
2002        {
2003            return self.user.config.theme.highlight_background;
2004        }
2005        waves
2006            .items_tree
2007            .get_visible(vidx)
2008            .and_then(|visible| waves.displayed_items.get(&visible.item_ref))
2009            .and_then(super::displayed_item::DisplayedItem::background_color)
2010            .and_then(|color| self.user.config.theme.get_color(color))
2011            .unwrap_or_else(|| self.get_default_alternating_background_color(item_count))
2012    }
2013
2014    fn get_default_alternating_background_color(&self, item_count: usize) -> Color32 {
2015        // Set background color
2016        if self.user.config.theme.alt_frequency != 0
2017            && (item_count / self.user.config.theme.alt_frequency) % 2 == 1
2018        {
2019            self.user.config.theme.canvas_colors.alt_background
2020        } else {
2021            Color32::TRANSPARENT
2022        }
2023    }
2024
2025    /// Draw the default timeline at the top of the canvas
2026    pub fn draw_default_timeline(
2027        &self,
2028        waves: &WaveData,
2029        ctx: &DrawingContext,
2030        viewport_idx: usize,
2031    ) {
2032        let ticks = self.get_ticks_for_viewport_idx(waves, viewport_idx, ctx.cfg);
2033        let wave_top_padding = self.user.config.layout.waveforms_gap;
2034
2035        waves.draw_ticks(
2036            self.user.config.theme.foreground,
2037            &ticks,
2038            ctx,
2039            wave_top_padding,
2040            emath::Align2::CENTER_TOP,
2041        );
2042    }
2043}
2044
2045pub fn draw_true_name(
2046    true_name: &TrueName,
2047    layout_job: &mut LayoutJob,
2048    font: &FontId,
2049    foreground: Color32,
2050    char_width: f32,
2051    allowed_space: f32,
2052    line_height: f32,
2053) {
2054    let char_budget = (allowed_space / char_width) as usize;
2055
2056    match true_name {
2057        TrueName::SourceCode {
2058            line_number,
2059            before,
2060            this,
2061            after,
2062        } => {
2063            let before_chars = before.chars().collect::<Vec<_>>();
2064            let this_chars = this.chars().collect::<Vec<_>>();
2065            let after_chars = after.chars().collect::<Vec<_>>();
2066            let line_num = format!("{line_number} ");
2067            let important_chars = line_num.len() + this_chars.len();
2068            let required_extra_chars = before_chars.len() + after_chars.len();
2069
2070            // If everything fits, things are very easy
2071            let (line_num, before, this, after) =
2072                if char_budget >= important_chars + required_extra_chars {
2073                    (line_num, before.clone(), this.clone(), after.clone())
2074                } else if char_budget > important_chars {
2075                    // How many extra chars we have available
2076                    let extra_chars = char_budget - important_chars;
2077
2078                    let max_from_before = (extra_chars as f32 * 0.5).ceil() as usize;
2079                    let max_from_after = (extra_chars as f32 * 0.5).floor() as usize;
2080
2081                    let (chars_from_before, chars_from_after) =
2082                        if max_from_before > before_chars.len() {
2083                            (before_chars.len(), extra_chars - before_chars.len())
2084                        } else if max_from_after > after_chars.len() {
2085                            (extra_chars - after_chars.len(), before_chars.len())
2086                        } else {
2087                            (max_from_before, max_from_after)
2088                        };
2089
2090                    let mut before = before_chars
2091                        .into_iter()
2092                        .rev()
2093                        .take(chars_from_before)
2094                        .rev()
2095                        .collect::<Vec<_>>();
2096                    if !before.is_empty() {
2097                        before[0] = '…';
2098                    }
2099                    let mut after = after_chars
2100                        .into_iter()
2101                        .take(chars_from_after)
2102                        .collect::<Vec<_>>();
2103                    if !after.is_empty() {
2104                        let last_elem = after.len() - 1;
2105                        after[last_elem] = '…';
2106                    }
2107
2108                    (
2109                        line_num,
2110                        before.into_iter().collect(),
2111                        this.clone(),
2112                        after.into_iter().collect(),
2113                    )
2114                } else {
2115                    // If we can't even fit the whole important part,
2116                    // we'll prefer the line number
2117                    let from_line_num = line_num.len();
2118                    let from_this = char_budget.saturating_sub(from_line_num);
2119                    let this = this
2120                        .chars()
2121                        .take(from_this)
2122                        .enumerate()
2123                        .map(|(i, c)| if i == from_this - 1 { '…' } else { c })
2124                        .collect();
2125                    (line_num, String::new(), this, String::new())
2126                };
2127
2128            layout_job.append(
2129                &line_num,
2130                0.0,
2131                TextFormat {
2132                    font_id: font.clone(),
2133                    color: foreground.gamma_multiply(0.75),
2134                    line_height: Some(line_height),
2135                    valign: Align::Center,
2136                    ..Default::default()
2137                },
2138            );
2139            layout_job.append(
2140                &before,
2141                0.0,
2142                TextFormat {
2143                    font_id: font.clone(),
2144                    color: foreground.gamma_multiply(0.5),
2145                    line_height: Some(line_height),
2146                    valign: Align::Center,
2147                    ..Default::default()
2148                },
2149            );
2150            layout_job.append(
2151                &this,
2152                0.0,
2153                TextFormat {
2154                    font_id: font.clone(),
2155                    color: foreground,
2156                    line_height: Some(line_height),
2157                    valign: Align::Center,
2158                    ..Default::default()
2159                },
2160            );
2161            layout_job.append(
2162                after.trim_end(),
2163                0.0,
2164                TextFormat {
2165                    font_id: font.clone(),
2166                    color: foreground.gamma_multiply(0.5),
2167                    line_height: Some(line_height),
2168                    valign: Align::Center,
2169                    ..Default::default()
2170                },
2171            );
2172        }
2173    }
2174}