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