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