Skip to main content

libsurfer/
displayed_item.rs

1//! The items that are drawn in the main wave form view: waves, dividers, etc.
2use ecolor::Color32;
3use egui::{FontSelection, RichText, Style, WidgetText};
4use emath::Align;
5use epaint::text::LayoutJob;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8
9use crate::analog_signal_cache::AnalogCacheEntry;
10use surfer_translation_types::VariableInfo;
11
12use crate::translation::DynTranslator;
13use crate::wave_container::VariableMeta;
14
15use crate::config::SurferConfig;
16use crate::transaction_container::TransactionStreamRef;
17use crate::wave_container::{FieldRef, VariableRef, VariableRefExt, WaveContainer};
18use crate::{
19    marker::DEFAULT_MARKER_NAME, time::DEFAULT_TIMELINE_NAME, variable_name_type::VariableNameType,
20};
21
22const DEFAULT_DIVIDER_NAME: &str = "";
23
24/// Key for the [`crate::wave_data::WaveData::displayed_items`] hash map
25#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
26#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
27pub struct DisplayedItemRef(pub usize);
28
29impl From<usize> for DisplayedItemRef {
30    fn from(item: usize) -> Self {
31        DisplayedItemRef(item)
32    }
33}
34
35#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)]
36pub struct DisplayedFieldRef {
37    pub item: DisplayedItemRef,
38    pub field: Vec<String>,
39}
40
41impl DisplayedFieldRef {
42    #[must_use]
43    pub fn without_field(&self) -> DisplayedFieldRef {
44        DisplayedFieldRef {
45            item: self.item,
46            field: vec![],
47        }
48    }
49}
50
51impl From<DisplayedItemRef> for DisplayedFieldRef {
52    fn from(item: DisplayedItemRef) -> Self {
53        DisplayedFieldRef {
54            item,
55            field: vec![],
56        }
57    }
58}
59
60#[derive(Serialize, Deserialize, Clone)]
61pub enum DisplayedItem {
62    Variable(DisplayedVariable),
63    Divider(DisplayedDivider),
64    Marker(DisplayedMarker),
65    TimeLine(DisplayedTimeLine),
66    Placeholder(DisplayedPlaceholder),
67    Stream(DisplayedStream),
68    Group(DisplayedGroup),
69}
70
71#[derive(Serialize, Deserialize, Clone)]
72pub struct FieldFormat {
73    pub field: Vec<String>,
74    pub format: String,
75}
76
77#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Default)]
78pub enum AnalogRenderStyle {
79    #[default]
80    Step,
81    Interpolated,
82}
83
84impl AnalogRenderStyle {
85    #[must_use]
86    pub const fn label(self) -> &'static str {
87        match self {
88            Self::Step => "Step",
89            Self::Interpolated => "Interpolated",
90        }
91    }
92}
93
94#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Default)]
95pub enum AnalogYAxisScale {
96    #[default]
97    Viewport,
98    Global,
99    TypeLimits,
100}
101
102impl AnalogYAxisScale {
103    #[must_use]
104    pub const fn label(self) -> &'static str {
105        match self {
106            Self::Viewport => "Viewport",
107            Self::Global => "Global",
108            Self::TypeLimits => "Type Limits",
109        }
110    }
111}
112
113#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Default)]
114pub struct AnalogSettings {
115    pub render_style: AnalogRenderStyle,
116    pub y_axis_scale: AnalogYAxisScale,
117}
118
119impl AnalogSettings {
120    /// Downgrade `TypeLimits` to `Global` when the translator doesn't support numeric ranges.
121    pub fn downgrade_type_limits(&mut self) {
122        if self.y_axis_scale == AnalogYAxisScale::TypeLimits {
123            self.y_axis_scale = AnalogYAxisScale::Global;
124        }
125    }
126}
127
128/// Per-variable analog state (settings + cache).
129///
130/// Presence means enabled, None means disabled.
131/// NOTE: Clone is NOT derived - see manual impl below for undo/redo compatibility.
132#[derive(Serialize, Deserialize)]
133pub struct AnalogVarState {
134    pub settings: AnalogSettings,
135    #[serde(skip)]
136    pub cache: Option<Arc<AnalogCacheEntry>>,
137}
138
139impl std::fmt::Debug for AnalogVarState {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.write_str("AnalogVarState")
142    }
143}
144
145// Manual Clone: cache is NOT cloned to avoid holding refs in undo/redo stack.
146// When state is restored from undo/redo, caches are rebuilt on demand.
147impl Clone for AnalogVarState {
148    fn clone(&self) -> Self {
149        Self {
150            settings: self.settings,
151            cache: None, // Intentionally not cloned - rebuilt on demand
152        }
153    }
154}
155
156impl PartialEq for AnalogVarState {
157    fn eq(&self, other: &Self) -> bool {
158        self.settings == other.settings
159    }
160}
161
162impl AnalogVarState {
163    #[must_use]
164    pub fn new(settings: AnalogSettings) -> Self {
165        Self {
166            settings,
167            cache: None,
168        }
169    }
170}
171
172#[derive(Serialize, Deserialize, Clone)]
173pub struct DisplayedVariable {
174    pub variable_ref: VariableRef,
175    #[serde(skip)]
176    pub info: VariableInfo,
177    pub color: Option<String>,
178    pub background_color: Option<String>,
179    pub display_name: String,
180    pub display_name_type: VariableNameType,
181    pub manual_name: Option<String>,
182    pub format: Option<String>,
183    pub field_formats: Vec<FieldFormat>,
184    pub height_scaling_factor: Option<f32>,
185    pub analog: Option<AnalogVarState>,
186}
187
188impl DisplayedVariable {
189    /// Downgrade `TypeLimits` to `Global` when the translator doesn't support numeric ranges.
190    pub fn downgrade_type_limits_if_unsupported(
191        &mut self,
192        translator: &DynTranslator,
193        meta: &VariableMeta,
194    ) {
195        if let Some(ref mut analog) = self.analog
196            && translator.numeric_range(meta).is_none()
197        {
198            analog.settings.downgrade_type_limits();
199        }
200    }
201
202    #[must_use]
203    pub fn get_format(&self, field: &[String]) -> Option<&String> {
204        if field.is_empty() {
205            self.format.as_ref()
206        } else {
207            self.field_formats
208                .iter()
209                .find(|ff| ff.field == field)
210                .map(|ff| &ff.format)
211        }
212    }
213
214    /// Updates the variable after a new waveform has been loaded.
215    #[must_use]
216    pub fn update(
217        &self,
218        new_waves: &WaveContainer,
219        keep_unavailable: bool,
220    ) -> Option<DisplayedItem> {
221        match new_waves.update_variable_ref(&self.variable_ref) {
222            // variable is not available in the new waveform
223            None if keep_unavailable => {
224                Some(DisplayedItem::Placeholder(self.clone().into_placeholder()))
225            }
226            None => None,
227            Some(new_ref) => {
228                let mut res = self.clone();
229                res.variable_ref = new_ref;
230                Some(DisplayedItem::Variable(res))
231            }
232        }
233    }
234
235    #[must_use]
236    pub fn into_placeholder(mut self) -> DisplayedPlaceholder {
237        self.variable_ref.clear_id(); // placeholders do not refer to currently loaded variables
238        DisplayedPlaceholder {
239            variable_ref: self.variable_ref,
240            color: self.color,
241            background_color: self.background_color,
242            display_name: self.display_name,
243            display_name_type: self.display_name_type,
244            manual_name: self.manual_name,
245            format: self.format,
246            field_formats: self.field_formats,
247            height_scaling_factor: self.height_scaling_factor,
248            analog: self.analog,
249        }
250    }
251}
252
253#[derive(Serialize, Deserialize, Clone)]
254pub struct DisplayedDivider {
255    pub color: Option<String>,
256    pub background_color: Option<String>,
257    pub name: Option<String>,
258}
259
260#[derive(Serialize, Deserialize, Clone)]
261pub struct DisplayedMarker {
262    pub color: Option<String>,
263    pub background_color: Option<String>,
264    pub name: Option<String>,
265    pub idx: u8,
266}
267
268impl DisplayedMarker {
269    #[must_use]
270    pub fn marker_text(&self, color: Color32) -> WidgetText {
271        let style = Style::default();
272        let mut layout_job = LayoutJob::default();
273        self.rich_text(color, &style, &mut layout_job);
274        WidgetText::LayoutJob(layout_job.into())
275    }
276
277    pub fn rich_text(&self, color: Color32, style: &Style, layout_job: &mut LayoutJob) {
278        RichText::new(format!("{idx}: ", idx = self.idx))
279            .color(color)
280            .append_to(layout_job, style, FontSelection::Default, Align::Center);
281        RichText::new(self.marker_name())
282            .color(color)
283            .italics()
284            .append_to(layout_job, style, FontSelection::Default, Align::Center);
285    }
286
287    fn marker_name(&self) -> String {
288        self.name
289            .clone()
290            .unwrap_or_else(|| DEFAULT_MARKER_NAME.to_string())
291    }
292}
293
294#[derive(Serialize, Deserialize, Clone)]
295pub struct DisplayedTimeLine {
296    pub color: Option<String>,
297    pub background_color: Option<String>,
298    pub name: Option<String>,
299}
300
301#[derive(Serialize, Deserialize, Clone)]
302pub struct DisplayedPlaceholder {
303    pub variable_ref: VariableRef,
304    pub color: Option<String>,
305    pub background_color: Option<String>,
306    pub display_name: String,
307    pub display_name_type: VariableNameType,
308    pub manual_name: Option<String>,
309    pub format: Option<String>,
310    pub field_formats: Vec<FieldFormat>,
311    pub height_scaling_factor: Option<f32>,
312    pub analog: Option<AnalogVarState>,
313}
314
315impl DisplayedPlaceholder {
316    #[must_use]
317    pub fn into_variable(
318        self,
319        variable_info: VariableInfo,
320        updated_variable_ref: VariableRef,
321    ) -> DisplayedVariable {
322        DisplayedVariable {
323            variable_ref: updated_variable_ref,
324            info: variable_info,
325            color: self.color,
326            background_color: self.background_color,
327            display_name: self.display_name,
328            display_name_type: self.display_name_type,
329            manual_name: self.manual_name,
330            format: self.format,
331            field_formats: self.field_formats,
332            height_scaling_factor: self.height_scaling_factor,
333            analog: self.analog,
334        }
335    }
336
337    pub fn rich_text(&self, text_color: Color32, style: &Style, layout_job: &mut LayoutJob) {
338        let s = self.manual_name.as_ref().unwrap_or(&self.display_name);
339        RichText::new("Not available: ".to_owned() + s)
340            .color(text_color)
341            .italics()
342            .append_to(layout_job, style, FontSelection::Default, Align::Center);
343    }
344}
345
346#[derive(Serialize, Deserialize, Clone)]
347pub struct DisplayedStream {
348    pub transaction_stream_ref: TransactionStreamRef,
349    pub color: Option<String>,
350    pub background_color: Option<String>,
351    pub display_name: String,
352    pub manual_name: Option<String>,
353    pub rows: usize,
354}
355
356impl DisplayedStream {
357    pub fn rich_text(
358        &self,
359        text_color: Color32,
360        style: &Style,
361        config: &SurferConfig,
362        layout_job: &mut LayoutJob,
363    ) {
364        RichText::new(format!(
365            "{}{}",
366            self.manual_name.as_ref().unwrap_or(&self.display_name),
367            "\n".repeat(self.rows - 1)
368        ))
369        .color(text_color)
370        // TODO: What does setting this do? Is it for the multi-line transactions?
371        .line_height(Some(config.layout.transactions_line_height))
372        .append_to(layout_job, style, FontSelection::Default, Align::Center);
373    }
374}
375
376#[derive(Serialize, Deserialize, Clone)]
377pub struct DisplayedGroup {
378    pub name: String,
379    pub color: Option<String>,
380    pub background_color: Option<String>,
381    pub content: Vec<DisplayedItemRef>,
382    pub is_open: bool,
383}
384
385impl DisplayedGroup {
386    pub fn rich_text(&self, text_color: Color32, style: &Style, layout_job: &mut LayoutJob) {
387        RichText::new(self.name.clone())
388            .color(text_color)
389            .append_to(layout_job, style, FontSelection::Default, Align::Center);
390    }
391}
392
393impl DisplayedItem {
394    #[must_use]
395    pub fn color(&self) -> Option<&str> {
396        match self {
397            DisplayedItem::Variable(variable) => variable.color.as_deref(),
398            DisplayedItem::Divider(divider) => divider.color.as_deref(),
399            DisplayedItem::Marker(marker) => marker.color.as_deref(),
400            DisplayedItem::TimeLine(timeline) => timeline.color.as_deref(),
401            DisplayedItem::Placeholder(_) => None,
402            DisplayedItem::Stream(stream) => stream.color.as_deref(),
403            DisplayedItem::Group(group) => group.color.as_deref(),
404        }
405    }
406
407    pub fn set_color(&mut self, color_name: &Option<String>) {
408        match self {
409            DisplayedItem::Variable(variable) => variable.color.clone_from(color_name),
410            DisplayedItem::Divider(divider) => divider.color.clone_from(color_name),
411            DisplayedItem::Marker(marker) => marker.color.clone_from(color_name),
412            DisplayedItem::TimeLine(timeline) => timeline.color.clone_from(color_name),
413            DisplayedItem::Placeholder(placeholder) => placeholder.color.clone_from(color_name),
414            DisplayedItem::Stream(stream) => stream.color.clone_from(color_name),
415            DisplayedItem::Group(group) => group.color.clone_from(color_name),
416        }
417    }
418
419    #[must_use]
420    pub fn name(&self) -> String {
421        match self {
422            DisplayedItem::Variable(variable) => variable
423                .manual_name
424                .as_ref()
425                .unwrap_or(&variable.display_name)
426                .clone(),
427            DisplayedItem::Divider(divider) => divider
428                .name
429                .as_ref()
430                .unwrap_or(&DEFAULT_DIVIDER_NAME.to_string())
431                .clone(),
432            DisplayedItem::Marker(marker) => marker.marker_name(),
433            DisplayedItem::TimeLine(timeline) => timeline
434                .name
435                .as_ref()
436                .unwrap_or(&DEFAULT_TIMELINE_NAME.to_string())
437                .clone(),
438            DisplayedItem::Placeholder(placeholder) => placeholder
439                .manual_name
440                .as_ref()
441                .unwrap_or(&placeholder.display_name)
442                .clone(),
443            DisplayedItem::Stream(stream) => stream
444                .manual_name
445                .as_ref()
446                .unwrap_or(&stream.display_name)
447                .clone(),
448            DisplayedItem::Group(group) => group.name.clone(),
449        }
450    }
451
452    /// Widget displayed in variable list for the wave form, may include additional info compared to `name()`
453    pub fn add_to_layout_job(
454        &self,
455        color: Color32,
456        style: &Style,
457        layout_job: &mut LayoutJob,
458        field: Option<&FieldRef>,
459        config: &SurferConfig,
460    ) {
461        match self {
462            DisplayedItem::Variable(_) => {
463                let name = field
464                    .and_then(|f| f.field.last())
465                    .cloned()
466                    .unwrap_or_else(|| self.name());
467                RichText::new(name)
468                    .color(color)
469                    .line_height(Some(
470                        config.layout.waveforms_line_height * self.height_scaling_factor(),
471                    ))
472                    .append_to(layout_job, style, FontSelection::Default, Align::Center);
473            }
474            DisplayedItem::TimeLine(_) | DisplayedItem::Divider(_) => {
475                RichText::new(self.name()).color(color).italics().append_to(
476                    layout_job,
477                    style,
478                    FontSelection::Default,
479                    Align::Center,
480                );
481            }
482            DisplayedItem::Marker(marker) => {
483                marker.rich_text(color, style, layout_job);
484            }
485            DisplayedItem::Placeholder(placeholder) => {
486                let s = placeholder
487                    .manual_name
488                    .as_ref()
489                    .unwrap_or(&placeholder.display_name);
490                RichText::new("Not available: ".to_owned() + s)
491                    .color(color)
492                    .italics()
493                    .append_to(layout_job, style, FontSelection::Default, Align::Center);
494            }
495            DisplayedItem::Stream(stream) => {
496                RichText::new(format!("{}{}", self.name(), "\n".repeat(stream.rows - 1)))
497                    .color(color)
498                    .line_height(Some(config.layout.transactions_line_height))
499                    .append_to(layout_job, style, FontSelection::Default, Align::Center);
500            }
501            DisplayedItem::Group(group) => {
502                group.rich_text(color, style, layout_job);
503            }
504        }
505    }
506
507    pub fn set_name(&mut self, name: Option<String>) {
508        match self {
509            DisplayedItem::Variable(variable) => {
510                variable.manual_name = name;
511            }
512            DisplayedItem::Divider(divider) => {
513                divider.name = name;
514            }
515            DisplayedItem::Marker(marker) => {
516                marker.name = name;
517            }
518            DisplayedItem::TimeLine(timeline) => {
519                timeline.name = name;
520            }
521            DisplayedItem::Placeholder(placeholder) => {
522                placeholder.manual_name = name;
523            }
524            DisplayedItem::Stream(stream) => {
525                stream.manual_name = name;
526            }
527            DisplayedItem::Group(group) => {
528                group.name = name.unwrap_or_default();
529            }
530        }
531    }
532
533    #[must_use]
534    pub fn has_overwritten_name(&self) -> bool {
535        match self {
536            DisplayedItem::Variable(variable) => variable.manual_name.is_some(),
537            DisplayedItem::Placeholder(placeholder) => placeholder.manual_name.is_some(),
538            DisplayedItem::Stream(stream) => stream.manual_name.is_some(),
539            DisplayedItem::Divider(_)
540            | DisplayedItem::Marker(_)
541            | DisplayedItem::TimeLine(_)
542            | DisplayedItem::Group(_) => false,
543        }
544    }
545
546    #[must_use]
547    pub fn background_color(&self) -> Option<&str> {
548        match self {
549            DisplayedItem::Variable(variable) => variable.background_color.as_deref(),
550            DisplayedItem::Divider(divider) => divider.background_color.as_deref(),
551            DisplayedItem::Marker(marker) => marker.background_color.as_deref(),
552            DisplayedItem::TimeLine(timeline) => timeline.background_color.as_deref(),
553            DisplayedItem::Placeholder(_) => None,
554            DisplayedItem::Stream(stream) => stream.background_color.as_deref(),
555            DisplayedItem::Group(group) => group.background_color.as_deref(),
556        }
557    }
558
559    pub fn set_background_color(&mut self, color_name: &Option<String>) {
560        match self {
561            DisplayedItem::Variable(variable) => {
562                variable.background_color.clone_from(color_name);
563            }
564            DisplayedItem::Divider(divider) => {
565                divider.background_color.clone_from(color_name);
566            }
567            DisplayedItem::Marker(marker) => {
568                marker.background_color.clone_from(color_name);
569            }
570            DisplayedItem::TimeLine(timeline) => {
571                timeline.background_color.clone_from(color_name);
572            }
573            DisplayedItem::Placeholder(placeholder) => {
574                placeholder.background_color.clone_from(color_name);
575            }
576            DisplayedItem::Stream(stream) => {
577                stream.background_color.clone_from(color_name);
578            }
579            DisplayedItem::Group(group) => {
580                group.background_color.clone_from(color_name);
581            }
582        }
583    }
584
585    #[must_use]
586    pub fn height_scaling_factor(&self) -> f32 {
587        match self {
588            DisplayedItem::Variable(variable) => variable.height_scaling_factor,
589            DisplayedItem::Placeholder(placeholder) => placeholder.height_scaling_factor,
590            _ => None,
591        }
592        .unwrap_or(1.0)
593    }
594
595    pub fn set_height_scaling_factor(&mut self, scale: f32) {
596        match self {
597            DisplayedItem::Variable(variable) => variable.height_scaling_factor = Some(scale),
598            DisplayedItem::Placeholder(placeholder) => {
599                placeholder.height_scaling_factor = Some(scale);
600            }
601            _ => {}
602        }
603    }
604}