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
|
!/src-tauri
|
||||||
|
!/shared
|
||||||
|
|
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -859,6 +859,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert-base"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e56a404f2112d00ba80a516b60ca3744ef6bd776baa14126eb7d7c11b42cac8a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -1538,9 +1544,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.30"
|
version = "0.3.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -1568,9 +1574,9 @@ checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.30"
|
version = "0.3.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
@ -4124,6 +4130,9 @@ dependencies = [
|
||||||
name = "shared"
|
name = "shared"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"convert-base",
|
||||||
|
"futures-util",
|
||||||
|
"moonlight",
|
||||||
"wellen",
|
"wellen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -5038,9 +5047,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tray-icon"
|
name = "tray-icon"
|
||||||
version = "0.14.2"
|
version = "0.14.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b27516dfcfa22a9faaf192283a122bfbede38c1e59ef194e3c4db6549b419c0"
|
checksum = "3ad8319cca93189ea9ab1b290de0595960529750b6b8b501a399ed1ec3775d60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
|
|
|
@ -16,11 +16,12 @@ readme = "../README.md"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
shared = { path = "./shared" }
|
|
||||||
# wellen = { version = "0.9.9", features = ["serde1"] }
|
# wellen = { version = "0.9.9", features = ["serde1"] }
|
||||||
# wellen = { path = "../wellen/wellen", features = ["serde1"] }
|
# wellen = { path = "../wellen/wellen", features = ["serde1"] }
|
||||||
wellen = { git = "https://github.com/MartinKavik/wellen", features = ["serde1"], branch = "new_pub_types" }
|
wellen = { git = "https://github.com/MartinKavik/wellen", features = ["serde1"], branch = "new_pub_types" }
|
||||||
# moon = { path = "../../crates/moon" }
|
# moon = { path = "../../crates/moon" }
|
||||||
# zoon = { path = "../../crates/zoon" }
|
# zoon = { path = "../../crates/zoon" }
|
||||||
|
# moonlight = { path = "../../crates/zoon" }
|
||||||
zoon = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" }
|
zoon = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" }
|
||||||
moon = { 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",
|
"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]
|
[tasks.bundle]
|
||||||
description = "Compile in the release mode and create installation packages"
|
description = "Compile in the release mode and create installation packages"
|
||||||
dependencies = ["tauri_build", "show_release_paths"]
|
dependencies = ["tauri_build", "show_release_paths"]
|
||||||
|
@ -100,6 +109,11 @@ description = "Run `mzoon start`"
|
||||||
extend = "mzoon"
|
extend = "mzoon"
|
||||||
args = ["start"]
|
args = ["start"]
|
||||||
|
|
||||||
|
[tasks.mzoon_start_release]
|
||||||
|
description = "Run `mzoon start --release`"
|
||||||
|
extend = "mzoon"
|
||||||
|
args = ["start", "--release"]
|
||||||
|
|
||||||
[tasks.tauri_dev_with_cleanup]
|
[tasks.tauri_dev_with_cleanup]
|
||||||
description = "Run forked `tauri dev` with cleanup"
|
description = "Run forked `tauri dev` with cleanup"
|
||||||
run_task = { fork = true, cleanup_task = "kill_watchers", name = ["tauri_dev"] }
|
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"
|
description = "Run forked `mzoon start` with cleanup"
|
||||||
run_task = { fork = true, cleanup_task = "kill_watchers", name = ["mzoon_start"] }
|
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]
|
[tasks.kill_watchers]
|
||||||
description = "Kill the cargo-make/makers process and all its children / forked processes"
|
description = "Kill the cargo-make/makers process and all its children / forked processes"
|
||||||
script_runner = "@duckscript"
|
script_runner = "@duckscript"
|
||||||
|
|
|
@ -15,8 +15,10 @@ origins = ["*"]
|
||||||
frontend = [
|
frontend = [
|
||||||
"public",
|
"public",
|
||||||
"frontend/Cargo.toml",
|
"frontend/Cargo.toml",
|
||||||
"frontend/typescript/bundles",
|
|
||||||
"frontend/src",
|
"frontend/src",
|
||||||
|
"frontend/typescript/bundles",
|
||||||
|
"shared/Cargo.toml",
|
||||||
|
"shared/src",
|
||||||
]
|
]
|
||||||
backend = [
|
backend = [
|
||||||
"backend/Cargo.toml",
|
"backend/Cargo.toml",
|
||||||
|
|
43
README.md
43
README.md
|
@ -4,11 +4,23 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -22,7 +34,7 @@
|
||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
### Start:
|
### Start the desktop version:
|
||||||
|
|
||||||
1. `makers start`
|
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`
|
1. `makers start_browser`
|
||||||
2. Ctrl + Click the server URL mentioned in the terminal log
|
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:
|
### Steps before pushing:
|
||||||
|
|
||||||
1. `makers format`
|
1. `makers format`
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Production build:
|
|
||||||
|
|
||||||
1. `makers bundle`
|
### Test files
|
||||||
2. Runnable executable is in `target/release`
|
|
||||||
3. Installable bundles specific for the platform are in `target/release/bundle`
|
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"
|
wasm-bindgen-test = "0.3.19"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
shared.workspace = true
|
|
||||||
zoon.workspace = true
|
zoon.workspace = true
|
||||||
wellen.workspace = true
|
wellen.workspace = true
|
||||||
|
shared = { path = "../shared", features = ["frontend"] }
|
||||||
web-sys = { version = "*", features = ["FileSystemFileHandle"] }
|
web-sys = { version = "*", features = ["FileSystemFileHandle"] }
|
||||||
gloo-file = { version = "0.3.0", features = ["futures"] }
|
gloo-file = { version = "0.3.0", features = ["futures"] }
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::{platform, HierarchyAndTimeTable, Layout};
|
use crate::{platform, Layout};
|
||||||
use futures_util::join;
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::ops::Not;
|
use std::ops::Not;
|
||||||
|
@ -35,7 +34,7 @@ struct ScopeForUI {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ControlsPanel {
|
pub struct ControlsPanel {
|
||||||
selected_scope_ref: Mutable<Option<wellen::ScopeRef>>,
|
selected_scope_ref: Mutable<Option<wellen::ScopeRef>>,
|
||||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||||
layout: Mutable<Layout>,
|
layout: Mutable<Layout>,
|
||||||
loaded_filename: Mutable<Option<Filename>>,
|
loaded_filename: Mutable<Option<Filename>>,
|
||||||
|
@ -43,13 +42,13 @@ pub struct ControlsPanel {
|
||||||
|
|
||||||
impl ControlsPanel {
|
impl ControlsPanel {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||||
layout: Mutable<Layout>,
|
layout: Mutable<Layout>,
|
||||||
) -> impl Element {
|
) -> impl Element {
|
||||||
Self {
|
Self {
|
||||||
selected_scope_ref: <_>::default(),
|
selected_scope_ref: <_>::default(),
|
||||||
hierarchy_and_time_table,
|
hierarchy,
|
||||||
selected_var_refs,
|
selected_var_refs,
|
||||||
layout,
|
layout,
|
||||||
loaded_filename: <_>::default(),
|
loaded_filename: <_>::default(),
|
||||||
|
@ -60,7 +59,7 @@ impl ControlsPanel {
|
||||||
fn triggers(&self) -> Vec<TaskHandle> {
|
fn triggers(&self) -> Vec<TaskHandle> {
|
||||||
vec![Task::start_droppable(clone!((self => s) async move {
|
vec![Task::start_droppable(clone!((self => s) async move {
|
||||||
let was_some = Cell::new(false);
|
let was_some = Cell::new(false);
|
||||||
s.hierarchy_and_time_table
|
s.hierarchy
|
||||||
.signal_ref(Option::is_some)
|
.signal_ref(Option::is_some)
|
||||||
.dedupe()
|
.dedupe()
|
||||||
.for_each_sync(clone!((s) move |is_some| {
|
.for_each_sync(clone!((s) move |is_some| {
|
||||||
|
@ -81,8 +80,8 @@ impl ControlsPanel {
|
||||||
let layout = self.layout.clone();
|
let layout = self.layout.clone();
|
||||||
let layout_and_hierarchy_signal = map_ref! {
|
let layout_and_hierarchy_signal = map_ref! {
|
||||||
let layout = layout.signal(),
|
let layout = layout.signal(),
|
||||||
let hierarchy_and_time_table = self.hierarchy_and_time_table.signal_cloned() => {
|
let hierarchy = self.hierarchy.signal_cloned() => {
|
||||||
(*layout, hierarchy_and_time_table.clone().map(|(hierarchy, _)| hierarchy))
|
(*layout, hierarchy.clone())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Column::new()
|
Column::new()
|
||||||
|
@ -99,7 +98,6 @@ impl ControlsPanel {
|
||||||
Layout::Columns => Height::fill().max(MILLER_COLUMN_MAX_HEIGHT),
|
Layout::Columns => Height::fill().max(MILLER_COLUMN_MAX_HEIGHT),
|
||||||
},
|
},
|
||||||
)))
|
)))
|
||||||
.s(Scrollbars::both())
|
|
||||||
.s(Padding::all(20))
|
.s(Padding::all(20))
|
||||||
.s(Gap::new().y(40))
|
.s(Gap::new().y(40))
|
||||||
.s(Align::new().top())
|
.s(Align::new().top())
|
||||||
|
@ -111,9 +109,9 @@ impl ControlsPanel {
|
||||||
.item(self.layout_switcher()),
|
.item(self.layout_switcher()),
|
||||||
)
|
)
|
||||||
.item_signal(
|
.item_signal(
|
||||||
self.hierarchy_and_time_table
|
self.hierarchy
|
||||||
.signal_cloned()
|
.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(
|
.item_signal(layout_and_hierarchy_signal.map(
|
||||||
clone!((self => s) move |(layout, hierarchy)| {
|
clone!((self => s) move |(layout, hierarchy)| {
|
||||||
|
@ -127,7 +125,7 @@ impl ControlsPanel {
|
||||||
#[cfg(FASTWAVE_PLATFORM = "TAURI")]
|
#[cfg(FASTWAVE_PLATFORM = "TAURI")]
|
||||||
fn load_button(&self) -> impl Element {
|
fn load_button(&self) -> impl Element {
|
||||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
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 loaded_filename = self.loaded_filename.clone();
|
||||||
Button::new()
|
Button::new()
|
||||||
.s(Padding::new().x(20).y(10))
|
.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_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||||
.on_press(move || {
|
.on_press(move || {
|
||||||
let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut();
|
let mut hierarchy_lock = hierarchy.lock_mut();
|
||||||
if hierarchy_and_time_table_lock.is_some() {
|
if hierarchy_lock.is_some() {
|
||||||
*hierarchy_and_time_table_lock = None;
|
*hierarchy_lock = None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
drop(hierarchy_and_time_table_lock);
|
drop(hierarchy_lock);
|
||||||
let hierarchy_and_time_table = hierarchy_and_time_table.clone();
|
let hierarchy = hierarchy.clone();
|
||||||
let loaded_filename = loaded_filename.clone();
|
let loaded_filename = loaded_filename.clone();
|
||||||
Task::start(async move {
|
Task::start(async move {
|
||||||
if let Some(filename) = platform::pick_and_load_waveform(None).await {
|
if let Some(filename) = platform::pick_and_load_waveform(None).await {
|
||||||
loaded_filename.set_neq(Some(filename));
|
loaded_filename.set_neq(Some(filename));
|
||||||
let (hierarchy, time_table) =
|
hierarchy.set(Some(Rc::new(platform::get_hierarchy().await)))
|
||||||
join!(platform::get_hierarchy(), platform::get_time_table());
|
|
||||||
hierarchy_and_time_table
|
|
||||||
.set(Some((Rc::new(hierarchy), Rc::new(time_table))))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -167,7 +162,7 @@ impl ControlsPanel {
|
||||||
#[cfg(FASTWAVE_PLATFORM = "BROWSER")]
|
#[cfg(FASTWAVE_PLATFORM = "BROWSER")]
|
||||||
fn load_button(&self) -> impl Element {
|
fn load_button(&self) -> impl Element {
|
||||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
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 loaded_filename = self.loaded_filename.clone();
|
||||||
let file_input_id = "file_input";
|
let file_input_id = "file_input";
|
||||||
Row::new()
|
Row::new()
|
||||||
|
@ -175,7 +170,8 @@ impl ControlsPanel {
|
||||||
Label::new()
|
Label::new()
|
||||||
.s(Padding::new().x(20).y(10))
|
.s(Padding::new().x(20).y(10))
|
||||||
.s(Background::new().color_signal(
|
.s(Background::new().color_signal(
|
||||||
hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
|
hovered_signal
|
||||||
|
.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
|
||||||
))
|
))
|
||||||
.s(Align::new().left())
|
.s(Align::new().left())
|
||||||
.s(RoundedCorners::all(15))
|
.s(RoundedCorners::all(15))
|
||||||
|
@ -188,50 +184,52 @@ impl ControlsPanel {
|
||||||
))
|
))
|
||||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||||
.for_input(file_input_id)
|
.for_input(file_input_id)
|
||||||
.on_click_event_with_options(EventOptions::new().preventable(), clone!((hierarchy_and_time_table) move |event| {
|
.on_click_event_with_options(
|
||||||
let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut();
|
EventOptions::new().preventable(),
|
||||||
if hierarchy_and_time_table_lock.is_some() {
|
clone!((hierarchy) move |event| {
|
||||||
*hierarchy_and_time_table_lock = None;
|
let mut hierarchy_lock = hierarchy.lock_mut();
|
||||||
if let RawMouseEvent::Click(raw_event) = event.raw_event {
|
if hierarchy_lock.is_some() {
|
||||||
// @TODO Move to MoonZoon as a new API
|
*hierarchy_lock = None;
|
||||||
raw_event.prevent_default();
|
if let RawMouseEvent::Click(raw_event) = event.raw_event {
|
||||||
|
// @TODO Move to MoonZoon as a new API
|
||||||
|
raw_event.prevent_default();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
}),
|
||||||
}
|
),
|
||||||
}))
|
|
||||||
)
|
)
|
||||||
.item(
|
.item(
|
||||||
// @TODO https://github.com/MoonZoon/MoonZoon/issues/39
|
// @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
|
// + 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()
|
TextInput::new().id(file_input_id).update_raw_el(|raw_el| {
|
||||||
.id(file_input_id)
|
let dom_element = raw_el.dom_element();
|
||||||
.update_raw_el(|raw_el| {
|
raw_el
|
||||||
let dom_element = raw_el.dom_element();
|
.style("display", "none")
|
||||||
raw_el
|
.attr("type", "file")
|
||||||
.style("display", "none")
|
.event_handler(move |_: events::Input| {
|
||||||
.attr("type", "file")
|
let Some(file_list) =
|
||||||
.event_handler(move |_: events::Input| {
|
dom_element.files().map(gloo_file::FileList::from)
|
||||||
let Some(file_list) = dom_element.files().map(gloo_file::FileList::from) else {
|
else {
|
||||||
zoon::println!("file list is `None`");
|
zoon::println!("file list is `None`");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(file) = file_list.first().cloned() else {
|
let Some(file) = file_list.first().cloned() else {
|
||||||
zoon::println!("file list is empty");
|
zoon::println!("file list is empty");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let hierarchy_and_time_table = hierarchy_and_time_table.clone();
|
let hierarchy = hierarchy.clone();
|
||||||
let loaded_filename = loaded_filename.clone();
|
let loaded_filename = loaded_filename.clone();
|
||||||
Task::start(async move {
|
Task::start(async move {
|
||||||
if let Some(filename) = platform::pick_and_load_waveform(Some(file)).await {
|
if let Some(filename) =
|
||||||
loaded_filename.set_neq(Some(filename));
|
platform::pick_and_load_waveform(Some(file)).await
|
||||||
let (hierarchy, time_table) =
|
{
|
||||||
join!(platform::get_hierarchy(), platform::get_time_table());
|
loaded_filename.set_neq(Some(filename));
|
||||||
hierarchy_and_time_table
|
hierarchy.set(Some(Rc::new(platform::get_hierarchy().await)))
|
||||||
.set(Some((Rc::new(hierarchy), Rc::new(time_table))))
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,6 +561,7 @@ impl ControlsPanel {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lazy loading to not freeze the main thread
|
// 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;
|
const CHUNK_SIZE: usize = 50;
|
||||||
let mut chunked_vars_for_ui: Vec<Vec<VarForUI>> = <_>::default();
|
let mut chunked_vars_for_ui: Vec<Vec<VarForUI>> = <_>::default();
|
||||||
let mut chunk = Vec::with_capacity(CHUNK_SIZE);
|
let mut chunk = Vec::with_capacity(CHUNK_SIZE);
|
||||||
|
|
|
@ -9,8 +9,6 @@ use controls_panel::ControlsPanel;
|
||||||
mod waveform_panel;
|
mod waveform_panel;
|
||||||
use waveform_panel::WaveformPanel;
|
use waveform_panel::WaveformPanel;
|
||||||
|
|
||||||
type HierarchyAndTimeTable = (Rc<wellen::Hierarchy>, Rc<wellen::TimeTable>);
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy, Default)]
|
||||||
enum Layout {
|
enum Layout {
|
||||||
Tree,
|
Tree,
|
||||||
|
@ -28,7 +26,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn root() -> impl Element {
|
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 selected_var_refs: MutableVec<wellen::VarRef> = <_>::default();
|
||||||
let layout: Mutable<Layout> = <_>::default();
|
let layout: Mutable<Layout> = <_>::default();
|
||||||
Column::new()
|
Column::new()
|
||||||
|
@ -40,17 +38,26 @@ fn root() -> impl Element {
|
||||||
.s(Height::fill())
|
.s(Height::fill())
|
||||||
.s(Gap::new().x(15))
|
.s(Gap::new().x(15))
|
||||||
.item(ControlsPanel::new(
|
.item(ControlsPanel::new(
|
||||||
hierarchy_and_time_table.clone(),
|
hierarchy.clone(),
|
||||||
selected_var_refs.clone(),
|
selected_var_refs.clone(),
|
||||||
layout.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(
|
.item_signal(
|
||||||
hierarchy_and_time_table.clone(),
|
layout
|
||||||
selected_var_refs.clone(),
|
.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
|
platform::get_hierarchy().await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_time_table() -> wellen::TimeTable {
|
pub async fn load_signal_and_get_timeline(
|
||||||
platform::get_time_table().await
|
signal_ref: wellen::SignalRef,
|
||||||
}
|
timeline_zoom: f64,
|
||||||
|
timeline_viewport_width: u32,
|
||||||
pub async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
|
timeline_viewport_x: i32,
|
||||||
platform::load_and_get_signal(signal_ref).await
|
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) {
|
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()
|
serde_json::from_value(serde_json::to_value(hierarchy).unwrap_throw()).unwrap_throw()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn get_time_table() -> wellen::TimeTable {
|
pub(super) async fn load_signal_and_get_timeline(
|
||||||
let waveform = STORE.waveform.lock().unwrap_throw();
|
signal_ref: wellen::SignalRef,
|
||||||
let time_table = waveform.as_ref().unwrap_throw().time_table();
|
timeline_zoom: f64,
|
||||||
// @TODO Wrap `time_table` in `Waveform` with `Rc/Arc` or add the method `take` / `clone` or refactor?
|
timeline_viewport_width: u32,
|
||||||
serde_json::from_value(serde_json::to_value(time_table).unwrap_throw()).unwrap_throw()
|
timeline_viewport_x: i32,
|
||||||
}
|
block_height: u32,
|
||||||
|
var_format: shared::VarFormat,
|
||||||
pub(super) async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
|
) -> shared::Timeline {
|
||||||
let mut waveform_lock = STORE.waveform.lock().unwrap_throw();
|
let mut waveform_lock = STORE.waveform.lock().unwrap();
|
||||||
let waveform = waveform_lock.as_mut().unwrap_throw();
|
let waveform = waveform_lock.as_mut().unwrap();
|
||||||
// @TODO maybe run it in a thread to not block the main one and then
|
waveform.load_signals_multi_threaded(&[signal_ref]);
|
||||||
waveform.load_signals(&[signal_ref]);
|
let signal = waveform.get_signal(signal_ref).unwrap();
|
||||||
let signal = waveform.get_signal(signal_ref).unwrap_throw();
|
let time_table = waveform.time_table();
|
||||||
// @TODO `clone` / `Rc/Arc` / refactor?
|
let timeline = shared::signal_to_timeline(
|
||||||
serde_json::from_value(serde_json::to_value(signal).unwrap_throw()).unwrap_throw()
|
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) {
|
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()
|
serde_wasm_bindgen::from_value(tauri_glue::get_hierarchy().await.unwrap_throw()).unwrap_throw()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn get_time_table() -> wellen::TimeTable {
|
pub(super) async fn load_signal_and_get_timeline(
|
||||||
serde_wasm_bindgen::from_value(tauri_glue::get_time_table().await.unwrap_throw()).unwrap_throw()
|
signal_ref: wellen::SignalRef,
|
||||||
}
|
timeline_zoom: f64,
|
||||||
|
timeline_viewport_width: u32,
|
||||||
pub(super) async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
|
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(
|
serde_wasm_bindgen::from_value(
|
||||||
tauri_glue::load_and_get_signal(signal_ref.index())
|
tauri_glue::load_signal_and_get_timeline(
|
||||||
.await
|
signal_ref.index(),
|
||||||
.unwrap_throw(),
|
timeline_zoom,
|
||||||
|
timeline_viewport_width,
|
||||||
|
timeline_viewport_x,
|
||||||
|
block_height,
|
||||||
|
var_format,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_throw(),
|
||||||
)
|
)
|
||||||
.unwrap_throw()
|
.unwrap_throw()
|
||||||
}
|
}
|
||||||
|
@ -52,10 +63,14 @@ mod tauri_glue {
|
||||||
pub async fn get_hierarchy() -> Result<JsValue, JsValue>;
|
pub async fn get_hierarchy() -> Result<JsValue, JsValue>;
|
||||||
|
|
||||||
#[wasm_bindgen(catch)]
|
#[wasm_bindgen(catch)]
|
||||||
pub async fn get_time_table() -> Result<JsValue, JsValue>;
|
pub async fn load_signal_and_get_timeline(
|
||||||
|
signal_ref_index: usize,
|
||||||
#[wasm_bindgen(catch)]
|
timeline_zoom: f64,
|
||||||
pub async fn load_and_get_signal(signal_ref_index: usize) -> Result<JsValue, JsValue>;
|
timeline_viewport_width: u32,
|
||||||
|
timeline_viewport_x: i32,
|
||||||
|
block_height: u32,
|
||||||
|
var_format: JsValue,
|
||||||
|
) -> Result<JsValue, JsValue>;
|
||||||
|
|
||||||
#[wasm_bindgen(catch)]
|
#[wasm_bindgen(catch)]
|
||||||
pub async fn unload_signal(signal_ref_index: usize) -> Result<(), JsValue>;
|
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 wellen::GetItem;
|
||||||
use zoon::{eprintln, *};
|
use zoon::*;
|
||||||
|
|
||||||
mod pixi_canvas;
|
mod pixi_canvas;
|
||||||
use pixi_canvas::{PixiCanvas, PixiController};
|
use pixi_canvas::{PixiCanvas, PixiController};
|
||||||
|
@ -11,21 +12,24 @@ const ROW_GAP: u32 = 4;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WaveformPanel {
|
pub struct WaveformPanel {
|
||||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||||
|
canvas_controller: Mutable<ReadOnlyMutable<Option<PixiController>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WaveformPanel {
|
impl WaveformPanel {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||||
) -> impl Element {
|
) -> impl Element {
|
||||||
Self {
|
Self {
|
||||||
selected_var_refs,
|
selected_var_refs,
|
||||||
hierarchy_and_time_table,
|
hierarchy,
|
||||||
|
canvas_controller: Mutable::new(Mutable::default().read_only()),
|
||||||
}
|
}
|
||||||
.root()
|
.root()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @TODO autoscroll down
|
||||||
fn root(&self) -> impl Element {
|
fn root(&self) -> impl Element {
|
||||||
let selected_vars_panel_height_getter: Mutable<u32> = <_>::default();
|
let selected_vars_panel_height_getter: Mutable<u32> = <_>::default();
|
||||||
Row::new()
|
Row::new()
|
||||||
|
@ -51,27 +55,29 @@ impl WaveformPanel {
|
||||||
|
|
||||||
fn canvas(&self, selected_vars_panel_height: ReadOnlyMutable<u32>) -> impl Element {
|
fn canvas(&self, selected_vars_panel_height: ReadOnlyMutable<u32>) -> impl Element {
|
||||||
let selected_var_refs = self.selected_var_refs.clone();
|
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)
|
PixiCanvas::new(ROW_HEIGHT, ROW_GAP)
|
||||||
.s(Align::new().top())
|
.s(Align::new().top())
|
||||||
.s(Width::fill())
|
.s(Width::fill())
|
||||||
.s(Height::exact_signal(selected_vars_panel_height.signal()))
|
.s(Height::exact_signal(selected_vars_panel_height.signal()))
|
||||||
.s(RoundedCorners::new().right(15))
|
|
||||||
.task_with_controller(move |controller| {
|
.task_with_controller(move |controller| {
|
||||||
selected_var_refs.signal_vec().delay_remove(clone!((hierarchy_and_time_table) move |var_ref| {
|
canvas_controller.set(controller.clone());
|
||||||
clone!((var_ref, hierarchy_and_time_table) async move {
|
selected_var_refs.signal_vec().delay_remove(clone!((hierarchy) move |var_ref| {
|
||||||
if let Some(hierarchy_and_time_table) = hierarchy_and_time_table.get_cloned() {
|
clone!((var_ref, hierarchy) async move {
|
||||||
platform::unload_signal(hierarchy_and_time_table.0.get(var_ref).signal_ref()).await;
|
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| {
|
})).for_each(clone!((controller, hierarchy) move |vec_diff| {
|
||||||
clone!((controller, hierarchy_and_time_table) async move {
|
clone!((controller, hierarchy) async move {
|
||||||
match vec_diff {
|
match vec_diff {
|
||||||
VecDiff::Replace { values } => {
|
VecDiff::Replace { values } => {
|
||||||
let controller = controller.wait_for_some_cloned().await;
|
let controller = controller.wait_for_some_cloned().await;
|
||||||
controller.clear_vars();
|
controller.clear_vars();
|
||||||
for var_ref in values {
|
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`") }
|
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::Move { old_index: _, new_index: _ } => { todo!("`task_with_controller` + `Move`") }
|
||||||
VecDiff::Push { value: var_ref } => {
|
VecDiff::Push { value: var_ref } => {
|
||||||
if let Some(controller) = controller.lock_ref().as_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 {} => {
|
VecDiff::Pop {} => {
|
||||||
|
@ -105,38 +111,34 @@ impl WaveformPanel {
|
||||||
|
|
||||||
async fn push_var(
|
async fn push_var(
|
||||||
controller: &PixiController,
|
controller: &PixiController,
|
||||||
hierarchy_and_time_table: &Mutable<Option<HierarchyAndTimeTable>>,
|
hierarchy: &Mutable<Option<Rc<wellen::Hierarchy>>>,
|
||||||
var_ref: wellen::VarRef,
|
var_ref: wellen::VarRef,
|
||||||
) {
|
) {
|
||||||
let (hierarchy, time_table) = hierarchy_and_time_table.get_cloned().unwrap();
|
let hierarchy = hierarchy.get_cloned().unwrap();
|
||||||
if time_table.is_empty() {
|
|
||||||
eprintln!("timetable is empty");
|
let var_format = shared::VarFormat::default();
|
||||||
return;
|
|
||||||
}
|
|
||||||
let last_time = time_table.last().copied().unwrap_throw();
|
|
||||||
|
|
||||||
let var = hierarchy.get(var_ref);
|
let var = hierarchy.get(var_ref);
|
||||||
let signal_ref = var.signal_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();
|
let timescale = hierarchy.timescale();
|
||||||
// @TODO remove
|
// @TODO remove
|
||||||
zoon::println!("{timescale:?}");
|
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'
|
// 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(
|
fn selected_var_panel(
|
||||||
|
@ -144,27 +146,74 @@ impl WaveformPanel {
|
||||||
index: ReadOnlyMutable<Option<usize>>,
|
index: ReadOnlyMutable<Option<usize>>,
|
||||||
var_ref: wellen::VarRef,
|
var_ref: wellen::VarRef,
|
||||||
) -> Option<impl Element> {
|
) -> Option<impl Element> {
|
||||||
let Some((hierarchy, _)) = self.hierarchy_and_time_table.get_cloned() else {
|
let Some(hierarchy) = self.hierarchy.get_cloned() else {
|
||||||
None?
|
None?
|
||||||
};
|
};
|
||||||
let var = hierarchy.get(var_ref);
|
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 selected_var_refs = self.selected_var_refs.clone();
|
||||||
|
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||||
Button::new()
|
Button::new()
|
||||||
.s(Height::exact(ROW_HEIGHT))
|
.s(Height::exact(ROW_HEIGHT))
|
||||||
.s(Background::new().color(color!("SlateBlue", 0.8)))
|
.s(Width::growable())
|
||||||
.s(RoundedCorners::new().left(15))
|
.s(Background::new().color_signal(
|
||||||
|
hovered_signal.map_bool(|| color!("SlateBlue"), || color!("SlateBlue", 0.8)),
|
||||||
|
))
|
||||||
|
.s(RoundedCorners::new().left(15).right(5))
|
||||||
.label(
|
.label(
|
||||||
El::new()
|
El::new()
|
||||||
.s(Align::center())
|
.s(Align::new().left())
|
||||||
.s(Padding::new().left(20).right(17).y(10))
|
.s(Padding::new().left(20).right(17).y(10))
|
||||||
.child(name),
|
.child(name),
|
||||||
)
|
)
|
||||||
|
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||||
.on_press(move || {
|
.on_press(move || {
|
||||||
if let Some(index) = index.get() {
|
if let Some(index) = index.get() {
|
||||||
selected_var_refs.lock_mut().remove(index);
|
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;
|
pub use js_bridge::PixiController;
|
||||||
|
use std::rc::Rc;
|
||||||
use zoon::*;
|
use zoon::*;
|
||||||
|
|
||||||
pub struct PixiCanvas {
|
pub struct PixiCanvas {
|
||||||
|
@ -35,16 +37,45 @@ impl PixiCanvas {
|
||||||
let height = Mutable::new(0);
|
let height = Mutable::new(0);
|
||||||
let resize_task = Task::start_droppable(
|
let resize_task = Task::start_droppable(
|
||||||
map_ref! {
|
map_ref! {
|
||||||
let _ = width.signal(),
|
let width = width.signal(),
|
||||||
let _ = height.signal() => ()
|
let height = height.signal() => (*width, *height)
|
||||||
}
|
}
|
||||||
.for_each_sync(clone!((controller) move |_| {
|
.dedupe()
|
||||||
if let Some(controller) = controller.lock_ref().as_ref() {
|
.throttle(|| Timer::sleep(50))
|
||||||
controller.queue_resize();
|
.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);
|
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 {
|
Self {
|
||||||
controller: controller.read_only(),
|
controller: controller.read_only(),
|
||||||
width: width.read_only(),
|
width: width.read_only(),
|
||||||
|
@ -56,14 +87,36 @@ impl PixiCanvas {
|
||||||
width.set_neq(new_width);
|
width.set_neq(new_width);
|
||||||
height.set_neq(new_height);
|
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 {
|
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;
|
pixi_controller.init(&element).await;
|
||||||
controller.set(Some(pixi_controller));
|
controller.set(Some(pixi_controller));
|
||||||
});
|
});
|
||||||
}))
|
}))
|
||||||
.after_remove(move |_| {
|
.after_remove(move |_| {
|
||||||
|
drop(timeline_getter);
|
||||||
drop(resize_task);
|
drop(resize_task);
|
||||||
drop(task_with_controller);
|
drop(task_with_controller);
|
||||||
if let Some(controller) = controller.take() {
|
if let Some(controller) = controller.take() {
|
||||||
|
@ -87,6 +140,24 @@ impl PixiCanvas {
|
||||||
mod js_bridge {
|
mod js_bridge {
|
||||||
use zoon::*;
|
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`
|
// Note: Add all corresponding methods to `frontend/typescript/pixi_canvas/pixi_canvas.ts`
|
||||||
#[wasm_bindgen(module = "/typescript/bundles/pixi_canvas.js")]
|
#[wasm_bindgen(module = "/typescript/bundles/pixi_canvas.js")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
@ -95,24 +166,56 @@ mod js_bridge {
|
||||||
|
|
||||||
// @TODO `row_height` and `row_gap` is FastWave-specific
|
// @TODO `row_height` and `row_gap` is FastWave-specific
|
||||||
#[wasm_bindgen(constructor)]
|
#[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)]
|
#[wasm_bindgen(method)]
|
||||||
pub async fn init(this: &PixiController, parent_element: &JsValue);
|
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)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn destroy(this: &PixiController);
|
pub fn destroy(this: &PixiController);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[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 --
|
// -- 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)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn remove_var(this: &PixiController, index: usize);
|
pub fn remove_var(this: &PixiController, index: usize);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[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)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn pop_var(this: &PixiController);
|
pub fn pop_var(this: &PixiController);
|
||||||
|
|
|
@ -35117,9 +35117,11 @@ var Text = class extends AbstractText {
|
||||||
};
|
};
|
||||||
|
|
||||||
// node_modules/pixi.js/lib/index.mjs
|
// node_modules/pixi.js/lib/index.mjs
|
||||||
|
init_Texture();
|
||||||
init_textureFrom();
|
init_textureFrom();
|
||||||
init_Container();
|
init_Container();
|
||||||
init_Graphics();
|
init_Graphics();
|
||||||
|
init_Sprite();
|
||||||
init_TextStyle();
|
init_TextStyle();
|
||||||
init_eventemitter3();
|
init_eventemitter3();
|
||||||
var import_earcut2 = __toESM(require_earcut(), 1);
|
var import_earcut2 = __toESM(require_earcut(), 1);
|
||||||
|
@ -35131,20 +35133,32 @@ var PixiController = class {
|
||||||
// -- FastWave-specific --
|
// -- FastWave-specific --
|
||||||
var_signal_rows = [];
|
var_signal_rows = [];
|
||||||
var_signal_rows_container = new Container();
|
var_signal_rows_container = new Container();
|
||||||
|
// @TODO reset `timeline_*` on file unload?
|
||||||
|
timeline_zoom;
|
||||||
|
timeline_viewport_width;
|
||||||
|
timeline_viewport_x;
|
||||||
row_height;
|
row_height;
|
||||||
row_gap;
|
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.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_height = row_height;
|
||||||
this.row_gap = row_gap;
|
this.row_gap = row_gap;
|
||||||
this.app.stage.addChild(this.var_signal_rows_container);
|
this.app.stage.addChild(this.var_signal_rows_container);
|
||||||
|
this.timeline_getter = timeline_getter;
|
||||||
}
|
}
|
||||||
async init(parent_element) {
|
async init(parent_element) {
|
||||||
await this.app.init({ background: "DarkSlateBlue", antialias: true, resizeTo: parent_element });
|
await this.app.init({ background: "DarkSlateBlue", antialias: true, resizeTo: parent_element });
|
||||||
parent_element.appendChild(this.app.canvas);
|
parent_element.appendChild(this.app.canvas);
|
||||||
}
|
}
|
||||||
// Default automatic Pixi resizing is not reliable
|
// Default automatic Pixi resizing according to the parent is not reliable
|
||||||
queue_resize() {
|
// 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();
|
this.app.queueResize();
|
||||||
}
|
}
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -35159,14 +35173,84 @@ var PixiController = class {
|
||||||
};
|
};
|
||||||
this.app.destroy(rendererDestroyOptions, options);
|
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 --
|
// -- 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) {
|
remove_var(index) {
|
||||||
if (typeof this.var_signal_rows[index] !== "undefined") {
|
if (typeof this.var_signal_rows[index] !== "undefined") {
|
||||||
this.var_signal_rows[index].destroy();
|
this.var_signal_rows[index].destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
push_var(timeline) {
|
push_var(signal_ref_index, timeline, var_format) {
|
||||||
new VarSignalRow(
|
new VarSignalRow(
|
||||||
|
signal_ref_index,
|
||||||
|
var_format,
|
||||||
timeline,
|
timeline,
|
||||||
this.app,
|
this.app,
|
||||||
this.var_signal_rows,
|
this.var_signal_rows,
|
||||||
|
@ -35183,30 +35267,30 @@ var PixiController = class {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var VarSignalRow = class {
|
var VarSignalRow = class {
|
||||||
app;
|
signal_ref_index;
|
||||||
|
var_format;
|
||||||
timeline;
|
timeline;
|
||||||
last_time;
|
app;
|
||||||
formatter;
|
|
||||||
timeline_for_ui;
|
|
||||||
owner;
|
owner;
|
||||||
index_in_owner;
|
index_in_owner;
|
||||||
rows_container;
|
rows_container;
|
||||||
row_height;
|
row_height;
|
||||||
row_gap;
|
row_gap;
|
||||||
row_height_with_gap;
|
row_height_with_gap;
|
||||||
renderer_resize_callback = () => this.redraw_on_canvas_resize();
|
|
||||||
// -- elements --
|
|
||||||
row_container = new Container();
|
row_container = new Container();
|
||||||
|
row_container_background;
|
||||||
signal_blocks_container = new Container();
|
signal_blocks_container = new Container();
|
||||||
constructor(timeline, app, owner, rows_container, row_height, row_gap) {
|
label_style = new TextStyle({
|
||||||
this.app = app;
|
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.timeline = timeline;
|
||||||
this.last_time = timeline[timeline.length - 1][0];
|
this.app = app;
|
||||||
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.row_height = row_height;
|
this.row_height = row_height;
|
||||||
this.row_gap = row_gap;
|
this.row_gap = row_gap;
|
||||||
this.row_height_with_gap = row_height + row_gap;
|
this.row_height_with_gap = row_height + row_gap;
|
||||||
|
@ -35214,58 +35298,44 @@ var VarSignalRow = class {
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
this.owner.push(this);
|
this.owner.push(this);
|
||||||
this.rows_container = rows_container;
|
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.row_container.y = this.index_in_owner * this.row_height_with_gap;
|
||||||
this.rows_container.addChild(this.row_container);
|
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);
|
this.row_container.addChild(this.signal_blocks_container);
|
||||||
const label_style = new TextStyle({
|
this.draw();
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
redraw_on_canvas_resize() {
|
set_var_format(var_format) {
|
||||||
for (let index = 0; index < this.timeline_for_ui.length; index++) {
|
this.var_format = var_format;
|
||||||
const x2 = this.timeline[index][0] / this.last_time * this.app.screen.width;
|
}
|
||||||
this.timeline_for_ui[index][0] = x2;
|
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) => {
|
this.row_container_background.width = this.app.screen.width;
|
||||||
if (index == this.timeline_for_ui.length - 1) {
|
this.signal_blocks_container.removeChildren();
|
||||||
return;
|
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() {
|
decrement_index() {
|
||||||
|
@ -35273,7 +35343,6 @@ var VarSignalRow = class {
|
||||||
this.row_container.y -= this.row_height_with_gap;
|
this.row_container.y -= this.row_height_with_gap;
|
||||||
}
|
}
|
||||||
destroy() {
|
destroy() {
|
||||||
this.app.renderer.off("resize", this.renderer_resize_callback);
|
|
||||||
this.owner.splice(this.index_in_owner, 1);
|
this.owner.splice(this.index_in_owner, 1);
|
||||||
this.rows_container.removeChildAt(this.index_in_owner);
|
this.rows_container.removeChildAt(this.index_in_owner);
|
||||||
this.row_container.destroy(true);
|
this.row_container.destroy(true);
|
||||||
|
|
|
@ -2520,19 +2520,22 @@ async function pick_and_load_waveform() {
|
||||||
async function get_hierarchy() {
|
async function get_hierarchy() {
|
||||||
return await invoke2("get_hierarchy");
|
return await invoke2("get_hierarchy");
|
||||||
}
|
}
|
||||||
async function get_time_table() {
|
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("get_time_table");
|
return await invoke2("load_signal_and_get_timeline", {
|
||||||
}
|
signal_ref_index,
|
||||||
async function load_and_get_signal(signal_ref_index) {
|
timeline_zoom,
|
||||||
return await invoke2("load_and_get_signal", { signal_ref_index });
|
timeline_viewport_width,
|
||||||
|
timeline_viewport_x,
|
||||||
|
block_height,
|
||||||
|
var_format
|
||||||
|
});
|
||||||
}
|
}
|
||||||
async function unload_signal(signal_ref_index) {
|
async function unload_signal(signal_ref_index) {
|
||||||
return await invoke2("unload_signal", { signal_ref_index });
|
return await invoke2("unload_signal", { signal_ref_index });
|
||||||
}
|
}
|
||||||
export {
|
export {
|
||||||
get_hierarchy,
|
get_hierarchy,
|
||||||
get_time_table,
|
load_signal_and_get_timeline,
|
||||||
load_and_get_signal,
|
|
||||||
pick_and_load_waveform,
|
pick_and_load_waveform,
|
||||||
show_window,
|
show_window,
|
||||||
unload_signal
|
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;
|
// @TODO sync with Rust and `tauri_glue.ts`
|
||||||
type BitString = string;
|
type Timeline = {
|
||||||
type Timeline = Array<[Time, BitString]>;
|
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;
|
// @TODO sync with Rust
|
||||||
type TimelineForUI = Array<[X, string]>;
|
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 {
|
export class PixiController {
|
||||||
app: Application
|
app: Application
|
||||||
// -- FastWave-specific --
|
// -- FastWave-specific --
|
||||||
var_signal_rows: Array<VarSignalRow> = [];
|
var_signal_rows: Array<VarSignalRow> = [];
|
||||||
var_signal_rows_container = new Container();
|
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_height: number;
|
||||||
row_gap: 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();
|
this.app = new Application();
|
||||||
// -- FastWave-specific --
|
// -- 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_height = row_height;
|
||||||
this.row_gap = row_gap;
|
this.row_gap = row_gap;
|
||||||
this.app.stage.addChild(this.var_signal_rows_container);
|
this.app.stage.addChild(this.var_signal_rows_container);
|
||||||
|
this.timeline_getter = timeline_getter;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(parent_element: HTMLElement) {
|
async init(parent_element: HTMLElement) {
|
||||||
|
@ -28,8 +73,13 @@ export class PixiController {
|
||||||
parent_element.appendChild(this.app.canvas);
|
parent_element.appendChild(this.app.canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default automatic Pixi resizing is not reliable
|
// Default automatic Pixi resizing according to the parent is not reliable
|
||||||
queue_resize() {
|
// 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();
|
this.app.queueResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,16 +96,93 @@ export class PixiController {
|
||||||
this.app.destroy(rendererDestroyOptions, options);
|
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 --
|
// -- 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) {
|
remove_var(index: number) {
|
||||||
if (typeof this.var_signal_rows[index] !== 'undefined') {
|
if (typeof this.var_signal_rows[index] !== 'undefined') {
|
||||||
this.var_signal_rows[index].destroy();
|
this.var_signal_rows[index].destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
push_var(timeline: Timeline) {
|
push_var(signal_ref_index: number, timeline: Timeline, var_format: VarFormat) {
|
||||||
new VarSignalRow(
|
new VarSignalRow(
|
||||||
|
signal_ref_index,
|
||||||
|
var_format,
|
||||||
timeline,
|
timeline,
|
||||||
this.app,
|
this.app,
|
||||||
this.var_signal_rows,
|
this.var_signal_rows,
|
||||||
|
@ -75,23 +202,29 @@ export class PixiController {
|
||||||
}
|
}
|
||||||
|
|
||||||
class VarSignalRow {
|
class VarSignalRow {
|
||||||
app: Application;
|
signal_ref_index: number;
|
||||||
|
var_format: VarFormat;
|
||||||
timeline: Timeline;
|
timeline: Timeline;
|
||||||
last_time: Time;
|
app: Application;
|
||||||
formatter: (signal_value: BitString) => string;
|
|
||||||
timeline_for_ui: TimelineForUI;
|
|
||||||
owner: Array<VarSignalRow>;
|
owner: Array<VarSignalRow>;
|
||||||
index_in_owner: number;
|
index_in_owner: number;
|
||||||
rows_container: Container;
|
rows_container: Container;
|
||||||
row_height: number;
|
row_height: number;
|
||||||
row_gap: number;
|
row_gap: number;
|
||||||
row_height_with_gap: number;
|
row_height_with_gap: number;
|
||||||
renderer_resize_callback = () => this.redraw_on_canvas_resize();
|
|
||||||
// -- elements --
|
|
||||||
row_container = new Container();
|
row_container = new Container();
|
||||||
|
row_container_background: Sprite;
|
||||||
signal_blocks_container = new Container();
|
signal_blocks_container = new Container();
|
||||||
|
label_style = new TextStyle({
|
||||||
|
align: "center",
|
||||||
|
fill: "White",
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: '"Courier New", monospace',
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
signal_ref_index: number,
|
||||||
|
var_format: VarFormat,
|
||||||
timeline: Timeline,
|
timeline: Timeline,
|
||||||
app: Application,
|
app: Application,
|
||||||
owner: Array<VarSignalRow>,
|
owner: Array<VarSignalRow>,
|
||||||
|
@ -99,16 +232,10 @@ class VarSignalRow {
|
||||||
row_height: number,
|
row_height: number,
|
||||||
row_gap: number,
|
row_gap: number,
|
||||||
) {
|
) {
|
||||||
this.app = app;
|
this.signal_ref_index = signal_ref_index;
|
||||||
|
this.var_format = var_format;
|
||||||
this.timeline = timeline;
|
this.timeline = timeline;
|
||||||
this.last_time = timeline[timeline.length - 1][0];
|
this.app = app;
|
||||||
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.row_height = row_height;
|
this.row_height = row_height;
|
||||||
this.row_gap = row_gap;
|
this.row_gap = row_gap;
|
||||||
|
@ -120,81 +247,66 @@ class VarSignalRow {
|
||||||
|
|
||||||
this.rows_container = rows_container;
|
this.rows_container = rows_container;
|
||||||
|
|
||||||
this.draw();
|
|
||||||
this.app.renderer.on("resize", this.renderer_resize_callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
draw() {
|
|
||||||
// row_container
|
// row_container
|
||||||
this.row_container.y = this.index_in_owner * this.row_height_with_gap;
|
this.row_container.y = this.index_in_owner * this.row_height_with_gap;
|
||||||
this.rows_container.addChild(this.row_container);
|
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);
|
this.row_container.addChild(this.signal_blocks_container);
|
||||||
|
|
||||||
const label_style = new TextStyle({
|
this.draw();
|
||||||
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(([x, value], index) => {
|
set_var_format(var_format: VarFormat) {
|
||||||
if (index == this.timeline_for_ui.length - 1) {
|
this.var_format = var_format;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
const block_width = this.timeline_for_ui[index+1][0] - x;
|
|
||||||
const block_height = this.row_height;
|
|
||||||
|
|
||||||
|
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
|
// signal_block
|
||||||
const signal_block = new Container();
|
const signal_block = new Container();
|
||||||
signal_block.x = x;
|
signal_block.x = timeline_block.x;
|
||||||
this.signal_blocks_container.addChild(signal_block);
|
this.signal_blocks_container.addChild(signal_block);
|
||||||
|
|
||||||
// background
|
// background
|
||||||
|
const gap_between_blocks = 2;
|
||||||
const background = new Graphics()
|
const background = new Graphics()
|
||||||
.roundRect(0, 0, block_width, block_height, 15)
|
.rect(gap_between_blocks / 2, 0, timeline_block.width - gap_between_blocks, timeline_block.height)
|
||||||
.fill("SlateBlue");
|
.fill('SlateBlue');
|
||||||
background.label = "background";
|
|
||||||
signal_block.addChild(background);
|
signal_block.addChild(background);
|
||||||
|
|
||||||
// label
|
// label
|
||||||
const label = new Text({ text: value, style: label_style });
|
if (timeline_block.label !== undefined) {
|
||||||
label.x = (block_width - label.width) / 2;
|
const label = new Text();
|
||||||
label.y = (block_height - label.height) / 2;
|
label.text = timeline_block.label.text;
|
||||||
label.visible = label.width < block_width;
|
label.style = this.label_style;
|
||||||
label.label = "label";
|
label.x = timeline_block.label.x;
|
||||||
signal_block.addChild(label);
|
label.y = timeline_block.label.y;
|
||||||
})
|
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;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
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() {
|
decrement_index() {
|
||||||
|
@ -203,7 +315,6 @@ class VarSignalRow {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.app.renderer.off("resize", this.renderer_resize_callback);
|
|
||||||
this.owner.splice(this.index_in_owner, 1);
|
this.owner.splice(this.index_in_owner, 1);
|
||||||
this.rows_container.removeChildAt(this.index_in_owner);
|
this.rows_container.removeChildAt(this.index_in_owner);
|
||||||
this.row_container.destroy(true);
|
this.row_container.destroy(true);
|
||||||
|
|
|
@ -6,8 +6,8 @@ const invoke = core.invoke;
|
||||||
|
|
||||||
type Filename = string;
|
type Filename = string;
|
||||||
type WellenHierarchy = unknown;
|
type WellenHierarchy = unknown;
|
||||||
type WellenTimeTable = unknown;
|
type Timeline = unknown;
|
||||||
type WellenSignal = unknown;
|
type VarFormat = unknown;
|
||||||
|
|
||||||
export async function show_window(): Promise<void> {
|
export async function show_window(): Promise<void> {
|
||||||
return await invoke("show_window");
|
return await invoke("show_window");
|
||||||
|
@ -21,12 +21,22 @@ export async function get_hierarchy(): Promise<WellenHierarchy> {
|
||||||
return await invoke("get_hierarchy");
|
return await invoke("get_hierarchy");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_time_table(): Promise<WellenTimeTable> {
|
export async function load_signal_and_get_timeline(
|
||||||
return await invoke("get_time_table");
|
signal_ref_index: number,
|
||||||
}
|
timeline_zoom: number,
|
||||||
|
timeline_viewport_width: number,
|
||||||
export async function load_and_get_signal(signal_ref_index: number): Promise<WellenSignal> {
|
timeline_viewport_x: number,
|
||||||
return await invoke("load_and_get_signal", { signal_ref_index });
|
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> {
|
export async function unload_signal(signal_ref_index: number): Promise<void> {
|
||||||
|
|
|
@ -9,3 +9,11 @@ publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wellen.workspace = true
|
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;
|
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 = [] }
|
tauri-build = { version = "=2.0.0-beta.17", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
shared.workspace = true
|
|
||||||
wellen.workspace = true
|
wellen.workspace = true
|
||||||
|
shared = { path = "../shared", features = ["backend"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "=2.0.0-beta.22", features = ["macos-private-api", "linux-ipc-protocol"] }
|
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")]
|
#[tauri::command(rename_all = "snake_case")]
|
||||||
async fn get_time_table(store: tauri::State<'_, Store>) -> Result<serde_json::Value, ()> {
|
async fn load_signal_and_get_timeline(
|
||||||
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(
|
|
||||||
signal_ref_index: usize,
|
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>,
|
store: tauri::State<'_, Store>,
|
||||||
) -> Result<serde_json::Value, ()> {
|
) -> Result<serde_json::Value, ()> {
|
||||||
|
// @TODO run (all?) in a blocking thread?
|
||||||
let signal_ref = wellen::SignalRef::from_index(signal_ref_index).unwrap();
|
let signal_ref = wellen::SignalRef::from_index(signal_ref_index).unwrap();
|
||||||
let mut waveform_lock = store.waveform.lock().unwrap();
|
let mut waveform_lock = store.waveform.lock().unwrap();
|
||||||
let waveform = waveform_lock.as_mut().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]);
|
waveform.load_signals_multi_threaded(&[signal_ref]);
|
||||||
let signal = waveform.get_signal(signal_ref).unwrap();
|
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")]
|
#[tauri::command(rename_all = "snake_case")]
|
||||||
|
@ -85,8 +92,7 @@ pub fn run() {
|
||||||
show_window,
|
show_window,
|
||||||
pick_and_load_waveform,
|
pick_and_load_waveform,
|
||||||
get_hierarchy,
|
get_hierarchy,
|
||||||
get_time_table,
|
load_signal_and_get_timeline,
|
||||||
load_and_get_signal,
|
|
||||||
unload_signal,
|
unload_signal,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|
Loading…
Reference in a new issue