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