Compare commits

...
This repository has been archived on 2025-05-11. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

17 commits

Author SHA1 Message Date
Martin Kavík
3b12e0d22d video_load_save_selected_vars.gif, video_javascript_commands.gif 2024-06-19 13:26:47 +02:00
Martin Kavík
15c6dd58c7 save selected vars 2024-06-19 01:43:31 +02:00
Martin Kavík
1160efe0e3 platform::load_file_with_selected_vars, simple_vcd.fw.js 2024-06-18 22:42:11 +02:00
Martin Kavík
26a8250220 automatically hide WaveformPanel 2024-06-18 19:56:03 +02:00
Martin Kavík
d30b0d7782 keys info 2024-06-18 17:59:19 +02:00
Martin Kavík
f08b9565a4 var name ellipsis 2024-06-18 17:24:46 +02:00
Martin Kavík
793ad37457 selected_vars_controls, TREE_MAX_WIDTH 2024-06-18 15:57:14 +02:00
Martin Kavík
29bdb2f833 commands vertical resize 2024-06-17 19:53:56 +02:00
Martin Kavík
24d2c0b2dc loaded_filename 2024-06-17 19:36:44 +02:00
Martin Kavík
0a4e84d3d2 command panel layout improvements 2024-06-17 18:50:09 +02:00
Martin Kavík
b55d876fd2 clear_selected_vars, select_vars, selected_vars 2024-06-17 18:40:22 +02:00
Martin Kavík
b0af834166 fmt 2024-06-17 11:36:00 +02:00
Martin Kavík
8bb9f76a78 layout fixes 2024-06-17 11:35:13 +02:00
Martin Kavík
d259432486 header_panel.rs 2024-06-17 10:15:05 +02:00
Martin Kavík
f7df478154 mouse wheel prevent default, commands for miller columns 2024-06-17 09:12:01 +02:00
Martin Kavík
de0f4d8750 clear_variables 2024-06-17 07:48:15 +02:00
Martin Kavík
1312c30c72 script_bridge.rs, index.js 2024-06-16 01:20:12 +02:00
23 changed files with 780 additions and 214 deletions

View file

@ -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",
]

View file

@ -23,6 +23,16 @@
Zoom and all formats
</p>
<p align="center">
<img width="800" src="docs/video_javascript_commands.gif" alt="Fastwave - Javascript commands" />
Javascript commands
</p>
<p align="center">
<img width="800" src="docs/video_load_save_selected_vars.gif" alt="Fastwave - Load and save selected variables" />
Load and save selected variables
</p>
---
### Install requirements:

2
backend/index.js Normal file
View file

@ -0,0 +1,2 @@
import { FW } from '/_api/pkg/frontend.js';
window.FW = FW;

View file

@ -1,11 +1,14 @@
use moon::*;
async fn frontend() -> Frontend {
Frontend::new().title("FastWave").append_to_head(concat!(
"<style>",
include_str!("../style.css"),
"</style>"
))
Frontend::new()
.title("FastWave")
.append_to_head(concat!("<style>", include_str!("../style.css"), "</style>"))
.append_to_head(concat!(
"<script type=\"module\">",
include_str!("../index.js"),
"</script>"
))
}
async fn up_msg_handler(_: UpMsgRequest<()>) {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -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<Option<wellen::ScopeRef>>,
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
selected_var_refs: MutableVec<wellen::VarRef>,
layout: Mutable<Layout>,
loaded_filename: Mutable<Option<Filename>>,
@ -42,16 +42,17 @@ pub struct ControlsPanel {
impl ControlsPanel {
pub fn new(
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
selected_var_refs: MutableVec<wellen::VarRef>,
layout: Mutable<Layout>,
loaded_filename: Mutable<Option<Filename>>,
) -> 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<wellen::Hierarchy>) -> impl Element {
fn scopes_panel(&self, hierarchy: Arc<wellen::Hierarchy>) -> 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<wellen::Hierarchy>) -> impl Element {
fn scopes_list(&self, hierarchy: Arc<wellen::Hierarchy>) -> 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<wellen::Hierarchy>) -> impl Element {
fn vars_panel(&self, hierarchy: Arc<wellen::Hierarchy>) -> 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<wellen::Hierarchy>,
hierarchy: Arc<wellen::Hierarchy>,
) -> 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")),

View file

@ -0,0 +1,303 @@
use crate::{platform, script_bridge, Filename, Layout};
use std::sync::Arc;
use zoon::*;
pub struct HeaderPanel {
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
layout: Mutable<Layout>,
loaded_filename: Mutable<Option<Filename>>,
}
impl HeaderPanel {
pub fn new(
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
layout: Mutable<Layout>,
loaded_filename: Mutable<Option<Filename>>,
) -> 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<Option<Result<JsValue, JsValue>>> = <_>::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<Option<Result<JsValue, JsValue>>>,
) -> 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<Option<Result<JsValue, JsValue>>>,
) -> 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<Option<Result<JsValue, JsValue>>>,
) -> 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<Option<Result<JsValue, JsValue>>>,
) -> 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(),
}
}))
}
}

View file

@ -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<wellen::VarRef>,
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
loaded_filename: Mutable<Option<Filename>>,
}
static STORE: Lazy<Store> = lazy::default();
fn main() {
start_app("app", root);
Task::start(async {
@ -26,38 +41,56 @@ fn main() {
}
fn root() -> impl Element {
let hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>> = <_>::default();
let selected_var_refs: MutableVec<wellen::VarRef> = <_>::default();
let hierarchy = STORE.hierarchy.clone();
let selected_var_refs = STORE.selected_var_refs.clone();
let layout: Mutable<Layout> = <_>::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(),
)))
}
}
)
}

View file

@ -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<gloo_file::File>) -> Option<Fil
platform::pick_and_load_waveform(file).await
}
// @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<gloo_file::File>) -> Option<JavascriptCode> {
platform::load_file_with_selected_vars(file).await
}
pub async fn get_hierarchy() -> wellen::Hierarchy {
platform::get_hierarchy().await
}

View file

@ -4,11 +4,11 @@ use wellen::simple::Waveform;
use zoon::*;
#[derive(Default)]
struct Store {
struct BrowserPlatformStore {
waveform: Mutex<Option<Waveform>>,
}
static STORE: Lazy<Store> = lazy::default();
static BROWSER_PLATFORM_STORE: Lazy<BrowserPlatformStore> = 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<gloo_file::File>,
) -> Option<super::JavascriptCode> {
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]);
}

View file

@ -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<gloo_file::File>,
) -> Option<super::JavascriptCode> {
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<JsValue, JsValue>;
#[wasm_bindgen(catch)]
pub async fn load_file_with_selected_vars() -> Result<JsValue, JsValue>;
#[wasm_bindgen(catch)]
pub async fn get_hierarchy() -> Result<JsValue, JsValue>;

View file

@ -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<JsValue, JsValue>;
}
#[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<FullVarName>) -> 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::<Vec<_>>();
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<String> {
STORE.loaded_filename.get_cloned()
}
/// JS: `FW.selected_vars()` -> `["simple_tb.s.A", "simple_tb.s.B"]`
pub fn selected_vars() -> Vec<FullVarName> {
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()
}
}

View file

@ -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<wellen::VarRef>,
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
loaded_filename: Mutable<Option<Filename>>,
canvas_controller: Mutable<ReadOnlyMutable<Option<PixiController>>>,
}
impl WaveformPanel {
pub fn new(
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
selected_var_refs: MutableVec<wellen::VarRef>,
loaded_filename: Mutable<Option<Filename>>,
) -> 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::<web_sys::HtmlElement>().click();
a.remove();
})
}
// @TODO autoscroll down
fn vars_and_timelines_panel(&self) -> impl Element {
let selected_vars_panel_height_getter: Mutable<u32> = <_>::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<Option<Rc<wellen::Hierarchy>>>,
hierarchy: &Mutable<Option<Arc<wellen::Hierarchy>>>,
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),

View file

@ -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(),

View file

@ -0,0 +1,5 @@
if (FW.loaded_filename() === "{LOADED_FILENAME}") {
FW.select_vars([
{FULL_VAR_NAMES}
])
}

View file

@ -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();

View file

@ -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,

View file

@ -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();

View file

@ -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<Filename | undefined> {
return await invoke("pick_and_load_waveform");
}
export async function load_file_with_selected_vars(): Promise<JavascriptCode | undefined> {
return await invoke("load_file_with_selected_vars");
}
export async function get_hierarchy(): Promise<WellenHierarchy> {
return await invoke("get_hierarchy");
}

View file

@ -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<Option<JavascriptCode>, ()> {
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<serde_json::Value, ()> {
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,

View file

@ -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"
])
}

View file

@ -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"
])
}