Scripting, save & reload vars, layout improvements #3
|
@ -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",
|
||||
]
|
||||
|
|
10
README.md
10
README.md
|
@ -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
2
backend/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { FW } from '/_api/pkg/frontend.js';
|
||||
window.FW = FW;
|
|
@ -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<()>) {}
|
||||
|
|
BIN
docs/video_javascript_commands.gif
Normal file
BIN
docs/video_javascript_commands.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 593 KiB |
BIN
docs/video_load_save_selected_vars.gif
Normal file
BIN
docs/video_load_save_selected_vars.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
|
@ -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")),
|
||||
|
|
303
frontend/src/header_panel.rs
Normal file
303
frontend/src/header_panel.rs
Normal 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(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
70
frontend/src/script_bridge.rs
Normal file
70
frontend/src/script_bridge.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
5
frontend/src/waveform_panel/template_vars.px.js
Normal file
5
frontend/src/waveform_panel/template_vars.px.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
if (FW.loaded_filename() === "{LOADED_FILENAME}") {
|
||||
FW.select_vars([
|
||||
{FULL_VAR_NAMES}
|
||||
])
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
7
test_files/simple_vcd_vars.fw.js
Normal file
7
test_files/simple_vcd_vars.fw.js
Normal 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"
|
||||
])
|
||||
}
|
8
test_files/wave_27_fst_vars.fw.js
Normal file
8
test_files/wave_27_fst_vars.fw.js
Normal 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"
|
||||
])
|
||||
}
|
Loading…
Reference in a new issue