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