diff --git a/.taurignore b/.taurignore index 669df74..74cd7c0 100644 --- a/.taurignore +++ b/.taurignore @@ -1,2 +1,3 @@ /* !/src-tauri +!/shared diff --git a/Cargo.lock b/Cargo.lock index 78eb858..c0ef9a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -859,6 +859,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "convert-base" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56a404f2112d00ba80a516b60ca3744ef6bd776baa14126eb7d7c11b42cac8a" + [[package]] name = "convert_case" version = "0.4.0" @@ -1538,9 +1544,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -1568,9 +1574,9 @@ checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -4124,6 +4130,9 @@ dependencies = [ name = "shared" version = "0.1.0" dependencies = [ + "convert-base", + "futures-util", + "moonlight", "wellen", ] @@ -5038,9 +5047,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b27516dfcfa22a9faaf192283a122bfbede38c1e59ef194e3c4db6549b419c0" +checksum = "3ad8319cca93189ea9ab1b290de0595960529750b6b8b501a399ed1ec3775d60" dependencies = [ "cocoa", "core-graphics", diff --git a/Cargo.toml b/Cargo.toml index f7fe8b2..5ab2fb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,12 @@ readme = "../README.md" publish = false [workspace.dependencies] -shared = { path = "./shared" } # wellen = { version = "0.9.9", features = ["serde1"] } # wellen = { path = "../wellen/wellen", features = ["serde1"] } wellen = { git = "https://github.com/MartinKavik/wellen", features = ["serde1"], branch = "new_pub_types" } # moon = { path = "../../crates/moon" } # zoon = { path = "../../crates/zoon" } +# moonlight = { path = "../../crates/zoon" } zoon = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" } moon = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" } +moonlight = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" } diff --git a/Makefile.toml b/Makefile.toml index 6f1c212..236fb15 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -38,6 +38,15 @@ run_task = { fork = true, parallel = true, name = [ "watch_tauri_glue", ]} +[tasks.start_browser_release] +description = "Run without Tauri in the browser & watch Typescript and Rust in the release mode" +dependencies = ["store_current_process_id"] +run_task = { fork = true, parallel = true, name = [ + "mzoon_start_release_with_cleanup", + "watch_pixi_canvas", + "watch_tauri_glue", +]} + [tasks.bundle] description = "Compile in the release mode and create installation packages" dependencies = ["tauri_build", "show_release_paths"] @@ -100,6 +109,11 @@ description = "Run `mzoon start`" extend = "mzoon" args = ["start"] +[tasks.mzoon_start_release] +description = "Run `mzoon start --release`" +extend = "mzoon" +args = ["start", "--release"] + [tasks.tauri_dev_with_cleanup] description = "Run forked `tauri dev` with cleanup" run_task = { fork = true, cleanup_task = "kill_watchers", name = ["tauri_dev"] } @@ -108,6 +122,10 @@ run_task = { fork = true, cleanup_task = "kill_watchers", name = ["tauri_dev"] } description = "Run forked `mzoon start` with cleanup" run_task = { fork = true, cleanup_task = "kill_watchers", name = ["mzoon_start"] } +[tasks.mzoon_start_release_with_cleanup] +description = "Run forked `mzoon start` with cleanup" +run_task = { fork = true, cleanup_task = "kill_watchers", name = ["mzoon_start_release"] } + [tasks.kill_watchers] description = "Kill the cargo-make/makers process and all its children / forked processes" script_runner = "@duckscript" diff --git a/MoonZoon.toml b/MoonZoon.toml index 3e0c013..d53ee41 100644 --- a/MoonZoon.toml +++ b/MoonZoon.toml @@ -15,8 +15,10 @@ origins = ["*"] frontend = [ "public", "frontend/Cargo.toml", - "frontend/typescript/bundles", "frontend/src", + "frontend/typescript/bundles", + "shared/Cargo.toml", + "shared/src", ] backend = [ "backend/Cargo.toml", diff --git a/README.md b/README.md index d702e3e..158b34d 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,23 @@ ---

- fastwave_screenshot_firefox + Fastwave - Browser (Firefox) + Browser (Firefox)

- fastwave_video_desktop + Fastwave - Desktop, miller columns and tree + Desktop, miller columns and tree +

+ +

+ Fastwave - Zoom, pan and basic number formats + Zoom, pan and basic number formats +

+ +

+ Fastwave - Zoom and all formats + Zoom and all formats

--- @@ -22,7 +34,7 @@ ___ -### Start: +### Start the desktop version: 1. `makers start` @@ -39,21 +51,34 @@ Troubleshooting: --- -### Start in the browser: +### Production build of the desktop version: + +1. `makers bundle` +2. Runnable executable is in `target/release` +3. Installable bundles specific for the platform are in `target/release/bundle` + +--- + +### Start in a browser: 1. `makers start_browser` 2. Ctrl + Click the server URL mentioned in the terminal log --- +### Start in a browser in the release mode: + +1. `makers start_browser_release` +2. Ctrl + Click the server URL mentioned in the terminal log + +--- + ### Steps before pushing: 1. `makers format` ---- -### Production build: -1. `makers bundle` -2. Runnable executable is in `target/release` -3. Installable bundles specific for the platform are in `target/release/bundle` +### Test files + +See the folder `test_files`. diff --git a/docs/video_zoom_formatting.gif b/docs/video_zoom_formatting.gif new file mode 100644 index 0000000..0ade85f Binary files /dev/null and b/docs/video_zoom_formatting.gif differ diff --git a/docs/video_zoom_formatting_simple.gif b/docs/video_zoom_formatting_simple.gif new file mode 100644 index 0000000..6df92e1 Binary files /dev/null and b/docs/video_zoom_formatting_simple.gif differ diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 9d27365..930650e 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -11,8 +11,8 @@ publish.workspace = true wasm-bindgen-test = "0.3.19" [dependencies] -shared.workspace = true zoon.workspace = true wellen.workspace = true +shared = { path = "../shared", features = ["frontend"] } web-sys = { version = "*", features = ["FileSystemFileHandle"] } gloo-file = { version = "0.3.0", features = ["futures"] } diff --git a/frontend/src/controls_panel.rs b/frontend/src/controls_panel.rs index b92e0e1..69fff23 100644 --- a/frontend/src/controls_panel.rs +++ b/frontend/src/controls_panel.rs @@ -1,5 +1,4 @@ -use crate::{platform, HierarchyAndTimeTable, Layout}; -use futures_util::join; +use crate::{platform, Layout}; use std::cell::Cell; use std::mem; use std::ops::Not; @@ -35,7 +34,7 @@ struct ScopeForUI { #[derive(Clone)] pub struct ControlsPanel { selected_scope_ref: Mutable>, - hierarchy_and_time_table: Mutable>, + hierarchy: Mutable>>, selected_var_refs: MutableVec, layout: Mutable, loaded_filename: Mutable>, @@ -43,13 +42,13 @@ pub struct ControlsPanel { impl ControlsPanel { pub fn new( - hierarchy_and_time_table: Mutable>, + hierarchy: Mutable>>, selected_var_refs: MutableVec, layout: Mutable, ) -> impl Element { Self { selected_scope_ref: <_>::default(), - hierarchy_and_time_table, + hierarchy, selected_var_refs, layout, loaded_filename: <_>::default(), @@ -60,7 +59,7 @@ impl ControlsPanel { fn triggers(&self) -> Vec { vec![Task::start_droppable(clone!((self => s) async move { let was_some = Cell::new(false); - s.hierarchy_and_time_table + s.hierarchy .signal_ref(Option::is_some) .dedupe() .for_each_sync(clone!((s) move |is_some| { @@ -81,8 +80,8 @@ impl ControlsPanel { let layout = self.layout.clone(); let layout_and_hierarchy_signal = map_ref! { let layout = layout.signal(), - let hierarchy_and_time_table = self.hierarchy_and_time_table.signal_cloned() => { - (*layout, hierarchy_and_time_table.clone().map(|(hierarchy, _)| hierarchy)) + let hierarchy = self.hierarchy.signal_cloned() => { + (*layout, hierarchy.clone()) } }; Column::new() @@ -99,7 +98,6 @@ impl ControlsPanel { Layout::Columns => Height::fill().max(MILLER_COLUMN_MAX_HEIGHT), }, ))) - .s(Scrollbars::both()) .s(Padding::all(20)) .s(Gap::new().y(40)) .s(Align::new().top()) @@ -111,9 +109,9 @@ impl ControlsPanel { .item(self.layout_switcher()), ) .item_signal( - self.hierarchy_and_time_table + self.hierarchy .signal_cloned() - .map_some(clone!((self => s) move |(hierarchy, _)| s.scopes_panel(hierarchy))), + .map_some(clone!((self => s) move |hierarchy| s.scopes_panel(hierarchy))), ) .item_signal(layout_and_hierarchy_signal.map( clone!((self => s) move |(layout, hierarchy)| { @@ -127,7 +125,7 @@ impl ControlsPanel { #[cfg(FASTWAVE_PLATFORM = "TAURI")] fn load_button(&self) -> impl Element { let (hovered, hovered_signal) = Mutable::new_and_signal(false); - let hierarchy_and_time_table = self.hierarchy_and_time_table.clone(); + let hierarchy = self.hierarchy.clone(); let loaded_filename = self.loaded_filename.clone(); Button::new() .s(Padding::new().x(20).y(10)) @@ -144,21 +142,18 @@ impl ControlsPanel { )) .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) .on_press(move || { - let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut(); - if hierarchy_and_time_table_lock.is_some() { - *hierarchy_and_time_table_lock = None; + let mut hierarchy_lock = hierarchy.lock_mut(); + if hierarchy_lock.is_some() { + *hierarchy_lock = None; return; } - drop(hierarchy_and_time_table_lock); - let hierarchy_and_time_table = hierarchy_and_time_table.clone(); + drop(hierarchy_lock); + let hierarchy = hierarchy.clone(); let loaded_filename = loaded_filename.clone(); Task::start(async move { if let Some(filename) = platform::pick_and_load_waveform(None).await { loaded_filename.set_neq(Some(filename)); - let (hierarchy, time_table) = - join!(platform::get_hierarchy(), platform::get_time_table()); - hierarchy_and_time_table - .set(Some((Rc::new(hierarchy), Rc::new(time_table)))) + hierarchy.set(Some(Rc::new(platform::get_hierarchy().await))) } }) }) @@ -167,7 +162,7 @@ impl ControlsPanel { #[cfg(FASTWAVE_PLATFORM = "BROWSER")] fn load_button(&self) -> impl Element { let (hovered, hovered_signal) = Mutable::new_and_signal(false); - let hierarchy_and_time_table = self.hierarchy_and_time_table.clone(); + let hierarchy = self.hierarchy.clone(); let loaded_filename = self.loaded_filename.clone(); let file_input_id = "file_input"; Row::new() @@ -175,7 +170,8 @@ impl ControlsPanel { Label::new() .s(Padding::new().x(20).y(10)) .s(Background::new().color_signal( - hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + hovered_signal + .map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), )) .s(Align::new().left()) .s(RoundedCorners::all(15)) @@ -188,50 +184,52 @@ impl ControlsPanel { )) .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) .for_input(file_input_id) - .on_click_event_with_options(EventOptions::new().preventable(), clone!((hierarchy_and_time_table) move |event| { - let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut(); - if hierarchy_and_time_table_lock.is_some() { - *hierarchy_and_time_table_lock = None; - if let RawMouseEvent::Click(raw_event) = event.raw_event { - // @TODO Move to MoonZoon as a new API - raw_event.prevent_default(); + .on_click_event_with_options( + EventOptions::new().preventable(), + clone!((hierarchy) move |event| { + let mut hierarchy_lock = hierarchy.lock_mut(); + if hierarchy_lock.is_some() { + *hierarchy_lock = None; + if let RawMouseEvent::Click(raw_event) = event.raw_event { + // @TODO Move to MoonZoon as a new API + raw_event.prevent_default(); + } + return; } - return; - } - })) + }), + ), ) .item( // @TODO https://github.com/MoonZoon/MoonZoon/issues/39 // + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_hidden_file_input_elements_using_the_click_method - TextInput::new() - .id(file_input_id) - .update_raw_el(|raw_el| { - let dom_element = raw_el.dom_element(); - raw_el - .style("display", "none") - .attr("type", "file") - .event_handler(move |_: events::Input| { - let Some(file_list) = dom_element.files().map(gloo_file::FileList::from) else { - zoon::println!("file list is `None`"); - return; - }; - let Some(file) = file_list.first().cloned() else { - zoon::println!("file list is empty"); - return; - }; - let hierarchy_and_time_table = hierarchy_and_time_table.clone(); - let loaded_filename = loaded_filename.clone(); - Task::start(async move { - if let Some(filename) = platform::pick_and_load_waveform(Some(file)).await { - loaded_filename.set_neq(Some(filename)); - let (hierarchy, time_table) = - join!(platform::get_hierarchy(), platform::get_time_table()); - hierarchy_and_time_table - .set(Some((Rc::new(hierarchy), Rc::new(time_table)))) - } - }) + TextInput::new().id(file_input_id).update_raw_el(|raw_el| { + let dom_element = raw_el.dom_element(); + raw_el + .style("display", "none") + .attr("type", "file") + .event_handler(move |_: events::Input| { + let Some(file_list) = + dom_element.files().map(gloo_file::FileList::from) + else { + zoon::println!("file list is `None`"); + return; + }; + let Some(file) = file_list.first().cloned() else { + zoon::println!("file list is empty"); + return; + }; + let hierarchy = hierarchy.clone(); + let loaded_filename = loaded_filename.clone(); + Task::start(async move { + if let Some(filename) = + platform::pick_and_load_waveform(Some(file)).await + { + loaded_filename.set_neq(Some(filename)); + hierarchy.set(Some(Rc::new(platform::get_hierarchy().await))) + } + }) }) - }) + }), ) } @@ -563,6 +561,7 @@ impl ControlsPanel { }); // Lazy loading to not freeze the main thread + // @TODO replace with grouping and/or virtual scroll (https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib) const CHUNK_SIZE: usize = 50; let mut chunked_vars_for_ui: Vec> = <_>::default(); let mut chunk = Vec::with_capacity(CHUNK_SIZE); diff --git a/frontend/src/main.rs b/frontend/src/main.rs index de4b4c1..e551c3d 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -9,8 +9,6 @@ use controls_panel::ControlsPanel; mod waveform_panel; use waveform_panel::WaveformPanel; -type HierarchyAndTimeTable = (Rc, Rc); - #[derive(Clone, Copy, Default)] enum Layout { Tree, @@ -28,7 +26,7 @@ fn main() { } fn root() -> impl Element { - let hierarchy_and_time_table: Mutable> = <_>::default(); + let hierarchy: Mutable>> = <_>::default(); let selected_var_refs: MutableVec = <_>::default(); let layout: Mutable = <_>::default(); Column::new() @@ -40,17 +38,26 @@ fn root() -> impl Element { .s(Height::fill()) .s(Gap::new().x(15)) .item(ControlsPanel::new( - hierarchy_and_time_table.clone(), + hierarchy.clone(), selected_var_refs.clone(), layout.clone(), )) - .item_signal(layout.signal().map(|layout| matches!(layout, Layout::Tree)).map_true(clone!((hierarchy_and_time_table, selected_var_refs) move || WaveformPanel::new( - hierarchy_and_time_table.clone(), - selected_var_refs.clone(), - )))) + .item_signal( + layout + .signal() + .map(|layout| matches!(layout, Layout::Tree)) + .map_true( + clone!((hierarchy, selected_var_refs) move || WaveformPanel::new( + hierarchy.clone(), + selected_var_refs.clone(), + )), + ), + ), + ) + .item_signal( + layout + .signal() + .map(|layout| matches!(layout, Layout::Columns)) + .map_true(move || WaveformPanel::new(hierarchy.clone(), selected_var_refs.clone())), ) - .item_signal(layout.signal().map(|layout| matches!(layout, Layout::Columns)).map_true(move || WaveformPanel::new( - hierarchy_and_time_table.clone(), - selected_var_refs.clone(), - ))) } diff --git a/frontend/src/platform.rs b/frontend/src/platform.rs index 297bee4..73c7f73 100644 --- a/frontend/src/platform.rs +++ b/frontend/src/platform.rs @@ -29,12 +29,23 @@ pub async fn get_hierarchy() -> wellen::Hierarchy { platform::get_hierarchy().await } -pub async fn get_time_table() -> wellen::TimeTable { - platform::get_time_table().await -} - -pub async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal { - platform::load_and_get_signal(signal_ref).await +pub async fn load_signal_and_get_timeline( + signal_ref: wellen::SignalRef, + timeline_zoom: f64, + timeline_viewport_width: u32, + timeline_viewport_x: i32, + block_height: u32, + var_format: shared::VarFormat, +) -> shared::Timeline { + platform::load_signal_and_get_timeline( + signal_ref, + timeline_zoom, + timeline_viewport_width, + timeline_viewport_x, + block_height, + var_format, + ) + .await } pub async fn unload_signal(signal_ref: wellen::SignalRef) { diff --git a/frontend/src/platform/browser.rs b/frontend/src/platform/browser.rs index dc4d2eb..09f94fb 100644 --- a/frontend/src/platform/browser.rs +++ b/frontend/src/platform/browser.rs @@ -74,21 +74,29 @@ pub(super) async fn get_hierarchy() -> wellen::Hierarchy { serde_json::from_value(serde_json::to_value(hierarchy).unwrap_throw()).unwrap_throw() } -pub(super) async fn get_time_table() -> wellen::TimeTable { - let waveform = STORE.waveform.lock().unwrap_throw(); - let time_table = waveform.as_ref().unwrap_throw().time_table(); - // @TODO Wrap `time_table` in `Waveform` with `Rc/Arc` or add the method `take` / `clone` or refactor? - serde_json::from_value(serde_json::to_value(time_table).unwrap_throw()).unwrap_throw() -} - -pub(super) async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal { - let mut waveform_lock = STORE.waveform.lock().unwrap_throw(); - let waveform = waveform_lock.as_mut().unwrap_throw(); - // @TODO maybe run it in a thread to not block the main one and then - waveform.load_signals(&[signal_ref]); - let signal = waveform.get_signal(signal_ref).unwrap_throw(); - // @TODO `clone` / `Rc/Arc` / refactor? - serde_json::from_value(serde_json::to_value(signal).unwrap_throw()).unwrap_throw() +pub(super) async fn load_signal_and_get_timeline( + signal_ref: wellen::SignalRef, + timeline_zoom: f64, + timeline_viewport_width: u32, + timeline_viewport_x: i32, + block_height: u32, + var_format: shared::VarFormat, +) -> shared::Timeline { + let mut waveform_lock = STORE.waveform.lock().unwrap(); + let waveform = waveform_lock.as_mut().unwrap(); + waveform.load_signals_multi_threaded(&[signal_ref]); + let signal = waveform.get_signal(signal_ref).unwrap(); + let time_table = waveform.time_table(); + let timeline = shared::signal_to_timeline( + signal, + time_table, + timeline_zoom, + timeline_viewport_width, + timeline_viewport_x, + block_height, + var_format, + ); + timeline } pub(super) async fn unload_signal(signal_ref: wellen::SignalRef) { diff --git a/frontend/src/platform/tauri.rs b/frontend/src/platform/tauri.rs index cd97554..30300be 100644 --- a/frontend/src/platform/tauri.rs +++ b/frontend/src/platform/tauri.rs @@ -17,15 +17,26 @@ pub(super) async fn get_hierarchy() -> wellen::Hierarchy { serde_wasm_bindgen::from_value(tauri_glue::get_hierarchy().await.unwrap_throw()).unwrap_throw() } -pub(super) async fn get_time_table() -> wellen::TimeTable { - serde_wasm_bindgen::from_value(tauri_glue::get_time_table().await.unwrap_throw()).unwrap_throw() -} - -pub(super) async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal { +pub(super) async fn load_signal_and_get_timeline( + signal_ref: wellen::SignalRef, + timeline_zoom: f64, + timeline_viewport_width: u32, + timeline_viewport_x: i32, + block_height: u32, + var_format: shared::VarFormat, +) -> shared::Timeline { + let var_format = serde_wasm_bindgen::to_value(&var_format).unwrap_throw(); serde_wasm_bindgen::from_value( - tauri_glue::load_and_get_signal(signal_ref.index()) - .await - .unwrap_throw(), + tauri_glue::load_signal_and_get_timeline( + signal_ref.index(), + timeline_zoom, + timeline_viewport_width, + timeline_viewport_x, + block_height, + var_format, + ) + .await + .unwrap_throw(), ) .unwrap_throw() } @@ -52,10 +63,14 @@ mod tauri_glue { pub async fn get_hierarchy() -> Result; #[wasm_bindgen(catch)] - pub async fn get_time_table() -> Result; - - #[wasm_bindgen(catch)] - pub async fn load_and_get_signal(signal_ref_index: usize) -> Result; + pub async fn load_signal_and_get_timeline( + signal_ref_index: usize, + timeline_zoom: f64, + timeline_viewport_width: u32, + timeline_viewport_x: i32, + block_height: u32, + var_format: JsValue, + ) -> Result; #[wasm_bindgen(catch)] pub async fn unload_signal(signal_ref_index: usize) -> Result<(), JsValue>; diff --git a/frontend/src/waveform_panel.rs b/frontend/src/waveform_panel.rs index 80b2b39..db09a44 100644 --- a/frontend/src/waveform_panel.rs +++ b/frontend/src/waveform_panel.rs @@ -1,6 +1,7 @@ -use crate::{platform, HierarchyAndTimeTable}; +use crate::platform; +use std::rc::Rc; use wellen::GetItem; -use zoon::{eprintln, *}; +use zoon::*; mod pixi_canvas; use pixi_canvas::{PixiCanvas, PixiController}; @@ -11,21 +12,24 @@ const ROW_GAP: u32 = 4; #[derive(Clone)] pub struct WaveformPanel { selected_var_refs: MutableVec, - hierarchy_and_time_table: Mutable>, + hierarchy: Mutable>>, + canvas_controller: Mutable>>, } impl WaveformPanel { pub fn new( - hierarchy_and_time_table: Mutable>, + hierarchy: Mutable>>, selected_var_refs: MutableVec, ) -> impl Element { Self { selected_var_refs, - hierarchy_and_time_table, + hierarchy, + canvas_controller: Mutable::new(Mutable::default().read_only()), } .root() } + // @TODO autoscroll down fn root(&self) -> impl Element { let selected_vars_panel_height_getter: Mutable = <_>::default(); Row::new() @@ -51,27 +55,29 @@ impl WaveformPanel { fn canvas(&self, selected_vars_panel_height: ReadOnlyMutable) -> impl Element { let selected_var_refs = self.selected_var_refs.clone(); - let hierarchy_and_time_table = self.hierarchy_and_time_table.clone(); + let hierarchy = self.hierarchy.clone(); + let canvas_controller = self.canvas_controller.clone(); PixiCanvas::new(ROW_HEIGHT, ROW_GAP) .s(Align::new().top()) .s(Width::fill()) .s(Height::exact_signal(selected_vars_panel_height.signal())) - .s(RoundedCorners::new().right(15)) .task_with_controller(move |controller| { - selected_var_refs.signal_vec().delay_remove(clone!((hierarchy_and_time_table) move |var_ref| { - clone!((var_ref, hierarchy_and_time_table) async move { - if let Some(hierarchy_and_time_table) = hierarchy_and_time_table.get_cloned() { - platform::unload_signal(hierarchy_and_time_table.0.get(var_ref).signal_ref()).await; + canvas_controller.set(controller.clone()); + selected_var_refs.signal_vec().delay_remove(clone!((hierarchy) move |var_ref| { + clone!((var_ref, hierarchy) async move { + if let Some(hierarchy) = hierarchy.get_cloned() { + // @TODO unload only when no other selected variable use it? + platform::unload_signal(hierarchy.get(var_ref).signal_ref()).await; } }) - })).for_each(clone!((controller, hierarchy_and_time_table) move |vec_diff| { - clone!((controller, hierarchy_and_time_table) async move { + })).for_each(clone!((controller, hierarchy) move |vec_diff| { + clone!((controller, hierarchy) async move { match vec_diff { VecDiff::Replace { values } => { let controller = controller.wait_for_some_cloned().await; controller.clear_vars(); for var_ref in values { - Self::push_var(&controller, &hierarchy_and_time_table, var_ref).await; + Self::push_var(&controller, &hierarchy, var_ref).await; } }, VecDiff::InsertAt { index: _, value: _ } => { todo!("`task_with_controller` + `InsertAt`") } @@ -84,7 +90,7 @@ impl WaveformPanel { VecDiff::Move { old_index: _, new_index: _ } => { todo!("`task_with_controller` + `Move`") } VecDiff::Push { value: var_ref } => { if let Some(controller) = controller.lock_ref().as_ref() { - Self::push_var(controller, &hierarchy_and_time_table, var_ref).await; + Self::push_var(controller, &hierarchy, var_ref).await; } } VecDiff::Pop {} => { @@ -105,38 +111,34 @@ impl WaveformPanel { async fn push_var( controller: &PixiController, - hierarchy_and_time_table: &Mutable>, + hierarchy: &Mutable>>, var_ref: wellen::VarRef, ) { - let (hierarchy, time_table) = hierarchy_and_time_table.get_cloned().unwrap(); - if time_table.is_empty() { - eprintln!("timetable is empty"); - return; - } - let last_time = time_table.last().copied().unwrap_throw(); + let hierarchy = hierarchy.get_cloned().unwrap(); + + let var_format = shared::VarFormat::default(); let var = hierarchy.get(var_ref); let signal_ref = var.signal_ref(); - let signal = platform::load_and_get_signal(signal_ref).await; + let timeline = platform::load_signal_and_get_timeline( + signal_ref, + controller.get_timeline_zoom(), + controller.get_timeline_viewport_width(), + controller.get_timeline_viewport_x(), + ROW_HEIGHT, + var_format, + ) + .await; let timescale = hierarchy.timescale(); // @TODO remove zoon::println!("{timescale:?}"); - let mut timeline: Vec<(wellen::Time, String)> = signal - .iter_changes() - .map(|(time_index, signal_value)| { - (time_table[time_index as usize], signal_value.to_string()) - }) - .collect(); - if timeline.is_empty() { - eprintln!("timeline is empty"); - return; - } - timeline.push((last_time, timeline.last().cloned().unwrap_throw().1)); - // Note: Sync `timeline`'s type with the `Timeline` in `frontend/typescript/pixi_canvas/pixi_canvas.ts' - controller.push_var(serde_wasm_bindgen::to_value(&timeline).unwrap_throw()); + let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw(); + let signal_ref_index = signal_ref.index(); + let var_format = serde_wasm_bindgen::to_value(&var_format).unwrap_throw(); + controller.push_var(signal_ref_index, timeline, var_format); } fn selected_var_panel( @@ -144,27 +146,74 @@ impl WaveformPanel { index: ReadOnlyMutable>, var_ref: wellen::VarRef, ) -> Option { - let Some((hierarchy, _)) = self.hierarchy_and_time_table.get_cloned() else { + let Some(hierarchy) = self.hierarchy.get_cloned() else { None? }; let var = hierarchy.get(var_ref); - let name: &str = var.name(&hierarchy); + Row::new() + .item(self.selected_var_name_button(var.name(&hierarchy), index.clone())) + .item(self.selected_var_format_button(index)) + .apply(Some) + } + + fn selected_var_name_button( + &self, + name: &str, + index: ReadOnlyMutable>, + ) -> impl Element { let selected_var_refs = self.selected_var_refs.clone(); + let (hovered, hovered_signal) = Mutable::new_and_signal(false); Button::new() .s(Height::exact(ROW_HEIGHT)) - .s(Background::new().color(color!("SlateBlue", 0.8))) - .s(RoundedCorners::new().left(15)) + .s(Width::growable()) + .s(Background::new().color_signal( + hovered_signal.map_bool(|| color!("SlateBlue"), || color!("SlateBlue", 0.8)), + )) + .s(RoundedCorners::new().left(15).right(5)) .label( El::new() - .s(Align::center()) + .s(Align::new().left()) .s(Padding::new().left(20).right(17).y(10)) .child(name), ) + .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) .on_press(move || { if let Some(index) = index.get() { selected_var_refs.lock_mut().remove(index); } }) - .apply(Some) + } + + fn selected_var_format_button(&self, index: ReadOnlyMutable>) -> 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(), + ); + } + } + }) } } diff --git a/frontend/src/waveform_panel/pixi_canvas.rs b/frontend/src/waveform_panel/pixi_canvas.rs index 52a4c5a..532eded 100644 --- a/frontend/src/waveform_panel/pixi_canvas.rs +++ b/frontend/src/waveform_panel/pixi_canvas.rs @@ -1,4 +1,6 @@ +use crate::platform; pub use js_bridge::PixiController; +use std::rc::Rc; use zoon::*; pub struct PixiCanvas { @@ -35,16 +37,45 @@ impl PixiCanvas { let height = Mutable::new(0); let resize_task = Task::start_droppable( map_ref! { - let _ = width.signal(), - let _ = height.signal() => () + let width = width.signal(), + let height = height.signal() => (*width, *height) } - .for_each_sync(clone!((controller) move |_| { - if let Some(controller) = controller.lock_ref().as_ref() { - controller.queue_resize(); - } - })), + .dedupe() + .throttle(|| Timer::sleep(50)) + .for_each( + clone!((controller) move |(width, height)| clone!((controller) async move { + if let Some(controller) = controller.lock_ref().as_ref() { + controller.resize(width, height).await + } + })), + ), ); let task_with_controller = Mutable::new(None); + // -- FastWave-specific -- + let timeline_getter = Rc::new(Closure::new( + |signal_ref_index, + timeline_zoom, + timeline_viewport_width, + timeline_viewport_x, + row_height, + var_format| { + future_to_promise(async move { + let signal_ref = wellen::SignalRef::from_index(signal_ref_index).unwrap_throw(); + let timeline = platform::load_signal_and_get_timeline( + signal_ref, + timeline_zoom, + timeline_viewport_width, + timeline_viewport_x, + row_height, + serde_wasm_bindgen::from_value(var_format).unwrap_throw(), + ) + .await; + let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw(); + Ok(timeline) + }) + }, + )); + // -- // -- Self { controller: controller.read_only(), width: width.read_only(), @@ -56,14 +87,36 @@ impl PixiCanvas { width.set_neq(new_width); height.set_neq(new_height); })) - .after_insert(clone!((controller) move |element| { + .update_raw_el(|raw_el| { + // @TODO rewrite to a native Zoon API + raw_el.event_handler( + clone!((controller) move |event: events_extra::WheelEvent| { + if let Some(controller) = controller.lock_ref().as_ref() { + controller.zoom_or_pan( + event.delta_y(), + event.shift_key(), + event.offset_x() as u32, + ); + } + }), + ) + }) + .after_insert(clone!((controller, timeline_getter) move |element| { Task::start(async move { - let pixi_controller = js_bridge::PixiController::new(row_height, row_gap); + let pixi_controller = js_bridge::PixiController::new( + 1., + width.get(), + 0, + row_height, + row_gap, + &timeline_getter + ); pixi_controller.init(&element).await; controller.set(Some(pixi_controller)); }); })) .after_remove(move |_| { + drop(timeline_getter); drop(resize_task); drop(task_with_controller); if let Some(controller) = controller.take() { @@ -87,6 +140,24 @@ impl PixiCanvas { mod js_bridge { use zoon::*; + type TimelinePromise = js_sys::Promise; + type SignalRefIndex = usize; + type TimelineZoom = f64; + type TimelineViewportWidth = u32; + type TimelineViewportX = i32; + type RowHeight = u32; + type VarFormatJs = JsValue; + type TimelineGetter = Closure< + dyn FnMut( + SignalRefIndex, + TimelineZoom, + TimelineViewportWidth, + TimelineViewportX, + RowHeight, + VarFormatJs, + ) -> TimelinePromise, + >; + // Note: Add all corresponding methods to `frontend/typescript/pixi_canvas/pixi_canvas.ts` #[wasm_bindgen(module = "/typescript/bundles/pixi_canvas.js")] extern "C" { @@ -95,24 +166,56 @@ mod js_bridge { // @TODO `row_height` and `row_gap` is FastWave-specific #[wasm_bindgen(constructor)] - pub fn new(row_height: u32, row_gap: u32) -> PixiController; + pub fn new( + timeline_zoom: f64, + timeline_viewport_width: u32, + timeline_viewport_x: i32, + row_height: u32, + row_gap: u32, + timeline_getter: &TimelineGetter, + ) -> PixiController; #[wasm_bindgen(method)] pub async fn init(this: &PixiController, parent_element: &JsValue); + #[wasm_bindgen(method)] + pub async fn resize(this: &PixiController, width: u32, height: u32); + #[wasm_bindgen(method)] pub fn destroy(this: &PixiController); #[wasm_bindgen(method)] - pub fn queue_resize(this: &PixiController); + pub fn get_timeline_zoom(this: &PixiController) -> f64; + + #[wasm_bindgen(method)] + pub fn get_timeline_viewport_width(this: &PixiController) -> u32; + + #[wasm_bindgen(method)] + pub fn get_timeline_viewport_x(this: &PixiController) -> i32; // -- FastWave-specific -- + #[wasm_bindgen(method)] + pub fn set_var_format(this: &PixiController, index: usize, var_format: JsValue); + + #[wasm_bindgen(method)] + pub fn zoom_or_pan( + this: &PixiController, + wheel_delta_y: f64, + shift_key: bool, + offset_x: u32, + ); + #[wasm_bindgen(method)] pub fn remove_var(this: &PixiController, index: usize); #[wasm_bindgen(method)] - pub fn push_var(this: &PixiController, timeline: JsValue); + pub fn push_var( + this: &PixiController, + signal_ref_index: usize, + timeline: JsValue, + var_format: JsValue, + ); #[wasm_bindgen(method)] pub fn pop_var(this: &PixiController); diff --git a/frontend/typescript/bundles/pixi_canvas.js b/frontend/typescript/bundles/pixi_canvas.js index 5ab8f18..d542b5e 100644 --- a/frontend/typescript/bundles/pixi_canvas.js +++ b/frontend/typescript/bundles/pixi_canvas.js @@ -35117,9 +35117,11 @@ var Text = class extends AbstractText { }; // node_modules/pixi.js/lib/index.mjs +init_Texture(); init_textureFrom(); init_Container(); init_Graphics(); +init_Sprite(); init_TextStyle(); init_eventemitter3(); var import_earcut2 = __toESM(require_earcut(), 1); @@ -35131,20 +35133,32 @@ var PixiController = class { // -- FastWave-specific -- var_signal_rows = []; var_signal_rows_container = new Container(); + // @TODO reset `timeline_*` on file unload? + timeline_zoom; + timeline_viewport_width; + timeline_viewport_x; row_height; row_gap; - constructor(row_height, row_gap) { + timeline_getter; + constructor(timeline_zoom, timeline_viewport_width, timeline_viewport_x, row_height, row_gap, timeline_getter) { this.app = new Application(); + this.timeline_zoom = timeline_zoom; + this.timeline_viewport_width = timeline_viewport_width; + this.timeline_viewport_x = timeline_viewport_x; this.row_height = row_height; this.row_gap = row_gap; this.app.stage.addChild(this.var_signal_rows_container); + this.timeline_getter = timeline_getter; } async init(parent_element) { await this.app.init({ background: "DarkSlateBlue", antialias: true, resizeTo: parent_element }); parent_element.appendChild(this.app.canvas); } - // Default automatic Pixi resizing is not reliable - queue_resize() { + // Default automatic Pixi resizing according to the parent is not reliable + // and the `app.renderer`'s `resize` event is fired on every browser window size change + async resize(width, _height) { + this.timeline_viewport_width = width; + await this.redraw_all_rows(); this.app.queueResize(); } destroy() { @@ -35159,14 +35173,84 @@ var PixiController = class { }; this.app.destroy(rendererDestroyOptions, options); } + get_timeline_zoom() { + return this.timeline_zoom; + } + get_timeline_viewport_width() { + return this.timeline_viewport_width; + } + get_timeline_viewport_x() { + return this.timeline_viewport_x; + } // -- FastWave-specific -- + async redraw_all_rows() { + await Promise.all(this.var_signal_rows.map(async (row) => { + const timeline = await this.timeline_getter( + row.signal_ref_index, + this.timeline_zoom, + this.timeline_viewport_width, + this.timeline_viewport_x, + this.row_height, + row.var_format + ); + row.redraw(timeline); + })); + } + async redraw_row(index) { + const row = this.var_signal_rows[index]; + if (typeof row !== "undefined") { + const timeline = await this.timeline_getter( + row.signal_ref_index, + this.timeline_zoom, + this.timeline_viewport_width, + this.timeline_viewport_x, + this.row_height, + row.var_format + ); + row.redraw(timeline); + } + } + async set_var_format(index, var_format) { + const row = this.var_signal_rows[index]; + if (typeof row !== "undefined") { + row.set_var_format(var_format); + this.redraw_row(index); + } + } + async zoom_or_pan(wheel_delta_y, shift_key, offset_x) { + if (shift_key) { + this.timeline_viewport_x += Math.sign(wheel_delta_y) * 20; + } else { + const offset_x_ratio = offset_x / this.timeline_viewport_width; + const old_timeline_width = this.timeline_viewport_width * this.timeline_zoom; + const new_zoom = this.timeline_zoom - Math.sign(wheel_delta_y) * this.timeline_zoom * 0.5; + const new_timeline_width = this.timeline_viewport_width * new_zoom; + if (new_timeline_width < this.timeline_viewport_width) { + this.timeline_zoom = 1; + this.timeline_viewport_x = 0; + } else { + const timeline_width_difference = new_timeline_width - old_timeline_width; + this.timeline_viewport_x += timeline_width_difference * offset_x_ratio; + this.timeline_zoom = new_zoom; + } + } + const timeline_width = this.timeline_viewport_width * this.timeline_zoom; + if (this.timeline_viewport_x < 0) { + this.timeline_viewport_x = 0; + } else if (this.timeline_viewport_x + this.timeline_viewport_width > timeline_width) { + this.timeline_viewport_x = timeline_width - this.timeline_viewport_width; + } + this.redraw_all_rows(); + } remove_var(index) { if (typeof this.var_signal_rows[index] !== "undefined") { this.var_signal_rows[index].destroy(); } } - push_var(timeline) { + push_var(signal_ref_index, timeline, var_format) { new VarSignalRow( + signal_ref_index, + var_format, timeline, this.app, this.var_signal_rows, @@ -35183,30 +35267,30 @@ var PixiController = class { } }; var VarSignalRow = class { - app; + signal_ref_index; + var_format; timeline; - last_time; - formatter; - timeline_for_ui; + app; owner; index_in_owner; rows_container; row_height; row_gap; row_height_with_gap; - renderer_resize_callback = () => this.redraw_on_canvas_resize(); - // -- elements -- row_container = new Container(); + row_container_background; signal_blocks_container = new Container(); - constructor(timeline, app, owner, rows_container, row_height, row_gap) { - this.app = app; + label_style = new TextStyle({ + align: "center", + fill: "White", + fontSize: 16, + fontFamily: '"Courier New", monospace' + }); + constructor(signal_ref_index, var_format, timeline, app, owner, rows_container, row_height, row_gap) { + this.signal_ref_index = signal_ref_index; + this.var_format = var_format; this.timeline = timeline; - this.last_time = timeline[timeline.length - 1][0]; - this.formatter = (signal_value) => parseInt(signal_value, 2).toString(16); - this.timeline_for_ui = this.timeline.map(([time, value]) => { - const x2 = time / this.last_time * this.app.screen.width; - return [x2, this.formatter(value)]; - }); + this.app = app; this.row_height = row_height; this.row_gap = row_gap; this.row_height_with_gap = row_height + row_gap; @@ -35214,58 +35298,44 @@ var VarSignalRow = class { this.owner = owner; this.owner.push(this); this.rows_container = rows_container; - this.draw(); - this.app.renderer.on("resize", this.renderer_resize_callback); - } - draw() { this.row_container.y = this.index_in_owner * this.row_height_with_gap; this.rows_container.addChild(this.row_container); + this.row_container_background = new Sprite(); + this.row_container_background.texture = Texture.WHITE; + this.row_container_background.tint = "0x550099"; + this.row_container_background.height = this.row_height; + this.row_container.addChild(this.row_container_background); this.row_container.addChild(this.signal_blocks_container); - const label_style = new TextStyle({ - align: "center", - fill: "White", - fontSize: 16, - fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"' - }); - this.timeline_for_ui.forEach(([x2, value], index) => { - if (index == this.timeline_for_ui.length - 1) { - return; - } - const block_width = this.timeline_for_ui[index + 1][0] - x2; - const block_height = this.row_height; - const signal_block = new Container(); - signal_block.x = x2; - this.signal_blocks_container.addChild(signal_block); - const background = new Graphics().roundRect(0, 0, block_width, block_height, 15).fill("SlateBlue"); - background.label = "background"; - signal_block.addChild(background); - const label = new Text({ text: value, style: label_style }); - label.x = (block_width - label.width) / 2; - label.y = (block_height - label.height) / 2; - label.visible = label.width < block_width; - label.label = "label"; - signal_block.addChild(label); - }); + this.draw(); } - redraw_on_canvas_resize() { - for (let index = 0; index < this.timeline_for_ui.length; index++) { - const x2 = this.timeline[index][0] / this.last_time * this.app.screen.width; - this.timeline_for_ui[index][0] = x2; + set_var_format(var_format) { + this.var_format = var_format; + } + redraw(timeline) { + this.timeline = timeline; + this.draw(); + } + draw() { + if (this.app === null || this.app.screen === null) { + return; } - this.timeline_for_ui.forEach(([x2, _value], index) => { - if (index == this.timeline_for_ui.length - 1) { - return; + this.row_container_background.width = this.app.screen.width; + this.signal_blocks_container.removeChildren(); + this.timeline.blocks.forEach((timeline_block) => { + const signal_block = new Container(); + signal_block.x = timeline_block.x; + this.signal_blocks_container.addChild(signal_block); + const gap_between_blocks = 2; + const background = new Graphics().rect(gap_between_blocks / 2, 0, timeline_block.width - gap_between_blocks, timeline_block.height).fill("SlateBlue"); + signal_block.addChild(background); + if (timeline_block.label !== void 0) { + const label = new Text(); + label.text = timeline_block.label.text; + label.style = this.label_style; + label.x = timeline_block.label.x; + label.y = timeline_block.label.y; + signal_block.addChild(label); } - const block_width = this.timeline_for_ui[index + 1][0] - x2; - const block_height = this.row_height; - const signal_block = this.signal_blocks_container.getChildAt(index); - signal_block.x = x2; - const background = signal_block.getChildByLabel("background"); - background.width = block_width; - const label = signal_block.getChildByLabel("label"); - label.x = (block_width - label.width) / 2; - label.y = (block_height - label.height) / 2; - label.visible = label.width < block_width; }); } decrement_index() { @@ -35273,7 +35343,6 @@ var VarSignalRow = class { this.row_container.y -= this.row_height_with_gap; } destroy() { - this.app.renderer.off("resize", this.renderer_resize_callback); this.owner.splice(this.index_in_owner, 1); this.rows_container.removeChildAt(this.index_in_owner); this.row_container.destroy(true); diff --git a/frontend/typescript/bundles/tauri_glue.js b/frontend/typescript/bundles/tauri_glue.js index 3a14939..a81bef0 100644 --- a/frontend/typescript/bundles/tauri_glue.js +++ b/frontend/typescript/bundles/tauri_glue.js @@ -2520,19 +2520,22 @@ async function pick_and_load_waveform() { async function get_hierarchy() { return await invoke2("get_hierarchy"); } -async function get_time_table() { - return await invoke2("get_time_table"); -} -async function load_and_get_signal(signal_ref_index) { - return await invoke2("load_and_get_signal", { signal_ref_index }); +async function load_signal_and_get_timeline(signal_ref_index, timeline_zoom, timeline_viewport_width, timeline_viewport_x, block_height, var_format) { + return await invoke2("load_signal_and_get_timeline", { + signal_ref_index, + timeline_zoom, + timeline_viewport_width, + timeline_viewport_x, + block_height, + var_format + }); } async function unload_signal(signal_ref_index) { return await invoke2("unload_signal", { signal_ref_index }); } export { get_hierarchy, - get_time_table, - load_and_get_signal, + load_signal_and_get_timeline, pick_and_load_waveform, show_window, unload_signal diff --git a/frontend/typescript/pixi_canvas/pixi_canvas.ts b/frontend/typescript/pixi_canvas/pixi_canvas.ts index 97bd6fc..04e0935 100644 --- a/frontend/typescript/pixi_canvas/pixi_canvas.ts +++ b/frontend/typescript/pixi_canvas/pixi_canvas.ts @@ -1,26 +1,71 @@ -import { Application, Text, Graphics, Container, TextStyle } from "pixi.js"; +import { Application, Text, Graphics, Container, TextStyle, Sprite, Texture } from "pixi.js"; -type Time = number; -type BitString = string; -type Timeline = Array<[Time, BitString]>; +// @TODO sync with Rust and `tauri_glue.ts` +type Timeline = { + blocks: Array +} +type TimelineBlock = { + x: number, + width: number, + height: number, + label: TimeLineBlockLabel | undefined, +} +type TimeLineBlockLabel = { + text: string, + x: number, + y: number, +} -type X = number; -type TimelineForUI = Array<[X, string]>; +// @TODO sync with Rust +enum VarFormat { + ASCII, + Binary, + BinaryWithGroups, + Hexadecimal, + Octal, + Signed, + Unsigned, +} + +type TimelineGetter = ( + signal_ref_index: number, + timeline_zoom: number, + timeline_viewport_width: number, + timeline_viewport_x: number, + row_height: number, + var_format: VarFormat +) => Promise; export class PixiController { app: Application // -- FastWave-specific -- var_signal_rows: Array = []; var_signal_rows_container = new Container(); + // @TODO reset `timeline_*` on file unload? + timeline_zoom: number; + timeline_viewport_width: number; + timeline_viewport_x: number; row_height: number; row_gap: number; + timeline_getter: TimelineGetter; - constructor(row_height: number, row_gap: number) { + constructor( + timeline_zoom: number, + timeline_viewport_width: number, + timeline_viewport_x: number, + row_height: number, + row_gap: number, + timeline_getter: TimelineGetter + ) { this.app = new Application(); // -- FastWave-specific -- + this.timeline_zoom = timeline_zoom; + this.timeline_viewport_width = timeline_viewport_width; + this.timeline_viewport_x = timeline_viewport_x; this.row_height = row_height; this.row_gap = row_gap; this.app.stage.addChild(this.var_signal_rows_container); + this.timeline_getter = timeline_getter; } async init(parent_element: HTMLElement) { @@ -28,8 +73,13 @@ export class PixiController { parent_element.appendChild(this.app.canvas); } - // Default automatic Pixi resizing is not reliable - queue_resize() { + // Default automatic Pixi resizing according to the parent is not reliable + // and the `app.renderer`'s `resize` event is fired on every browser window size change + async resize(width: number, _height: number) { + // -- FastWave-specific -- + this.timeline_viewport_width = width; + await this.redraw_all_rows(); + // -- // -- this.app.queueResize(); } @@ -46,16 +96,93 @@ export class PixiController { this.app.destroy(rendererDestroyOptions, options); } + get_timeline_zoom() { + return this.timeline_zoom; + } + + get_timeline_viewport_width() { + return this.timeline_viewport_width; + } + + get_timeline_viewport_x() { + return this.timeline_viewport_x; + } + // -- FastWave-specific -- + async redraw_all_rows() { + await Promise.all(this.var_signal_rows.map(async row => { + const timeline = await this.timeline_getter( + row.signal_ref_index, + this.timeline_zoom, + this.timeline_viewport_width, + this.timeline_viewport_x, + this.row_height, + row.var_format + ); + row.redraw(timeline); + })) + } + + async redraw_row(index: number) { + const row = this.var_signal_rows[index]; + if (typeof row !== 'undefined') { + const timeline = await this.timeline_getter( + row.signal_ref_index, + this.timeline_zoom, + this.timeline_viewport_width, + this.timeline_viewport_x, + this.row_height, + row.var_format + ); + row.redraw(timeline); + } + } + + async set_var_format(index: number, var_format: VarFormat) { + const row = this.var_signal_rows[index]; + if (typeof row !== 'undefined') { + row.set_var_format(var_format); + this.redraw_row(index); + } + } + + async zoom_or_pan(wheel_delta_y: number, shift_key: boolean, offset_x: number) { + if (shift_key) { + this.timeline_viewport_x += Math.sign(wheel_delta_y) * 20; + } else { + const offset_x_ratio = offset_x / this.timeline_viewport_width; + const old_timeline_width = this.timeline_viewport_width * this.timeline_zoom; + const new_zoom = this.timeline_zoom - Math.sign(wheel_delta_y) * this.timeline_zoom * 0.5; + const new_timeline_width = this.timeline_viewport_width * new_zoom; + if (new_timeline_width < this.timeline_viewport_width) { + this.timeline_zoom = 1; + this.timeline_viewport_x = 0; + } else { + const timeline_width_difference = new_timeline_width - old_timeline_width; + this.timeline_viewport_x += timeline_width_difference * offset_x_ratio; + this.timeline_zoom = new_zoom; + } + } + const timeline_width = this.timeline_viewport_width * this.timeline_zoom; + if (this.timeline_viewport_x < 0) { + this.timeline_viewport_x = 0; + } else if (this.timeline_viewport_x + this.timeline_viewport_width > timeline_width) { + this.timeline_viewport_x = timeline_width - this.timeline_viewport_width; + } + this.redraw_all_rows(); + } + remove_var(index: number) { if (typeof this.var_signal_rows[index] !== 'undefined') { this.var_signal_rows[index].destroy(); } } - push_var(timeline: Timeline) { + push_var(signal_ref_index: number, timeline: Timeline, var_format: VarFormat) { new VarSignalRow( + signal_ref_index, + var_format, timeline, this.app, this.var_signal_rows, @@ -75,23 +202,29 @@ export class PixiController { } class VarSignalRow { - app: Application; + signal_ref_index: number; + var_format: VarFormat; timeline: Timeline; - last_time: Time; - formatter: (signal_value: BitString) => string; - timeline_for_ui: TimelineForUI; + app: Application; owner: Array; index_in_owner: number; rows_container: Container; row_height: number; row_gap: number; row_height_with_gap: number; - renderer_resize_callback = () => this.redraw_on_canvas_resize(); - // -- elements -- row_container = new Container(); + row_container_background: Sprite; signal_blocks_container = new Container(); + label_style = new TextStyle({ + align: "center", + fill: "White", + fontSize: 16, + fontFamily: '"Courier New", monospace', + }); constructor( + signal_ref_index: number, + var_format: VarFormat, timeline: Timeline, app: Application, owner: Array, @@ -99,16 +232,10 @@ class VarSignalRow { row_height: number, row_gap: number, ) { - this.app = app; - + this.signal_ref_index = signal_ref_index; + this.var_format = var_format; this.timeline = timeline; - this.last_time = timeline[timeline.length - 1][0]; - this.formatter = signal_value => parseInt(signal_value, 2).toString(16); - - this.timeline_for_ui = this.timeline.map(([time, value]) => { - const x = time / this.last_time * this.app.screen.width; - return [x, this.formatter(value)] - }); + this.app = app; this.row_height = row_height; this.row_gap = row_gap; @@ -120,81 +247,66 @@ class VarSignalRow { this.rows_container = rows_container; - this.draw(); - this.app.renderer.on("resize", this.renderer_resize_callback); - } - - draw() { // row_container this.row_container.y = this.index_in_owner * this.row_height_with_gap; this.rows_container.addChild(this.row_container); - // signal_block_container + // row background + this.row_container_background = new Sprite(); + this.row_container_background.texture = Texture.WHITE; + this.row_container_background.tint = '0x550099'; + this.row_container_background.height = this.row_height; + this.row_container.addChild(this.row_container_background); + + // signal_blocks_container this.row_container.addChild(this.signal_blocks_container); - const label_style = new TextStyle({ - align: "center", - fill: "White", - fontSize: 16, - fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"', - }); + this.draw(); + } - this.timeline_for_ui.forEach(([x, value], index) => { - if (index == this.timeline_for_ui.length - 1) { - return; - } - const block_width = this.timeline_for_ui[index+1][0] - x; - const block_height = this.row_height; + set_var_format(var_format: VarFormat) { + this.var_format = var_format; + } + redraw(timeline: Timeline) { + this.timeline = timeline; + this.draw(); + } + + draw() { + // Screen can be null when we are, for instance, switching between miller columns and tree layout + // and then the canvas has to be recreated + if (this.app === null || this.app.screen === null) { + return; + } + + this.row_container_background.width = this.app.screen.width; + + // @TODO optimize by reusing a pool of blocks instead or removing all children on every redraw? + this.signal_blocks_container.removeChildren(); + this.timeline.blocks.forEach(timeline_block => { // signal_block const signal_block = new Container(); - signal_block.x = x; + signal_block.x = timeline_block.x; this.signal_blocks_container.addChild(signal_block); // background + const gap_between_blocks = 2; const background = new Graphics() - .roundRect(0, 0, block_width, block_height, 15) - .fill("SlateBlue"); - background.label = "background"; + .rect(gap_between_blocks / 2, 0, timeline_block.width - gap_between_blocks, timeline_block.height) + .fill('SlateBlue'); signal_block.addChild(background); // label - const label = new Text({ text: value, style: label_style }); - label.x = (block_width - label.width) / 2; - label.y = (block_height - label.height) / 2; - label.visible = label.width < block_width; - label.label = "label"; - signal_block.addChild(label); - }) - } - - redraw_on_canvas_resize() { - for (let index = 0; index < this.timeline_for_ui.length; index++) { - const x = this.timeline[index][0] / this.last_time * this.app.screen.width; - this.timeline_for_ui[index][0] = x; - } - this.timeline_for_ui.forEach(([x, _value], index) => { - if (index == this.timeline_for_ui.length - 1) { - return; + if (timeline_block.label !== undefined) { + const label = new Text(); + label.text = timeline_block.label.text; + label.style = this.label_style; + label.x = timeline_block.label.x; + label.y = timeline_block.label.y; + signal_block.addChild(label); } - - const block_width = this.timeline_for_ui[index+1][0] - x; - const block_height = this.row_height; - - // signal_block - const signal_block = this.signal_blocks_container.getChildAt(index); - signal_block.x = x; - - // background - const background = signal_block.getChildByLabel("background")!; - background.width = block_width; - - // label - const label = signal_block.getChildByLabel("label")!; - label.x = (block_width - label.width) / 2; - label.y = (block_height - label.height) / 2; - label.visible = label.width < block_width; - }) + }); } decrement_index() { @@ -203,7 +315,6 @@ class VarSignalRow { } destroy() { - this.app.renderer.off("resize", this.renderer_resize_callback); this.owner.splice(this.index_in_owner, 1); this.rows_container.removeChildAt(this.index_in_owner); this.row_container.destroy(true); diff --git a/frontend/typescript/tauri_glue/tauri_glue.ts b/frontend/typescript/tauri_glue/tauri_glue.ts index d075e4f..2945a41 100644 --- a/frontend/typescript/tauri_glue/tauri_glue.ts +++ b/frontend/typescript/tauri_glue/tauri_glue.ts @@ -6,8 +6,8 @@ const invoke = core.invoke; type Filename = string; type WellenHierarchy = unknown; -type WellenTimeTable = unknown; -type WellenSignal = unknown; +type Timeline = unknown; +type VarFormat = unknown; export async function show_window(): Promise { return await invoke("show_window"); @@ -21,12 +21,22 @@ export async function get_hierarchy(): Promise { return await invoke("get_hierarchy"); } -export async function get_time_table(): Promise { - return await invoke("get_time_table"); -} - -export async function load_and_get_signal(signal_ref_index: number): Promise { - return await invoke("load_and_get_signal", { signal_ref_index }); +export async function load_signal_and_get_timeline( + signal_ref_index: number, + timeline_zoom: number, + timeline_viewport_width: number, + timeline_viewport_x: number, + block_height: number, + var_format: VarFormat, +): Promise { + 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 { diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 545e49d..29e17bc 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -9,3 +9,11 @@ publish.workspace = true [dependencies] wellen.workspace = true +moonlight.workspace = true +convert-base = "1.1.2" +# @TODO update `futures_util_ext` - add feature `sink`, set exact `futures-util` version +futures-util = { version = "0.3.30", features = ["sink"] } + +[features] +frontend = ["moonlight/frontend"] +backend = ["moonlight/backend"] diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 89b0b3b..63f2435 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1 +1,32 @@ +use moonlight::*; + +mod var_format; +pub use var_format::VarFormat; + +mod signal_to_timeline; +pub use signal_to_timeline::signal_to_timeline; + pub mod wellen_helpers; + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(crate = "serde")] +pub struct Timeline { + pub blocks: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(crate = "serde")] +pub struct TimelineBlock { + pub x: i32, + pub width: u32, + pub height: u32, + pub label: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(crate = "serde")] +pub struct TimeLineBlockLabel { + pub text: String, + pub x: u32, + pub y: u32, +} diff --git a/shared/src/signal_to_timeline.rs b/shared/src/signal_to_timeline.rs new file mode 100644 index 0000000..f2d05b6 --- /dev/null +++ b/shared/src/signal_to_timeline.rs @@ -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 } +} diff --git a/shared/src/var_format.rs b/shared/src/var_format.rs new file mode 100644 index 0000000..dc1d844 --- /dev/null +++ b/shared/src/var_format.rs @@ -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::>(); + let mut base = convert_base::Convert::new(2, 16); + let output = base.convert::(&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::>(); + let mut base = convert_base::Convert::new(2, 8); + let output = base.convert::(&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::>(); + + // 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::(&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::>(); + let mut base = convert_base::Convert::new(2, 10); + let output = base.convert::(&ones_and_zeros); + let value: String = output + .into_iter() + .rev() + .map(|number| char::from_digit(number, 10).unwrap()) + .collect(); + value + } + } + } +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6bec432..490242c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,8 +16,8 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "=2.0.0-beta.17", features = [] } [dependencies] -shared.workspace = true wellen.workspace = true +shared = { path = "../shared", features = ["backend"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "=2.0.0-beta.22", features = ["macos-private-api", "linux-ipc-protocol"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index daf6974..d985c1c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -40,25 +40,32 @@ async fn get_hierarchy(store: tauri::State<'_, Store>) -> Result) -> Result { - let waveform = store.waveform.lock().unwrap(); - let time_table = waveform.as_ref().unwrap().time_table(); - Ok(serde_json::to_value(time_table).unwrap()) -} - -#[tauri::command(rename_all = "snake_case")] -async fn load_and_get_signal( +async fn load_signal_and_get_timeline( signal_ref_index: usize, + timeline_zoom: f64, + timeline_viewport_width: u32, + timeline_viewport_x: i32, + block_height: u32, + var_format: shared::VarFormat, store: tauri::State<'_, Store>, ) -> Result { + // @TODO run (all?) in a blocking thread? let signal_ref = wellen::SignalRef::from_index(signal_ref_index).unwrap(); let mut waveform_lock = store.waveform.lock().unwrap(); let waveform = waveform_lock.as_mut().unwrap(); - // @TODO maybe run it in a thread to not block the main one and then - // make the command async or return the result through a Tauri channel waveform.load_signals_multi_threaded(&[signal_ref]); let signal = waveform.get_signal(signal_ref).unwrap(); - Ok(serde_json::to_value(signal).unwrap()) + let time_table = waveform.time_table(); + let timeline = shared::signal_to_timeline( + signal, + time_table, + timeline_zoom, + timeline_viewport_width, + timeline_viewport_x, + block_height, + var_format, + ); + Ok(serde_json::to_value(timeline).unwrap()) } #[tauri::command(rename_all = "snake_case")] @@ -85,8 +92,7 @@ pub fn run() { show_window, pick_and_load_waveform, get_hierarchy, - get_time_table, - load_and_get_signal, + load_signal_and_get_timeline, unload_signal, ]) .run(tauri::generate_context!())