Compare commits

...

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 = 8080
# port = 8443 # port = 8443
https = false 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" backend_log_level = "warn" # "error" / "warn" / "info" / "debug" / "trace"
[redirect] [redirect]
@ -23,4 +25,6 @@ frontend = [
backend = [ backend = [
"backend/Cargo.toml", "backend/Cargo.toml",
"backend/src", "backend/src",
"backend/index.js",
"backend/style.css",
] ]

View file

@ -23,6 +23,16 @@
Zoom and all formats Zoom and all formats
</p> </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: ### 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::*; use moon::*;
async fn frontend() -> Frontend { async fn frontend() -> Frontend {
Frontend::new().title("FastWave").append_to_head(concat!( Frontend::new()
"<style>", .title("FastWave")
include_str!("../style.css"), .append_to_head(concat!("<style>", include_str!("../style.css"), "</style>"))
"</style>" .append_to_head(concat!(
)) "<script type=\"module\">",
include_str!("../index.js"),
"</script>"
))
} }
async fn up_msg_handler(_: UpMsgRequest<()>) {} 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::cell::Cell;
use std::mem; use std::mem;
use std::ops::Not; use std::ops::Not;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use wellen::GetItem; use wellen::GetItem;
use zoon::*; 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; const MILLER_COLUMN_MAX_HEIGHT: u32 = 500;
const TREE_MAX_WIDTH: u32 = 600;
type Filename = String;
#[derive(Clone)] #[derive(Clone)]
struct VarForUI { struct VarForUI {
@ -34,7 +34,7 @@ struct ScopeForUI {
#[derive(Clone)] #[derive(Clone)]
pub struct ControlsPanel { pub struct ControlsPanel {
selected_scope_ref: Mutable<Option<wellen::ScopeRef>>, 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>, selected_var_refs: MutableVec<wellen::VarRef>,
layout: Mutable<Layout>, layout: Mutable<Layout>,
loaded_filename: Mutable<Option<Filename>>, loaded_filename: Mutable<Option<Filename>>,
@ -42,16 +42,17 @@ pub struct ControlsPanel {
impl ControlsPanel { impl ControlsPanel {
pub fn new( pub fn new(
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>, hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
selected_var_refs: MutableVec<wellen::VarRef>, selected_var_refs: MutableVec<wellen::VarRef>,
layout: Mutable<Layout>, layout: Mutable<Layout>,
loaded_filename: Mutable<Option<Filename>>,
) -> impl Element { ) -> impl Element {
Self { Self {
selected_scope_ref: <_>::default(), selected_scope_ref: <_>::default(),
hierarchy, hierarchy,
selected_var_refs, selected_var_refs,
layout, layout,
loaded_filename: <_>::default(), loaded_filename,
} }
.root() .root()
} }
@ -92,6 +93,12 @@ impl ControlsPanel {
.map(|layout| matches!(layout, Layout::Columns)) .map(|layout| matches!(layout, Layout::Columns))
.map_true(|| Width::fill()), .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( .s(Height::with_signal_self(layout.signal().map(
move |layout| match layout { move |layout| match layout {
Layout::Tree => Height::fill(), Layout::Tree => Height::fill(),
@ -101,13 +108,6 @@ impl ControlsPanel {
.s(Padding::all(20)) .s(Padding::all(20))
.s(Gap::new().y(40)) .s(Gap::new().y(40))
.s(Align::new().top()) .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( .item_signal(
self.hierarchy self.hierarchy
.signal_cloned() .signal_cloned()
@ -122,143 +122,9 @@ impl ControlsPanel {
)) ))
} }
#[cfg(FASTWAVE_PLATFORM = "TAURI")] fn scopes_panel(&self, hierarchy: Arc<wellen::Hierarchy>) -> impl Element {
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 {
Column::new() Column::new()
.s(Height::fill().min(150)) .s(Height::fill())
.s(Scrollbars::y_and_clip_x()) .s(Scrollbars::y_and_clip_x())
.s(Gap::new().y(20)) .s(Gap::new().y(20))
.s(Width::fill()) .s(Width::fill())
@ -271,7 +137,7 @@ impl ControlsPanel {
.item(self.scopes_list(hierarchy)) .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 layout = self.layout.clone();
let mut scopes_for_ui = Vec::new(); let mut scopes_for_ui = Vec::new();
let mut max_level_index: usize = 0; let mut max_level_index: usize = 0;
@ -307,7 +173,7 @@ impl ControlsPanel {
let s = self.clone(); let s = self.clone();
El::new() El::new()
.s(Height::fill()) .s(Height::fill())
.s(Scrollbars::both()) .s(Scrollbars::y_and_clip_x())
.s(Width::fill()) .s(Width::fill())
.child_signal(layout.signal().map(move |layout| match layout { .child_signal(layout.signal().map(move |layout| match layout {
Layout::Tree => { Layout::Tree => {
@ -421,7 +287,6 @@ impl ControlsPanel {
Layout::Tree => level * 30, Layout::Tree => level * 30,
Layout::Columns => 0, Layout::Columns => 0,
}))) })))
.s(Width::default().max(SCOPE_VAR_ROW_MAX_WIDTH))
.after_remove(move |_| { .after_remove(move |_| {
drop(task_collapse_on_parent_collapse); drop(task_collapse_on_parent_collapse);
drop(task_expand_or_collapse_on_selected_scope_in_level_change); drop(task_expand_or_collapse_on_selected_scope_in_level_change);
@ -512,7 +377,6 @@ impl ControlsPanel {
) -> impl Element { ) -> impl Element {
Button::new() Button::new()
.s(Padding::new().x(15).y(5)) .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_hovered_change(move |is_hovered| button_hovered.set_neq(is_hovered))
.on_press( .on_press(
clone!((self.selected_scope_ref => selected_scope_ref, scope_for_ui) move || { clone!((self.selected_scope_ref => selected_scope_ref, scope_for_ui) move || {
@ -523,12 +387,14 @@ impl ControlsPanel {
.label(scope_for_ui.name) .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(); let selected_scope_ref = self.selected_scope_ref.clone();
Column::new() Column::new()
.s(Align::new().top())
.s(Gap::new().y(20)) .s(Gap::new().y(20))
.s(Height::fill().min(150)) .s(Height::fill().min(150))
.s(Scrollbars::y_and_clip_x()) .s(Width::fill())
.s(Scrollbars::both())
.item_signal( .item_signal(
self.layout self.layout
.signal() .signal()
@ -544,7 +410,7 @@ impl ControlsPanel {
fn vars_list( fn vars_list(
&self, &self,
selected_scope_ref: wellen::ScopeRef, selected_scope_ref: wellen::ScopeRef,
hierarchy: Rc<wellen::Hierarchy>, hierarchy: Arc<wellen::Hierarchy>,
) -> impl Element { ) -> impl Element {
let vars_for_ui = hierarchy let vars_for_ui = hierarchy
.get(selected_scope_ref) .get(selected_scope_ref)
@ -583,17 +449,18 @@ impl ControlsPanel {
} }
})); }));
let layout = self.layout.clone();
Column::new() Column::new()
.s(Width::with_signal_self( .s(Width::with_signal_self(layout.signal().map(
self.layout move |layout| match layout {
.signal() Layout::Tree => Width::fill(),
.map(|layout| matches!(layout, Layout::Columns)) Layout::Columns => Width::default().min(MILLER_COLUMN_SCOPE_VAR_ROW_MIN_WIDTH),
.map_true(|| Width::default().min(SCOPE_VAR_ROW_MAX_WIDTH)), },
)) )))
.s(Align::new().left()) .s(Align::new().left())
.s(Gap::new().y(10)) .s(Gap::new().y(10))
.s(Height::fill()) .s(Height::fill())
.s(Scrollbars::y_and_clip_x()) .s(Scrollbars::both())
.items_signal_vec( .items_signal_vec(
vars_for_ui_mutable_vec vars_for_ui_mutable_vec
.signal_vec_cloned() .signal_vec_cloned()
@ -605,13 +472,13 @@ impl ControlsPanel {
fn var_row(&self, var_for_ui: VarForUI) -> impl Element { fn var_row(&self, var_for_ui: VarForUI) -> impl Element {
Row::new() Row::new()
.s(Gap::new().x(10)) .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_button(var_for_ui.clone()))
.item(self.var_tag_type(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_index(var_for_ui.clone()))
.item(self.var_tag_bit(var_for_ui.clone())) .item(self.var_tag_bit(var_for_ui.clone()))
.item(self.var_tag_direction(var_for_ui)) .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 { 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(); let selected_var_ref = self.selected_var_refs.clone();
El::new().child( El::new().child(
Button::new() Button::new()
.s(Font::new().wrap_anywhere())
.s(Padding::new().x(15).y(5)) .s(Padding::new().x(15).y(5))
.s(Background::new().color_signal( .s(Background::new().color_signal(
hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), 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::*; use zoon::*;
mod platform; mod platform;
mod script_bridge;
mod controls_panel; mod controls_panel;
use controls_panel::ControlsPanel; use controls_panel::ControlsPanel;
@ -9,6 +10,9 @@ use controls_panel::ControlsPanel;
mod waveform_panel; mod waveform_panel;
use waveform_panel::WaveformPanel; use waveform_panel::WaveformPanel;
mod header_panel;
use header_panel::HeaderPanel;
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
enum Layout { enum Layout {
Tree, Tree,
@ -16,6 +20,17 @@ enum Layout {
Columns, 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() { fn main() {
start_app("app", root); start_app("app", root);
Task::start(async { Task::start(async {
@ -26,38 +41,56 @@ fn main() {
} }
fn root() -> impl Element { fn root() -> impl Element {
let hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>> = <_>::default(); let hierarchy = STORE.hierarchy.clone();
let selected_var_refs: MutableVec<wellen::VarRef> = <_>::default(); let selected_var_refs = STORE.selected_var_refs.clone();
let layout: Mutable<Layout> = <_>::default(); let layout: Mutable<Layout> = <_>::default();
let loaded_filename = STORE.loaded_filename.clone();
Column::new() Column::new()
.s(Height::fill()) .s(Height::fill())
.s(Scrollbars::y_and_clip_x()) .s(Scrollbars::y_and_clip_x())
.s(Font::new().color(color!("Lavender"))) .s(Font::new().color(color!("Lavender")))
.item(HeaderPanel::new(
hierarchy.clone(),
layout.clone(),
loaded_filename.clone(),
))
.item( .item(
Row::new() Row::new()
.s(Height::fill()) .s(Scrollbars::y_and_clip_x())
.s(Gap::new().x(15)) .s(Gap::new().x(15))
.s(Height::growable().min(150))
.item(ControlsPanel::new( .item(ControlsPanel::new(
hierarchy.clone(), hierarchy.clone(),
selected_var_refs.clone(), selected_var_refs.clone(),
layout.clone(), layout.clone(),
loaded_filename.clone(),
)) ))
.item_signal( .item_signal({
layout let hierarchy = hierarchy.clone();
.signal() let selected_var_refs = selected_var_refs.clone();
.map(|layout| matches!(layout, Layout::Tree)) let loaded_filename = loaded_filename.clone();
.map_true( map_ref!{
clone!((hierarchy, selected_var_refs) move || WaveformPanel::new( 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(), hierarchy.clone(),
selected_var_refs.clone(), selected_var_refs.clone(),
)), loaded_filename.clone(),
), )))
), }
}
}),
) )
.item_signal( .item_signal(
layout map_ref!{
.signal() let layout = layout.signal(),
.map(|layout| matches!(layout, Layout::Columns)) let hierarchy_is_some = hierarchy.signal_ref(Option::is_some) => {
.map_true(move || WaveformPanel::new(hierarchy.clone(), selected_var_refs.clone())), (*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; use browser as platform;
type Filename = String; type Filename = String;
type JavascriptCode = String;
pub async fn show_window() { pub async fn show_window() {
platform::show_window().await 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 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 { pub async fn get_hierarchy() -> wellen::Hierarchy {
platform::get_hierarchy().await platform::get_hierarchy().await
} }

View file

@ -4,11 +4,11 @@ use wellen::simple::Waveform;
use zoon::*; use zoon::*;
#[derive(Default)] #[derive(Default)]
struct Store { struct BrowserPlatformStore {
waveform: Mutex<Option<Waveform>>, waveform: Mutex<Option<Waveform>>,
} }
static STORE: Lazy<Store> = lazy::default(); static BROWSER_PLATFORM_STORE: Lazy<BrowserPlatformStore> = lazy::default();
pub(super) async fn show_window() {} pub(super) async fn show_window() {}
@ -25,7 +25,7 @@ pub(super) async fn pick_and_load_waveform(
let Ok(waveform) = waveform else { let Ok(waveform) = waveform else {
panic!("Waveform file reading failed") panic!("Waveform file reading failed")
}; };
*STORE.waveform.lock().unwrap_throw() = Some(waveform); *BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw() = Some(waveform);
Some(file.name()) Some(file.name())
} }
@ -63,12 +63,28 @@ pub(super) async fn pick_and_load_waveform(
// let Ok(waveform) = waveform else { // let Ok(waveform) = waveform else {
// panic!("Waveform file reading failed") // panic!("Waveform file reading failed")
// }; // };
// *STORE.waveform.lock().unwrap_throw() = Some(waveform); // *BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw() = Some(waveform);
// Some(file.name()) // 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 { 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(); let hierarchy = waveform.as_ref().unwrap_throw().hierarchy();
// @TODO Wrap `hierarchy` in `Waveform` with `Rc/Arc` or add the method `take` / `clone` or refactor? // @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() 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, block_height: u32,
var_format: shared::VarFormat, var_format: shared::VarFormat,
) -> shared::Timeline { ) -> 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(); let waveform = waveform_lock.as_mut().unwrap();
waveform.load_signals_multi_threaded(&[signal_ref]); waveform.load_signals_multi_threaded(&[signal_ref]);
let signal = waveform.get_signal(signal_ref).unwrap(); 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) { 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(); let waveform = waveform_lock.as_mut().unwrap_throw();
waveform.unload_signals(&[signal_ref]); waveform.unload_signals(&[signal_ref]);
} }

View file

@ -13,6 +13,15 @@ pub(super) async fn pick_and_load_waveform(
.as_string() .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 { pub(super) async fn get_hierarchy() -> wellen::Hierarchy {
serde_wasm_bindgen::from_value(tauri_glue::get_hierarchy().await.unwrap_throw()).unwrap_throw() serde_wasm_bindgen::from_value(tauri_glue::get_hierarchy().await.unwrap_throw()).unwrap_throw()
} }
@ -59,6 +68,9 @@ mod tauri_glue {
#[wasm_bindgen(catch)] #[wasm_bindgen(catch)]
pub async fn pick_and_load_waveform() -> Result<JsValue, JsValue>; 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)] #[wasm_bindgen(catch)]
pub async fn get_hierarchy() -> Result<JsValue, JsValue>; 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 crate::{platform, script_bridge, Filename};
use std::rc::Rc; use std::sync::Arc;
use wellen::GetItem; use wellen::GetItem;
use zoon::*; use zoon::*;
@ -12,28 +12,209 @@ const ROW_GAP: u32 = 4;
#[derive(Clone)] #[derive(Clone)]
pub struct WaveformPanel { pub struct WaveformPanel {
selected_var_refs: MutableVec<wellen::VarRef>, 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>>>, canvas_controller: Mutable<ReadOnlyMutable<Option<PixiController>>>,
} }
impl WaveformPanel { impl WaveformPanel {
pub fn new( pub fn new(
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>, hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
selected_var_refs: MutableVec<wellen::VarRef>, selected_var_refs: MutableVec<wellen::VarRef>,
loaded_filename: Mutable<Option<Filename>>,
) -> impl Element { ) -> impl Element {
Self { Self {
selected_var_refs, selected_var_refs,
hierarchy, hierarchy,
loaded_filename,
canvas_controller: Mutable::new(Mutable::default().read_only()), canvas_controller: Mutable::new(Mutable::default().read_only()),
} }
.root() .root()
} }
// @TODO autoscroll down
fn root(&self) -> impl Element { 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(); let selected_vars_panel_height_getter: Mutable<u32> = <_>::default();
Row::new() Row::new()
.s(Padding::all(20))
.s(Scrollbars::y_and_clip_x()) .s(Scrollbars::y_and_clip_x())
.s(Width::growable()) .s(Width::growable())
.s(Height::fill()) .s(Height::fill())
@ -111,7 +292,7 @@ impl WaveformPanel {
async fn push_var( async fn push_var(
controller: &PixiController, controller: &PixiController,
hierarchy: &Mutable<Option<Rc<wellen::Hierarchy>>>, hierarchy: &Mutable<Option<Arc<wellen::Hierarchy>>>,
var_ref: wellen::VarRef, var_ref: wellen::VarRef,
) { ) {
let hierarchy = hierarchy.get_cloned().unwrap(); let hierarchy = hierarchy.get_cloned().unwrap();
@ -130,9 +311,8 @@ impl WaveformPanel {
) )
.await; .await;
let timescale = hierarchy.timescale(); // @TODO render timeline with time units
// @TODO remove // let timescale = hierarchy.timescale();
zoon::println!("{timescale:?}");
// Note: Sync `timeline`'s type with the `Timeline` in `frontend/typescript/pixi_canvas/pixi_canvas.ts' // 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(); let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw();
@ -172,6 +352,16 @@ impl WaveformPanel {
.s(RoundedCorners::new().left(15).right(5)) .s(RoundedCorners::new().left(15).right(5))
.label( .label(
El::new() 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(Align::new().left())
.s(Padding::new().left(20).right(17).y(10)) .s(Padding::new().left(20).right(17).y(10))
.child(name), .child(name),

View file

@ -89,8 +89,10 @@ impl PixiCanvas {
})) }))
.update_raw_el(|raw_el| { .update_raw_el(|raw_el| {
// @TODO rewrite to a native Zoon API // @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| { clone!((controller) move |event: events_extra::WheelEvent| {
event.prevent_default();
if let Some(controller) = controller.lock_ref().as_ref() { if let Some(controller) = controller.lock_ref().as_ref() {
controller.zoom_or_pan( controller.zoom_or_pan(
event.delta_y(), 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(); this.draw();
} }
draw() { draw() {
if (this.app === null || this.app.screen === null) { if (this?.app?.screen?.width === void 0) {
return; 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.signal_blocks_container.removeChildren();
this.timeline.blocks.forEach((timeline_block) => { this.timeline.blocks.forEach((timeline_block) => {
const signal_block = new Container(); const signal_block = new Container();

View file

@ -2517,6 +2517,9 @@ async function show_window() {
async function pick_and_load_waveform() { async function pick_and_load_waveform() {
return await invoke2("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() { async function get_hierarchy() {
return await invoke2("get_hierarchy"); return await invoke2("get_hierarchy");
} }
@ -2535,6 +2538,7 @@ async function unload_signal(signal_ref_index) {
} }
export { export {
get_hierarchy, get_hierarchy,
load_file_with_selected_vars,
load_signal_and_get_timeline, load_signal_and_get_timeline,
pick_and_load_waveform, pick_and_load_waveform,
show_window, show_window,

View file

@ -275,12 +275,14 @@ class VarSignalRow {
draw() { draw() {
// Screen can be null when we are, for instance, switching between miller columns and tree layout // Screen can be null when we are, for instance, switching between miller columns and tree layout
// and then the canvas has to be recreated // and then the canvas has to be recreated.
if (this.app === null || this.app.screen === null) { if (this?.app?.screen?.width === undefined) {
return; return;
} }
// Workaround for "TypeError: Cannot read properties of null (reading 'orig')"
this.row_container_background.width = this.app.screen.width; 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? // @TODO optimize by reusing a pool of blocks instead or removing all children on every redraw?
this.signal_blocks_container.removeChildren(); this.signal_blocks_container.removeChildren();

View file

@ -5,6 +5,7 @@ import { core } from '@tauri-apps/api'
const invoke = core.invoke; const invoke = core.invoke;
type Filename = string; type Filename = string;
type JavascriptCode = string;
type WellenHierarchy = unknown; type WellenHierarchy = unknown;
type Timeline = unknown; type Timeline = unknown;
type VarFormat = 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"); 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> { export async function get_hierarchy(): Promise<WellenHierarchy> {
return await invoke("get_hierarchy"); return await invoke("get_hierarchy");
} }

View file

@ -1,8 +1,10 @@
use std::fs;
use std::sync::Mutex; use std::sync::Mutex;
use tauri_plugin_dialog::DialogExt; use tauri_plugin_dialog::DialogExt;
use wellen::simple::Waveform; use wellen::simple::Waveform;
type Filename = String; type Filename = String;
type JavascriptCode = String;
#[derive(Default)] #[derive(Default)]
struct Store { struct Store {
@ -32,6 +34,18 @@ async fn pick_and_load_waveform(
Ok(Some(file_response.name.unwrap())) 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")] #[tauri::command(rename_all = "snake_case")]
async fn get_hierarchy(store: tauri::State<'_, Store>) -> Result<serde_json::Value, ()> { async fn get_hierarchy(store: tauri::State<'_, Store>) -> Result<serde_json::Value, ()> {
let waveform = store.waveform.lock().unwrap(); let waveform = store.waveform.lock().unwrap();
@ -91,6 +105,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
show_window, show_window,
pick_and_load_waveform, pick_and_load_waveform,
load_file_with_selected_vars,
get_hierarchy, get_hierarchy,
load_signal_and_get_timeline, load_signal_and_get_timeline,
unload_signal, 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"
])
}