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