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). Presence means enabled, None means disabled.
129/// NOTE: Clone is NOT derived - see manual impl below for undo/redo compatibility.
130#[derive(Serialize, Deserialize)]
131pub struct AnalogVarState {
132    pub settings: AnalogSettings,
133    #[serde(skip)]
134    pub cache: Option<Arc<AnalogCacheEntry>>,
135}
136
137impl std::fmt::Debug for AnalogVarState {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        f.write_str("AnalogVarState")
140    }
141}
142
143// Manual Clone: cache is NOT cloned to avoid holding refs in undo/redo stack.
144// When state is restored from undo/redo, caches are rebuilt on demand.
145impl Clone for AnalogVarState {
146    fn clone(&self) -> Self {
147        Self {
148            settings: self.settings,
149            cache: None, // Intentionally not cloned - rebuilt on demand
150        }
151    }
152}
153
154impl PartialEq for AnalogVarState {
155    fn eq(&self, other: &Self) -> bool {
156        self.settings == other.settings
157    }
158}
159
160impl AnalogVarState {
161    #[must_use]
162    pub fn new(settings: AnalogSettings) -> Self {
163        Self {
164            settings,
165            cache: None,
166        }
167    }
168}
169
170#[derive(Serialize, Deserialize, Clone)]
171pub struct DisplayedVariable {
172    pub variable_ref: VariableRef,
173    #[serde(skip)]
174    pub info: VariableInfo,
175    pub color: Option<String>,
176    pub background_color: Option<String>,
177    pub display_name: String,
178    pub display_name_type: VariableNameType,
179    pub manual_name: Option<String>,
180    pub format: Option<String>,
181    pub field_formats: Vec<FieldFormat>,
182    pub height_scaling_factor: Option<f32>,
183    pub analog: Option<AnalogVarState>,
184}
185
186impl DisplayedVariable {
187    /// Downgrade `TypeLimits` to `Global` when the translator doesn't support numeric ranges.
188    pub fn downgrade_type_limits_if_unsupported(
189        &mut self,
190        translator: &DynTranslator,
191        meta: &VariableMeta,
192    ) {
193        if let Some(ref mut analog) = self.analog
194            && translator.numeric_range(meta).is_none()
195        {
196            analog.settings.downgrade_type_limits();
197        }
198    }
199
200    #[must_use]
201    pub fn get_format(&self, field: &[String]) -> Option<&String> {
202        if field.is_empty() {
203            self.format.as_ref()
204        } else {
205            self.field_formats
206                .iter()
207                .find(|ff| ff.field == field)
208                .map(|ff| &ff.format)
209        }
210    }
211
212    /// Updates the variable after a new waveform has been loaded.
213    #[must_use]
214    pub fn update(
215        &self,
216        new_waves: &WaveContainer,
217        keep_unavailable: bool,
218    ) -> Option<DisplayedItem> {
219        match new_waves.update_variable_ref(&self.variable_ref) {
220            // variable is not available in the new waveform
221            None if keep_unavailable => {
222                Some(DisplayedItem::Placeholder(self.clone().into_placeholder()))
223            }
224            None => None,
225            Some(new_ref) => {
226                let mut res = self.clone();
227                res.variable_ref = new_ref;
228                Some(DisplayedItem::Variable(res))
229            }
230        }
231    }
232
233    #[must_use]
234    pub fn into_placeholder(mut self) -> DisplayedPlaceholder {
235        self.variable_ref.clear_id(); // placeholders do not refer to currently loaded variables
236        DisplayedPlaceholder {
237            variable_ref: self.variable_ref,
238            color: self.color,
239            background_color: self.background_color,
240            display_name: self.display_name,
241            display_name_type: self.display_name_type,
242            manual_name: self.manual_name,
243            format: self.format,
244            field_formats: self.field_formats,
245            height_scaling_factor: self.height_scaling_factor,
246            analog: self.analog,
247        }
248    }
249}
250
251#[derive(Serialize, Deserialize, Clone)]
252pub struct DisplayedDivider {
253    pub color: Option<String>,
254    pub background_color: Option<String>,
255    pub name: Option<String>,
256}
257
258#[derive(Serialize, Deserialize, Clone)]
259pub struct DisplayedMarker {
260    pub color: Option<String>,
261    pub background_color: Option<String>,
262    pub name: Option<String>,
263    pub idx: u8,
264}
265
266impl DisplayedMarker {
267    #[must_use]
268    pub fn marker_text(&self, color: Color32) -> WidgetText {
269        let style = Style::default();
270        let mut layout_job = LayoutJob::default();
271        self.rich_text(color, &style, &mut layout_job);
272        WidgetText::LayoutJob(layout_job.into())
273    }
274
275    pub fn rich_text(&self, color: Color32, style: &Style, layout_job: &mut LayoutJob) {
276        RichText::new(format!("{idx}: ", idx = self.idx))
277            .color(color)
278            .append_to(layout_job, style, FontSelection::Default, Align::Center);
279        RichText::new(self.marker_name())
280            .color(color)
281            .italics()
282            .append_to(layout_job, style, FontSelection::Default, Align::Center);
283    }
284
285    fn marker_name(&self) -> String {
286        self.name
287            .clone()
288            .unwrap_or_else(|| DEFAULT_MARKER_NAME.to_string())
289    }
290}
291
292#[derive(Serialize, Deserialize, Clone)]
293pub struct DisplayedTimeLine {
294    pub color: Option<String>,
295    pub background_color: Option<String>,
296    pub name: Option<String>,
297}
298
299#[derive(Serialize, Deserialize, Clone)]
300pub struct DisplayedPlaceholder {
301    pub variable_ref: VariableRef,
302    pub color: Option<String>,
303    pub background_color: Option<String>,
304    pub display_name: String,
305    pub display_name_type: VariableNameType,
306    pub manual_name: Option<String>,
307    pub format: Option<String>,
308    pub field_formats: Vec<FieldFormat>,
309    pub height_scaling_factor: Option<f32>,
310    pub analog: Option<AnalogVarState>,
311}
312
313impl DisplayedPlaceholder {
314    #[must_use]
315    pub fn into_variable(
316        self,
317        variable_info: VariableInfo,
318        updated_variable_ref: VariableRef,
319    ) -> DisplayedVariable {
320        DisplayedVariable {
321            variable_ref: updated_variable_ref,
322            info: variable_info,
323            color: self.color,
324            background_color: self.background_color,
325            display_name: self.display_name,
326            display_name_type: self.display_name_type,
327            manual_name: self.manual_name,
328            format: self.format,
329            field_formats: self.field_formats,
330            height_scaling_factor: self.height_scaling_factor,
331            analog: self.analog,
332        }
333    }
334
335    pub fn rich_text(&self, text_color: Color32, style: &Style, layout_job: &mut LayoutJob) {
336        let s = self.manual_name.as_ref().unwrap_or(&self.display_name);
337        RichText::new("Not available: ".to_owned() + s)
338            .color(text_color)
339            .italics()
340            .append_to(layout_job, style, FontSelection::Default, Align::Center);
341    }
342}
343
344#[derive(Serialize, Deserialize, Clone)]
345pub struct DisplayedStream {
346    pub transaction_stream_ref: TransactionStreamRef,
347    pub color: Option<String>,
348    pub background_color: Option<String>,
349    pub display_name: String,
350    pub manual_name: Option<String>,
351    pub rows: usize,
352}
353
354impl DisplayedStream {
355    pub fn rich_text(
356        &self,
357        text_color: Color32,
358        style: &Style,
359        config: &SurferConfig,
360        layout_job: &mut LayoutJob,
361    ) {
362        RichText::new(format!(
363            "{}{}",
364            self.manual_name.as_ref().unwrap_or(&self.display_name),
365            "\n".repeat(self.rows - 1)
366        ))
367        .color(text_color)
368        // TODO: What does setting this do? Is it for the multi-line transactions?
369        .line_height(Some(config.layout.transactions_line_height))
370        .append_to(layout_job, style, FontSelection::Default, Align::Center);
371    }
372}
373
374#[derive(Serialize, Deserialize, Clone)]
375pub struct DisplayedGroup {
376    pub name: String,
377    pub color: Option<String>,
378    pub background_color: Option<String>,
379    pub content: Vec<DisplayedItemRef>,
380    pub is_open: bool,
381}
382
383impl DisplayedGroup {
384    pub fn rich_text(&self, text_color: Color32, style: &Style, layout_job: &mut LayoutJob) {
385        RichText::new(self.name.clone())
386            .color(text_color)
387            .append_to(layout_job, style, FontSelection::Default, Align::Center);
388    }
389}
390
391impl DisplayedItem {
392    #[must_use]
393    pub fn color(&self) -> Option<&str> {
394        match self {
395            DisplayedItem::Variable(variable) => variable.color.as_deref(),
396            DisplayedItem::Divider(divider) => divider.color.as_deref(),
397            DisplayedItem::Marker(marker) => marker.color.as_deref(),
398            DisplayedItem::TimeLine(timeline) => timeline.color.as_deref(),
399            DisplayedItem::Placeholder(_) => None,
400            DisplayedItem::Stream(stream) => stream.color.as_deref(),
401            DisplayedItem::Group(group) => group.color.as_deref(),
402        }
403    }
404
405    pub fn set_color(&mut self, color_name: &Option<String>) {
406        match self {
407            DisplayedItem::Variable(variable) => variable.color.clone_from(color_name),
408            DisplayedItem::Divider(divider) => divider.color.clone_from(color_name),
409            DisplayedItem::Marker(marker) => marker.color.clone_from(color_name),
410            DisplayedItem::TimeLine(timeline) => timeline.color.clone_from(color_name),
411            DisplayedItem::Placeholder(placeholder) => placeholder.color.clone_from(color_name),
412            DisplayedItem::Stream(stream) => stream.color.clone_from(color_name),
413            DisplayedItem::Group(group) => group.color.clone_from(color_name),
414        }
415    }
416
417    #[must_use]
418    pub fn name(&self) -> String {
419        match self {
420            DisplayedItem::Variable(variable) => variable
421                .manual_name
422                .as_ref()
423                .unwrap_or(&variable.display_name)
424                .clone(),
425            DisplayedItem::Divider(divider) => divider
426                .name
427                .as_ref()
428                .unwrap_or(&DEFAULT_DIVIDER_NAME.to_string())
429                .clone(),
430            DisplayedItem::Marker(marker) => marker.marker_name(),
431            DisplayedItem::TimeLine(timeline) => timeline
432                .name
433                .as_ref()
434                .unwrap_or(&DEFAULT_TIMELINE_NAME.to_string())
435                .clone(),
436            DisplayedItem::Placeholder(placeholder) => placeholder
437                .manual_name
438                .as_ref()
439                .unwrap_or(&placeholder.display_name)
440                .clone(),
441            DisplayedItem::Stream(stream) => stream
442                .manual_name
443                .as_ref()
444                .unwrap_or(&stream.display_name)
445                .clone(),
446            DisplayedItem::Group(group) => group.name.clone(),
447        }
448    }
449
450    /// Widget displayed in variable list for the wave form, may include additional info compared to `name()`
451    pub fn add_to_layout_job(
452        &self,
453        color: Color32,
454        style: &Style,
455        layout_job: &mut LayoutJob,
456        field: Option<&FieldRef>,
457        config: &SurferConfig,
458    ) {
459        match self {
460            DisplayedItem::Variable(_) => {
461                let name = field
462                    .and_then(|f| f.field.last())
463                    .cloned()
464                    .unwrap_or_else(|| self.name());
465                RichText::new(name)
466                    .color(color)
467                    .line_height(Some(
468                        config.layout.waveforms_line_height * self.height_scaling_factor(),
469                    ))
470                    .append_to(layout_job, style, FontSelection::Default, Align::Center);
471            }
472            DisplayedItem::TimeLine(_) | DisplayedItem::Divider(_) => {
473                RichText::new(self.name()).color(color).italics().append_to(
474                    layout_job,
475                    style,
476                    FontSelection::Default,
477                    Align::Center,
478                );
479            }
480            DisplayedItem::Marker(marker) => {
481                marker.rich_text(color, style, layout_job);
482            }
483            DisplayedItem::Placeholder(placeholder) => {
484                let s = placeholder
485                    .manual_name
486                    .as_ref()
487                    .unwrap_or(&placeholder.display_name);
488                RichText::new("Not available: ".to_owned() + s)
489                    .color(color)
490                    .italics()
491                    .append_to(layout_job, style, FontSelection::Default, Align::Center);
492            }
493            DisplayedItem::Stream(stream) => {
494                RichText::new(format!("{}{}", self.name(), "\n".repeat(stream.rows - 1)))
495                    .color(color)
496                    .line_height(Some(config.layout.transactions_line_height))
497                    .append_to(layout_job, style, FontSelection::Default, Align::Center);
498            }
499            DisplayedItem::Group(group) => {
500                group.rich_text(color, style, layout_job);
501            }
502        }
503    }
504
505    pub fn set_name(&mut self, name: Option<String>) {
506        match self {
507            DisplayedItem::Variable(variable) => {
508                variable.manual_name = name;
509            }
510            DisplayedItem::Divider(divider) => {
511                divider.name = name;
512            }
513            DisplayedItem::Marker(marker) => {
514                marker.name = name;
515            }
516            DisplayedItem::TimeLine(timeline) => {
517                timeline.name = name;
518            }
519            DisplayedItem::Placeholder(placeholder) => {
520                placeholder.manual_name = name;
521            }
522            DisplayedItem::Stream(stream) => {
523                stream.manual_name = name;
524            }
525            DisplayedItem::Group(group) => {
526                group.name = name.unwrap_or_default();
527            }
528        }
529    }
530
531    #[must_use]
532    pub fn has_overwritten_name(&self) -> bool {
533        match self {
534            DisplayedItem::Variable(variable) => variable.manual_name.is_some(),
535            DisplayedItem::Placeholder(placeholder) => placeholder.manual_name.is_some(),
536            DisplayedItem::Stream(stream) => stream.manual_name.is_some(),
537            DisplayedItem::Divider(_)
538            | DisplayedItem::Marker(_)
539            | DisplayedItem::TimeLine(_)
540            | DisplayedItem::Group(_) => false,
541        }
542    }
543
544    #[must_use]
545    pub fn background_color(&self) -> Option<&str> {
546        match self {
547            DisplayedItem::Variable(variable) => variable.background_color.as_deref(),
548            DisplayedItem::Divider(divider) => divider.background_color.as_deref(),
549            DisplayedItem::Marker(marker) => marker.background_color.as_deref(),
550            DisplayedItem::TimeLine(timeline) => timeline.background_color.as_deref(),
551            DisplayedItem::Placeholder(_) => None,
552            DisplayedItem::Stream(stream) => stream.background_color.as_deref(),
553            DisplayedItem::Group(group) => group.background_color.as_deref(),
554        }
555    }
556
557    pub fn set_background_color(&mut self, color_name: &Option<String>) {
558        match self {
559            DisplayedItem::Variable(variable) => {
560                variable.background_color.clone_from(color_name);
561            }
562            DisplayedItem::Divider(divider) => {
563                divider.background_color.clone_from(color_name);
564            }
565            DisplayedItem::Marker(marker) => {
566                marker.background_color.clone_from(color_name);
567            }
568            DisplayedItem::TimeLine(timeline) => {
569                timeline.background_color.clone_from(color_name);
570            }
571            DisplayedItem::Placeholder(placeholder) => {
572                placeholder.background_color.clone_from(color_name);
573            }
574            DisplayedItem::Stream(stream) => {
575                stream.background_color.clone_from(color_name);
576            }
577            DisplayedItem::Group(group) => {
578                group.background_color.clone_from(color_name);
579            }
580        }
581    }
582
583    #[must_use]
584    pub fn height_scaling_factor(&self) -> f32 {
585        match self {
586            DisplayedItem::Variable(variable) => variable.height_scaling_factor,
587            DisplayedItem::Placeholder(placeholder) => placeholder.height_scaling_factor,
588            _ => None,
589        }
590        .unwrap_or(1.0)
591    }
592
593    pub fn set_height_scaling_factor(&mut self, scale: f32) {
594        match self {
595            DisplayedItem::Variable(variable) => variable.height_scaling_factor = Some(scale),
596            DisplayedItem::Placeholder(placeholder) => {
597                placeholder.height_scaling_factor = Some(scale);
598            }
599            _ => {}
600        }
601    }
602}