libsurfer/
state.rs

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