Zoom, pan, value formatting #2

Merged
MartinKavik merged 23 commits from zoom_pan into main 2024-06-14 20:39:30 +00:00
26 changed files with 1079 additions and 355 deletions

View file

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

21
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

@ -4,11 +4,23 @@
---
<p align="center">
<img width="800" src="docs/screenshot_firefox.png" alt="fastwave_screenshot_firefox" />
<img width="800" src="docs/screenshot_firefox.png" alt="Fastwave - Browser (Firefox)" />
Browser (Firefox)
</p>
<p align="center">
<img width="800" src="docs/video_desktop.gif" alt="fastwave_video_desktop" />
<img width="800" src="docs/video_desktop.gif" alt="Fastwave - Desktop, miller columns and tree" />
Desktop, miller columns and tree
</p>
<p align="center">
<img width="800" src="docs/video_zoom_formatting_simple.gif" alt="Fastwave - Zoom, pan and basic number formats" />
Zoom, pan and basic number formats
</p>
<p align="center">
<img width="800" src="docs/video_zoom_formatting.gif" alt="Fastwave - Zoom and all formats" />
Zoom and all formats
</p>
---
@ -22,7 +34,7 @@
___
### Start:
### Start the desktop version:
1. `makers start`
@ -39,21 +51,34 @@ Troubleshooting:
---
### Start in the browser:
### Production build of the desktop version:
1. `makers bundle`
2. Runnable executable is in `target/release`
3. Installable bundles specific for the platform are in `target/release/bundle`
---
### Start in a browser:
1. `makers start_browser`
2. Ctrl + Click the server URL mentioned in the terminal log
---
### Start in a browser in the release mode:
1. `makers start_browser_release`
2. Ctrl + Click the server URL mentioned in the terminal log
---
### Steps before pushing:
1. `makers format`
---
### Production build:
1. `makers bundle`
2. Runnable executable is in `target/release`
3. Installable bundles specific for the platform are in `target/release/bundle`
### Test files
See the folder `test_files`.

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

View file

@ -1,5 +1,4 @@
use crate::{platform, HierarchyAndTimeTable, Layout};
use futures_util::join;
use crate::{platform, Layout};
use std::cell::Cell;
use std::mem;
use std::ops::Not;
@ -35,7 +34,7 @@ struct ScopeForUI {
#[derive(Clone)]
pub struct ControlsPanel {
selected_scope_ref: Mutable<Option<wellen::ScopeRef>>,
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
selected_var_refs: MutableVec<wellen::VarRef>,
layout: Mutable<Layout>,
loaded_filename: Mutable<Option<Filename>>,
@ -43,13 +42,13 @@ pub struct ControlsPanel {
impl ControlsPanel {
pub fn new(
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
selected_var_refs: MutableVec<wellen::VarRef>,
layout: Mutable<Layout>,
) -> impl Element {
Self {
selected_scope_ref: <_>::default(),
hierarchy_and_time_table,
hierarchy,
selected_var_refs,
layout,
loaded_filename: <_>::default(),
@ -60,7 +59,7 @@ impl ControlsPanel {
fn triggers(&self) -> Vec<TaskHandle> {
vec![Task::start_droppable(clone!((self => s) async move {
let was_some = Cell::new(false);
s.hierarchy_and_time_table
s.hierarchy
.signal_ref(Option::is_some)
.dedupe()
.for_each_sync(clone!((s) move |is_some| {
@ -81,8 +80,8 @@ impl ControlsPanel {
let layout = self.layout.clone();
let layout_and_hierarchy_signal = map_ref! {
let layout = layout.signal(),
let hierarchy_and_time_table = self.hierarchy_and_time_table.signal_cloned() => {
(*layout, hierarchy_and_time_table.clone().map(|(hierarchy, _)| hierarchy))
let hierarchy = self.hierarchy.signal_cloned() => {
(*layout, hierarchy.clone())
}
};
Column::new()
@ -99,7 +98,6 @@ impl ControlsPanel {
Layout::Columns => Height::fill().max(MILLER_COLUMN_MAX_HEIGHT),
},
)))
.s(Scrollbars::both())
.s(Padding::all(20))
.s(Gap::new().y(40))
.s(Align::new().top())
@ -111,9 +109,9 @@ impl ControlsPanel {
.item(self.layout_switcher()),
)
.item_signal(
self.hierarchy_and_time_table
self.hierarchy
.signal_cloned()
.map_some(clone!((self => s) move |(hierarchy, _)| s.scopes_panel(hierarchy))),
.map_some(clone!((self => s) move |hierarchy| s.scopes_panel(hierarchy))),
)
.item_signal(layout_and_hierarchy_signal.map(
clone!((self => s) move |(layout, hierarchy)| {
@ -127,7 +125,7 @@ impl ControlsPanel {
#[cfg(FASTWAVE_PLATFORM = "TAURI")]
fn load_button(&self) -> impl Element {
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
let hierarchy_and_time_table = self.hierarchy_and_time_table.clone();
let hierarchy = self.hierarchy.clone();
let loaded_filename = self.loaded_filename.clone();
Button::new()
.s(Padding::new().x(20).y(10))
@ -144,21 +142,18 @@ impl ControlsPanel {
))
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
.on_press(move || {
let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut();
if hierarchy_and_time_table_lock.is_some() {
*hierarchy_and_time_table_lock = None;
let mut hierarchy_lock = hierarchy.lock_mut();
if hierarchy_lock.is_some() {
*hierarchy_lock = None;
return;
}
drop(hierarchy_and_time_table_lock);
let hierarchy_and_time_table = hierarchy_and_time_table.clone();
drop(hierarchy_lock);
let hierarchy = hierarchy.clone();
let loaded_filename = loaded_filename.clone();
Task::start(async move {
if let Some(filename) = platform::pick_and_load_waveform(None).await {
loaded_filename.set_neq(Some(filename));
let (hierarchy, time_table) =
join!(platform::get_hierarchy(), platform::get_time_table());
hierarchy_and_time_table
.set(Some((Rc::new(hierarchy), Rc::new(time_table))))
hierarchy.set(Some(Rc::new(platform::get_hierarchy().await)))
}
})
})
@ -167,7 +162,7 @@ impl ControlsPanel {
#[cfg(FASTWAVE_PLATFORM = "BROWSER")]
fn load_button(&self) -> impl Element {
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
let hierarchy_and_time_table = self.hierarchy_and_time_table.clone();
let hierarchy = self.hierarchy.clone();
let loaded_filename = self.loaded_filename.clone();
let file_input_id = "file_input";
Row::new()
@ -175,7 +170,8 @@ impl ControlsPanel {
Label::new()
.s(Padding::new().x(20).y(10))
.s(Background::new().color_signal(
hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
hovered_signal
.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
))
.s(Align::new().left())
.s(RoundedCorners::all(15))
@ -188,50 +184,52 @@ impl ControlsPanel {
))
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
.for_input(file_input_id)
.on_click_event_with_options(EventOptions::new().preventable(), clone!((hierarchy_and_time_table) move |event| {
let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut();
if hierarchy_and_time_table_lock.is_some() {
*hierarchy_and_time_table_lock = None;
if let RawMouseEvent::Click(raw_event) = event.raw_event {
// @TODO Move to MoonZoon as a new API
raw_event.prevent_default();
.on_click_event_with_options(
EventOptions::new().preventable(),
clone!((hierarchy) move |event| {
let mut hierarchy_lock = hierarchy.lock_mut();
if hierarchy_lock.is_some() {
*hierarchy_lock = None;
if let RawMouseEvent::Click(raw_event) = event.raw_event {
// @TODO Move to MoonZoon as a new API
raw_event.prevent_default();
}
return;
}
return;
}
}))
}),
),
)
.item(
// @TODO https://github.com/MoonZoon/MoonZoon/issues/39
// + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_hidden_file_input_elements_using_the_click_method
TextInput::new()
.id(file_input_id)
.update_raw_el(|raw_el| {
let dom_element = raw_el.dom_element();
raw_el
.style("display", "none")
.attr("type", "file")
.event_handler(move |_: events::Input| {
let Some(file_list) = dom_element.files().map(gloo_file::FileList::from) else {
zoon::println!("file list is `None`");
return;
};
let Some(file) = file_list.first().cloned() else {
zoon::println!("file list is empty");
return;
};
let hierarchy_and_time_table = hierarchy_and_time_table.clone();
let loaded_filename = loaded_filename.clone();
Task::start(async move {
if let Some(filename) = platform::pick_and_load_waveform(Some(file)).await {
loaded_filename.set_neq(Some(filename));
let (hierarchy, time_table) =
join!(platform::get_hierarchy(), platform::get_time_table());
hierarchy_and_time_table
.set(Some((Rc::new(hierarchy), Rc::new(time_table))))
}
})
TextInput::new().id(file_input_id).update_raw_el(|raw_el| {
let dom_element = raw_el.dom_element();
raw_el
.style("display", "none")
.attr("type", "file")
.event_handler(move |_: events::Input| {
let Some(file_list) =
dom_element.files().map(gloo_file::FileList::from)
else {
zoon::println!("file list is `None`");
return;
};
let Some(file) = file_list.first().cloned() else {
zoon::println!("file list is empty");
return;
};
let hierarchy = hierarchy.clone();
let loaded_filename = loaded_filename.clone();
Task::start(async move {
if let Some(filename) =
platform::pick_and_load_waveform(Some(file)).await
{
loaded_filename.set_neq(Some(filename));
hierarchy.set(Some(Rc::new(platform::get_hierarchy().await)))
}
})
})
})
}),
)
}
@ -563,6 +561,7 @@ impl ControlsPanel {
});
// Lazy loading to not freeze the main thread
// @TODO replace with grouping and/or virtual scroll (https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib)
const CHUNK_SIZE: usize = 50;
let mut chunked_vars_for_ui: Vec<Vec<VarForUI>> = <_>::default();
let mut chunk = Vec::with_capacity(CHUNK_SIZE);

View file

@ -9,8 +9,6 @@ use controls_panel::ControlsPanel;
mod waveform_panel;
use waveform_panel::WaveformPanel;
type HierarchyAndTimeTable = (Rc<wellen::Hierarchy>, Rc<wellen::TimeTable>);
#[derive(Clone, Copy, Default)]
enum Layout {
Tree,
@ -28,7 +26,7 @@ fn main() {
}
fn root() -> impl Element {
let hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>> = <_>::default();
let hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>> = <_>::default();
let selected_var_refs: MutableVec<wellen::VarRef> = <_>::default();
let layout: Mutable<Layout> = <_>::default();
Column::new()
@ -40,17 +38,26 @@ fn root() -> impl Element {
.s(Height::fill())
.s(Gap::new().x(15))
.item(ControlsPanel::new(
hierarchy_and_time_table.clone(),
hierarchy.clone(),
selected_var_refs.clone(),
layout.clone(),
))
.item_signal(layout.signal().map(|layout| matches!(layout, Layout::Tree)).map_true(clone!((hierarchy_and_time_table, selected_var_refs) move || WaveformPanel::new(
hierarchy_and_time_table.clone(),
selected_var_refs.clone(),
))))
.item_signal(
layout
.signal()
.map(|layout| matches!(layout, Layout::Tree))
.map_true(
clone!((hierarchy, selected_var_refs) move || WaveformPanel::new(
hierarchy.clone(),
selected_var_refs.clone(),
)),
),
),
)
.item_signal(
layout
.signal()
.map(|layout| matches!(layout, Layout::Columns))
.map_true(move || WaveformPanel::new(hierarchy.clone(), selected_var_refs.clone())),
)
.item_signal(layout.signal().map(|layout| matches!(layout, Layout::Columns)).map_true(move || WaveformPanel::new(
hierarchy_and_time_table.clone(),
selected_var_refs.clone(),
)))
}

View file

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

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()
}
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) {

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()
}
pub(super) async fn get_time_table() -> wellen::TimeTable {
serde_wasm_bindgen::from_value(tauri_glue::get_time_table().await.unwrap_throw()).unwrap_throw()
}
pub(super) async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
pub(super) async fn load_signal_and_get_timeline(
signal_ref: wellen::SignalRef,
timeline_zoom: f64,
timeline_viewport_width: u32,
timeline_viewport_x: i32,
block_height: u32,
var_format: shared::VarFormat,
) -> shared::Timeline {
let var_format = serde_wasm_bindgen::to_value(&var_format).unwrap_throw();
serde_wasm_bindgen::from_value(
tauri_glue::load_and_get_signal(signal_ref.index())
.await
.unwrap_throw(),
tauri_glue::load_signal_and_get_timeline(
signal_ref.index(),
timeline_zoom,
timeline_viewport_width,
timeline_viewport_x,
block_height,
var_format,
)
.await
.unwrap_throw(),
)
.unwrap_throw()
}
@ -52,10 +63,14 @@ mod tauri_glue {
pub async fn get_hierarchy() -> Result<JsValue, JsValue>;
#[wasm_bindgen(catch)]
pub async fn get_time_table() -> Result<JsValue, JsValue>;
#[wasm_bindgen(catch)]
pub async fn load_and_get_signal(signal_ref_index: usize) -> Result<JsValue, JsValue>;
pub async fn load_signal_and_get_timeline(
signal_ref_index: usize,
timeline_zoom: f64,
timeline_viewport_width: u32,
timeline_viewport_x: i32,
block_height: u32,
var_format: JsValue,
) -> Result<JsValue, JsValue>;
#[wasm_bindgen(catch)]
pub async fn unload_signal(signal_ref_index: usize) -> Result<(), JsValue>;

View file

@ -1,6 +1,7 @@
use crate::{platform, HierarchyAndTimeTable};
use crate::platform;
use std::rc::Rc;
use wellen::GetItem;
use zoon::{eprintln, *};
use zoon::*;
mod pixi_canvas;
use pixi_canvas::{PixiCanvas, PixiController};
@ -11,21 +12,24 @@ const ROW_GAP: u32 = 4;
#[derive(Clone)]
pub struct WaveformPanel {
selected_var_refs: MutableVec<wellen::VarRef>,
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
canvas_controller: Mutable<ReadOnlyMutable<Option<PixiController>>>,
}
impl WaveformPanel {
pub fn new(
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
hierarchy: Mutable<Option<Rc<wellen::Hierarchy>>>,
selected_var_refs: MutableVec<wellen::VarRef>,
) -> impl Element {
Self {
selected_var_refs,
hierarchy_and_time_table,
hierarchy,
canvas_controller: Mutable::new(Mutable::default().read_only()),
}
.root()
}
// @TODO autoscroll down
fn root(&self) -> impl Element {
let selected_vars_panel_height_getter: Mutable<u32> = <_>::default();
Row::new()
@ -51,27 +55,29 @@ impl WaveformPanel {
fn canvas(&self, selected_vars_panel_height: ReadOnlyMutable<u32>) -> impl Element {
let selected_var_refs = self.selected_var_refs.clone();
let hierarchy_and_time_table = self.hierarchy_and_time_table.clone();
let hierarchy = self.hierarchy.clone();
let canvas_controller = self.canvas_controller.clone();
PixiCanvas::new(ROW_HEIGHT, ROW_GAP)
.s(Align::new().top())
.s(Width::fill())
.s(Height::exact_signal(selected_vars_panel_height.signal()))
.s(RoundedCorners::new().right(15))
.task_with_controller(move |controller| {
selected_var_refs.signal_vec().delay_remove(clone!((hierarchy_and_time_table) move |var_ref| {
clone!((var_ref, hierarchy_and_time_table) async move {
if let Some(hierarchy_and_time_table) = hierarchy_and_time_table.get_cloned() {
platform::unload_signal(hierarchy_and_time_table.0.get(var_ref).signal_ref()).await;
canvas_controller.set(controller.clone());
selected_var_refs.signal_vec().delay_remove(clone!((hierarchy) move |var_ref| {
clone!((var_ref, hierarchy) async move {
if let Some(hierarchy) = hierarchy.get_cloned() {
// @TODO unload only when no other selected variable use it?
platform::unload_signal(hierarchy.get(var_ref).signal_ref()).await;
}
})
})).for_each(clone!((controller, hierarchy_and_time_table) move |vec_diff| {
clone!((controller, hierarchy_and_time_table) async move {
})).for_each(clone!((controller, hierarchy) move |vec_diff| {
clone!((controller, hierarchy) async move {
match vec_diff {
VecDiff::Replace { values } => {
let controller = controller.wait_for_some_cloned().await;
controller.clear_vars();
for var_ref in values {
Self::push_var(&controller, &hierarchy_and_time_table, var_ref).await;
Self::push_var(&controller, &hierarchy, var_ref).await;
}
},
VecDiff::InsertAt { index: _, value: _ } => { todo!("`task_with_controller` + `InsertAt`") }
@ -84,7 +90,7 @@ impl WaveformPanel {
VecDiff::Move { old_index: _, new_index: _ } => { todo!("`task_with_controller` + `Move`") }
VecDiff::Push { value: var_ref } => {
if let Some(controller) = controller.lock_ref().as_ref() {
Self::push_var(controller, &hierarchy_and_time_table, var_ref).await;
Self::push_var(controller, &hierarchy, var_ref).await;
}
}
VecDiff::Pop {} => {
@ -105,38 +111,34 @@ impl WaveformPanel {
async fn push_var(
controller: &PixiController,
hierarchy_and_time_table: &Mutable<Option<HierarchyAndTimeTable>>,
hierarchy: &Mutable<Option<Rc<wellen::Hierarchy>>>,
var_ref: wellen::VarRef,
) {
let (hierarchy, time_table) = hierarchy_and_time_table.get_cloned().unwrap();
if time_table.is_empty() {
eprintln!("timetable is empty");
return;
}
let last_time = time_table.last().copied().unwrap_throw();
let hierarchy = hierarchy.get_cloned().unwrap();
let var_format = shared::VarFormat::default();
let var = hierarchy.get(var_ref);
let signal_ref = var.signal_ref();
let signal = platform::load_and_get_signal(signal_ref).await;
let timeline = platform::load_signal_and_get_timeline(
signal_ref,
controller.get_timeline_zoom(),
controller.get_timeline_viewport_width(),
controller.get_timeline_viewport_x(),
ROW_HEIGHT,
var_format,
)
.await;
let timescale = hierarchy.timescale();
// @TODO remove
zoon::println!("{timescale:?}");
let mut timeline: Vec<(wellen::Time, String)> = signal
.iter_changes()
.map(|(time_index, signal_value)| {
(time_table[time_index as usize], signal_value.to_string())
})
.collect();
if timeline.is_empty() {
eprintln!("timeline is empty");
return;
}
timeline.push((last_time, timeline.last().cloned().unwrap_throw().1));
// Note: Sync `timeline`'s type with the `Timeline` in `frontend/typescript/pixi_canvas/pixi_canvas.ts'
controller.push_var(serde_wasm_bindgen::to_value(&timeline).unwrap_throw());
let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw();
let signal_ref_index = signal_ref.index();
let var_format = serde_wasm_bindgen::to_value(&var_format).unwrap_throw();
controller.push_var(signal_ref_index, timeline, var_format);
}
fn selected_var_panel(
@ -144,27 +146,74 @@ impl WaveformPanel {
index: ReadOnlyMutable<Option<usize>>,
var_ref: wellen::VarRef,
) -> Option<impl Element> {
let Some((hierarchy, _)) = self.hierarchy_and_time_table.get_cloned() else {
let Some(hierarchy) = self.hierarchy.get_cloned() else {
None?
};
let var = hierarchy.get(var_ref);
let name: &str = var.name(&hierarchy);
Row::new()
.item(self.selected_var_name_button(var.name(&hierarchy), index.clone()))
.item(self.selected_var_format_button(index))
.apply(Some)
}
fn selected_var_name_button(
&self,
name: &str,
index: ReadOnlyMutable<Option<usize>>,
) -> impl Element {
let selected_var_refs = self.selected_var_refs.clone();
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
Button::new()
.s(Height::exact(ROW_HEIGHT))
.s(Background::new().color(color!("SlateBlue", 0.8)))
.s(RoundedCorners::new().left(15))
.s(Width::growable())
.s(Background::new().color_signal(
hovered_signal.map_bool(|| color!("SlateBlue"), || color!("SlateBlue", 0.8)),
))
.s(RoundedCorners::new().left(15).right(5))
.label(
El::new()
.s(Align::center())
.s(Align::new().left())
.s(Padding::new().left(20).right(17).y(10))
.child(name),
)
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
.on_press(move || {
if let Some(index) = index.get() {
selected_var_refs.lock_mut().remove(index);
}
})
.apply(Some)
}
fn selected_var_format_button(&self, index: ReadOnlyMutable<Option<usize>>) -> impl Element {
let var_format = Mutable::new(shared::VarFormat::default());
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
let canvas_controller = self.canvas_controller.clone();
Button::new()
.s(Height::exact(ROW_HEIGHT))
.s(Width::exact(70))
.s(Background::new().color_signal(
hovered_signal.map_bool(|| color!("SlateBlue"), || color!("SlateBlue", 0.8)),
))
.s(RoundedCorners::new().left(5))
.label(
El::new()
.s(Align::center())
.s(Padding::new().left(20).right(17).y(10))
.child_signal(var_format.signal().map(|format| format.as_static_str())),
)
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
.on_press(move || {
let next_format = var_format.get().next();
var_format.set(next_format);
if let Some(canvas_controller) = canvas_controller.get_cloned().lock_ref().as_ref()
{
if let Some(index) = index.get() {
canvas_controller.set_var_format(
index,
serde_wasm_bindgen::to_value(&next_format).unwrap_throw(),
);
}
}
})
}
}

View file

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

View file

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

View file

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

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;
type BitString = string;
type Timeline = Array<[Time, BitString]>;
// @TODO sync with Rust and `tauri_glue.ts`
type Timeline = {
blocks: Array<TimelineBlock>
}
type TimelineBlock = {
x: number,
width: number,
height: number,
label: TimeLineBlockLabel | undefined,
}
type TimeLineBlockLabel = {
text: string,
x: number,
y: number,
}
type X = number;
type TimelineForUI = Array<[X, string]>;
// @TODO sync with Rust
enum VarFormat {
ASCII,
Binary,
BinaryWithGroups,
Hexadecimal,
Octal,
Signed,
Unsigned,
}
type TimelineGetter = (
signal_ref_index: number,
timeline_zoom: number,
timeline_viewport_width: number,
timeline_viewport_x: number,
row_height: number,
var_format: VarFormat
) => Promise<Timeline>;
export class PixiController {
app: Application
// -- FastWave-specific --
var_signal_rows: Array<VarSignalRow> = [];
var_signal_rows_container = new Container();
// @TODO reset `timeline_*` on file unload?
timeline_zoom: number;
timeline_viewport_width: number;
timeline_viewport_x: number;
row_height: number;
row_gap: number;
timeline_getter: TimelineGetter;
constructor(row_height: number, row_gap: number) {
constructor(
timeline_zoom: number,
timeline_viewport_width: number,
timeline_viewport_x: number,
row_height: number,
row_gap: number,
timeline_getter: TimelineGetter
) {
this.app = new Application();
// -- FastWave-specific --
this.timeline_zoom = timeline_zoom;
this.timeline_viewport_width = timeline_viewport_width;
this.timeline_viewport_x = timeline_viewport_x;
this.row_height = row_height;
this.row_gap = row_gap;
this.app.stage.addChild(this.var_signal_rows_container);
this.timeline_getter = timeline_getter;
}
async init(parent_element: HTMLElement) {
@ -28,8 +73,13 @@ export class PixiController {
parent_element.appendChild(this.app.canvas);
}
// Default automatic Pixi resizing is not reliable
queue_resize() {
// Default automatic Pixi resizing according to the parent is not reliable
// and the `app.renderer`'s `resize` event is fired on every browser window size change
async resize(width: number, _height: number) {
// -- FastWave-specific --
this.timeline_viewport_width = width;
await this.redraw_all_rows();
// -- // --
this.app.queueResize();
}
@ -46,16 +96,93 @@ export class PixiController {
this.app.destroy(rendererDestroyOptions, options);
}
get_timeline_zoom() {
return this.timeline_zoom;
}
get_timeline_viewport_width() {
return this.timeline_viewport_width;
}
get_timeline_viewport_x() {
return this.timeline_viewport_x;
}
// -- FastWave-specific --
async redraw_all_rows() {
await Promise.all(this.var_signal_rows.map(async row => {
const timeline = await this.timeline_getter(
row.signal_ref_index,
this.timeline_zoom,
this.timeline_viewport_width,
this.timeline_viewport_x,
this.row_height,
row.var_format
);
row.redraw(timeline);
}))
}
async redraw_row(index: number) {
const row = this.var_signal_rows[index];
if (typeof row !== 'undefined') {
const timeline = await this.timeline_getter(
row.signal_ref_index,
this.timeline_zoom,
this.timeline_viewport_width,
this.timeline_viewport_x,
this.row_height,
row.var_format
);
row.redraw(timeline);
}
}
async set_var_format(index: number, var_format: VarFormat) {
const row = this.var_signal_rows[index];
if (typeof row !== 'undefined') {
row.set_var_format(var_format);
this.redraw_row(index);
}
}
async zoom_or_pan(wheel_delta_y: number, shift_key: boolean, offset_x: number) {
if (shift_key) {
this.timeline_viewport_x += Math.sign(wheel_delta_y) * 20;
} else {
const offset_x_ratio = offset_x / this.timeline_viewport_width;
const old_timeline_width = this.timeline_viewport_width * this.timeline_zoom;
const new_zoom = this.timeline_zoom - Math.sign(wheel_delta_y) * this.timeline_zoom * 0.5;
const new_timeline_width = this.timeline_viewport_width * new_zoom;
if (new_timeline_width < this.timeline_viewport_width) {
this.timeline_zoom = 1;
this.timeline_viewport_x = 0;
} else {
const timeline_width_difference = new_timeline_width - old_timeline_width;
this.timeline_viewport_x += timeline_width_difference * offset_x_ratio;
this.timeline_zoom = new_zoom;
}
}
const timeline_width = this.timeline_viewport_width * this.timeline_zoom;
if (this.timeline_viewport_x < 0) {
this.timeline_viewport_x = 0;
} else if (this.timeline_viewport_x + this.timeline_viewport_width > timeline_width) {
this.timeline_viewport_x = timeline_width - this.timeline_viewport_width;
}
this.redraw_all_rows();
}
remove_var(index: number) {
if (typeof this.var_signal_rows[index] !== 'undefined') {
this.var_signal_rows[index].destroy();
}
}
push_var(timeline: Timeline) {
push_var(signal_ref_index: number, timeline: Timeline, var_format: VarFormat) {
new VarSignalRow(
signal_ref_index,
var_format,
timeline,
this.app,
this.var_signal_rows,
@ -75,23 +202,29 @@ export class PixiController {
}
class VarSignalRow {
app: Application;
signal_ref_index: number;
var_format: VarFormat;
timeline: Timeline;
last_time: Time;
formatter: (signal_value: BitString) => string;
timeline_for_ui: TimelineForUI;
app: Application;
owner: Array<VarSignalRow>;
index_in_owner: number;
rows_container: Container;
row_height: number;
row_gap: number;
row_height_with_gap: number;
renderer_resize_callback = () => this.redraw_on_canvas_resize();
// -- elements --
row_container = new Container();
row_container_background: Sprite;
signal_blocks_container = new Container();
label_style = new TextStyle({
align: "center",
fill: "White",
fontSize: 16,
fontFamily: '"Courier New", monospace',
});
constructor(
signal_ref_index: number,
var_format: VarFormat,
timeline: Timeline,
app: Application,
owner: Array<VarSignalRow>,
@ -99,16 +232,10 @@ class VarSignalRow {
row_height: number,
row_gap: number,
) {
this.app = app;
this.signal_ref_index = signal_ref_index;
this.var_format = var_format;
this.timeline = timeline;
this.last_time = timeline[timeline.length - 1][0];
this.formatter = signal_value => parseInt(signal_value, 2).toString(16);
this.timeline_for_ui = this.timeline.map(([time, value]) => {
const x = time / this.last_time * this.app.screen.width;
return [x, this.formatter(value)]
});
this.app = app;
this.row_height = row_height;
this.row_gap = row_gap;
@ -120,81 +247,66 @@ class VarSignalRow {
this.rows_container = rows_container;
this.draw();
this.app.renderer.on("resize", this.renderer_resize_callback);
}
draw() {
// row_container
this.row_container.y = this.index_in_owner * this.row_height_with_gap;
this.rows_container.addChild(this.row_container);
// signal_block_container
// row background
this.row_container_background = new Sprite();
this.row_container_background.texture = Texture.WHITE;
this.row_container_background.tint = '0x550099';
this.row_container_background.height = this.row_height;
this.row_container.addChild(this.row_container_background);
// signal_blocks_container
this.row_container.addChild(this.signal_blocks_container);
const label_style = new TextStyle({
align: "center",
fill: "White",
fontSize: 16,
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
});
this.draw();
}
this.timeline_for_ui.forEach(([x, value], index) => {
if (index == this.timeline_for_ui.length - 1) {
return;
}
const block_width = this.timeline_for_ui[index+1][0] - x;
const block_height = this.row_height;
set_var_format(var_format: VarFormat) {
this.var_format = var_format;
}
redraw(timeline: Timeline) {
this.timeline = timeline;
this.draw();
}
draw() {
// Screen can be null when we are, for instance, switching between miller columns and tree layout
// and then the canvas has to be recreated
if (this.app === null || this.app.screen === null) {
return;
}
this.row_container_background.width = this.app.screen.width;
// @TODO optimize by reusing a pool of blocks instead or removing all children on every redraw?
this.signal_blocks_container.removeChildren();
this.timeline.blocks.forEach(timeline_block => {
// signal_block
const signal_block = new Container();
signal_block.x = x;
signal_block.x = timeline_block.x;
this.signal_blocks_container.addChild(signal_block);
// background
const gap_between_blocks = 2;
const background = new Graphics()
.roundRect(0, 0, block_width, block_height, 15)
.fill("SlateBlue");
background.label = "background";
.rect(gap_between_blocks / 2, 0, timeline_block.width - gap_between_blocks, timeline_block.height)
.fill('SlateBlue');
signal_block.addChild(background);
// label
const label = new Text({ text: value, style: label_style });
label.x = (block_width - label.width) / 2;
label.y = (block_height - label.height) / 2;
label.visible = label.width < block_width;
label.label = "label";
signal_block.addChild(label);
})
}
redraw_on_canvas_resize() {
for (let index = 0; index < this.timeline_for_ui.length; index++) {
const x = this.timeline[index][0] / this.last_time * this.app.screen.width;
this.timeline_for_ui[index][0] = x;
}
this.timeline_for_ui.forEach(([x, _value], index) => {
if (index == this.timeline_for_ui.length - 1) {
return;
if (timeline_block.label !== undefined) {
const label = new Text();
label.text = timeline_block.label.text;
label.style = this.label_style;
label.x = timeline_block.label.x;
label.y = timeline_block.label.y;
signal_block.addChild(label);
}
const block_width = this.timeline_for_ui[index+1][0] - x;
const block_height = this.row_height;
// signal_block
const signal_block = this.signal_blocks_container.getChildAt(index);
signal_block.x = x;
// background
const background = signal_block.getChildByLabel("background")!;
background.width = block_width;
// label
const label = signal_block.getChildByLabel("label")!;
label.x = (block_width - label.width) / 2;
label.y = (block_height - label.height) / 2;
label.visible = label.width < block_width;
})
});
}
decrement_index() {
@ -203,7 +315,6 @@ class VarSignalRow {
}
destroy() {
this.app.renderer.off("resize", this.renderer_resize_callback);
this.owner.splice(this.index_in_owner, 1);
this.rows_container.removeChildAt(this.index_in_owner);
this.row_container.destroy(true);

View file

@ -6,8 +6,8 @@ const invoke = core.invoke;
type Filename = string;
type WellenHierarchy = unknown;
type WellenTimeTable = unknown;
type WellenSignal = unknown;
type Timeline = unknown;
type VarFormat = unknown;
export async function show_window(): Promise<void> {
return await invoke("show_window");
@ -21,12 +21,22 @@ export async function get_hierarchy(): Promise<WellenHierarchy> {
return await invoke("get_hierarchy");
}
export async function get_time_table(): Promise<WellenTimeTable> {
return await invoke("get_time_table");
}
export async function load_and_get_signal(signal_ref_index: number): Promise<WellenSignal> {
return await invoke("load_and_get_signal", { signal_ref_index });
export async function load_signal_and_get_timeline(
signal_ref_index: number,
timeline_zoom: number,
timeline_viewport_width: number,
timeline_viewport_x: number,
block_height: number,
var_format: VarFormat,
): Promise<Timeline> {
return await invoke("load_signal_and_get_timeline", {
signal_ref_index,
timeline_zoom,
timeline_viewport_width,
timeline_viewport_x,
block_height,
var_format
});
}
export async function unload_signal(signal_ref_index: number): Promise<void> {

View file

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

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;
#[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 = [] }
[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"] }

View file

@ -40,25 +40,32 @@ async fn get_hierarchy(store: tauri::State<'_, Store>) -> Result<serde_json::Val
}
#[tauri::command(rename_all = "snake_case")]
async fn get_time_table(store: tauri::State<'_, Store>) -> Result<serde_json::Value, ()> {
let waveform = store.waveform.lock().unwrap();
let time_table = waveform.as_ref().unwrap().time_table();
Ok(serde_json::to_value(time_table).unwrap())
}
#[tauri::command(rename_all = "snake_case")]
async fn load_and_get_signal(
async fn load_signal_and_get_timeline(
signal_ref_index: usize,
timeline_zoom: f64,
timeline_viewport_width: u32,
timeline_viewport_x: i32,
block_height: u32,
var_format: shared::VarFormat,
store: tauri::State<'_, Store>,
) -> Result<serde_json::Value, ()> {
// @TODO run (all?) in a blocking thread?
let signal_ref = wellen::SignalRef::from_index(signal_ref_index).unwrap();
let mut waveform_lock = store.waveform.lock().unwrap();
let waveform = waveform_lock.as_mut().unwrap();
// @TODO maybe run it in a thread to not block the main one and then
// make the command async or return the result through a Tauri channel
waveform.load_signals_multi_threaded(&[signal_ref]);
let signal = waveform.get_signal(signal_ref).unwrap();
Ok(serde_json::to_value(signal).unwrap())
let time_table = waveform.time_table();
let timeline = shared::signal_to_timeline(
signal,
time_table,
timeline_zoom,
timeline_viewport_width,
timeline_viewport_x,
block_height,
var_format,
);
Ok(serde_json::to_value(timeline).unwrap())
}
#[tauri::command(rename_all = "snake_case")]
@ -85,8 +92,7 @@ pub fn run() {
show_window,
pick_and_load_waveform,
get_hierarchy,
get_time_table,
load_and_get_signal,
load_signal_and_get_timeline,
unload_signal,
])
.run(tauri::generate_context!())