Skip to main content

libsurfer/
displayed_item.rs

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