libsurfer/
state.rs

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