Compare commits

...

23 commits

Author SHA1 Message Date
Martin Kavík 09eab06e4e pan & zoom boundaries + related gifs 2024-06-14 22:01:23 +02:00
Martin Kavík f324b57f71 hide invisible blocks 2024-06-14 01:50:45 +02:00
Martin Kavík 7a43a9ae4f zoom with offset x 2024-06-12 01:17:19 +02:00
Martin Kavík 547539db9f zoom_or_pan, fix first row width 2024-06-11 23:59:06 +02:00
Martin Kavík 9b76ecf38f timeline_width/viewport_width/viewport_x 2024-06-11 17:12:12 +02:00
Martin Kavík af90adfe20 signal_to_timeline.rs 2024-06-10 00:27:01 +02:00
Martin Kavík 1ed1661b16 set_var_format 2024-06-09 22:59:01 +02:00
Martin Kavík 6926b0176f format BinaryWithGroups and ASCII 2024-06-09 19:42:51 +02:00
Martin Kavík d09caf835c tauri dev watches shared, VarFormat::format 2024-06-09 01:38:44 +02:00
Martin Kavík 6859c726f5 selected_var_format_button 2024-06-08 23:54:13 +02:00
Martin Kavík 10425580dd load_signal_and_get_timeline in browser.rs, tasks.start_browser_release 2024-06-08 18:24:11 +02:00
Martin Kavík aea55fa9e3 Test files in README, notes 2024-06-08 01:52:26 +02:00
Martin Kavík b308fa85fb convert_base 2024-06-08 01:11:18 +02:00
Martin Kavík f6d8f9d8ce remove HierarchyAndTimeTable 2024-06-07 23:18:06 +02:00
Martin Kavík 869a31ca5f await redraw, u128 for bitstring parsing 2024-06-07 23:06:14 +02:00
Martin Kavík 8e285c7b5e timeline_getter 2024-06-07 22:51:53 +02:00
Martin Kavík 8ded9a11f6 redraw rows 2024-06-07 18:49:39 +02:00
Martin Kavík 26565dd684 fmt 2024-06-07 02:31:20 +02:00
Martin Kavík 29560ae616 canvas redesign 2024-06-07 01:41:10 +02:00
Martin Kavík d3cc0eb860 remove load_and_get_signal, rename timeline to load_signal_and_get_timeline 2024-06-07 00:45:05 +02:00
Martin Kavík aeb95b982f signal_to_timeline 2024-06-07 00:38:49 +02:00
Martin Kavík a37675b094 platform::timeline 2024-06-06 22:14:47 +02:00
Martin Kavík 6c4845b81d signal block optimizations 2024-06-06 18:57:13 +02:00
26 changed files with 1079 additions and 355 deletions

View file

@ -1,2 +1,3 @@
/* /*
!/src-tauri !/src-tauri
!/shared

21
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

@ -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`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
View 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
}
}
}
}

View file

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

View file

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