Compare commits
23 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
09eab06e4e | ||
![]() |
f324b57f71 | ||
![]() |
7a43a9ae4f | ||
![]() |
547539db9f | ||
![]() |
9b76ecf38f | ||
![]() |
af90adfe20 | ||
![]() |
1ed1661b16 | ||
![]() |
6926b0176f | ||
![]() |
d09caf835c | ||
![]() |
6859c726f5 | ||
![]() |
10425580dd | ||
![]() |
aea55fa9e3 | ||
![]() |
b308fa85fb | ||
![]() |
f6d8f9d8ce | ||
![]() |
869a31ca5f | ||
![]() |
8e285c7b5e | ||
![]() |
8ded9a11f6 | ||
![]() |
26565dd684 | ||
![]() |
29560ae616 | ||
![]() |
d3cc0eb860 | ||
![]() |
aeb95b982f | ||
![]() |
a37675b094 | ||
![]() |
6c4845b81d |
|
@ -1,2 +1,3 @@
|
|||
/*
|
||||
!/src-tauri
|
||||
!/shared
|
||||
|
|
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -859,6 +859,12 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert-base"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e56a404f2112d00ba80a516b60ca3744ef6bd776baa14126eb7d7c11b42cac8a"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
|
@ -1538,9 +1544,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.30"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
@ -1568,9 +1574,9 @@ checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
|||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.30"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
||||
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
|
@ -4124,6 +4130,9 @@ dependencies = [
|
|||
name = "shared"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"convert-base",
|
||||
"futures-util",
|
||||
"moonlight",
|
||||
"wellen",
|
||||
]
|
||||
|
||||
|
@ -5038,9 +5047,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tray-icon"
|
||||
version = "0.14.2"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b27516dfcfa22a9faaf192283a122bfbede38c1e59ef194e3c4db6549b419c0"
|
||||
checksum = "3ad8319cca93189ea9ab1b290de0595960529750b6b8b501a399ed1ec3775d60"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"core-graphics",
|
||||
|
|
|
@ -16,11 +16,12 @@ readme = "../README.md"
|
|||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
shared = { path = "./shared" }
|
||||
# wellen = { version = "0.9.9", features = ["serde1"] }
|
||||
# wellen = { path = "../wellen/wellen", features = ["serde1"] }
|
||||
wellen = { git = "https://github.com/MartinKavik/wellen", features = ["serde1"], branch = "new_pub_types" }
|
||||
# moon = { path = "../../crates/moon" }
|
||||
# zoon = { path = "../../crates/zoon" }
|
||||
# moonlight = { path = "../../crates/zoon" }
|
||||
zoon = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" }
|
||||
moon = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" }
|
||||
moonlight = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" }
|
||||
|
|
|
@ -38,6 +38,15 @@ run_task = { fork = true, parallel = true, name = [
|
|||
"watch_tauri_glue",
|
||||
]}
|
||||
|
||||
[tasks.start_browser_release]
|
||||
description = "Run without Tauri in the browser & watch Typescript and Rust in the release mode"
|
||||
dependencies = ["store_current_process_id"]
|
||||
run_task = { fork = true, parallel = true, name = [
|
||||
"mzoon_start_release_with_cleanup",
|
||||
"watch_pixi_canvas",
|
||||
"watch_tauri_glue",
|
||||
]}
|
||||
|
||||
[tasks.bundle]
|
||||
description = "Compile in the release mode and create installation packages"
|
||||
dependencies = ["tauri_build", "show_release_paths"]
|
||||
|
@ -100,6 +109,11 @@ description = "Run `mzoon start`"
|
|||
extend = "mzoon"
|
||||
args = ["start"]
|
||||
|
||||
[tasks.mzoon_start_release]
|
||||
description = "Run `mzoon start --release`"
|
||||
extend = "mzoon"
|
||||
args = ["start", "--release"]
|
||||
|
||||
[tasks.tauri_dev_with_cleanup]
|
||||
description = "Run forked `tauri dev` with cleanup"
|
||||
run_task = { fork = true, cleanup_task = "kill_watchers", name = ["tauri_dev"] }
|
||||
|
@ -108,6 +122,10 @@ run_task = { fork = true, cleanup_task = "kill_watchers", name = ["tauri_dev"] }
|
|||
description = "Run forked `mzoon start` with cleanup"
|
||||
run_task = { fork = true, cleanup_task = "kill_watchers", name = ["mzoon_start"] }
|
||||
|
||||
[tasks.mzoon_start_release_with_cleanup]
|
||||
description = "Run forked `mzoon start` with cleanup"
|
||||
run_task = { fork = true, cleanup_task = "kill_watchers", name = ["mzoon_start_release"] }
|
||||
|
||||
[tasks.kill_watchers]
|
||||
description = "Kill the cargo-make/makers process and all its children / forked processes"
|
||||
script_runner = "@duckscript"
|
||||
|
|
|
@ -15,8 +15,10 @@ origins = ["*"]
|
|||
frontend = [
|
||||
"public",
|
||||
"frontend/Cargo.toml",
|
||||
"frontend/typescript/bundles",
|
||||
"frontend/src",
|
||||
"frontend/typescript/bundles",
|
||||
"shared/Cargo.toml",
|
||||
"shared/src",
|
||||
]
|
||||
backend = [
|
||||
"backend/Cargo.toml",
|
||||
|
|
43
README.md
43
README.md
|
@ -4,11 +4,23 @@
|
|||
---
|
||||
|
||||
<p align="center">
|
||||
<img width="800" src="docs/screenshot_firefox.png" alt="fastwave_screenshot_firefox" />
|
||||
<img width="800" src="docs/screenshot_firefox.png" alt="Fastwave - Browser (Firefox)" />
|
||||
Browser (Firefox)
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_desktop.gif" alt="fastwave_video_desktop" />
|
||||
<img width="800" src="docs/video_desktop.gif" alt="Fastwave - Desktop, miller columns and tree" />
|
||||
Desktop, miller columns and tree
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_zoom_formatting_simple.gif" alt="Fastwave - Zoom, pan and basic number formats" />
|
||||
Zoom, pan and basic number formats
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_zoom_formatting.gif" alt="Fastwave - Zoom and all formats" />
|
||||
Zoom and all formats
|
||||
</p>
|
||||
|
||||
---
|
||||
|
@ -22,7 +34,7 @@
|
|||
|
||||
___
|
||||
|
||||
### Start:
|
||||
### Start the desktop version:
|
||||
|
||||
1. `makers start`
|
||||
|
||||
|
@ -39,21 +51,34 @@ Troubleshooting:
|
|||
|
||||
---
|
||||
|
||||
### Start in the browser:
|
||||
### Production build of the desktop version:
|
||||
|
||||
1. `makers bundle`
|
||||
2. Runnable executable is in `target/release`
|
||||
3. Installable bundles specific for the platform are in `target/release/bundle`
|
||||
|
||||
---
|
||||
|
||||
### Start in a browser:
|
||||
|
||||
1. `makers start_browser`
|
||||
2. Ctrl + Click the server URL mentioned in the terminal log
|
||||
|
||||
---
|
||||
|
||||
### Start in a browser in the release mode:
|
||||
|
||||
1. `makers start_browser_release`
|
||||
2. Ctrl + Click the server URL mentioned in the terminal log
|
||||
|
||||
---
|
||||
|
||||
### Steps before pushing:
|
||||
|
||||
1. `makers format`
|
||||
|
||||
---
|
||||
|
||||
### Production build:
|
||||
|
||||
1. `makers bundle`
|
||||
2. Runnable executable is in `target/release`
|
||||
3. Installable bundles specific for the platform are in `target/release/bundle`
|
||||
### Test files
|
||||
|
||||
See the folder `test_files`.
|
||||
|
|
BIN
docs/video_zoom_formatting.gif
Normal file
BIN
docs/video_zoom_formatting.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 612 KiB |
BIN
docs/video_zoom_formatting_simple.gif
Normal file
BIN
docs/video_zoom_formatting_simple.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
|
@ -11,8 +11,8 @@ publish.workspace = true
|
|||
wasm-bindgen-test = "0.3.19"
|
||||
|
||||
[dependencies]
|
||||
shared.workspace = true
|
||||
zoon.workspace = true
|
||||
wellen.workspace = true
|
||||
shared = { path = "../shared", features = ["frontend"] }
|
||||
web-sys = { version = "*", features = ["FileSystemFileHandle"] }
|
||||
gloo-file = { version = "0.3.0", features = ["futures"] }
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::{platform, HierarchyAndTimeTable, Layout};
|
||||
use futures_util::join;
|
||||
use crate::{platform, Layout};
|
||||
use std::cell::Cell;
|
||||
use std::mem;
|
||||
use std::ops::Not;
|
||||
|
@ -35,7 +34,7 @@ struct ScopeForUI {
|
|||
#[derive(Clone)]
|
||||
pub struct ControlsPanel {
|
||||
selected_scope_ref: Mutable<Option<wellen::ScopeRef>>,
|
||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||
layout: Mutable<Layout>,
|
||||
loaded_filename: Mutable<Option<Filename>>,
|
||||
|
@ -43,13 +42,13 @@ pub struct ControlsPanel {
|
|||
|
||||
impl ControlsPanel {
|
||||
pub fn new(
|
||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||
layout: Mutable<Layout>,
|
||||
) -> impl Element {
|
||||
Self {
|
||||
selected_scope_ref: <_>::default(),
|
||||
hierarchy_and_time_table,
|
||||
hierarchy,
|
||||
selected_var_refs,
|
||||
layout,
|
||||
loaded_filename: <_>::default(),
|
||||
|
@ -60,7 +59,7 @@ impl ControlsPanel {
|
|||
fn triggers(&self) -> Vec<TaskHandle> {
|
||||
vec![Task::start_droppable(clone!((self => s) async move {
|
||||
let was_some = Cell::new(false);
|
||||
s.hierarchy_and_time_table
|
||||
s.hierarchy
|
||||
.signal_ref(Option::is_some)
|
||||
.dedupe()
|
||||
.for_each_sync(clone!((s) move |is_some| {
|
||||
|
@ -81,8 +80,8 @@ impl ControlsPanel {
|
|||
let layout = self.layout.clone();
|
||||
let layout_and_hierarchy_signal = map_ref! {
|
||||
let layout = layout.signal(),
|
||||
let hierarchy_and_time_table = self.hierarchy_and_time_table.signal_cloned() => {
|
||||
(*layout, hierarchy_and_time_table.clone().map(|(hierarchy, _)| hierarchy))
|
||||
let hierarchy = self.hierarchy.signal_cloned() => {
|
||||
(*layout, hierarchy.clone())
|
||||
}
|
||||
};
|
||||
Column::new()
|
||||
|
@ -99,7 +98,6 @@ impl ControlsPanel {
|
|||
Layout::Columns => Height::fill().max(MILLER_COLUMN_MAX_HEIGHT),
|
||||
},
|
||||
)))
|
||||
.s(Scrollbars::both())
|
||||
.s(Padding::all(20))
|
||||
.s(Gap::new().y(40))
|
||||
.s(Align::new().top())
|
||||
|
@ -111,9 +109,9 @@ impl ControlsPanel {
|
|||
.item(self.layout_switcher()),
|
||||
)
|
||||
.item_signal(
|
||||
self.hierarchy_and_time_table
|
||||
self.hierarchy
|
||||
.signal_cloned()
|
||||
.map_some(clone!((self => s) move |(hierarchy, _)| s.scopes_panel(hierarchy))),
|
||||
.map_some(clone!((self => s) move |hierarchy| s.scopes_panel(hierarchy))),
|
||||
)
|
||||
.item_signal(layout_and_hierarchy_signal.map(
|
||||
clone!((self => s) move |(layout, hierarchy)| {
|
||||
|
@ -127,7 +125,7 @@ impl ControlsPanel {
|
|||
#[cfg(FASTWAVE_PLATFORM = "TAURI")]
|
||||
fn load_button(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let hierarchy_and_time_table = self.hierarchy_and_time_table.clone();
|
||||
let hierarchy = self.hierarchy.clone();
|
||||
let loaded_filename = self.loaded_filename.clone();
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
|
@ -144,21 +142,18 @@ impl ControlsPanel {
|
|||
))
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut();
|
||||
if hierarchy_and_time_table_lock.is_some() {
|
||||
*hierarchy_and_time_table_lock = None;
|
||||
let mut hierarchy_lock = hierarchy.lock_mut();
|
||||
if hierarchy_lock.is_some() {
|
||||
*hierarchy_lock = None;
|
||||
return;
|
||||
}
|
||||
drop(hierarchy_and_time_table_lock);
|
||||
let hierarchy_and_time_table = hierarchy_and_time_table.clone();
|
||||
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));
|
||||
let (hierarchy, time_table) =
|
||||
join!(platform::get_hierarchy(), platform::get_time_table());
|
||||
hierarchy_and_time_table
|
||||
.set(Some((Rc::new(hierarchy), Rc::new(time_table))))
|
||||
hierarchy.set(Some(Rc::new(platform::get_hierarchy().await)))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -167,7 +162,7 @@ impl ControlsPanel {
|
|||
#[cfg(FASTWAVE_PLATFORM = "BROWSER")]
|
||||
fn load_button(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let hierarchy_and_time_table = self.hierarchy_and_time_table.clone();
|
||||
let hierarchy = self.hierarchy.clone();
|
||||
let loaded_filename = self.loaded_filename.clone();
|
||||
let file_input_id = "file_input";
|
||||
Row::new()
|
||||
|
@ -175,7 +170,8 @@ impl ControlsPanel {
|
|||
Label::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
|
||||
hovered_signal
|
||||
.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
|
@ -188,50 +184,52 @@ impl ControlsPanel {
|
|||
))
|
||||
.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_and_time_table) move |event| {
|
||||
let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut();
|
||||
if hierarchy_and_time_table_lock.is_some() {
|
||||
*hierarchy_and_time_table_lock = None;
|
||||
if let RawMouseEvent::Click(raw_event) = event.raw_event {
|
||||
// @TODO Move to MoonZoon as a new API
|
||||
raw_event.prevent_default();
|
||||
.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;
|
||||
}
|
||||
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_and_time_table = hierarchy_and_time_table.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));
|
||||
let (hierarchy, time_table) =
|
||||
join!(platform::get_hierarchy(), platform::get_time_table());
|
||||
hierarchy_and_time_table
|
||||
.set(Some((Rc::new(hierarchy), Rc::new(time_table))))
|
||||
}
|
||||
})
|
||||
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)))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -563,6 +561,7 @@ impl ControlsPanel {
|
|||
});
|
||||
|
||||
// Lazy loading to not freeze the main thread
|
||||
// @TODO replace with grouping and/or virtual scroll (https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib)
|
||||
const CHUNK_SIZE: usize = 50;
|
||||
let mut chunked_vars_for_ui: Vec<Vec<VarForUI>> = <_>::default();
|
||||
let mut chunk = Vec::with_capacity(CHUNK_SIZE);
|
||||
|
|
|
@ -9,8 +9,6 @@ use controls_panel::ControlsPanel;
|
|||
mod waveform_panel;
|
||||
use waveform_panel::WaveformPanel;
|
||||
|
||||
type HierarchyAndTimeTable = (Rc<wellen::Hierarchy>, Rc<wellen::TimeTable>);
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
enum Layout {
|
||||
Tree,
|
||||
|
@ -28,7 +26,7 @@ fn main() {
|
|||
}
|
||||
|
||||
fn root() -> impl Element {
|
||||
let hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>> = <_>::default();
|
||||
let hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>> = <_>::default();
|
||||
let selected_var_refs: MutableVec<wellen::VarRef> = <_>::default();
|
||||
let layout: Mutable<Layout> = <_>::default();
|
||||
Column::new()
|
||||
|
@ -40,17 +38,26 @@ fn root() -> impl Element {
|
|||
.s(Height::fill())
|
||||
.s(Gap::new().x(15))
|
||||
.item(ControlsPanel::new(
|
||||
hierarchy_and_time_table.clone(),
|
||||
hierarchy.clone(),
|
||||
selected_var_refs.clone(),
|
||||
layout.clone(),
|
||||
))
|
||||
.item_signal(layout.signal().map(|layout| matches!(layout, Layout::Tree)).map_true(clone!((hierarchy_and_time_table, selected_var_refs) move || WaveformPanel::new(
|
||||
hierarchy_and_time_table.clone(),
|
||||
selected_var_refs.clone(),
|
||||
))))
|
||||
.item_signal(
|
||||
layout
|
||||
.signal()
|
||||
.map(|layout| matches!(layout, Layout::Tree))
|
||||
.map_true(
|
||||
clone!((hierarchy, selected_var_refs) move || WaveformPanel::new(
|
||||
hierarchy.clone(),
|
||||
selected_var_refs.clone(),
|
||||
)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.item_signal(
|
||||
layout
|
||||
.signal()
|
||||
.map(|layout| matches!(layout, Layout::Columns))
|
||||
.map_true(move || WaveformPanel::new(hierarchy.clone(), selected_var_refs.clone())),
|
||||
)
|
||||
.item_signal(layout.signal().map(|layout| matches!(layout, Layout::Columns)).map_true(move || WaveformPanel::new(
|
||||
hierarchy_and_time_table.clone(),
|
||||
selected_var_refs.clone(),
|
||||
)))
|
||||
}
|
||||
|
|
|
@ -29,12 +29,23 @@ pub async fn get_hierarchy() -> wellen::Hierarchy {
|
|||
platform::get_hierarchy().await
|
||||
}
|
||||
|
||||
pub async fn get_time_table() -> wellen::TimeTable {
|
||||
platform::get_time_table().await
|
||||
}
|
||||
|
||||
pub async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
|
||||
platform::load_and_get_signal(signal_ref).await
|
||||
pub async fn load_signal_and_get_timeline(
|
||||
signal_ref: wellen::SignalRef,
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: shared::VarFormat,
|
||||
) -> shared::Timeline {
|
||||
platform::load_signal_and_get_timeline(
|
||||
signal_ref,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn unload_signal(signal_ref: wellen::SignalRef) {
|
||||
|
|
|
@ -74,21 +74,29 @@ pub(super) async fn get_hierarchy() -> wellen::Hierarchy {
|
|||
serde_json::from_value(serde_json::to_value(hierarchy).unwrap_throw()).unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn get_time_table() -> wellen::TimeTable {
|
||||
let waveform = STORE.waveform.lock().unwrap_throw();
|
||||
let time_table = waveform.as_ref().unwrap_throw().time_table();
|
||||
// @TODO Wrap `time_table` in `Waveform` with `Rc/Arc` or add the method `take` / `clone` or refactor?
|
||||
serde_json::from_value(serde_json::to_value(time_table).unwrap_throw()).unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
|
||||
let mut waveform_lock = STORE.waveform.lock().unwrap_throw();
|
||||
let waveform = waveform_lock.as_mut().unwrap_throw();
|
||||
// @TODO maybe run it in a thread to not block the main one and then
|
||||
waveform.load_signals(&[signal_ref]);
|
||||
let signal = waveform.get_signal(signal_ref).unwrap_throw();
|
||||
// @TODO `clone` / `Rc/Arc` / refactor?
|
||||
serde_json::from_value(serde_json::to_value(signal).unwrap_throw()).unwrap_throw()
|
||||
pub(super) async fn load_signal_and_get_timeline(
|
||||
signal_ref: wellen::SignalRef,
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: shared::VarFormat,
|
||||
) -> shared::Timeline {
|
||||
let mut waveform_lock = 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();
|
||||
let time_table = waveform.time_table();
|
||||
let timeline = shared::signal_to_timeline(
|
||||
signal,
|
||||
time_table,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format,
|
||||
);
|
||||
timeline
|
||||
}
|
||||
|
||||
pub(super) async fn unload_signal(signal_ref: wellen::SignalRef) {
|
||||
|
|
|
@ -17,15 +17,26 @@ pub(super) async fn get_hierarchy() -> wellen::Hierarchy {
|
|||
serde_wasm_bindgen::from_value(tauri_glue::get_hierarchy().await.unwrap_throw()).unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn get_time_table() -> wellen::TimeTable {
|
||||
serde_wasm_bindgen::from_value(tauri_glue::get_time_table().await.unwrap_throw()).unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
|
||||
pub(super) async fn load_signal_and_get_timeline(
|
||||
signal_ref: wellen::SignalRef,
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: shared::VarFormat,
|
||||
) -> shared::Timeline {
|
||||
let var_format = serde_wasm_bindgen::to_value(&var_format).unwrap_throw();
|
||||
serde_wasm_bindgen::from_value(
|
||||
tauri_glue::load_and_get_signal(signal_ref.index())
|
||||
.await
|
||||
.unwrap_throw(),
|
||||
tauri_glue::load_signal_and_get_timeline(
|
||||
signal_ref.index(),
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format,
|
||||
)
|
||||
.await
|
||||
.unwrap_throw(),
|
||||
)
|
||||
.unwrap_throw()
|
||||
}
|
||||
|
@ -52,10 +63,14 @@ mod tauri_glue {
|
|||
pub async fn get_hierarchy() -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn get_time_table() -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn load_and_get_signal(signal_ref_index: usize) -> Result<JsValue, JsValue>;
|
||||
pub async fn load_signal_and_get_timeline(
|
||||
signal_ref_index: usize,
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: JsValue,
|
||||
) -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn unload_signal(signal_ref_index: usize) -> Result<(), JsValue>;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{platform, HierarchyAndTimeTable};
|
||||
use crate::platform;
|
||||
use std::rc::Rc;
|
||||
use wellen::GetItem;
|
||||
use zoon::{eprintln, *};
|
||||
use zoon::*;
|
||||
|
||||
mod pixi_canvas;
|
||||
use pixi_canvas::{PixiCanvas, PixiController};
|
||||
|
@ -11,21 +12,24 @@ const ROW_GAP: u32 = 4;
|
|||
#[derive(Clone)]
|
||||
pub struct WaveformPanel {
|
||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||
canvas_controller: Mutable<ReadOnlyMutable<Option<PixiController>>>,
|
||||
}
|
||||
|
||||
impl WaveformPanel {
|
||||
pub fn new(
|
||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||
) -> impl Element {
|
||||
Self {
|
||||
selected_var_refs,
|
||||
hierarchy_and_time_table,
|
||||
hierarchy,
|
||||
canvas_controller: Mutable::new(Mutable::default().read_only()),
|
||||
}
|
||||
.root()
|
||||
}
|
||||
|
||||
// @TODO autoscroll down
|
||||
fn root(&self) -> impl Element {
|
||||
let selected_vars_panel_height_getter: Mutable<u32> = <_>::default();
|
||||
Row::new()
|
||||
|
@ -51,27 +55,29 @@ impl WaveformPanel {
|
|||
|
||||
fn canvas(&self, selected_vars_panel_height: ReadOnlyMutable<u32>) -> impl Element {
|
||||
let selected_var_refs = self.selected_var_refs.clone();
|
||||
let hierarchy_and_time_table = self.hierarchy_and_time_table.clone();
|
||||
let hierarchy = self.hierarchy.clone();
|
||||
let canvas_controller = self.canvas_controller.clone();
|
||||
PixiCanvas::new(ROW_HEIGHT, ROW_GAP)
|
||||
.s(Align::new().top())
|
||||
.s(Width::fill())
|
||||
.s(Height::exact_signal(selected_vars_panel_height.signal()))
|
||||
.s(RoundedCorners::new().right(15))
|
||||
.task_with_controller(move |controller| {
|
||||
selected_var_refs.signal_vec().delay_remove(clone!((hierarchy_and_time_table) move |var_ref| {
|
||||
clone!((var_ref, hierarchy_and_time_table) async move {
|
||||
if let Some(hierarchy_and_time_table) = hierarchy_and_time_table.get_cloned() {
|
||||
platform::unload_signal(hierarchy_and_time_table.0.get(var_ref).signal_ref()).await;
|
||||
canvas_controller.set(controller.clone());
|
||||
selected_var_refs.signal_vec().delay_remove(clone!((hierarchy) move |var_ref| {
|
||||
clone!((var_ref, hierarchy) async move {
|
||||
if let Some(hierarchy) = hierarchy.get_cloned() {
|
||||
// @TODO unload only when no other selected variable use it?
|
||||
platform::unload_signal(hierarchy.get(var_ref).signal_ref()).await;
|
||||
}
|
||||
})
|
||||
})).for_each(clone!((controller, hierarchy_and_time_table) move |vec_diff| {
|
||||
clone!((controller, hierarchy_and_time_table) async move {
|
||||
})).for_each(clone!((controller, hierarchy) move |vec_diff| {
|
||||
clone!((controller, hierarchy) async move {
|
||||
match vec_diff {
|
||||
VecDiff::Replace { values } => {
|
||||
let controller = controller.wait_for_some_cloned().await;
|
||||
controller.clear_vars();
|
||||
for var_ref in values {
|
||||
Self::push_var(&controller, &hierarchy_and_time_table, var_ref).await;
|
||||
Self::push_var(&controller, &hierarchy, var_ref).await;
|
||||
}
|
||||
},
|
||||
VecDiff::InsertAt { index: _, value: _ } => { todo!("`task_with_controller` + `InsertAt`") }
|
||||
|
@ -84,7 +90,7 @@ impl WaveformPanel {
|
|||
VecDiff::Move { old_index: _, new_index: _ } => { todo!("`task_with_controller` + `Move`") }
|
||||
VecDiff::Push { value: var_ref } => {
|
||||
if let Some(controller) = controller.lock_ref().as_ref() {
|
||||
Self::push_var(controller, &hierarchy_and_time_table, var_ref).await;
|
||||
Self::push_var(controller, &hierarchy, var_ref).await;
|
||||
}
|
||||
}
|
||||
VecDiff::Pop {} => {
|
||||
|
@ -105,38 +111,34 @@ impl WaveformPanel {
|
|||
|
||||
async fn push_var(
|
||||
controller: &PixiController,
|
||||
hierarchy_and_time_table: &Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: &Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||
var_ref: wellen::VarRef,
|
||||
) {
|
||||
let (hierarchy, time_table) = hierarchy_and_time_table.get_cloned().unwrap();
|
||||
if time_table.is_empty() {
|
||||
eprintln!("timetable is empty");
|
||||
return;
|
||||
}
|
||||
let last_time = time_table.last().copied().unwrap_throw();
|
||||
let hierarchy = hierarchy.get_cloned().unwrap();
|
||||
|
||||
let var_format = shared::VarFormat::default();
|
||||
|
||||
let var = hierarchy.get(var_ref);
|
||||
let signal_ref = var.signal_ref();
|
||||
let signal = platform::load_and_get_signal(signal_ref).await;
|
||||
let timeline = platform::load_signal_and_get_timeline(
|
||||
signal_ref,
|
||||
controller.get_timeline_zoom(),
|
||||
controller.get_timeline_viewport_width(),
|
||||
controller.get_timeline_viewport_x(),
|
||||
ROW_HEIGHT,
|
||||
var_format,
|
||||
)
|
||||
.await;
|
||||
|
||||
let timescale = hierarchy.timescale();
|
||||
// @TODO remove
|
||||
zoon::println!("{timescale:?}");
|
||||
|
||||
let mut timeline: Vec<(wellen::Time, String)> = signal
|
||||
.iter_changes()
|
||||
.map(|(time_index, signal_value)| {
|
||||
(time_table[time_index as usize], signal_value.to_string())
|
||||
})
|
||||
.collect();
|
||||
if timeline.is_empty() {
|
||||
eprintln!("timeline is empty");
|
||||
return;
|
||||
}
|
||||
timeline.push((last_time, timeline.last().cloned().unwrap_throw().1));
|
||||
|
||||
// Note: Sync `timeline`'s type with the `Timeline` in `frontend/typescript/pixi_canvas/pixi_canvas.ts'
|
||||
controller.push_var(serde_wasm_bindgen::to_value(&timeline).unwrap_throw());
|
||||
let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw();
|
||||
let signal_ref_index = signal_ref.index();
|
||||
let var_format = serde_wasm_bindgen::to_value(&var_format).unwrap_throw();
|
||||
controller.push_var(signal_ref_index, timeline, var_format);
|
||||
}
|
||||
|
||||
fn selected_var_panel(
|
||||
|
@ -144,27 +146,74 @@ impl WaveformPanel {
|
|||
index: ReadOnlyMutable<Option<usize>>,
|
||||
var_ref: wellen::VarRef,
|
||||
) -> Option<impl Element> {
|
||||
let Some((hierarchy, _)) = self.hierarchy_and_time_table.get_cloned() else {
|
||||
let Some(hierarchy) = self.hierarchy.get_cloned() else {
|
||||
None?
|
||||
};
|
||||
let var = hierarchy.get(var_ref);
|
||||
let name: &str = var.name(&hierarchy);
|
||||
Row::new()
|
||||
.item(self.selected_var_name_button(var.name(&hierarchy), index.clone()))
|
||||
.item(self.selected_var_format_button(index))
|
||||
.apply(Some)
|
||||
}
|
||||
|
||||
fn selected_var_name_button(
|
||||
&self,
|
||||
name: &str,
|
||||
index: ReadOnlyMutable<Option<usize>>,
|
||||
) -> impl Element {
|
||||
let selected_var_refs = self.selected_var_refs.clone();
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
Button::new()
|
||||
.s(Height::exact(ROW_HEIGHT))
|
||||
.s(Background::new().color(color!("SlateBlue", 0.8)))
|
||||
.s(RoundedCorners::new().left(15))
|
||||
.s(Width::growable())
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| color!("SlateBlue"), || color!("SlateBlue", 0.8)),
|
||||
))
|
||||
.s(RoundedCorners::new().left(15).right(5))
|
||||
.label(
|
||||
El::new()
|
||||
.s(Align::center())
|
||||
.s(Align::new().left())
|
||||
.s(Padding::new().left(20).right(17).y(10))
|
||||
.child(name),
|
||||
)
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
if let Some(index) = index.get() {
|
||||
selected_var_refs.lock_mut().remove(index);
|
||||
}
|
||||
})
|
||||
.apply(Some)
|
||||
}
|
||||
|
||||
fn selected_var_format_button(&self, index: ReadOnlyMutable<Option<usize>>) -> impl Element {
|
||||
let var_format = Mutable::new(shared::VarFormat::default());
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let canvas_controller = self.canvas_controller.clone();
|
||||
Button::new()
|
||||
.s(Height::exact(ROW_HEIGHT))
|
||||
.s(Width::exact(70))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| color!("SlateBlue"), || color!("SlateBlue", 0.8)),
|
||||
))
|
||||
.s(RoundedCorners::new().left(5))
|
||||
.label(
|
||||
El::new()
|
||||
.s(Align::center())
|
||||
.s(Padding::new().left(20).right(17).y(10))
|
||||
.child_signal(var_format.signal().map(|format| format.as_static_str())),
|
||||
)
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
let next_format = var_format.get().next();
|
||||
var_format.set(next_format);
|
||||
if let Some(canvas_controller) = canvas_controller.get_cloned().lock_ref().as_ref()
|
||||
{
|
||||
if let Some(index) = index.get() {
|
||||
canvas_controller.set_var_format(
|
||||
index,
|
||||
serde_wasm_bindgen::to_value(&next_format).unwrap_throw(),
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use crate::platform;
|
||||
pub use js_bridge::PixiController;
|
||||
use std::rc::Rc;
|
||||
use zoon::*;
|
||||
|
||||
pub struct PixiCanvas {
|
||||
|
@ -35,16 +37,45 @@ impl PixiCanvas {
|
|||
let height = Mutable::new(0);
|
||||
let resize_task = Task::start_droppable(
|
||||
map_ref! {
|
||||
let _ = width.signal(),
|
||||
let _ = height.signal() => ()
|
||||
let width = width.signal(),
|
||||
let height = height.signal() => (*width, *height)
|
||||
}
|
||||
.for_each_sync(clone!((controller) move |_| {
|
||||
if let Some(controller) = controller.lock_ref().as_ref() {
|
||||
controller.queue_resize();
|
||||
}
|
||||
})),
|
||||
.dedupe()
|
||||
.throttle(|| Timer::sleep(50))
|
||||
.for_each(
|
||||
clone!((controller) move |(width, height)| clone!((controller) async move {
|
||||
if let Some(controller) = controller.lock_ref().as_ref() {
|
||||
controller.resize(width, height).await
|
||||
}
|
||||
})),
|
||||
),
|
||||
);
|
||||
let task_with_controller = Mutable::new(None);
|
||||
// -- FastWave-specific --
|
||||
let timeline_getter = Rc::new(Closure::new(
|
||||
|signal_ref_index,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
row_height,
|
||||
var_format| {
|
||||
future_to_promise(async move {
|
||||
let signal_ref = wellen::SignalRef::from_index(signal_ref_index).unwrap_throw();
|
||||
let timeline = platform::load_signal_and_get_timeline(
|
||||
signal_ref,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
row_height,
|
||||
serde_wasm_bindgen::from_value(var_format).unwrap_throw(),
|
||||
)
|
||||
.await;
|
||||
let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw();
|
||||
Ok(timeline)
|
||||
})
|
||||
},
|
||||
));
|
||||
// -- // --
|
||||
Self {
|
||||
controller: controller.read_only(),
|
||||
width: width.read_only(),
|
||||
|
@ -56,14 +87,36 @@ impl PixiCanvas {
|
|||
width.set_neq(new_width);
|
||||
height.set_neq(new_height);
|
||||
}))
|
||||
.after_insert(clone!((controller) move |element| {
|
||||
.update_raw_el(|raw_el| {
|
||||
// @TODO rewrite to a native Zoon API
|
||||
raw_el.event_handler(
|
||||
clone!((controller) move |event: events_extra::WheelEvent| {
|
||||
if let Some(controller) = controller.lock_ref().as_ref() {
|
||||
controller.zoom_or_pan(
|
||||
event.delta_y(),
|
||||
event.shift_key(),
|
||||
event.offset_x() as u32,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.after_insert(clone!((controller, timeline_getter) move |element| {
|
||||
Task::start(async move {
|
||||
let pixi_controller = js_bridge::PixiController::new(row_height, row_gap);
|
||||
let pixi_controller = js_bridge::PixiController::new(
|
||||
1.,
|
||||
width.get(),
|
||||
0,
|
||||
row_height,
|
||||
row_gap,
|
||||
&timeline_getter
|
||||
);
|
||||
pixi_controller.init(&element).await;
|
||||
controller.set(Some(pixi_controller));
|
||||
});
|
||||
}))
|
||||
.after_remove(move |_| {
|
||||
drop(timeline_getter);
|
||||
drop(resize_task);
|
||||
drop(task_with_controller);
|
||||
if let Some(controller) = controller.take() {
|
||||
|
@ -87,6 +140,24 @@ impl PixiCanvas {
|
|||
mod js_bridge {
|
||||
use zoon::*;
|
||||
|
||||
type TimelinePromise = js_sys::Promise;
|
||||
type SignalRefIndex = usize;
|
||||
type TimelineZoom = f64;
|
||||
type TimelineViewportWidth = u32;
|
||||
type TimelineViewportX = i32;
|
||||
type RowHeight = u32;
|
||||
type VarFormatJs = JsValue;
|
||||
type TimelineGetter = Closure<
|
||||
dyn FnMut(
|
||||
SignalRefIndex,
|
||||
TimelineZoom,
|
||||
TimelineViewportWidth,
|
||||
TimelineViewportX,
|
||||
RowHeight,
|
||||
VarFormatJs,
|
||||
) -> TimelinePromise,
|
||||
>;
|
||||
|
||||
// Note: Add all corresponding methods to `frontend/typescript/pixi_canvas/pixi_canvas.ts`
|
||||
#[wasm_bindgen(module = "/typescript/bundles/pixi_canvas.js")]
|
||||
extern "C" {
|
||||
|
@ -95,24 +166,56 @@ mod js_bridge {
|
|||
|
||||
// @TODO `row_height` and `row_gap` is FastWave-specific
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(row_height: u32, row_gap: u32) -> PixiController;
|
||||
pub fn new(
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
row_height: u32,
|
||||
row_gap: u32,
|
||||
timeline_getter: &TimelineGetter,
|
||||
) -> PixiController;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub async fn init(this: &PixiController, parent_element: &JsValue);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub async fn resize(this: &PixiController, width: u32, height: u32);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn destroy(this: &PixiController);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn queue_resize(this: &PixiController);
|
||||
pub fn get_timeline_zoom(this: &PixiController) -> f64;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn get_timeline_viewport_width(this: &PixiController) -> u32;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn get_timeline_viewport_x(this: &PixiController) -> i32;
|
||||
|
||||
// -- FastWave-specific --
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn set_var_format(this: &PixiController, index: usize, var_format: JsValue);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn zoom_or_pan(
|
||||
this: &PixiController,
|
||||
wheel_delta_y: f64,
|
||||
shift_key: bool,
|
||||
offset_x: u32,
|
||||
);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn remove_var(this: &PixiController, index: usize);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn push_var(this: &PixiController, timeline: JsValue);
|
||||
pub fn push_var(
|
||||
this: &PixiController,
|
||||
signal_ref_index: usize,
|
||||
timeline: JsValue,
|
||||
var_format: JsValue,
|
||||
);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn pop_var(this: &PixiController);
|
||||
|
|
|
@ -35117,9 +35117,11 @@ var Text = class extends AbstractText {
|
|||
};
|
||||
|
||||
// node_modules/pixi.js/lib/index.mjs
|
||||
init_Texture();
|
||||
init_textureFrom();
|
||||
init_Container();
|
||||
init_Graphics();
|
||||
init_Sprite();
|
||||
init_TextStyle();
|
||||
init_eventemitter3();
|
||||
var import_earcut2 = __toESM(require_earcut(), 1);
|
||||
|
@ -35131,20 +35133,32 @@ var PixiController = class {
|
|||
// -- FastWave-specific --
|
||||
var_signal_rows = [];
|
||||
var_signal_rows_container = new Container();
|
||||
// @TODO reset `timeline_*` on file unload?
|
||||
timeline_zoom;
|
||||
timeline_viewport_width;
|
||||
timeline_viewport_x;
|
||||
row_height;
|
||||
row_gap;
|
||||
constructor(row_height, row_gap) {
|
||||
timeline_getter;
|
||||
constructor(timeline_zoom, timeline_viewport_width, timeline_viewport_x, row_height, row_gap, timeline_getter) {
|
||||
this.app = new Application();
|
||||
this.timeline_zoom = timeline_zoom;
|
||||
this.timeline_viewport_width = timeline_viewport_width;
|
||||
this.timeline_viewport_x = timeline_viewport_x;
|
||||
this.row_height = row_height;
|
||||
this.row_gap = row_gap;
|
||||
this.app.stage.addChild(this.var_signal_rows_container);
|
||||
this.timeline_getter = timeline_getter;
|
||||
}
|
||||
async init(parent_element) {
|
||||
await this.app.init({ background: "DarkSlateBlue", antialias: true, resizeTo: parent_element });
|
||||
parent_element.appendChild(this.app.canvas);
|
||||
}
|
||||
// Default automatic Pixi resizing is not reliable
|
||||
queue_resize() {
|
||||
// Default automatic Pixi resizing according to the parent is not reliable
|
||||
// and the `app.renderer`'s `resize` event is fired on every browser window size change
|
||||
async resize(width, _height) {
|
||||
this.timeline_viewport_width = width;
|
||||
await this.redraw_all_rows();
|
||||
this.app.queueResize();
|
||||
}
|
||||
destroy() {
|
||||
|
@ -35159,14 +35173,84 @@ var PixiController = class {
|
|||
};
|
||||
this.app.destroy(rendererDestroyOptions, options);
|
||||
}
|
||||
get_timeline_zoom() {
|
||||
return this.timeline_zoom;
|
||||
}
|
||||
get_timeline_viewport_width() {
|
||||
return this.timeline_viewport_width;
|
||||
}
|
||||
get_timeline_viewport_x() {
|
||||
return this.timeline_viewport_x;
|
||||
}
|
||||
// -- FastWave-specific --
|
||||
async redraw_all_rows() {
|
||||
await Promise.all(this.var_signal_rows.map(async (row) => {
|
||||
const timeline = await this.timeline_getter(
|
||||
row.signal_ref_index,
|
||||
this.timeline_zoom,
|
||||
this.timeline_viewport_width,
|
||||
this.timeline_viewport_x,
|
||||
this.row_height,
|
||||
row.var_format
|
||||
);
|
||||
row.redraw(timeline);
|
||||
}));
|
||||
}
|
||||
async redraw_row(index) {
|
||||
const row = this.var_signal_rows[index];
|
||||
if (typeof row !== "undefined") {
|
||||
const timeline = await this.timeline_getter(
|
||||
row.signal_ref_index,
|
||||
this.timeline_zoom,
|
||||
this.timeline_viewport_width,
|
||||
this.timeline_viewport_x,
|
||||
this.row_height,
|
||||
row.var_format
|
||||
);
|
||||
row.redraw(timeline);
|
||||
}
|
||||
}
|
||||
async set_var_format(index, var_format) {
|
||||
const row = this.var_signal_rows[index];
|
||||
if (typeof row !== "undefined") {
|
||||
row.set_var_format(var_format);
|
||||
this.redraw_row(index);
|
||||
}
|
||||
}
|
||||
async zoom_or_pan(wheel_delta_y, shift_key, offset_x) {
|
||||
if (shift_key) {
|
||||
this.timeline_viewport_x += Math.sign(wheel_delta_y) * 20;
|
||||
} else {
|
||||
const offset_x_ratio = offset_x / this.timeline_viewport_width;
|
||||
const old_timeline_width = this.timeline_viewport_width * this.timeline_zoom;
|
||||
const new_zoom = this.timeline_zoom - Math.sign(wheel_delta_y) * this.timeline_zoom * 0.5;
|
||||
const new_timeline_width = this.timeline_viewport_width * new_zoom;
|
||||
if (new_timeline_width < this.timeline_viewport_width) {
|
||||
this.timeline_zoom = 1;
|
||||
this.timeline_viewport_x = 0;
|
||||
} else {
|
||||
const timeline_width_difference = new_timeline_width - old_timeline_width;
|
||||
this.timeline_viewport_x += timeline_width_difference * offset_x_ratio;
|
||||
this.timeline_zoom = new_zoom;
|
||||
}
|
||||
}
|
||||
const timeline_width = this.timeline_viewport_width * this.timeline_zoom;
|
||||
if (this.timeline_viewport_x < 0) {
|
||||
this.timeline_viewport_x = 0;
|
||||
} else if (this.timeline_viewport_x + this.timeline_viewport_width > timeline_width) {
|
||||
this.timeline_viewport_x = timeline_width - this.timeline_viewport_width;
|
||||
}
|
||||
this.redraw_all_rows();
|
||||
}
|
||||
remove_var(index) {
|
||||
if (typeof this.var_signal_rows[index] !== "undefined") {
|
||||
this.var_signal_rows[index].destroy();
|
||||
}
|
||||
}
|
||||
push_var(timeline) {
|
||||
push_var(signal_ref_index, timeline, var_format) {
|
||||
new VarSignalRow(
|
||||
signal_ref_index,
|
||||
var_format,
|
||||
timeline,
|
||||
this.app,
|
||||
this.var_signal_rows,
|
||||
|
@ -35183,30 +35267,30 @@ var PixiController = class {
|
|||
}
|
||||
};
|
||||
var VarSignalRow = class {
|
||||
app;
|
||||
signal_ref_index;
|
||||
var_format;
|
||||
timeline;
|
||||
last_time;
|
||||
formatter;
|
||||
timeline_for_ui;
|
||||
app;
|
||||
owner;
|
||||
index_in_owner;
|
||||
rows_container;
|
||||
row_height;
|
||||
row_gap;
|
||||
row_height_with_gap;
|
||||
renderer_resize_callback = () => this.redraw_on_canvas_resize();
|
||||
// -- elements --
|
||||
row_container = new Container();
|
||||
row_container_background;
|
||||
signal_blocks_container = new Container();
|
||||
constructor(timeline, app, owner, rows_container, row_height, row_gap) {
|
||||
this.app = app;
|
||||
label_style = new TextStyle({
|
||||
align: "center",
|
||||
fill: "White",
|
||||
fontSize: 16,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
});
|
||||
constructor(signal_ref_index, var_format, timeline, app, owner, rows_container, row_height, row_gap) {
|
||||
this.signal_ref_index = signal_ref_index;
|
||||
this.var_format = var_format;
|
||||
this.timeline = timeline;
|
||||
this.last_time = timeline[timeline.length - 1][0];
|
||||
this.formatter = (signal_value) => parseInt(signal_value, 2).toString(16);
|
||||
this.timeline_for_ui = this.timeline.map(([time, value]) => {
|
||||
const x2 = time / this.last_time * this.app.screen.width;
|
||||
return [x2, this.formatter(value)];
|
||||
});
|
||||
this.app = app;
|
||||
this.row_height = row_height;
|
||||
this.row_gap = row_gap;
|
||||
this.row_height_with_gap = row_height + row_gap;
|
||||
|
@ -35214,58 +35298,44 @@ var VarSignalRow = class {
|
|||
this.owner = owner;
|
||||
this.owner.push(this);
|
||||
this.rows_container = rows_container;
|
||||
this.draw();
|
||||
this.app.renderer.on("resize", this.renderer_resize_callback);
|
||||
}
|
||||
draw() {
|
||||
this.row_container.y = this.index_in_owner * this.row_height_with_gap;
|
||||
this.rows_container.addChild(this.row_container);
|
||||
this.row_container_background = new Sprite();
|
||||
this.row_container_background.texture = Texture.WHITE;
|
||||
this.row_container_background.tint = "0x550099";
|
||||
this.row_container_background.height = this.row_height;
|
||||
this.row_container.addChild(this.row_container_background);
|
||||
this.row_container.addChild(this.signal_blocks_container);
|
||||
const label_style = new TextStyle({
|
||||
align: "center",
|
||||
fill: "White",
|
||||
fontSize: 16,
|
||||
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"'
|
||||
});
|
||||
this.timeline_for_ui.forEach(([x2, value], index) => {
|
||||
if (index == this.timeline_for_ui.length - 1) {
|
||||
return;
|
||||
}
|
||||
const block_width = this.timeline_for_ui[index + 1][0] - x2;
|
||||
const block_height = this.row_height;
|
||||
const signal_block = new Container();
|
||||
signal_block.x = x2;
|
||||
this.signal_blocks_container.addChild(signal_block);
|
||||
const background = new Graphics().roundRect(0, 0, block_width, block_height, 15).fill("SlateBlue");
|
||||
background.label = "background";
|
||||
signal_block.addChild(background);
|
||||
const label = new Text({ text: value, style: label_style });
|
||||
label.x = (block_width - label.width) / 2;
|
||||
label.y = (block_height - label.height) / 2;
|
||||
label.visible = label.width < block_width;
|
||||
label.label = "label";
|
||||
signal_block.addChild(label);
|
||||
});
|
||||
this.draw();
|
||||
}
|
||||
redraw_on_canvas_resize() {
|
||||
for (let index = 0; index < this.timeline_for_ui.length; index++) {
|
||||
const x2 = this.timeline[index][0] / this.last_time * this.app.screen.width;
|
||||
this.timeline_for_ui[index][0] = x2;
|
||||
set_var_format(var_format) {
|
||||
this.var_format = var_format;
|
||||
}
|
||||
redraw(timeline) {
|
||||
this.timeline = timeline;
|
||||
this.draw();
|
||||
}
|
||||
draw() {
|
||||
if (this.app === null || this.app.screen === null) {
|
||||
return;
|
||||
}
|
||||
this.timeline_for_ui.forEach(([x2, _value], index) => {
|
||||
if (index == this.timeline_for_ui.length - 1) {
|
||||
return;
|
||||
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();
|
||||
signal_block.x = timeline_block.x;
|
||||
this.signal_blocks_container.addChild(signal_block);
|
||||
const gap_between_blocks = 2;
|
||||
const background = new Graphics().rect(gap_between_blocks / 2, 0, timeline_block.width - gap_between_blocks, timeline_block.height).fill("SlateBlue");
|
||||
signal_block.addChild(background);
|
||||
if (timeline_block.label !== void 0) {
|
||||
const label = new Text();
|
||||
label.text = timeline_block.label.text;
|
||||
label.style = this.label_style;
|
||||
label.x = timeline_block.label.x;
|
||||
label.y = timeline_block.label.y;
|
||||
signal_block.addChild(label);
|
||||
}
|
||||
const block_width = this.timeline_for_ui[index + 1][0] - x2;
|
||||
const block_height = this.row_height;
|
||||
const signal_block = this.signal_blocks_container.getChildAt(index);
|
||||
signal_block.x = x2;
|
||||
const background = signal_block.getChildByLabel("background");
|
||||
background.width = block_width;
|
||||
const label = signal_block.getChildByLabel("label");
|
||||
label.x = (block_width - label.width) / 2;
|
||||
label.y = (block_height - label.height) / 2;
|
||||
label.visible = label.width < block_width;
|
||||
});
|
||||
}
|
||||
decrement_index() {
|
||||
|
@ -35273,7 +35343,6 @@ var VarSignalRow = class {
|
|||
this.row_container.y -= this.row_height_with_gap;
|
||||
}
|
||||
destroy() {
|
||||
this.app.renderer.off("resize", this.renderer_resize_callback);
|
||||
this.owner.splice(this.index_in_owner, 1);
|
||||
this.rows_container.removeChildAt(this.index_in_owner);
|
||||
this.row_container.destroy(true);
|
||||
|
|
|
@ -2520,19 +2520,22 @@ async function pick_and_load_waveform() {
|
|||
async function get_hierarchy() {
|
||||
return await invoke2("get_hierarchy");
|
||||
}
|
||||
async function get_time_table() {
|
||||
return await invoke2("get_time_table");
|
||||
}
|
||||
async function load_and_get_signal(signal_ref_index) {
|
||||
return await invoke2("load_and_get_signal", { signal_ref_index });
|
||||
async function load_signal_and_get_timeline(signal_ref_index, timeline_zoom, timeline_viewport_width, timeline_viewport_x, block_height, var_format) {
|
||||
return await invoke2("load_signal_and_get_timeline", {
|
||||
signal_ref_index,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format
|
||||
});
|
||||
}
|
||||
async function unload_signal(signal_ref_index) {
|
||||
return await invoke2("unload_signal", { signal_ref_index });
|
||||
}
|
||||
export {
|
||||
get_hierarchy,
|
||||
get_time_table,
|
||||
load_and_get_signal,
|
||||
load_signal_and_get_timeline,
|
||||
pick_and_load_waveform,
|
||||
show_window,
|
||||
unload_signal
|
||||
|
|
|
@ -1,26 +1,71 @@
|
|||
import { Application, Text, Graphics, Container, TextStyle } from "pixi.js";
|
||||
import { Application, Text, Graphics, Container, TextStyle, Sprite, Texture } from "pixi.js";
|
||||
|
||||
type Time = number;
|
||||
type BitString = string;
|
||||
type Timeline = Array<[Time, BitString]>;
|
||||
// @TODO sync with Rust and `tauri_glue.ts`
|
||||
type Timeline = {
|
||||
blocks: Array<TimelineBlock>
|
||||
}
|
||||
type TimelineBlock = {
|
||||
x: number,
|
||||
width: number,
|
||||
height: number,
|
||||
label: TimeLineBlockLabel | undefined,
|
||||
}
|
||||
type TimeLineBlockLabel = {
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
}
|
||||
|
||||
type X = number;
|
||||
type TimelineForUI = Array<[X, string]>;
|
||||
// @TODO sync with Rust
|
||||
enum VarFormat {
|
||||
ASCII,
|
||||
Binary,
|
||||
BinaryWithGroups,
|
||||
Hexadecimal,
|
||||
Octal,
|
||||
Signed,
|
||||
Unsigned,
|
||||
}
|
||||
|
||||
type TimelineGetter = (
|
||||
signal_ref_index: number,
|
||||
timeline_zoom: number,
|
||||
timeline_viewport_width: number,
|
||||
timeline_viewport_x: number,
|
||||
row_height: number,
|
||||
var_format: VarFormat
|
||||
) => Promise<Timeline>;
|
||||
|
||||
export class PixiController {
|
||||
app: Application
|
||||
// -- FastWave-specific --
|
||||
var_signal_rows: Array<VarSignalRow> = [];
|
||||
var_signal_rows_container = new Container();
|
||||
// @TODO reset `timeline_*` on file unload?
|
||||
timeline_zoom: number;
|
||||
timeline_viewport_width: number;
|
||||
timeline_viewport_x: number;
|
||||
row_height: number;
|
||||
row_gap: number;
|
||||
timeline_getter: TimelineGetter;
|
||||
|
||||
constructor(row_height: number, row_gap: number) {
|
||||
constructor(
|
||||
timeline_zoom: number,
|
||||
timeline_viewport_width: number,
|
||||
timeline_viewport_x: number,
|
||||
row_height: number,
|
||||
row_gap: number,
|
||||
timeline_getter: TimelineGetter
|
||||
) {
|
||||
this.app = new Application();
|
||||
// -- FastWave-specific --
|
||||
this.timeline_zoom = timeline_zoom;
|
||||
this.timeline_viewport_width = timeline_viewport_width;
|
||||
this.timeline_viewport_x = timeline_viewport_x;
|
||||
this.row_height = row_height;
|
||||
this.row_gap = row_gap;
|
||||
this.app.stage.addChild(this.var_signal_rows_container);
|
||||
this.timeline_getter = timeline_getter;
|
||||
}
|
||||
|
||||
async init(parent_element: HTMLElement) {
|
||||
|
@ -28,8 +73,13 @@ export class PixiController {
|
|||
parent_element.appendChild(this.app.canvas);
|
||||
}
|
||||
|
||||
// Default automatic Pixi resizing is not reliable
|
||||
queue_resize() {
|
||||
// Default automatic Pixi resizing according to the parent is not reliable
|
||||
// and the `app.renderer`'s `resize` event is fired on every browser window size change
|
||||
async resize(width: number, _height: number) {
|
||||
// -- FastWave-specific --
|
||||
this.timeline_viewport_width = width;
|
||||
await this.redraw_all_rows();
|
||||
// -- // --
|
||||
this.app.queueResize();
|
||||
}
|
||||
|
||||
|
@ -46,16 +96,93 @@ export class PixiController {
|
|||
this.app.destroy(rendererDestroyOptions, options);
|
||||
}
|
||||
|
||||
get_timeline_zoom() {
|
||||
return this.timeline_zoom;
|
||||
}
|
||||
|
||||
get_timeline_viewport_width() {
|
||||
return this.timeline_viewport_width;
|
||||
}
|
||||
|
||||
get_timeline_viewport_x() {
|
||||
return this.timeline_viewport_x;
|
||||
}
|
||||
|
||||
// -- FastWave-specific --
|
||||
|
||||
async redraw_all_rows() {
|
||||
await Promise.all(this.var_signal_rows.map(async row => {
|
||||
const timeline = await this.timeline_getter(
|
||||
row.signal_ref_index,
|
||||
this.timeline_zoom,
|
||||
this.timeline_viewport_width,
|
||||
this.timeline_viewport_x,
|
||||
this.row_height,
|
||||
row.var_format
|
||||
);
|
||||
row.redraw(timeline);
|
||||
}))
|
||||
}
|
||||
|
||||
async redraw_row(index: number) {
|
||||
const row = this.var_signal_rows[index];
|
||||
if (typeof row !== 'undefined') {
|
||||
const timeline = await this.timeline_getter(
|
||||
row.signal_ref_index,
|
||||
this.timeline_zoom,
|
||||
this.timeline_viewport_width,
|
||||
this.timeline_viewport_x,
|
||||
this.row_height,
|
||||
row.var_format
|
||||
);
|
||||
row.redraw(timeline);
|
||||
}
|
||||
}
|
||||
|
||||
async set_var_format(index: number, var_format: VarFormat) {
|
||||
const row = this.var_signal_rows[index];
|
||||
if (typeof row !== 'undefined') {
|
||||
row.set_var_format(var_format);
|
||||
this.redraw_row(index);
|
||||
}
|
||||
}
|
||||
|
||||
async zoom_or_pan(wheel_delta_y: number, shift_key: boolean, offset_x: number) {
|
||||
if (shift_key) {
|
||||
this.timeline_viewport_x += Math.sign(wheel_delta_y) * 20;
|
||||
} else {
|
||||
const offset_x_ratio = offset_x / this.timeline_viewport_width;
|
||||
const old_timeline_width = this.timeline_viewport_width * this.timeline_zoom;
|
||||
const new_zoom = this.timeline_zoom - Math.sign(wheel_delta_y) * this.timeline_zoom * 0.5;
|
||||
const new_timeline_width = this.timeline_viewport_width * new_zoom;
|
||||
if (new_timeline_width < this.timeline_viewport_width) {
|
||||
this.timeline_zoom = 1;
|
||||
this.timeline_viewport_x = 0;
|
||||
} else {
|
||||
const timeline_width_difference = new_timeline_width - old_timeline_width;
|
||||
this.timeline_viewport_x += timeline_width_difference * offset_x_ratio;
|
||||
this.timeline_zoom = new_zoom;
|
||||
}
|
||||
}
|
||||
const timeline_width = this.timeline_viewport_width * this.timeline_zoom;
|
||||
if (this.timeline_viewport_x < 0) {
|
||||
this.timeline_viewport_x = 0;
|
||||
} else if (this.timeline_viewport_x + this.timeline_viewport_width > timeline_width) {
|
||||
this.timeline_viewport_x = timeline_width - this.timeline_viewport_width;
|
||||
}
|
||||
this.redraw_all_rows();
|
||||
}
|
||||
|
||||
remove_var(index: number) {
|
||||
if (typeof this.var_signal_rows[index] !== 'undefined') {
|
||||
this.var_signal_rows[index].destroy();
|
||||
}
|
||||
}
|
||||
|
||||
push_var(timeline: Timeline) {
|
||||
push_var(signal_ref_index: number, timeline: Timeline, var_format: VarFormat) {
|
||||
new VarSignalRow(
|
||||
signal_ref_index,
|
||||
var_format,
|
||||
timeline,
|
||||
this.app,
|
||||
this.var_signal_rows,
|
||||
|
@ -75,23 +202,29 @@ export class PixiController {
|
|||
}
|
||||
|
||||
class VarSignalRow {
|
||||
app: Application;
|
||||
signal_ref_index: number;
|
||||
var_format: VarFormat;
|
||||
timeline: Timeline;
|
||||
last_time: Time;
|
||||
formatter: (signal_value: BitString) => string;
|
||||
timeline_for_ui: TimelineForUI;
|
||||
app: Application;
|
||||
owner: Array<VarSignalRow>;
|
||||
index_in_owner: number;
|
||||
rows_container: Container;
|
||||
row_height: number;
|
||||
row_gap: number;
|
||||
row_height_with_gap: number;
|
||||
renderer_resize_callback = () => this.redraw_on_canvas_resize();
|
||||
// -- elements --
|
||||
row_container = new Container();
|
||||
row_container_background: Sprite;
|
||||
signal_blocks_container = new Container();
|
||||
label_style = new TextStyle({
|
||||
align: "center",
|
||||
fill: "White",
|
||||
fontSize: 16,
|
||||
fontFamily: '"Courier New", monospace',
|
||||
});
|
||||
|
||||
constructor(
|
||||
signal_ref_index: number,
|
||||
var_format: VarFormat,
|
||||
timeline: Timeline,
|
||||
app: Application,
|
||||
owner: Array<VarSignalRow>,
|
||||
|
@ -99,16 +232,10 @@ class VarSignalRow {
|
|||
row_height: number,
|
||||
row_gap: number,
|
||||
) {
|
||||
this.app = app;
|
||||
|
||||
this.signal_ref_index = signal_ref_index;
|
||||
this.var_format = var_format;
|
||||
this.timeline = timeline;
|
||||
this.last_time = timeline[timeline.length - 1][0];
|
||||
this.formatter = signal_value => parseInt(signal_value, 2).toString(16);
|
||||
|
||||
this.timeline_for_ui = this.timeline.map(([time, value]) => {
|
||||
const x = time / this.last_time * this.app.screen.width;
|
||||
return [x, this.formatter(value)]
|
||||
});
|
||||
this.app = app;
|
||||
|
||||
this.row_height = row_height;
|
||||
this.row_gap = row_gap;
|
||||
|
@ -120,81 +247,66 @@ class VarSignalRow {
|
|||
|
||||
this.rows_container = rows_container;
|
||||
|
||||
this.draw();
|
||||
this.app.renderer.on("resize", this.renderer_resize_callback);
|
||||
}
|
||||
|
||||
draw() {
|
||||
// row_container
|
||||
this.row_container.y = this.index_in_owner * this.row_height_with_gap;
|
||||
this.rows_container.addChild(this.row_container);
|
||||
|
||||
// signal_block_container
|
||||
// row background
|
||||
this.row_container_background = new Sprite();
|
||||
this.row_container_background.texture = Texture.WHITE;
|
||||
this.row_container_background.tint = '0x550099';
|
||||
this.row_container_background.height = this.row_height;
|
||||
this.row_container.addChild(this.row_container_background);
|
||||
|
||||
// signal_blocks_container
|
||||
this.row_container.addChild(this.signal_blocks_container);
|
||||
|
||||
const label_style = new TextStyle({
|
||||
align: "center",
|
||||
fill: "White",
|
||||
fontSize: 16,
|
||||
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
|
||||
});
|
||||
this.draw();
|
||||
}
|
||||
|
||||
this.timeline_for_ui.forEach(([x, value], index) => {
|
||||
if (index == this.timeline_for_ui.length - 1) {
|
||||
return;
|
||||
}
|
||||
const block_width = this.timeline_for_ui[index+1][0] - x;
|
||||
const block_height = this.row_height;
|
||||
set_var_format(var_format: VarFormat) {
|
||||
this.var_format = var_format;
|
||||
}
|
||||
|
||||
redraw(timeline: Timeline) {
|
||||
this.timeline = timeline;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
this.timeline.blocks.forEach(timeline_block => {
|
||||
// signal_block
|
||||
const signal_block = new Container();
|
||||
signal_block.x = x;
|
||||
signal_block.x = timeline_block.x;
|
||||
this.signal_blocks_container.addChild(signal_block);
|
||||
|
||||
// background
|
||||
const gap_between_blocks = 2;
|
||||
const background = new Graphics()
|
||||
.roundRect(0, 0, block_width, block_height, 15)
|
||||
.fill("SlateBlue");
|
||||
background.label = "background";
|
||||
.rect(gap_between_blocks / 2, 0, timeline_block.width - gap_between_blocks, timeline_block.height)
|
||||
.fill('SlateBlue');
|
||||
signal_block.addChild(background);
|
||||
|
||||
// label
|
||||
const label = new Text({ text: value, style: label_style });
|
||||
label.x = (block_width - label.width) / 2;
|
||||
label.y = (block_height - label.height) / 2;
|
||||
label.visible = label.width < block_width;
|
||||
label.label = "label";
|
||||
signal_block.addChild(label);
|
||||
})
|
||||
}
|
||||
|
||||
redraw_on_canvas_resize() {
|
||||
for (let index = 0; index < this.timeline_for_ui.length; index++) {
|
||||
const x = this.timeline[index][0] / this.last_time * this.app.screen.width;
|
||||
this.timeline_for_ui[index][0] = x;
|
||||
}
|
||||
this.timeline_for_ui.forEach(([x, _value], index) => {
|
||||
if (index == this.timeline_for_ui.length - 1) {
|
||||
return;
|
||||
if (timeline_block.label !== undefined) {
|
||||
const label = new Text();
|
||||
label.text = timeline_block.label.text;
|
||||
label.style = this.label_style;
|
||||
label.x = timeline_block.label.x;
|
||||
label.y = timeline_block.label.y;
|
||||
signal_block.addChild(label);
|
||||
}
|
||||
|
||||
const block_width = this.timeline_for_ui[index+1][0] - x;
|
||||
const block_height = this.row_height;
|
||||
|
||||
// signal_block
|
||||
const signal_block = this.signal_blocks_container.getChildAt(index);
|
||||
signal_block.x = x;
|
||||
|
||||
// background
|
||||
const background = signal_block.getChildByLabel("background")!;
|
||||
background.width = block_width;
|
||||
|
||||
// label
|
||||
const label = signal_block.getChildByLabel("label")!;
|
||||
label.x = (block_width - label.width) / 2;
|
||||
label.y = (block_height - label.height) / 2;
|
||||
label.visible = label.width < block_width;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
decrement_index() {
|
||||
|
@ -203,7 +315,6 @@ class VarSignalRow {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this.app.renderer.off("resize", this.renderer_resize_callback);
|
||||
this.owner.splice(this.index_in_owner, 1);
|
||||
this.rows_container.removeChildAt(this.index_in_owner);
|
||||
this.row_container.destroy(true);
|
||||
|
|
|
@ -6,8 +6,8 @@ const invoke = core.invoke;
|
|||
|
||||
type Filename = string;
|
||||
type WellenHierarchy = unknown;
|
||||
type WellenTimeTable = unknown;
|
||||
type WellenSignal = unknown;
|
||||
type Timeline = unknown;
|
||||
type VarFormat = unknown;
|
||||
|
||||
export async function show_window(): Promise<void> {
|
||||
return await invoke("show_window");
|
||||
|
@ -21,12 +21,22 @@ export async function get_hierarchy(): Promise<WellenHierarchy> {
|
|||
return await invoke("get_hierarchy");
|
||||
}
|
||||
|
||||
export async function get_time_table(): Promise<WellenTimeTable> {
|
||||
return await invoke("get_time_table");
|
||||
}
|
||||
|
||||
export async function load_and_get_signal(signal_ref_index: number): Promise<WellenSignal> {
|
||||
return await invoke("load_and_get_signal", { signal_ref_index });
|
||||
export async function load_signal_and_get_timeline(
|
||||
signal_ref_index: number,
|
||||
timeline_zoom: number,
|
||||
timeline_viewport_width: number,
|
||||
timeline_viewport_x: number,
|
||||
block_height: number,
|
||||
var_format: VarFormat,
|
||||
): Promise<Timeline> {
|
||||
return await invoke("load_signal_and_get_timeline", {
|
||||
signal_ref_index,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format
|
||||
});
|
||||
}
|
||||
|
||||
export async function unload_signal(signal_ref_index: number): Promise<void> {
|
||||
|
|
|
@ -9,3 +9,11 @@ publish.workspace = true
|
|||
|
||||
[dependencies]
|
||||
wellen.workspace = true
|
||||
moonlight.workspace = true
|
||||
convert-base = "1.1.2"
|
||||
# @TODO update `futures_util_ext` - add feature `sink`, set exact `futures-util` version
|
||||
futures-util = { version = "0.3.30", features = ["sink"] }
|
||||
|
||||
[features]
|
||||
frontend = ["moonlight/frontend"]
|
||||
backend = ["moonlight/backend"]
|
||||
|
|
|
@ -1 +1,32 @@
|
|||
use moonlight::*;
|
||||
|
||||
mod var_format;
|
||||
pub use var_format::VarFormat;
|
||||
|
||||
mod signal_to_timeline;
|
||||
pub use signal_to_timeline::signal_to_timeline;
|
||||
|
||||
pub mod wellen_helpers;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
#[serde(crate = "serde")]
|
||||
pub struct Timeline {
|
||||
pub blocks: Vec<TimelineBlock>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
#[serde(crate = "serde")]
|
||||
pub struct TimelineBlock {
|
||||
pub x: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub label: Option<TimeLineBlockLabel>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
#[serde(crate = "serde")]
|
||||
pub struct TimeLineBlockLabel {
|
||||
pub text: String,
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
|
82
shared/src/signal_to_timeline.rs
Normal file
82
shared/src/signal_to_timeline.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn signal_to_timeline(
|
||||
signal: &wellen::Signal,
|
||||
time_table: &[wellen::Time],
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: VarFormat,
|
||||
) -> Timeline {
|
||||
const MIN_BLOCK_WIDTH: u32 = 3;
|
||||
// Courier New, 16px, sync with `label_style` in `pixi_canvas.rs`
|
||||
const LETTER_WIDTH: f64 = 9.61;
|
||||
const LETTER_HEIGHT: u32 = 18;
|
||||
const LABEL_X_PADDING: u32 = 10;
|
||||
|
||||
let Some(last_time) = time_table.last().copied() else {
|
||||
return Timeline::default();
|
||||
};
|
||||
|
||||
let last_time = last_time as f64;
|
||||
let timeline_viewport_x = timeline_viewport_x as f64;
|
||||
let timeline_width = timeline_viewport_width as f64 * timeline_zoom;
|
||||
|
||||
let mut x_value_pairs = signal
|
||||
.iter_changes()
|
||||
.map(|(index, value)| {
|
||||
let index = index as usize;
|
||||
let time = time_table[index] as f64;
|
||||
let x = time / last_time * timeline_width - timeline_viewport_x;
|
||||
(x, value)
|
||||
})
|
||||
.peekable();
|
||||
|
||||
// @TODO parallelize?
|
||||
let mut blocks = Vec::new();
|
||||
while let Some((block_x, value)) = x_value_pairs.next() {
|
||||
if block_x >= (timeline_viewport_width as f64) {
|
||||
break;
|
||||
}
|
||||
|
||||
let next_block_x = if let Some((next_block_x, _)) = x_value_pairs.peek() {
|
||||
*next_block_x
|
||||
} else {
|
||||
timeline_width - timeline_viewport_x
|
||||
};
|
||||
|
||||
let block_width = (next_block_x - block_x) as u32;
|
||||
if block_width < MIN_BLOCK_WIDTH {
|
||||
continue;
|
||||
}
|
||||
if block_x + (block_width as f64) <= 0. {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @TODO cache?
|
||||
let value = var_format.format(value);
|
||||
|
||||
let value_width = (value.chars().count() as f64 * LETTER_WIDTH) as u32;
|
||||
// @TODO Ellipsis instead of hiding?
|
||||
let label = if (value_width + (2 * LABEL_X_PADDING)) <= block_width {
|
||||
Some(TimeLineBlockLabel {
|
||||
text: value,
|
||||
x: (block_width - value_width) / 2,
|
||||
y: (block_height - LETTER_HEIGHT) / 2,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let block = TimelineBlock {
|
||||
x: block_x as i32,
|
||||
width: block_width,
|
||||
height: block_height,
|
||||
label,
|
||||
};
|
||||
blocks.push(block);
|
||||
}
|
||||
|
||||
Timeline { blocks }
|
||||
}
|
156
shared/src/var_format.rs
Normal file
156
shared/src/var_format.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
use moonlight::*;
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[serde(crate = "serde")]
|
||||
pub enum VarFormat {
|
||||
ASCII,
|
||||
Binary,
|
||||
BinaryWithGroups,
|
||||
#[default]
|
||||
Hexadecimal,
|
||||
Octal,
|
||||
Signed,
|
||||
Unsigned,
|
||||
}
|
||||
|
||||
impl VarFormat {
|
||||
pub fn as_static_str(&self) -> &'static str {
|
||||
match self {
|
||||
VarFormat::ASCII => "Text",
|
||||
VarFormat::Binary => "Bin",
|
||||
VarFormat::BinaryWithGroups => "Bins",
|
||||
VarFormat::Hexadecimal => "Hex",
|
||||
VarFormat::Octal => "Oct",
|
||||
VarFormat::Signed => "Int",
|
||||
VarFormat::Unsigned => "UInt",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Self {
|
||||
match self {
|
||||
VarFormat::ASCII => VarFormat::Binary,
|
||||
VarFormat::Binary => VarFormat::BinaryWithGroups,
|
||||
VarFormat::BinaryWithGroups => VarFormat::Hexadecimal,
|
||||
VarFormat::Hexadecimal => VarFormat::Octal,
|
||||
VarFormat::Octal => VarFormat::Signed,
|
||||
VarFormat::Signed => VarFormat::Unsigned,
|
||||
VarFormat::Unsigned => VarFormat::ASCII,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self, value: wellen::SignalValue) -> String {
|
||||
// @TODO optimize it by not using `.to_string` if possible
|
||||
let value = value.to_string();
|
||||
if value.is_empty() {
|
||||
return value;
|
||||
}
|
||||
match self {
|
||||
VarFormat::ASCII => {
|
||||
let mut formatted_value = String::new();
|
||||
for group_index in 0..value.len() / 8 {
|
||||
let offset = group_index * 8;
|
||||
let group = &value[offset..offset + 8];
|
||||
if let Ok(byte_char) = u8::from_str_radix(group, 2) {
|
||||
formatted_value.push(byte_char as char);
|
||||
}
|
||||
}
|
||||
formatted_value
|
||||
}
|
||||
VarFormat::Binary => value,
|
||||
VarFormat::BinaryWithGroups => {
|
||||
let char_count = value.len();
|
||||
value
|
||||
.chars()
|
||||
.enumerate()
|
||||
.fold(String::new(), |mut value, (index, one_or_zero)| {
|
||||
value.push(one_or_zero);
|
||||
let is_last = index == char_count - 1;
|
||||
if !is_last && (index + 1) % 4 == 0 {
|
||||
value.push(' ');
|
||||
}
|
||||
value
|
||||
})
|
||||
}
|
||||
VarFormat::Hexadecimal => {
|
||||
let ones_and_zeros = value
|
||||
.chars()
|
||||
.rev()
|
||||
.map(|char| char.to_digit(2).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut base = convert_base::Convert::new(2, 16);
|
||||
let output = base.convert::<u32, u32>(&ones_and_zeros);
|
||||
let value: String = output
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|number| char::from_digit(number, 16).unwrap())
|
||||
.collect();
|
||||
value
|
||||
}
|
||||
VarFormat::Octal => {
|
||||
let ones_and_zeros = value
|
||||
.chars()
|
||||
.rev()
|
||||
.map(|char| char.to_digit(2).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut base = convert_base::Convert::new(2, 8);
|
||||
let output = base.convert::<u32, u32>(&ones_and_zeros);
|
||||
let value: String = output
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|number| char::from_digit(number, 8).unwrap())
|
||||
.collect();
|
||||
value
|
||||
}
|
||||
VarFormat::Signed => {
|
||||
let mut ones_and_zeros = value
|
||||
.chars()
|
||||
.rev()
|
||||
.map(|char| char.to_digit(2).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// https://builtin.com/articles/twos-complement
|
||||
let sign = if ones_and_zeros.last().unwrap() == &0 {
|
||||
""
|
||||
} else {
|
||||
"-"
|
||||
};
|
||||
if sign == "-" {
|
||||
let mut one_found = false;
|
||||
for one_or_zero in &mut ones_and_zeros {
|
||||
if one_found {
|
||||
*one_or_zero = if one_or_zero == &0 { 1 } else { 0 }
|
||||
} else if one_or_zero == &1 {
|
||||
one_found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut base = convert_base::Convert::new(2, 10);
|
||||
let output = base.convert::<u32, u32>(&ones_and_zeros);
|
||||
let value_without_sign: String = output
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|number| char::from_digit(number, 10).unwrap())
|
||||
.collect();
|
||||
// @TODO chain `sign` before collecting?
|
||||
let value = sign.to_owned() + &value_without_sign;
|
||||
value
|
||||
}
|
||||
VarFormat::Unsigned => {
|
||||
let ones_and_zeros = value
|
||||
.chars()
|
||||
.rev()
|
||||
.map(|char| char.to_digit(2).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut base = convert_base::Convert::new(2, 10);
|
||||
let output = base.convert::<u32, u32>(&ones_and_zeros);
|
||||
let value: String = output
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|number| char::from_digit(number, 10).unwrap())
|
||||
.collect();
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,8 +16,8 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||
tauri-build = { version = "=2.0.0-beta.17", features = [] }
|
||||
|
||||
[dependencies]
|
||||
shared.workspace = true
|
||||
wellen.workspace = true
|
||||
shared = { path = "../shared", features = ["backend"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "=2.0.0-beta.22", features = ["macos-private-api", "linux-ipc-protocol"] }
|
||||
|
|
|
@ -40,25 +40,32 @@ async fn get_hierarchy(store: tauri::State<'_, Store>) -> Result<serde_json::Val
|
|||
}
|
||||
|
||||
#[tauri::command(rename_all = "snake_case")]
|
||||
async fn get_time_table(store: tauri::State<'_, Store>) -> Result<serde_json::Value, ()> {
|
||||
let waveform = store.waveform.lock().unwrap();
|
||||
let time_table = waveform.as_ref().unwrap().time_table();
|
||||
Ok(serde_json::to_value(time_table).unwrap())
|
||||
}
|
||||
|
||||
#[tauri::command(rename_all = "snake_case")]
|
||||
async fn load_and_get_signal(
|
||||
async fn load_signal_and_get_timeline(
|
||||
signal_ref_index: usize,
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: shared::VarFormat,
|
||||
store: tauri::State<'_, Store>,
|
||||
) -> Result<serde_json::Value, ()> {
|
||||
// @TODO run (all?) in a blocking thread?
|
||||
let signal_ref = wellen::SignalRef::from_index(signal_ref_index).unwrap();
|
||||
let mut waveform_lock = store.waveform.lock().unwrap();
|
||||
let waveform = waveform_lock.as_mut().unwrap();
|
||||
// @TODO maybe run it in a thread to not block the main one and then
|
||||
// make the command async or return the result through a Tauri channel
|
||||
waveform.load_signals_multi_threaded(&[signal_ref]);
|
||||
let signal = waveform.get_signal(signal_ref).unwrap();
|
||||
Ok(serde_json::to_value(signal).unwrap())
|
||||
let time_table = waveform.time_table();
|
||||
let timeline = shared::signal_to_timeline(
|
||||
signal,
|
||||
time_table,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format,
|
||||
);
|
||||
Ok(serde_json::to_value(timeline).unwrap())
|
||||
}
|
||||
|
||||
#[tauri::command(rename_all = "snake_case")]
|
||||
|
@ -85,8 +92,7 @@ pub fn run() {
|
|||
show_window,
|
||||
pick_and_load_waveform,
|
||||
get_hierarchy,
|
||||
get_time_table,
|
||||
load_and_get_signal,
|
||||
load_signal_and_get_timeline,
|
||||
unload_signal,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
|
Loading…
Reference in a new issue