diff --git a/MoonZoon.toml b/MoonZoon.toml index d53ee41..d06702c 100644 --- a/MoonZoon.toml +++ b/MoonZoon.toml @@ -1,7 +1,9 @@ port = 8080 # port = 8443 https = false -cache_busting = true +# @TODO how to import `pkg/frontend.js` with enabled cache busting? +# @TODO add a switch to enable Typescript generator in mzoon? +cache_busting = false backend_log_level = "warn" # "error" / "warn" / "info" / "debug" / "trace" [redirect] @@ -23,4 +25,6 @@ frontend = [ backend = [ "backend/Cargo.toml", "backend/src", + "backend/index.js", + "backend/style.css", ] diff --git a/README.md b/README.md index 158b34d..496712d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,16 @@ Zoom and all formats

+

+ Fastwave - Javascript commands + Javascript commands +

+ +

+ Fastwave - Load and save selected variables + Load and save selected variables +

+ --- ### Install requirements: diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..b1896bc --- /dev/null +++ b/backend/index.js @@ -0,0 +1,2 @@ +import { FW } from '/_api/pkg/frontend.js'; +window.FW = FW; diff --git a/backend/src/main.rs b/backend/src/main.rs index 1b86c9a..fe6bfd7 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,11 +1,14 @@ use moon::*; async fn frontend() -> Frontend { - Frontend::new().title("FastWave").append_to_head(concat!( - "" - )) + Frontend::new() + .title("FastWave") + .append_to_head(concat!("")) + .append_to_head(concat!( + "" + )) } async fn up_msg_handler(_: UpMsgRequest<()>) {} diff --git a/docs/video_javascript_commands.gif b/docs/video_javascript_commands.gif new file mode 100644 index 0000000..0a16dde Binary files /dev/null and b/docs/video_javascript_commands.gif differ diff --git a/docs/video_load_save_selected_vars.gif b/docs/video_load_save_selected_vars.gif new file mode 100644 index 0000000..0580fa8 Binary files /dev/null and b/docs/video_load_save_selected_vars.gif differ diff --git a/frontend/src/controls_panel.rs b/frontend/src/controls_panel.rs index 69fff23..521acdd 100644 --- a/frontend/src/controls_panel.rs +++ b/frontend/src/controls_panel.rs @@ -1,15 +1,15 @@ -use crate::{platform, Layout}; +use crate::{Filename, Layout}; use std::cell::Cell; use std::mem; use std::ops::Not; use std::rc::Rc; +use std::sync::Arc; use wellen::GetItem; use zoon::*; -const SCOPE_VAR_ROW_MAX_WIDTH: u32 = 480; +const MILLER_COLUMN_SCOPE_VAR_ROW_MIN_WIDTH: u32 = 480; const MILLER_COLUMN_MAX_HEIGHT: u32 = 500; - -type Filename = String; +const TREE_MAX_WIDTH: u32 = 600; #[derive(Clone)] struct VarForUI { @@ -34,7 +34,7 @@ struct ScopeForUI { #[derive(Clone)] pub struct ControlsPanel { selected_scope_ref: Mutable>, - hierarchy: Mutable>>, + hierarchy: Mutable>>, selected_var_refs: MutableVec, layout: Mutable, loaded_filename: Mutable>, @@ -42,16 +42,17 @@ pub struct ControlsPanel { impl ControlsPanel { pub fn new( - hierarchy: Mutable>>, + hierarchy: Mutable>>, selected_var_refs: MutableVec, layout: Mutable, + loaded_filename: Mutable>, ) -> impl Element { Self { selected_scope_ref: <_>::default(), hierarchy, selected_var_refs, layout, - loaded_filename: <_>::default(), + loaded_filename, } .root() } @@ -92,6 +93,12 @@ impl ControlsPanel { .map(|layout| matches!(layout, Layout::Columns)) .map_true(|| Width::fill()), )) + .s(Width::with_signal_self(layout.signal().map( + move |layout| match layout { + Layout::Tree => Width::growable().max(TREE_MAX_WIDTH), + Layout::Columns => Width::fill(), + }, + ))) .s(Height::with_signal_self(layout.signal().map( move |layout| match layout { Layout::Tree => Height::fill(), @@ -101,13 +108,6 @@ impl ControlsPanel { .s(Padding::all(20)) .s(Gap::new().y(40)) .s(Align::new().top()) - .item( - Row::new() - .s(Gap::both(15)) - .s(Align::new().left()) - .item(self.load_button()) - .item(self.layout_switcher()), - ) .item_signal( self.hierarchy .signal_cloned() @@ -122,143 +122,9 @@ impl ControlsPanel { )) } - #[cfg(FASTWAVE_PLATFORM = "TAURI")] - fn load_button(&self) -> impl Element { - let (hovered, hovered_signal) = Mutable::new_and_signal(false); - let hierarchy = self.hierarchy.clone(); - let loaded_filename = self.loaded_filename.clone(); - Button::new() - .s(Padding::new().x(20).y(10)) - .s(Background::new().color_signal( - hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), - )) - .s(Align::new().left()) - .s(RoundedCorners::all(15)) - .label(El::new().s(Font::new().no_wrap()).child_signal( - loaded_filename.signal_cloned().map_option( - |filename| format!("Unload {filename}"), - || format!("Load file.."), - ), - )) - .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) - .on_press(move || { - let mut hierarchy_lock = hierarchy.lock_mut(); - if hierarchy_lock.is_some() { - *hierarchy_lock = None; - return; - } - drop(hierarchy_lock); - let hierarchy = hierarchy.clone(); - let loaded_filename = loaded_filename.clone(); - Task::start(async move { - if let Some(filename) = platform::pick_and_load_waveform(None).await { - loaded_filename.set_neq(Some(filename)); - hierarchy.set(Some(Rc::new(platform::get_hierarchy().await))) - } - }) - }) - } - - #[cfg(FASTWAVE_PLATFORM = "BROWSER")] - fn load_button(&self) -> impl Element { - let (hovered, hovered_signal) = Mutable::new_and_signal(false); - let hierarchy = self.hierarchy.clone(); - let loaded_filename = self.loaded_filename.clone(); - let file_input_id = "file_input"; - Row::new() - .item( - Label::new() - .s(Padding::new().x(20).y(10)) - .s(Background::new().color_signal( - hovered_signal - .map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), - )) - .s(Align::new().left()) - .s(RoundedCorners::all(15)) - .s(Cursor::new(CursorIcon::Pointer)) - .label(El::new().s(Font::new().no_wrap()).child_signal( - loaded_filename.signal_cloned().map_option( - |filename| format!("Unload {filename}"), - || format!("Load file.."), - ), - )) - .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) - .for_input(file_input_id) - .on_click_event_with_options( - EventOptions::new().preventable(), - clone!((hierarchy) move |event| { - let mut hierarchy_lock = hierarchy.lock_mut(); - if hierarchy_lock.is_some() { - *hierarchy_lock = None; - if let RawMouseEvent::Click(raw_event) = event.raw_event { - // @TODO Move to MoonZoon as a new API - raw_event.prevent_default(); - } - return; - } - }), - ), - ) - .item( - // @TODO https://github.com/MoonZoon/MoonZoon/issues/39 - // + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_hidden_file_input_elements_using_the_click_method - TextInput::new().id(file_input_id).update_raw_el(|raw_el| { - let dom_element = raw_el.dom_element(); - raw_el - .style("display", "none") - .attr("type", "file") - .event_handler(move |_: events::Input| { - let Some(file_list) = - dom_element.files().map(gloo_file::FileList::from) - else { - zoon::println!("file list is `None`"); - return; - }; - let Some(file) = file_list.first().cloned() else { - zoon::println!("file list is empty"); - return; - }; - let hierarchy = hierarchy.clone(); - let loaded_filename = loaded_filename.clone(); - Task::start(async move { - if let Some(filename) = - platform::pick_and_load_waveform(Some(file)).await - { - loaded_filename.set_neq(Some(filename)); - hierarchy.set(Some(Rc::new(platform::get_hierarchy().await))) - } - }) - }) - }), - ) - } - - fn layout_switcher(&self) -> impl Element { - let layout = self.layout.clone(); - let (hovered, hovered_signal) = Mutable::new_and_signal(false); - Button::new() - .s(Padding::new().x(20).y(10)) - .s(Background::new().color_signal( - hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), - )) - .s(Align::new().left()) - .s(RoundedCorners::all(15)) - .label_signal(layout.signal().map(|layout| match layout { - Layout::Tree => "Columns", - Layout::Columns => "Tree", - })) - .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) - .on_press(move || { - layout.update(|layout| match layout { - Layout::Tree => Layout::Columns, - Layout::Columns => Layout::Tree, - }) - }) - } - - fn scopes_panel(&self, hierarchy: Rc) -> impl Element { + fn scopes_panel(&self, hierarchy: Arc) -> impl Element { Column::new() - .s(Height::fill().min(150)) + .s(Height::fill()) .s(Scrollbars::y_and_clip_x()) .s(Gap::new().y(20)) .s(Width::fill()) @@ -271,7 +137,7 @@ impl ControlsPanel { .item(self.scopes_list(hierarchy)) } - fn scopes_list(&self, hierarchy: Rc) -> impl Element { + fn scopes_list(&self, hierarchy: Arc) -> impl Element { let layout = self.layout.clone(); let mut scopes_for_ui = Vec::new(); let mut max_level_index: usize = 0; @@ -307,7 +173,7 @@ impl ControlsPanel { let s = self.clone(); El::new() .s(Height::fill()) - .s(Scrollbars::both()) + .s(Scrollbars::y_and_clip_x()) .s(Width::fill()) .child_signal(layout.signal().map(move |layout| match layout { Layout::Tree => { @@ -421,7 +287,6 @@ impl ControlsPanel { Layout::Tree => level * 30, Layout::Columns => 0, }))) - .s(Width::default().max(SCOPE_VAR_ROW_MAX_WIDTH)) .after_remove(move |_| { drop(task_collapse_on_parent_collapse); drop(task_expand_or_collapse_on_selected_scope_in_level_change); @@ -512,7 +377,6 @@ impl ControlsPanel { ) -> impl Element { Button::new() .s(Padding::new().x(15).y(5)) - .s(Font::new().wrap_anywhere()) .on_hovered_change(move |is_hovered| button_hovered.set_neq(is_hovered)) .on_press( clone!((self.selected_scope_ref => selected_scope_ref, scope_for_ui) move || { @@ -523,12 +387,14 @@ impl ControlsPanel { .label(scope_for_ui.name) } - fn vars_panel(&self, hierarchy: Rc) -> impl Element { + fn vars_panel(&self, hierarchy: Arc) -> impl Element { let selected_scope_ref = self.selected_scope_ref.clone(); Column::new() + .s(Align::new().top()) .s(Gap::new().y(20)) .s(Height::fill().min(150)) - .s(Scrollbars::y_and_clip_x()) + .s(Width::fill()) + .s(Scrollbars::both()) .item_signal( self.layout .signal() @@ -544,7 +410,7 @@ impl ControlsPanel { fn vars_list( &self, selected_scope_ref: wellen::ScopeRef, - hierarchy: Rc, + hierarchy: Arc, ) -> impl Element { let vars_for_ui = hierarchy .get(selected_scope_ref) @@ -583,17 +449,18 @@ impl ControlsPanel { } })); + let layout = self.layout.clone(); Column::new() - .s(Width::with_signal_self( - self.layout - .signal() - .map(|layout| matches!(layout, Layout::Columns)) - .map_true(|| Width::default().min(SCOPE_VAR_ROW_MAX_WIDTH)), - )) + .s(Width::with_signal_self(layout.signal().map( + move |layout| match layout { + Layout::Tree => Width::fill(), + Layout::Columns => Width::default().min(MILLER_COLUMN_SCOPE_VAR_ROW_MIN_WIDTH), + }, + ))) .s(Align::new().left()) .s(Gap::new().y(10)) .s(Height::fill()) - .s(Scrollbars::y_and_clip_x()) + .s(Scrollbars::both()) .items_signal_vec( vars_for_ui_mutable_vec .signal_vec_cloned() @@ -605,13 +472,13 @@ impl ControlsPanel { fn var_row(&self, var_for_ui: VarForUI) -> impl Element { Row::new() .s(Gap::new().x(10)) - .s(Padding::new().right(15)) - .s(Width::default().max(SCOPE_VAR_ROW_MAX_WIDTH)) .item(self.var_button(var_for_ui.clone())) .item(self.var_tag_type(var_for_ui.clone())) .item(self.var_tag_index(var_for_ui.clone())) .item(self.var_tag_bit(var_for_ui.clone())) .item(self.var_tag_direction(var_for_ui)) + // Note: Padding or 0 height don't work for some reasons here + .item(El::new().s(Width::exact(10)).s(Height::exact(1))) } fn var_button(&self, var_for_ui: VarForUI) -> impl Element { @@ -619,7 +486,6 @@ impl ControlsPanel { let selected_var_ref = self.selected_var_refs.clone(); El::new().child( Button::new() - .s(Font::new().wrap_anywhere()) .s(Padding::new().x(15).y(5)) .s(Background::new().color_signal( hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), diff --git a/frontend/src/header_panel.rs b/frontend/src/header_panel.rs new file mode 100644 index 0000000..72e5c25 --- /dev/null +++ b/frontend/src/header_panel.rs @@ -0,0 +1,303 @@ +use crate::{platform, script_bridge, Filename, Layout}; +use std::sync::Arc; +use zoon::*; + +pub struct HeaderPanel { + hierarchy: Mutable>>, + layout: Mutable, + loaded_filename: Mutable>, +} + +impl HeaderPanel { + pub fn new( + hierarchy: Mutable>>, + layout: Mutable, + loaded_filename: Mutable>, + ) -> impl Element { + Self { + hierarchy, + layout, + loaded_filename, + } + .root() + } + + fn root(&self) -> impl Element { + Row::new() + .s(Padding::new().x(20).y(15)) + .s(Gap::both(40)) + .item( + Row::new() + .s(Align::new().top()) + .s(Padding::new().top(5)) + .s(Gap::both(15)) + .item(self.load_button()) + .item(self.layout_switcher()), + ) + .item(self.command_panel()) + } + + #[cfg(FASTWAVE_PLATFORM = "TAURI")] + fn load_button(&self) -> impl Element { + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + let hierarchy = self.hierarchy.clone(); + let loaded_filename = self.loaded_filename.clone(); + Button::new() + .s(Padding::new().x(20).y(10)) + .s(Background::new().color_signal( + hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + )) + .s(Align::new().left()) + .s(RoundedCorners::all(15)) + .label(El::new().s(Font::new().no_wrap()).child_signal( + loaded_filename.signal_cloned().map_option( + |filename| format!("Unload {filename}"), + || format!("Load file.."), + ), + )) + .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) + .on_press(move || { + let mut hierarchy_lock = hierarchy.lock_mut(); + if hierarchy_lock.is_some() { + *hierarchy_lock = None; + return; + } + drop(hierarchy_lock); + let hierarchy = hierarchy.clone(); + let loaded_filename = loaded_filename.clone(); + Task::start(async move { + if let Some(filename) = platform::pick_and_load_waveform(None).await { + loaded_filename.set_neq(Some(filename)); + hierarchy.set(Some(Arc::new(platform::get_hierarchy().await))) + } + }) + }) + } + + #[cfg(FASTWAVE_PLATFORM = "BROWSER")] + fn load_button(&self) -> impl Element { + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + let hierarchy = self.hierarchy.clone(); + let loaded_filename = self.loaded_filename.clone(); + let file_input_id = "file_input_for_load_waveform_button"; + Row::new() + .item( + Label::new() + .s(Padding::new().x(20).y(10)) + .s(Background::new().color_signal( + hovered_signal + .map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + )) + .s(Align::new().left()) + .s(RoundedCorners::all(15)) + .s(Cursor::new(CursorIcon::Pointer)) + .label(El::new().s(Font::new().no_wrap()).child_signal( + loaded_filename.signal_cloned().map_option( + |filename| format!("Unload {filename}"), + || format!("Load file.."), + ), + )) + .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) + .for_input(file_input_id) + .on_click_event_with_options( + EventOptions::new().preventable(), + clone!((hierarchy) move |event| { + let mut hierarchy_lock = hierarchy.lock_mut(); + if hierarchy_lock.is_some() { + *hierarchy_lock = None; + if let RawMouseEvent::Click(raw_event) = event.raw_event { + // @TODO Move to MoonZoon as a new API + raw_event.prevent_default(); + } + return; + } + }), + ), + ) + .item( + // @TODO https://github.com/MoonZoon/MoonZoon/issues/39 + // + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_hidden_file_input_elements_using_the_click_method + TextInput::new().id(file_input_id).update_raw_el(|raw_el| { + let dom_element = raw_el.dom_element(); + raw_el + .style("display", "none") + .attr("type", "file") + .event_handler(move |_: events::Input| { + let Some(file_list) = + dom_element.files().map(gloo_file::FileList::from) + else { + zoon::println!("file list is `None`"); + return; + }; + let Some(file) = file_list.first().cloned() else { + zoon::println!("file list is empty"); + return; + }; + let hierarchy = hierarchy.clone(); + let loaded_filename = loaded_filename.clone(); + Task::start(async move { + if let Some(filename) = + platform::pick_and_load_waveform(Some(file)).await + { + loaded_filename.set_neq(Some(filename)); + hierarchy.set(Some(Arc::new(platform::get_hierarchy().await))) + } + }) + }) + }), + ) + } + + fn layout_switcher(&self) -> impl Element { + let layout = self.layout.clone(); + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + Button::new() + .s(Padding::new().x(20).y(10)) + .s(Background::new().color_signal( + hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + )) + .s(RoundedCorners::all(15)) + .label_signal(layout.signal().map(|layout| match layout { + Layout::Tree => "Columns", + Layout::Columns => "Tree", + })) + .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) + .on_press(move || { + layout.update(|layout| match layout { + Layout::Tree => Layout::Columns, + Layout::Columns => Layout::Tree, + }) + }) + } + + fn command_panel(&self) -> impl Element { + let command_result: Mutable>> = <_>::default(); + Row::new() + .s(Align::new().top()) + .s(Gap::both(30)) + .s(Scrollbars::both()) + .s(Width::fill()) + .item(self.command_editor_panel(command_result.clone())) + .item(self.command_result_panel(command_result.read_only())) + } + + fn command_editor_panel( + &self, + command_result: Mutable>>, + ) -> impl Element { + Column::new() + .s(Align::new().top()) + .s(Gap::new().y(10)) + .s(Width::growable()) + .item( + Row::new() + .s(Gap::new().x(15)) + .s(Padding::new().x(5)) + .item(El::new().child("Javascript commands")) + .item(El::new().s(Align::new().right()).child("Shift + Enter")), + ) + .item(self.command_editor(command_result)) + } + + fn command_editor( + &self, + command_result: Mutable>>, + ) -> impl Element { + let (script, script_signal) = Mutable::new_and_signal_cloned(String::new()); + // @TODO perhaps replace with an element with syntax highlighter like https://github.com/WebCoder49/code-input later + TextArea::new() + .s(Background::new().color(color!("SlateBlue"))) + .s(Padding::new().x(10).y(8)) + .s(RoundedCorners::all(15)) + .s(Height::default().min(50)) + .s(Width::fill().min(300)) + .s(Font::new() + .tracking(1) + .weight(FontWeight::Medium) + .color(color!("White")) + .family([FontFamily::new("Courier New"), FontFamily::Monospace])) + .s(Shadows::new([Shadow::new() + .inner() + .color(color!("DarkSlateBlue")) + .blur(4)])) + // @TODO `spellcheck` and `resize` to MZ API? (together with autocomplete and others?) + .update_raw_el(|raw_el| { + raw_el + .attr("spellcheck", "false") + .style("resize", "vertical") + }) + .placeholder( + Placeholder::new("FW.say_hello()").s(Font::new().color(color!("LightBlue"))), + ) + .label_hidden("command editor panel") + .text_signal(script_signal) + .on_change(clone!((script, command_result) move |text| { + script.set_neq(text); + command_result.set_neq(None); + })) + .on_key_down_event_with_options(EventOptions::new().preventable(), move |event| { + if event.key() == &Key::Enter { + let RawKeyboardEvent::KeyDown(raw_event) = event.raw_event.clone(); + if raw_event.shift_key() { + // @TODO move `prevent_default` to MZ API (next to the `pass_to_parent` method?) + raw_event.prevent_default(); + let result = script_bridge::strict_eval(&script.lock_ref()); + command_result.set(Some(result)); + } + } + }) + } + + fn command_result_panel( + &self, + command_result: ReadOnlyMutable>>, + ) -> impl Element { + Column::new() + .s(Gap::new().y(10)) + .s(Align::new().top()) + .s(Scrollbars::both()) + .s(Padding::new().x(5)) + .s(Width::growable().max(750)) + .item(El::new().child("Command result")) + .item(self.command_result_el(command_result)) + } + + fn command_result_el( + &self, + command_result: ReadOnlyMutable>>, + ) -> impl Element { + El::new() + .s(Font::new() + .tracking(1) + .weight(FontWeight::Medium) + .color(color!("White")) + .family([FontFamily::new("Courier New"), FontFamily::Monospace])) + .s(Scrollbars::both()) + .s(Height::default().max(100)) + .child_signal(command_result.signal_ref(|result| { + fn format_complex_js_value(js_value: &JsValue) -> String { + let value = format!("{js_value:?}"); + let value = value.strip_prefix("JsValue(").unwrap_throw(); + let value = value.strip_suffix(')').unwrap_throw(); + value.to_owned() + } + match result { + Some(Ok(js_value)) => { + if let Some(string_value) = js_value.as_string() { + string_value + } else if let Some(number_value) = js_value.as_f64() { + number_value.to_string() + } else if let Some(bool_value) = js_value.as_bool() { + bool_value.to_string() + } else { + format_complex_js_value(js_value) + } + } + Some(Err(js_value)) => { + format!("ERROR: {}", format_complex_js_value(js_value)) + } + None => "-".to_owned(), + } + })) + } +} diff --git a/frontend/src/main.rs b/frontend/src/main.rs index e551c3d..0db9ce7 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -1,7 +1,8 @@ -use std::rc::Rc; +use std::sync::Arc; use zoon::*; mod platform; +mod script_bridge; mod controls_panel; use controls_panel::ControlsPanel; @@ -9,6 +10,9 @@ use controls_panel::ControlsPanel; mod waveform_panel; use waveform_panel::WaveformPanel; +mod header_panel; +use header_panel::HeaderPanel; + #[derive(Clone, Copy, Default)] enum Layout { Tree, @@ -16,6 +20,17 @@ enum Layout { Columns, } +type Filename = String; + +#[derive(Default)] +struct Store { + selected_var_refs: MutableVec, + hierarchy: Mutable>>, + loaded_filename: Mutable>, +} + +static STORE: Lazy = lazy::default(); + fn main() { start_app("app", root); Task::start(async { @@ -26,38 +41,56 @@ fn main() { } fn root() -> impl Element { - let hierarchy: Mutable>> = <_>::default(); - let selected_var_refs: MutableVec = <_>::default(); + let hierarchy = STORE.hierarchy.clone(); + let selected_var_refs = STORE.selected_var_refs.clone(); let layout: Mutable = <_>::default(); + let loaded_filename = STORE.loaded_filename.clone(); Column::new() .s(Height::fill()) .s(Scrollbars::y_and_clip_x()) .s(Font::new().color(color!("Lavender"))) + .item(HeaderPanel::new( + hierarchy.clone(), + layout.clone(), + loaded_filename.clone(), + )) .item( Row::new() - .s(Height::fill()) + .s(Scrollbars::y_and_clip_x()) .s(Gap::new().x(15)) + .s(Height::growable().min(150)) .item(ControlsPanel::new( hierarchy.clone(), selected_var_refs.clone(), layout.clone(), + loaded_filename.clone(), )) - .item_signal( - layout - .signal() - .map(|layout| matches!(layout, Layout::Tree)) - .map_true( - clone!((hierarchy, selected_var_refs) move || WaveformPanel::new( + .item_signal({ + let hierarchy = hierarchy.clone(); + let selected_var_refs = selected_var_refs.clone(); + let loaded_filename = loaded_filename.clone(); + map_ref!{ + let layout = layout.signal(), + let hierarchy_is_some = hierarchy.signal_ref(Option::is_some) => { + (*hierarchy_is_some && matches!(layout, Layout::Tree)).then(clone!((hierarchy, selected_var_refs, loaded_filename) move || WaveformPanel::new( hierarchy.clone(), selected_var_refs.clone(), - )), - ), - ), + loaded_filename.clone(), + ))) + } + } + }), ) .item_signal( - layout - .signal() - .map(|layout| matches!(layout, Layout::Columns)) - .map_true(move || WaveformPanel::new(hierarchy.clone(), selected_var_refs.clone())), + map_ref!{ + let layout = layout.signal(), + let hierarchy_is_some = hierarchy.signal_ref(Option::is_some) => { + (*hierarchy_is_some && matches!(layout, Layout::Columns)).then(clone!((hierarchy, selected_var_refs, loaded_filename) move || WaveformPanel::new( + hierarchy.clone(), + selected_var_refs.clone(), + loaded_filename.clone(), + ))) + } + } ) } diff --git a/frontend/src/platform.rs b/frontend/src/platform.rs index 73c7f73..e152433 100644 --- a/frontend/src/platform.rs +++ b/frontend/src/platform.rs @@ -14,6 +14,7 @@ mod browser; use browser as platform; type Filename = String; +type JavascriptCode = String; pub async fn show_window() { platform::show_window().await @@ -25,6 +26,12 @@ pub async fn pick_and_load_waveform(file: Option) -> Option) -> Option { + platform::load_file_with_selected_vars(file).await +} + pub async fn get_hierarchy() -> wellen::Hierarchy { platform::get_hierarchy().await } diff --git a/frontend/src/platform/browser.rs b/frontend/src/platform/browser.rs index 09f94fb..363a5c2 100644 --- a/frontend/src/platform/browser.rs +++ b/frontend/src/platform/browser.rs @@ -4,11 +4,11 @@ use wellen::simple::Waveform; use zoon::*; #[derive(Default)] -struct Store { +struct BrowserPlatformStore { waveform: Mutex>, } -static STORE: Lazy = lazy::default(); +static BROWSER_PLATFORM_STORE: Lazy = lazy::default(); pub(super) async fn show_window() {} @@ -25,7 +25,7 @@ pub(super) async fn pick_and_load_waveform( let Ok(waveform) = waveform else { panic!("Waveform file reading failed") }; - *STORE.waveform.lock().unwrap_throw() = Some(waveform); + *BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw() = Some(waveform); Some(file.name()) } @@ -63,12 +63,28 @@ pub(super) async fn pick_and_load_waveform( // let Ok(waveform) = waveform else { // panic!("Waveform file reading failed") // }; -// *STORE.waveform.lock().unwrap_throw() = Some(waveform); +// *BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw() = Some(waveform); // Some(file.name()) // } +// @TODO allow only supported file type (*.fw.js) +// @TODO remove the `file` parameter once we don't have to use FileInput element +pub async fn load_file_with_selected_vars( + file: Option, +) -> Option { + let file = file.unwrap_throw(); + + let javascript_code = gloo_file::futures::read_as_text(&file).await.unwrap_throw(); + + Some(javascript_code) +} + +// @TODO Use alternative `load_file_with_selected_vars` version once `showOpenFilePicker` is supported by Safari and Firefox +// https://caniuse.com/mdn-api_window_showopenfilepicker +// (see the `pick_and_load_waveform` method above) + pub(super) async fn get_hierarchy() -> wellen::Hierarchy { - let waveform = STORE.waveform.lock().unwrap_throw(); + let waveform = BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw(); let hierarchy = waveform.as_ref().unwrap_throw().hierarchy(); // @TODO Wrap `hierarchy` in `Waveform` with `Rc/Arc` or add the method `take` / `clone` or refactor? serde_json::from_value(serde_json::to_value(hierarchy).unwrap_throw()).unwrap_throw() @@ -82,7 +98,7 @@ pub(super) async fn load_signal_and_get_timeline( block_height: u32, var_format: shared::VarFormat, ) -> shared::Timeline { - let mut waveform_lock = STORE.waveform.lock().unwrap(); + let mut waveform_lock = BROWSER_PLATFORM_STORE.waveform.lock().unwrap(); let waveform = waveform_lock.as_mut().unwrap(); waveform.load_signals_multi_threaded(&[signal_ref]); let signal = waveform.get_signal(signal_ref).unwrap(); @@ -100,7 +116,7 @@ pub(super) async fn load_signal_and_get_timeline( } pub(super) async fn unload_signal(signal_ref: wellen::SignalRef) { - let mut waveform_lock = STORE.waveform.lock().unwrap_throw(); + let mut waveform_lock = BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw(); let waveform = waveform_lock.as_mut().unwrap_throw(); waveform.unload_signals(&[signal_ref]); } diff --git a/frontend/src/platform/tauri.rs b/frontend/src/platform/tauri.rs index 30300be..aea34b6 100644 --- a/frontend/src/platform/tauri.rs +++ b/frontend/src/platform/tauri.rs @@ -13,6 +13,15 @@ pub(super) async fn pick_and_load_waveform( .as_string() } +pub(super) async fn load_file_with_selected_vars( + _file: Option, +) -> Option { + tauri_glue::load_file_with_selected_vars() + .await + .unwrap_throw() + .as_string() +} + pub(super) async fn get_hierarchy() -> wellen::Hierarchy { serde_wasm_bindgen::from_value(tauri_glue::get_hierarchy().await.unwrap_throw()).unwrap_throw() } @@ -59,6 +68,9 @@ mod tauri_glue { #[wasm_bindgen(catch)] pub async fn pick_and_load_waveform() -> Result; + #[wasm_bindgen(catch)] + pub async fn load_file_with_selected_vars() -> Result; + #[wasm_bindgen(catch)] pub async fn get_hierarchy() -> Result; diff --git a/frontend/src/script_bridge.rs b/frontend/src/script_bridge.rs new file mode 100644 index 0000000..0dcf94a --- /dev/null +++ b/frontend/src/script_bridge.rs @@ -0,0 +1,70 @@ +use crate::STORE; +use wellen::GetItem; +use zoon::*; + +type FullVarName = String; + +#[wasm_bindgen( + inline_js = r#"export function strict_eval(code) { "use strict"; return eval?.(`${code}`) }"# +)] +extern "C" { + #[wasm_bindgen(catch)] + pub fn strict_eval(code: &str) -> Result; +} + +#[wasm_bindgen] +pub struct FW; + +#[wasm_bindgen] +impl FW { + /// JS: `FW.say_hello()` -> `Hello!` + pub fn say_hello() -> String { + "Hello!".to_owned() + } + + /// JS: `FW.clear_selected_vars()` -> `4` + pub fn clear_selected_vars() -> usize { + let mut vars = STORE.selected_var_refs.lock_mut(); + let var_count = vars.len(); + vars.clear(); + var_count + } + + /// JS: `FW.select_vars(["simple_tb.s.A", "simple_tb.s.B"])` -> `2` + pub fn select_vars(full_var_names: Vec) -> usize { + if let Some(hierarchy) = STORE.hierarchy.get_cloned() { + let mut new_var_refs = Vec::new(); + for full_var_name in full_var_names { + let path_with_name = full_var_name.split_terminator('.').collect::>(); + if let Some((name, path)) = path_with_name.split_last() { + if let Some(var_ref) = hierarchy.lookup_var(path, name) { + new_var_refs.push(var_ref); + } + } + } + let var_ref_count = new_var_refs.len(); + STORE.selected_var_refs.lock_mut().replace(new_var_refs); + return var_ref_count; + } + 0 + } + + /// JS: `FW.loaded_filename()` -> `simple.vcd` + pub fn loaded_filename() -> Option { + STORE.loaded_filename.get_cloned() + } + + /// JS: `FW.selected_vars()` -> `["simple_tb.s.A", "simple_tb.s.B"]` + pub fn selected_vars() -> Vec { + if let Some(hierarchy) = STORE.hierarchy.get_cloned() { + let mut full_var_names = Vec::new(); + for var_ref in STORE.selected_var_refs.lock_ref().as_slice() { + let var = hierarchy.get(*var_ref); + let var_name = var.full_name(&hierarchy); + full_var_names.push(var_name); + } + return full_var_names; + } + Vec::new() + } +} diff --git a/frontend/src/waveform_panel.rs b/frontend/src/waveform_panel.rs index db09a44..f9f42e6 100644 --- a/frontend/src/waveform_panel.rs +++ b/frontend/src/waveform_panel.rs @@ -1,5 +1,5 @@ -use crate::platform; -use std::rc::Rc; +use crate::{platform, script_bridge, Filename}; +use std::sync::Arc; use wellen::GetItem; use zoon::*; @@ -12,28 +12,209 @@ const ROW_GAP: u32 = 4; #[derive(Clone)] pub struct WaveformPanel { selected_var_refs: MutableVec, - hierarchy: Mutable>>, + hierarchy: Mutable>>, + loaded_filename: Mutable>, canvas_controller: Mutable>>, } impl WaveformPanel { pub fn new( - hierarchy: Mutable>>, + hierarchy: Mutable>>, selected_var_refs: MutableVec, + loaded_filename: Mutable>, ) -> impl Element { Self { selected_var_refs, hierarchy, + loaded_filename, canvas_controller: Mutable::new(Mutable::default().read_only()), } .root() } - // @TODO autoscroll down fn root(&self) -> impl Element { + Column::new() + .s(Padding::all(20)) + .s(Scrollbars::y_and_clip_x()) + .s(Width::fill()) + .s(Height::fill()) + .s(Gap::new().y(20)) + .item(self.selected_vars_controls()) + .item(self.vars_and_timelines_panel()) + } + + fn selected_vars_controls(&self) -> impl Element { + Row::new() + .s(Align::center()) + .s(Gap::new().x(20)) + .s(Width::fill()) + .item(Spacer::fill()) + .item(self.load_save_selected_vars_buttons()) + .item(self.keys_info()) + } + + fn keys_info(&self) -> impl Element { + El::new().s(Width::fill()).child( + Row::new() + .s(Align::new().center_x()) + .s(Gap::new().x(15)) + .item(El::new().s(Font::new().no_wrap()).child("Zoom: Wheel")) + .item( + El::new() + .s(Font::new().no_wrap()) + .child("Pan: Shift + Wheel"), + ), + ) + } + + fn load_save_selected_vars_buttons(&self) -> impl Element { + Row::new() + .s(Gap::new().x(20)) + .item(self.load_selected_vars_button()) + .item( + El::new() + .s(Font::new().no_wrap()) + .child("Selected Variables"), + ) + .item(self.save_selected_vars_button()) + } + + #[cfg(FASTWAVE_PLATFORM = "TAURI")] + fn load_selected_vars_button(&self) -> impl Element { + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + Button::new() + .s(Padding::new().x(20).y(10)) + .s(Background::new().color_signal( + hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + )) + .s(Align::new().left()) + .s(RoundedCorners::all(15)) + .label("Load") + .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) + .on_press(|| { + Task::start(async move { + if let Some(javascript_code) = + platform::load_file_with_selected_vars(None).await + { + match script_bridge::strict_eval(&javascript_code) { + Ok(js_value) => { + zoon::println!("File with selected vars loaded: {js_value:?}") + } + Err(js_value) => { + zoon::eprintln!( + "Failed to load file with selected vars: {js_value:?}" + ) + } + } + } + }) + }) + } + + #[cfg(FASTWAVE_PLATFORM = "BROWSER")] + fn load_selected_vars_button(&self) -> impl Element { + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + let file_input_id = "file_input_for_load_selected_vars_button"; + Row::new() + .item( + Label::new() + .s(Padding::new().x(20).y(10)) + .s(Background::new().color_signal( + hovered_signal + .map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + )) + .s(Align::new().left()) + .s(RoundedCorners::all(15)) + .s(Cursor::new(CursorIcon::Pointer)) + .label("Load") + .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) + .for_input(file_input_id), + ) + .item( + // @TODO https://github.com/MoonZoon/MoonZoon/issues/39 + // + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_hidden_file_input_elements_using_the_click_method + TextInput::new().id(file_input_id).update_raw_el(|raw_el| { + let dom_element = raw_el.dom_element(); + raw_el + .style("display", "none") + .attr("type", "file") + .event_handler(move |_: events::Input| { + let Some(file_list) = + dom_element.files().map(gloo_file::FileList::from) + else { + zoon::println!("file list is `None`"); + return; + }; + let Some(file) = file_list.first().cloned() else { + zoon::println!("file list is empty"); + return; + }; + Task::start(async move { + if let Some(javascript_code) = + platform::load_file_with_selected_vars(Some(file)).await + { + match script_bridge::strict_eval(&javascript_code) { + Ok(js_value) => zoon::println!( + "File with selected vars loaded: {js_value:?}" + ), + Err(js_value) => zoon::eprintln!( + "Failed to load file with selected vars: {js_value:?}" + ), + } + } + }) + }) + }), + ) + } + + fn save_selected_vars_button(&self) -> impl Element { + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + let loaded_filename = self.loaded_filename.clone(); + let selected_var_refs = self.selected_var_refs.clone(); + let hierarchy = self.hierarchy.clone(); + Button::new() + .s(Padding::new().x(20).y(10)) + .s(Background::new().color_signal( + hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + )) + .s(RoundedCorners::all(15)) + .label("Save") + .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) + .on_press(move || { + let loaded_filename = loaded_filename.get_cloned().unwrap_throw(); + let file_name = format!("{}_vars.fw.js", loaded_filename.replace('.', "_")); + + let hierarchy = hierarchy.get_cloned().unwrap_throw(); + let mut full_var_names = Vec::new(); + for var_ref in selected_var_refs.lock_ref().as_slice() { + let var = hierarchy.get(*var_ref); + let var_name = var.full_name(&hierarchy); + full_var_names.push(format!("\"{var_name}\"")); + } + let full_var_names_string = full_var_names.join(",\n\t\t"); + let file_content = include_str!("waveform_panel/template_vars.px.js") + .replacen("{LOADED_FILENAME}", &loaded_filename, 1) + .replacen("{FULL_VAR_NAMES}", &full_var_names_string, 1); + + // @TODO we need to use ugly code with temp anchor element until (if ever) + // `showSaveFilePicker` is supported in Safari and Firefox (https://caniuse.com/?search=showSaveFilePicker) + let file = gloo_file::File::new(&file_name, file_content.as_str()); + let file_object_url = gloo_file::ObjectUrl::from(file); + let a = document().create_element("a").unwrap_throw(); + a.set_attribute("href", &file_object_url).unwrap_throw(); + a.set_attribute("download", &file_name).unwrap_throw(); + a.set_attribute("style", "display: none;").unwrap_throw(); + dom::body().append_child(&a).unwrap_throw(); + a.unchecked_ref::().click(); + a.remove(); + }) + } + + // @TODO autoscroll down + fn vars_and_timelines_panel(&self) -> impl Element { let selected_vars_panel_height_getter: Mutable = <_>::default(); Row::new() - .s(Padding::all(20)) .s(Scrollbars::y_and_clip_x()) .s(Width::growable()) .s(Height::fill()) @@ -111,7 +292,7 @@ impl WaveformPanel { async fn push_var( controller: &PixiController, - hierarchy: &Mutable>>, + hierarchy: &Mutable>>, var_ref: wellen::VarRef, ) { let hierarchy = hierarchy.get_cloned().unwrap(); @@ -130,9 +311,8 @@ impl WaveformPanel { ) .await; - let timescale = hierarchy.timescale(); - // @TODO remove - zoon::println!("{timescale:?}"); + // @TODO render timeline with time units + // let timescale = hierarchy.timescale(); // Note: Sync `timeline`'s type with the `Timeline` in `frontend/typescript/pixi_canvas/pixi_canvas.ts' let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw(); @@ -172,6 +352,16 @@ impl WaveformPanel { .s(RoundedCorners::new().left(15).right(5)) .label( El::new() + .update_raw_el(|raw_el| { + raw_el + // @TODO move `title` to MZ API? (as `native_tooltip`?) + .attr("title", name) + // Note: `text-overflow` / ellipsis` doesn't work with flex and dynamic sizes + .style("text-overflow", "ellipsis") + .style("display", "inline-block") + }) + .s(Scrollbars::both().visible(false)) + .s(Width::default().max(400)) .s(Align::new().left()) .s(Padding::new().left(20).right(17).y(10)) .child(name), diff --git a/frontend/src/waveform_panel/pixi_canvas.rs b/frontend/src/waveform_panel/pixi_canvas.rs index 532eded..ffc8806 100644 --- a/frontend/src/waveform_panel/pixi_canvas.rs +++ b/frontend/src/waveform_panel/pixi_canvas.rs @@ -89,8 +89,10 @@ impl PixiCanvas { })) .update_raw_el(|raw_el| { // @TODO rewrite to a native Zoon API - raw_el.event_handler( + raw_el.event_handler_with_options( + EventOptions::new().preventable(), clone!((controller) move |event: events_extra::WheelEvent| { + event.prevent_default(); if let Some(controller) = controller.lock_ref().as_ref() { controller.zoom_or_pan( event.delta_y(), diff --git a/frontend/src/waveform_panel/template_vars.px.js b/frontend/src/waveform_panel/template_vars.px.js new file mode 100644 index 0000000..784460c --- /dev/null +++ b/frontend/src/waveform_panel/template_vars.px.js @@ -0,0 +1,5 @@ +if (FW.loaded_filename() === "{LOADED_FILENAME}") { + FW.select_vars([ + {FULL_VAR_NAMES} + ]) +} diff --git a/frontend/typescript/bundles/pixi_canvas.js b/frontend/typescript/bundles/pixi_canvas.js index d542b5e..674bb70 100644 --- a/frontend/typescript/bundles/pixi_canvas.js +++ b/frontend/typescript/bundles/pixi_canvas.js @@ -35316,10 +35316,12 @@ var VarSignalRow = class { this.draw(); } draw() { - if (this.app === null || this.app.screen === null) { + if (this?.app?.screen?.width === void 0) { return; } - this.row_container_background.width = this.app.screen.width; + if (this?.row_container_background?._texture?.orig?.width !== void 0) { + this.row_container_background.width = this.app.screen.width; + } this.signal_blocks_container.removeChildren(); this.timeline.blocks.forEach((timeline_block) => { const signal_block = new Container(); diff --git a/frontend/typescript/bundles/tauri_glue.js b/frontend/typescript/bundles/tauri_glue.js index a81bef0..026793f 100644 --- a/frontend/typescript/bundles/tauri_glue.js +++ b/frontend/typescript/bundles/tauri_glue.js @@ -2517,6 +2517,9 @@ async function show_window() { async function pick_and_load_waveform() { return await invoke2("pick_and_load_waveform"); } +async function load_file_with_selected_vars() { + return await invoke2("load_file_with_selected_vars"); +} async function get_hierarchy() { return await invoke2("get_hierarchy"); } @@ -2535,6 +2538,7 @@ async function unload_signal(signal_ref_index) { } export { get_hierarchy, + load_file_with_selected_vars, load_signal_and_get_timeline, pick_and_load_waveform, show_window, diff --git a/frontend/typescript/pixi_canvas/pixi_canvas.ts b/frontend/typescript/pixi_canvas/pixi_canvas.ts index 04e0935..39b896d 100644 --- a/frontend/typescript/pixi_canvas/pixi_canvas.ts +++ b/frontend/typescript/pixi_canvas/pixi_canvas.ts @@ -275,12 +275,14 @@ class VarSignalRow { draw() { // Screen can be null when we are, for instance, switching between miller columns and tree layout - // and then the canvas has to be recreated - if (this.app === null || this.app.screen === null) { + // and then the canvas has to be recreated. + if (this?.app?.screen?.width === undefined) { return; } - - this.row_container_background.width = this.app.screen.width; + // Workaround for "TypeError: Cannot read properties of null (reading 'orig')" + if (this?.row_container_background?._texture?.orig?.width !== undefined) { + this.row_container_background.width = this.app.screen.width; + } // @TODO optimize by reusing a pool of blocks instead or removing all children on every redraw? this.signal_blocks_container.removeChildren(); diff --git a/frontend/typescript/tauri_glue/tauri_glue.ts b/frontend/typescript/tauri_glue/tauri_glue.ts index 2945a41..a4be66c 100644 --- a/frontend/typescript/tauri_glue/tauri_glue.ts +++ b/frontend/typescript/tauri_glue/tauri_glue.ts @@ -5,6 +5,7 @@ import { core } from '@tauri-apps/api' const invoke = core.invoke; type Filename = string; +type JavascriptCode = string; type WellenHierarchy = unknown; type Timeline = unknown; type VarFormat = unknown; @@ -17,6 +18,10 @@ export async function pick_and_load_waveform(): Promise { return await invoke("pick_and_load_waveform"); } +export async function load_file_with_selected_vars(): Promise { + return await invoke("load_file_with_selected_vars"); +} + export async function get_hierarchy(): Promise { return await invoke("get_hierarchy"); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d985c1c..c8752db 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,8 +1,10 @@ +use std::fs; use std::sync::Mutex; use tauri_plugin_dialog::DialogExt; use wellen::simple::Waveform; type Filename = String; +type JavascriptCode = String; #[derive(Default)] struct Store { @@ -32,6 +34,18 @@ async fn pick_and_load_waveform( Ok(Some(file_response.name.unwrap())) } +#[tauri::command(rename_all = "snake_case")] +async fn load_file_with_selected_vars(app: tauri::AppHandle) -> Result, ()> { + let Some(file_response) = app.dialog().file().blocking_pick_file() else { + return Ok(None); + }; + // @TODO Tokio's `fs` or a Tauri `fs`? + let Ok(javascript_code) = fs::read_to_string(file_response.path) else { + panic!("Selected vars file reading failed") + }; + Ok(Some(javascript_code)) +} + #[tauri::command(rename_all = "snake_case")] async fn get_hierarchy(store: tauri::State<'_, Store>) -> Result { let waveform = store.waveform.lock().unwrap(); @@ -91,6 +105,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ show_window, pick_and_load_waveform, + load_file_with_selected_vars, get_hierarchy, load_signal_and_get_timeline, unload_signal, diff --git a/test_files/simple_vcd_vars.fw.js b/test_files/simple_vcd_vars.fw.js new file mode 100644 index 0000000..4552b94 --- /dev/null +++ b/test_files/simple_vcd_vars.fw.js @@ -0,0 +1,7 @@ +if (FW.loaded_filename() === "simple.vcd") { + FW.select_vars([ + "simple_tb.s.A", + "simple_tb.s.A", + "simple_tb.s.B" + ]) +} diff --git a/test_files/wave_27_fst_vars.fw.js b/test_files/wave_27_fst_vars.fw.js new file mode 100644 index 0000000..fde21b8 --- /dev/null +++ b/test_files/wave_27_fst_vars.fw.js @@ -0,0 +1,8 @@ +if (FW.loaded_filename() === "wave_27.fst") { + FW.select_vars([ + "TOP.LsuPlugin_logic_bus_rsp_payload_error", + "TOP.LsuPlugin_logic_bus_rsp_payload_data", + "TOP.VexiiRiscv.integer_RegFilePlugin_logic_regfile_fpga.ramAsyncMwMux_1.io_writes_0_payload_data", + "TOP.VexiiRiscv.EmbeddedRiscvJtag_logic_onDebugCd_dmiDirect_logic.logic_jtagLogic_dmiStat_value_string" + ]) +}