Skip to main content

libsurfer/
view.rs

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