diff --git a/Cargo.lock b/Cargo.lock index 1100ddb..c0ef9a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1420,7 +1420,6 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" name = "fastwave" version = "0.1.0" dependencies = [ - "convert-base", "serde", "serde_json", "shared", @@ -1506,7 +1505,6 @@ dependencies = [ name = "frontend" version = "0.1.0" dependencies = [ - "convert-base", "gloo-file", "shared", "wasm-bindgen-test", @@ -4132,6 +4130,7 @@ dependencies = [ name = "shared" version = "0.1.0" dependencies = [ + "convert-base", "futures-util", "moonlight", "wellen", diff --git a/Cargo.toml b/Cargo.toml index 6ca9a5f..5ab2fb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ readme = "../README.md" publish = false [workspace.dependencies] -convert-base = "1.1.2" # 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" } diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 7ba178a..930650e 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -13,7 +13,6 @@ wasm-bindgen-test = "0.3.19" [dependencies] zoon.workspace = true wellen.workspace = true -convert-base.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 942df2e..69fff23 100644 --- a/frontend/src/controls_panel.rs +++ b/frontend/src/controls_panel.rs @@ -170,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)) @@ -183,47 +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) 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(); + .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 = 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))) - } - }) + 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))) + } + }) }) - }) + }), ) } diff --git a/frontend/src/main.rs b/frontend/src/main.rs index 778d6b6..e551c3d 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -1,5 +1,5 @@ -use zoon::*; use std::rc::Rc; +use zoon::*; mod platform; @@ -42,13 +42,22 @@ fn root() -> impl Element { selected_var_refs.clone(), layout.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::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.clone(), - selected_var_refs.clone(), - ))) } diff --git a/frontend/src/platform/browser.rs b/frontend/src/platform/browser.rs index ff18cfc..f0d2ce6 100644 --- a/frontend/src/platform/browser.rs +++ b/frontend/src/platform/browser.rs @@ -137,13 +137,7 @@ fn signal_to_timeline( continue; } - // @TODO dynamic formatter - // @TODO optimize it by not using `.to_string` if possible - let value = value.to_string(); - 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().map(|number| char::from_digit(number, 16).unwrap()).collect(); + let value = shared::VarFormat::default().format(value); let value_width = value.chars().count() as u32 * LETTER_WIDTH; let label = if (value_width + (2 * LABEL_X_PADDING)) <= block_width { diff --git a/frontend/src/waveform_panel.rs b/frontend/src/waveform_panel.rs index f9a4c3d..5cdac9f 100644 --- a/frontend/src/waveform_panel.rs +++ b/frontend/src/waveform_panel.rs @@ -126,7 +126,7 @@ impl WaveformPanel { zoon::println!("{timescale:?}"); // Note: Sync `timeline`'s type with the `Timeline` in `frontend/typescript/pixi_canvas/pixi_canvas.ts' - let timeline = 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(); controller.push_var(signal_ref_index, timeline); } @@ -140,23 +140,56 @@ impl WaveformPanel { 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)) + .item(self.selected_var_format_button()) + .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(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(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) -> impl Element { + let var_format = Mutable::new(shared::VarFormat::default()); + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + 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 || var_format.update(|format| format.next())) } } diff --git a/frontend/src/waveform_panel/pixi_canvas.rs b/frontend/src/waveform_panel/pixi_canvas.rs index b0a5687..95f6eb8 100644 --- a/frontend/src/waveform_panel/pixi_canvas.rs +++ b/frontend/src/waveform_panel/pixi_canvas.rs @@ -1,7 +1,7 @@ -pub use js_bridge::PixiController; -use zoon::*; -use std::rc::Rc; use crate::platform; +pub use js_bridge::PixiController; +use std::rc::Rc; +use zoon::*; pub struct PixiCanvas { raw_el: RawHtmlEl, @@ -41,22 +41,31 @@ impl PixiCanvas { let height = height.signal() => (*width, *height) } .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 - } - }))), + .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, screen_width, row_height| { - 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, screen_width, row_height).await; - let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw(); - Ok(timeline) - }) - })); + let timeline_getter = Rc::new(Closure::new( + |signal_ref_index, screen_width, row_height| { + 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, + screen_width, + row_height, + ) + .await; + let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw(); + Ok(timeline) + }) + }, + )); // -- // -- Self { controller: controller.read_only(), @@ -105,7 +114,8 @@ mod js_bridge { type SignalRefIndex = usize; type ScreenWidth = u32; type RowHeight = u32; - type TimelineGetter = Closure TimelinePromise>; + type TimelineGetter = + Closure TimelinePromise>; // Note: Add all corresponding methods to `frontend/typescript/pixi_canvas/pixi_canvas.ts` #[wasm_bindgen(module = "/typescript/bundles/pixi_canvas.js")] @@ -115,7 +125,11 @@ mod js_bridge { // @TODO `row_height` and `row_gap` is FastWave-specific #[wasm_bindgen(constructor)] - pub fn new(row_height: u32, row_gap: u32, timeline_getter: &TimelineGetter) -> PixiController; + pub fn new( + row_height: u32, + row_gap: u32, + timeline_getter: &TimelineGetter, + ) -> PixiController; #[wasm_bindgen(method)] pub async fn init(this: &PixiController, parent_element: &JsValue); diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 0583e93..29e17bc 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -10,6 +10,7 @@ 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"] } diff --git a/shared/src/lib.rs b/shared/src/lib.rs index d6abfe6..21f98e8 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,5 +1,8 @@ use moonlight::*; +mod var_format; +pub use var_format::VarFormat; + pub mod wellen_helpers; #[derive(Serialize, Deserialize, Debug, Default)] diff --git a/shared/src/var_format.rs b/shared/src/var_format.rs new file mode 100644 index 0000000..5d9eb9f --- /dev/null +++ b/shared/src/var_format.rs @@ -0,0 +1,54 @@ +#[derive(Default, Clone, Copy, Debug)] +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 => "i32", + VarFormat::Unsigned => "u32", + } + } + + 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(); + 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() + .map(|number| char::from_digit(number, 16).unwrap()) + .collect(); + value + } +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 66394d0..490242c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,7 +17,6 @@ tauri-build = { version = "=2.0.0-beta.17", features = [] } [dependencies] wellen.workspace = true -convert-base.workspace = true shared = { path = "../shared", features = ["backend"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9b98a0c..3ba64ea 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -131,13 +131,7 @@ fn signal_to_timeline( continue; } - // @TODO dynamic formatter - // @TODO optimize it by not using `.to_string` if possible - let value = value.to_string(); - 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().map(|number| char::from_digit(number, 16).unwrap()).collect(); + let value = shared::VarFormat::default().format(value); let value_width = value.chars().count() as u32 * LETTER_WIDTH; let label = if (value_width + (2 * LABEL_X_PADDING)) <= block_width {