Skip to main content

libsurfer/
state.rs

1use std::{
2    collections::{HashMap, HashSet, VecDeque},
3    mem,
4    path::PathBuf,
5};
6
7use crate::{
8    CanvasState, StartupParams,
9    clock_highlighting::ClockHighlightType,
10    config::{ArrowKeyBindings, AutoLoad, PrimaryMouseDrag, SurferConfig, TransitionValue},
11    data_container::DataContainer,
12    dialog::{OpenSiblingStateFileDialog, ReloadWaveformDialog},
13    displayed_item_tree::{DisplayedItemTree, VisibleItemIndex},
14    frame_buffer::FrameBufferSettings,
15    hierarchy::{HierarchyStyle, ParameterDisplayLocation},
16    message::Message,
17    system_state::SystemState,
18    time::{TimeStringFormatting, TimeUnit},
19    transaction_container::TransactionContainer,
20    variable_filter::VariableFilter,
21    viewport::Viewport,
22    wave_container::{ScopeRef, VariableRef, WaveContainer},
23    wave_data::WaveData,
24    wave_source::{LoadOptions, WaveFormat, WaveSource},
25};
26use egui::{
27    Visuals,
28    style::{Selection, WidgetVisuals, Widgets},
29};
30use epaint::{CornerRadius, Stroke};
31use eyre::{Context, Result};
32use itertools::Itertools;
33use serde::{Deserialize, Serialize};
34use surfer_translation_types::Translator;
35use surver::SurverFileInfo;
36use tracing::{error, info, trace, warn};
37
38/// The parts of the program state that need to be serialized when loading/saving state
39#[derive(Serialize, Deserialize)]
40pub struct UserState {
41    #[serde(skip)]
42    pub config: SurferConfig,
43
44    /// Overrides for the config show_* fields. Defaults to `config.show_*` if not present
45    pub(crate) show_hierarchy: Option<bool>,
46    pub(crate) show_menu: Option<bool>,
47    pub(crate) show_ticks: Option<bool>,
48    pub(crate) show_toolbar: Option<bool>,
49    pub(crate) show_tooltip: Option<bool>,
50    pub(crate) show_scope_tooltip: Option<bool>,
51    pub(crate) show_default_timeline: Option<bool>,
52    pub(crate) show_overview: Option<bool>,
53    pub(crate) show_statusbar: Option<bool>,
54    pub(crate) align_names_right: Option<bool>,
55    pub(crate) show_variable_indices: Option<bool>,
56    pub(crate) show_variable_direction: Option<bool>,
57    pub(crate) show_empty_scopes: Option<bool>,
58    pub(crate) show_hierarchy_icons: Option<bool>,
59    pub(crate) show_parameters_in_scopes: Option<bool>,
60    #[serde(default)]
61    pub(crate) parameter_display_location: Option<ParameterDisplayLocation>,
62    #[serde(default)]
63    pub(crate) highlight_focused: Option<bool>,
64    #[serde(default)]
65    pub(crate) fill_high_values: Option<bool>,
66    #[serde(default)]
67    pub(crate) primary_button_drag_behavior: Option<PrimaryMouseDrag>,
68    #[serde(default)]
69    pub(crate) arrow_key_bindings: Option<ArrowKeyBindings>,
70    #[serde(default)]
71    pub(crate) clock_highlight_type: Option<ClockHighlightType>,
72    #[serde(default)]
73    pub(crate) hierarchy_style: Option<HierarchyStyle>,
74    #[serde(default)]
75    pub(crate) autoload_sibling_state_files: Option<AutoLoad>,
76    #[serde(default)]
77    pub(crate) autoreload_files: Option<AutoLoad>,
78
79    pub(crate) waves: Option<WaveData>,
80    pub(crate) drag_started: bool,
81    pub(crate) drag_source_idx: Option<VisibleItemIndex>,
82    pub(crate) drag_target_idx: Option<crate::displayed_item_tree::TargetPosition>,
83
84    pub(crate) previous_waves: Option<WaveData>,
85
86    /// Count argument for movements
87    pub(crate) count: Option<String>,
88
89    // Vector of translators which have failed at the `translates` function for a variable.
90    pub(crate) blacklisted_translators: HashSet<(VariableRef, String)>,
91
92    pub(crate) show_about: bool,
93    pub(crate) show_keys: bool,
94    pub(crate) show_gestures: bool,
95    pub(crate) show_quick_start: bool,
96    pub(crate) show_license: bool,
97    pub(crate) show_performance: bool,
98    pub(crate) show_logs: bool,
99    pub(crate) show_cursor_window: bool,
100    #[serde(default)]
101    pub(crate) frame_buffer: FrameBufferSettings,
102    pub(crate) wanted_timeunit: TimeUnit,
103    pub(crate) time_string_format: Option<TimeStringFormatting>,
104    pub(crate) show_url_entry: bool,
105    /// Show a confirmation dialog asking the user for confirmation
106    /// that surfer should reload changed files from disk.
107    #[serde(skip, default)]
108    pub(crate) show_reload_suggestion: Option<ReloadWaveformDialog>,
109    #[serde(skip, default)]
110    pub(crate) show_open_sibling_state_file_suggestion: Option<OpenSiblingStateFileDialog>,
111    pub(crate) variable_name_filter_focused: bool,
112    pub(crate) variable_filter: VariableFilter,
113    //Sidepanel width
114    pub(crate) sidepanel_width: Option<f32>,
115    /// UI zoom factor if set by the user
116    pub(crate) ui_zoom_factor: Option<f32>,
117    #[serde(default)]
118    pub(crate) animation_enabled: Option<bool>,
119    #[serde(default)]
120    pub(crate) use_dinotrace_style: Option<bool>,
121    #[serde(skip, default)]
122    pub(crate) show_server_file_window: bool,
123    #[serde(skip, default)]
124    pub(crate) selected_server_file_index: Option<usize>,
125    #[serde(skip, default)]
126    pub(crate) surver_file_infos: Option<Vec<SurverFileInfo>>,
127    #[serde(skip, default)]
128    pub(crate) surver_url: Option<String>,
129    #[serde(default)]
130    pub(crate) transition_value: Option<TransitionValue>,
131
132    // Path of last saved-to state file
133    // Do not serialize as this causes a few issues and doesn't help:
134    // - We need to set it on load of a state anyways since the file could have been renamed
135    // - Bad interoperatility story between native and wasm builds
136    // - Sequencing issue in serialization, due to us having to run that async
137    #[serde(skip)]
138    pub state_file: Option<PathBuf>,
139}
140
141// Impl needed since for loading we need to put State into a Message
142// Snip out the actual contents to not completely spam the terminal
143impl std::fmt::Debug for UserState {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        write!(f, "SharedState {{ <snipped> }}")
146    }
147}
148
149impl UserState {
150    pub fn new(force_default_config: bool) -> Result<UserState> {
151        let config = SurferConfig::new(force_default_config)
152            .with_context(|| "Failed to load config file")?;
153
154        Ok(UserState {
155            config,
156            ..Default::default()
157        })
158    }
159}
160
161impl Default for UserState {
162    fn default() -> Self {
163        Self {
164            config: SurferConfig::default(),
165            show_hierarchy: None,
166            show_menu: None,
167            show_ticks: None,
168            show_toolbar: None,
169            show_tooltip: None,
170            show_scope_tooltip: None,
171            show_default_timeline: None,
172            show_overview: None,
173            show_statusbar: None,
174            align_names_right: None,
175            show_variable_indices: None,
176            show_variable_direction: None,
177            show_empty_scopes: None,
178            show_hierarchy_icons: None,
179            show_parameters_in_scopes: None,
180            parameter_display_location: None,
181            highlight_focused: None,
182            fill_high_values: None,
183            primary_button_drag_behavior: None,
184            arrow_key_bindings: None,
185            clock_highlight_type: None,
186            hierarchy_style: None,
187            autoload_sibling_state_files: None,
188            autoreload_files: None,
189            waves: None,
190            drag_started: false,
191            drag_source_idx: None,
192            drag_target_idx: None,
193            previous_waves: None,
194            count: None,
195            blacklisted_translators: HashSet::new(),
196            show_about: false,
197            show_keys: false,
198            show_gestures: false,
199            show_quick_start: false,
200            show_license: false,
201            show_performance: false,
202            show_logs: false,
203            show_cursor_window: false,
204            frame_buffer: FrameBufferSettings::default(),
205            wanted_timeunit: TimeUnit::None,
206            time_string_format: None,
207            show_url_entry: false,
208            show_reload_suggestion: None,
209            show_open_sibling_state_file_suggestion: None,
210            variable_name_filter_focused: false,
211            variable_filter: VariableFilter::new(),
212            sidepanel_width: None,
213            ui_zoom_factor: None,
214            state_file: None,
215            animation_enabled: None,
216            use_dinotrace_style: None,
217            selected_server_file_index: None,
218            show_server_file_window: false,
219            surver_file_infos: None,
220            surver_url: None,
221            transition_value: None,
222        }
223    }
224}
225
226impl SystemState {
227    pub fn with_params(mut self, args: StartupParams) -> Self {
228        self.user.previous_waves = self.user.waves;
229        self.user.waves = None;
230
231        // we turn the waveform argument and any startup command file into batch commands
232        self.batch_messages = VecDeque::new();
233
234        match args.waves {
235            Some(WaveSource::Url(url)) => {
236                self.add_batch_message(Message::LoadWaveformFileFromUrl(url, LoadOptions::KeepAll));
237            }
238            Some(WaveSource::File(file)) => {
239                self.add_batch_message(Message::LoadFile(file, LoadOptions::KeepAll));
240            }
241            Some(WaveSource::Data) => error!("Attempted to load data at startup"),
242            Some(WaveSource::Cxxrtl(url)) => {
243                self.add_batch_message(Message::SetupCxxrtl(url));
244            }
245            Some(WaveSource::DragAndDrop(_)) => {
246                error!("Attempted to load from drag and drop at startup (how?)");
247            }
248            None => {}
249        }
250
251        if let Some(port) = args.wcp_initiate {
252            let addr = format!("127.0.0.1:{port}");
253            self.add_batch_message(Message::StartWcpServer {
254                address: Some(addr),
255                initiate: true,
256            });
257        }
258
259        self.add_batch_commands(args.startup_commands);
260
261        self
262    }
263
264    pub fn wcp(&mut self) {
265        self.handle_wcp_commands();
266    }
267
268    pub(crate) fn get_scope(&mut self, scope: ScopeRef, recursive: bool) -> Vec<VariableRef> {
269        let Some(waves) = self.user.waves.as_mut() else {
270            return vec![];
271        };
272
273        let wave_cont = waves.inner.as_waves().unwrap();
274
275        let children = wave_cont.child_scopes(&scope);
276        let mut variables = wave_cont
277            .variables_in_scope(&scope)
278            .iter()
279            .sorted_by(|a, b| numeric_sort::cmp(&a.name, &b.name))
280            .cloned()
281            .collect_vec();
282
283        if recursive && let Ok(children) = children {
284            for child in children {
285                variables.append(&mut self.get_scope(child, true));
286            }
287        }
288
289        variables
290    }
291
292    pub(crate) fn on_waves_loaded(
293        &mut self,
294        filename: WaveSource,
295        format: WaveFormat,
296        new_waves: Box<WaveContainer>,
297        load_options: LoadOptions,
298    ) {
299        let filename_for_title = filename.clone();
300        info!("{format} file loaded");
301        let viewport = Viewport::new();
302        let viewports = [viewport].to_vec();
303
304        for translator in self.translators.all_translators() {
305            translator.set_wave_source(Some(filename.into_translation_type()));
306        }
307
308        let ((new_wave, load_commands), is_reload) =
309            if load_options != LoadOptions::Clear && self.user.waves.is_some() {
310                (
311                    self.user.waves.take().unwrap().update_with_waves(
312                        new_waves,
313                        filename,
314                        format,
315                        &self.translators,
316                        load_options == LoadOptions::KeepAll,
317                    ),
318                    true,
319                )
320            } else if let Some(old) = self.user.previous_waves.take() {
321                (
322                    old.update_with_waves(
323                        new_waves,
324                        filename,
325                        format,
326                        &self.translators,
327                        load_options == LoadOptions::KeepAll,
328                    ),
329                    true,
330                )
331            } else {
332                (
333                    (
334                        WaveData {
335                            inner: DataContainer::Waves(*new_waves),
336                            source: filename,
337                            format,
338                            active_scope: None,
339                            items_tree: DisplayedItemTree::default(),
340                            displayed_items: HashMap::new(),
341                            viewports,
342                            cursor: None,
343                            markers: HashMap::new(),
344                            focused_item: None,
345                            focused_transaction: (None, None),
346                            default_variable_name_type: self.user.config.default_variable_name_type,
347                            display_variable_indices: self.show_variable_indices(),
348                            scroll_offset: 0.,
349                            drawing_infos: vec![],
350                            top_item_draw_offset: 0.,
351                            total_height: 0.,
352                            display_item_ref_counter: 0,
353                            old_num_timestamps: None,
354                            graphics: HashMap::new(),
355                            cache_generation: 0,
356                            inflight_caches: HashMap::new(),
357                        },
358                        None,
359                    ),
360                    false,
361                )
362            };
363
364        if let Some(cmd) = load_commands {
365            self.load_variables(cmd);
366        }
367        self.invalidate_draw_commands();
368
369        self.user.waves = Some(new_wave);
370        // Update window title with waveform name
371        let title = match &filename_for_title {
372            WaveSource::File(path) => {
373                if let Some(name) = path.file_name() {
374                    format!("Surfer - {name}")
375                } else {
376                    "Surfer".to_string()
377                }
378            }
379            WaveSource::Url(url) => format!("Surfer - {url}"),
380            _ => "Surfer".to_string(),
381        };
382        if let Some(ctx) = self.context.as_ref() {
383            ctx.send_viewport_cmd(egui::ViewportCommand::Title(title));
384        }
385
386        if !is_reload && let Some(waves) = &mut self.user.waves {
387            // Set time unit
388            self.user.wanted_timeunit = waves.inner.metadata().timescale.unit;
389            // Possibly open state file load dialog
390            if waves.source.sibling_state_file().is_some() {
391                self.update(Message::SuggestOpenSiblingStateFile);
392            }
393        }
394    }
395
396    pub(crate) fn on_transaction_streams_loaded(
397        &mut self,
398        filename: WaveSource,
399        format: WaveFormat,
400        new_ftr: TransactionContainer,
401        _loaded_options: LoadOptions,
402    ) {
403        info!("Transaction streams are loaded.");
404
405        let viewport = Viewport::new();
406        let viewports = [viewport].to_vec();
407
408        let new_transaction_streams = WaveData {
409            inner: DataContainer::Transactions(new_ftr),
410            source: filename,
411            format,
412            active_scope: None,
413            items_tree: DisplayedItemTree::default(),
414            displayed_items: HashMap::new(),
415            viewports,
416            cursor: None,
417            markers: HashMap::new(),
418            focused_item: None,
419            focused_transaction: (None, None),
420            default_variable_name_type: self.user.config.default_variable_name_type,
421            display_variable_indices: self.show_variable_indices(),
422            scroll_offset: 0.,
423            drawing_infos: vec![],
424            top_item_draw_offset: 0.,
425            total_height: 0.,
426            display_item_ref_counter: 0,
427            old_num_timestamps: None,
428            graphics: HashMap::new(),
429            cache_generation: 0,
430            inflight_caches: HashMap::new(),
431        };
432
433        self.invalidate_draw_commands();
434
435        self.user.config.theme.alt_frequency = 0;
436        self.user.wanted_timeunit = new_transaction_streams.inner.metadata().timescale.unit;
437        self.user.waves = Some(new_transaction_streams);
438    }
439
440    #[cfg(test)]
441    pub(crate) fn handle_async_messages(&mut self) {
442        let mut msgs = vec![];
443        loop {
444            match self.channels.msg_receiver.try_recv() {
445                Ok(msg) => msgs.push(msg),
446                Err(std::sync::mpsc::TryRecvError::Empty) => break,
447                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
448                    trace!("Message sender disconnected");
449                    break;
450                }
451            }
452        }
453
454        while let Some(msg) = msgs.pop() {
455            self.update(msg);
456        }
457    }
458
459    pub(crate) fn push_async_messages(&mut self, msgs: &mut Vec<Message>) {
460        loop {
461            match self.channels.msg_receiver.try_recv() {
462                Ok(msg) => msgs.push(msg),
463                Err(std::sync::mpsc::TryRecvError::Empty) => break,
464                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
465                    trace!("Message sender disconnected");
466                    break;
467                }
468            }
469        }
470    }
471
472    pub fn get_visuals(&self) -> Visuals {
473        let widget_style = WidgetVisuals {
474            bg_fill: self.user.config.theme.secondary_ui_color.background,
475            fg_stroke: Stroke {
476                color: self.user.config.theme.secondary_ui_color.foreground,
477                width: 1.0,
478            },
479            weak_bg_fill: self.user.config.theme.secondary_ui_color.background,
480            bg_stroke: Stroke {
481                color: self.user.config.theme.border_color,
482                width: 1.0,
483            },
484            corner_radius: CornerRadius::same(2),
485            expansion: 0.0,
486        };
487
488        Visuals {
489            override_text_color: Some(self.user.config.theme.foreground),
490            extreme_bg_color: self.user.config.theme.secondary_ui_color.background,
491            panel_fill: self.user.config.theme.secondary_ui_color.background,
492            window_fill: self.user.config.theme.primary_ui_color.background,
493            window_stroke: Stroke {
494                width: 1.0,
495                color: self.user.config.theme.border_color,
496            },
497            selection: Selection {
498                bg_fill: self.user.config.theme.selected_elements_colors.background,
499                stroke: Stroke {
500                    color: self.user.config.theme.selected_elements_colors.foreground,
501                    width: 1.0,
502                },
503            },
504            widgets: Widgets {
505                noninteractive: widget_style,
506                inactive: widget_style,
507                hovered: widget_style,
508                active: widget_style,
509                open: widget_style,
510            },
511            ..Visuals::dark()
512        }
513    }
514
515    pub(crate) fn load_state(&mut self, mut loaded_state: Box<UserState>, path: Option<PathBuf>) {
516        // first swap everything, fix special cases afterwards
517        mem::swap(&mut self.user, &mut loaded_state);
518
519        // swap back waves for inner, source, format since we want to keep the file
520        // fix up all wave references from paths if a wave is loaded
521        mem::swap(&mut loaded_state.waves, &mut self.user.waves);
522        let load_commands = if let (Some(waves), Some(new_waves)) =
523            (&mut self.user.waves, &mut loaded_state.waves)
524        {
525            mem::swap(&mut waves.active_scope, &mut new_waves.active_scope);
526            let items = std::mem::take(&mut new_waves.displayed_items);
527            let items_tree = std::mem::take(&mut new_waves.items_tree);
528            let load_commands = waves.update_with_items(&items, items_tree, &self.translators);
529
530            mem::swap(&mut waves.viewports, &mut new_waves.viewports);
531            mem::swap(&mut waves.cursor, &mut new_waves.cursor);
532            mem::swap(&mut waves.markers, &mut new_waves.markers);
533            mem::swap(&mut waves.focused_item, &mut new_waves.focused_item);
534            waves.default_variable_name_type = new_waves.default_variable_name_type;
535            waves.scroll_offset = new_waves.scroll_offset;
536            load_commands
537        } else {
538            None
539        };
540        if let Some(load_commands) = load_commands {
541            self.load_variables(load_commands);
542        }
543
544        // reset drag to avoid confusion
545        self.user.drag_started = false;
546        self.user.drag_source_idx = None;
547        self.user.drag_target_idx = None;
548
549        // reset previous_waves & count to prevent unintuitive state here
550        self.user.previous_waves = None;
551        self.user.count = None;
552
553        // use just loaded path since path is not part of the export as it might have changed anyways
554        self.user.state_file = path;
555
556        self.invalidate_draw_commands();
557        if let Some(waves) = &mut self.user.waves {
558            waves.update_viewports();
559        }
560    }
561
562    /// Returns true if the waveform and all requested signals have been loaded.
563    /// Used for testing to make sure the GUI is at its final state before taking a
564    /// snapshot.
565    pub fn waves_fully_loaded(&self) -> bool {
566        self.user
567            .waves
568            .as_ref()
569            .is_some_and(|w| w.inner.is_fully_loaded())
570    }
571
572    /// Returns true if no analog caches are currently being built
573    pub fn analog_caches_ready(&self) -> bool {
574        self.user
575            .waves
576            .as_ref()
577            .is_none_or(|w| w.inflight_caches.is_empty())
578    }
579
580    /// Returns the current canvas state
581    pub(crate) fn current_canvas_state(waves: &WaveData, message: String) -> CanvasState {
582        CanvasState {
583            message,
584            focused_item: waves.focused_item,
585            focused_transaction: waves.focused_transaction.clone(),
586            items_tree: waves.items_tree.clone(),
587            displayed_items: waves.displayed_items.clone(),
588            markers: waves.markers.clone(),
589        }
590    }
591
592    /// Push the current canvas state to the undo stack
593    pub(crate) fn save_current_canvas(&mut self, message: String) {
594        if let Some(waves) = &self.user.waves {
595            self.undo_stack
596                .push(SystemState::current_canvas_state(waves, message));
597
598            if self.undo_stack.len() > self.user.config.undo_stack_size {
599                self.undo_stack.remove(0);
600            }
601            self.redo_stack.clear();
602        }
603    }
604
605    #[cfg(not(target_arch = "wasm32"))]
606    pub(crate) fn start_wcp_server(&mut self, address: Option<String>, initiate: bool) {
607        use wcp::wcp_server::WcpServer;
608
609        use crate::wcp;
610
611        if self.wcp_server_thread.as_ref().is_some()
612            || self
613                .wcp_running_signal
614                .load(std::sync::atomic::Ordering::Relaxed)
615        {
616            warn!("WCP HTTP server is already running");
617            return;
618        }
619        // TODO: Consider an unbounded channel?
620        let (wcp_s2c_sender, wcp_s2c_receiver) = tokio::sync::mpsc::channel(100);
621        let (wcp_c2s_sender, wcp_c2s_receiver) = tokio::sync::mpsc::channel(100);
622
623        self.channels.wcp_c2s_receiver = Some(wcp_c2s_receiver);
624        self.channels.wcp_s2c_sender = Some(wcp_s2c_sender);
625        let stop_signal_copy = self.wcp_stop_signal.clone();
626        stop_signal_copy.store(false, std::sync::atomic::Ordering::Relaxed);
627        let running_signal_copy = self.wcp_running_signal.clone();
628        running_signal_copy.store(true, std::sync::atomic::Ordering::Relaxed);
629        let greeted_signal_copy = self.wcp_greeted_signal.clone();
630        greeted_signal_copy.store(true, std::sync::atomic::Ordering::Relaxed);
631
632        let ctx = self.context.clone();
633        let address = address.unwrap_or(self.user.config.wcp.address.clone());
634        self.wcp_server_address = Some(address.clone());
635        self.wcp_server_thread = Some(tokio::spawn(async move {
636            let server = WcpServer::new(
637                address,
638                initiate,
639                wcp_c2s_sender,
640                wcp_s2c_receiver,
641                stop_signal_copy,
642                running_signal_copy,
643                greeted_signal_copy,
644                ctx,
645            )
646            .await;
647            match server {
648                Ok(mut server) => server.run().await,
649                Err(m) => {
650                    error!("Could not start WCP server. {m:?}");
651                }
652            }
653        }));
654    }
655
656    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
657    pub(crate) fn stop_wcp_server(&mut self) {
658        // stop wcp server if there is one running
659
660        if self.wcp_server_address.is_some() && self.wcp_server_thread.is_some() {
661            // signal the server to stop
662            self.wcp_stop_signal
663                .store(true, std::sync::atomic::Ordering::Relaxed);
664
665            self.wcp_server_thread = None;
666            self.wcp_server_address = None;
667            self.channels.wcp_s2c_sender = None;
668            self.channels.wcp_c2s_receiver = None;
669            info!("Stopped WCP server");
670        }
671    }
672}