From 5c778b4350f29229d54c0e1520969a1d76dd4699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kav=C3=ADk?= Date: Mon, 17 Jun 2024 10:15:05 +0200 Subject: [PATCH] header_panel.rs --- frontend/src/controls_panel.rs | 253 +---------------------------- frontend/src/header_panel.rs | 280 +++++++++++++++++++++++++++++++++ frontend/src/main.rs | 12 ++ 3 files changed, 295 insertions(+), 250 deletions(-) create mode 100644 frontend/src/header_panel.rs diff --git a/frontend/src/controls_panel.rs b/frontend/src/controls_panel.rs index c2734ba..3bc27b3 100644 --- a/frontend/src/controls_panel.rs +++ b/frontend/src/controls_panel.rs @@ -1,4 +1,4 @@ -use crate::{platform, script_bridge, Layout}; +use crate::{Layout, Filename}; use std::cell::Cell; use std::mem; use std::ops::Not; @@ -9,8 +9,6 @@ use zoon::*; const SCOPE_VAR_ROW_MAX_WIDTH: u32 = 480; const MILLER_COLUMN_MAX_HEIGHT: u32 = 500; -type Filename = String; - #[derive(Clone)] struct VarForUI { name: Rc, @@ -45,13 +43,14 @@ impl ControlsPanel { hierarchy: Mutable>>, selected_var_refs: MutableVec, layout: Mutable, + loaded_filename: Mutable>, ) -> impl Element { Self { selected_scope_ref: <_>::default(), hierarchy, selected_var_refs, layout, - loaded_filename: <_>::default(), + loaded_filename, } .root() } @@ -101,14 +100,6 @@ impl ControlsPanel { .s(Padding::all(20)) .s(Gap::new().y(40)) .s(Align::new().top()) - .item( - Row::new() - .s(Gap::both(15)) - .s(Align::new().left()) - .item(self.load_button()) - .item(self.layout_switcher()), - ) - .item(self.command_panel()) .item_signal( self.hierarchy .signal_cloned() @@ -123,244 +114,6 @@ impl ControlsPanel { )) } - #[cfg(FASTWAVE_PLATFORM = "TAURI")] - fn load_button(&self) -> impl Element { - let (hovered, hovered_signal) = Mutable::new_and_signal(false); - let hierarchy = self.hierarchy.clone(); - let loaded_filename = self.loaded_filename.clone(); - Button::new() - .s(Padding::new().x(20).y(10)) - .s(Background::new().color_signal( - hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), - )) - .s(Align::new().left()) - .s(RoundedCorners::all(15)) - .label(El::new().s(Font::new().no_wrap()).child_signal( - loaded_filename.signal_cloned().map_option( - |filename| format!("Unload {filename}"), - || format!("Load file.."), - ), - )) - .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) - .on_press(move || { - let mut hierarchy_lock = hierarchy.lock_mut(); - if hierarchy_lock.is_some() { - *hierarchy_lock = None; - return; - } - 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)); - hierarchy.set(Some(Rc::new(platform::get_hierarchy().await))) - } - }) - }) - } - - #[cfg(FASTWAVE_PLATFORM = "BROWSER")] - fn load_button(&self) -> impl Element { - let (hovered, hovered_signal) = Mutable::new_and_signal(false); - let hierarchy = self.hierarchy.clone(); - let loaded_filename = self.loaded_filename.clone(); - let file_input_id = "file_input"; - Row::new() - .item( - Label::new() - .s(Padding::new().x(20).y(10)) - .s(Background::new().color_signal( - hovered_signal - .map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), - )) - .s(Align::new().left()) - .s(RoundedCorners::all(15)) - .s(Cursor::new(CursorIcon::Pointer)) - .label(El::new().s(Font::new().no_wrap()).child_signal( - loaded_filename.signal_cloned().map_option( - |filename| format!("Unload {filename}"), - || format!("Load file.."), - ), - )) - .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(); - } - 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))) - } - }) - }) - }), - ) - } - - fn layout_switcher(&self) -> impl Element { - let layout = self.layout.clone(); - let (hovered, hovered_signal) = Mutable::new_and_signal(false); - Button::new() - .s(Padding::new().x(20).y(10)) - .s(Background::new().color_signal( - hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), - )) - .s(Align::new().left()) - .s(RoundedCorners::all(15)) - .label_signal(layout.signal().map(|layout| match layout { - Layout::Tree => "Columns", - Layout::Columns => "Tree", - })) - .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) - .on_press(move || { - layout.update(|layout| match layout { - Layout::Tree => Layout::Columns, - Layout::Columns => Layout::Tree, - }) - }) - } - - fn command_panel(&self) -> impl Element { - let command_result: Mutable>> = <_>::default(); - Row::new() - .s(Gap::both(30)) - .item(self.command_editor_panel(command_result.clone())) - .item(self.command_result_panel(command_result.read_only())) - } - - fn command_editor_panel( - &self, - command_result: Mutable>>, - ) -> impl Element { - Column::new() - .s(Gap::new().y(10)) - .item( - Row::new() - .s(Gap::new().x(15)) - .s(Padding::new().x(5)) - .item(El::new().child("Javascript command")) - .item(El::new().s(Align::new().right()).child("Shift + Enter")) - ) - .item(self.command_editor(command_result)) - } - - fn command_editor( - &self, - command_result: Mutable>>, - ) -> impl Element { - let (script, script_signal) = Mutable::new_and_signal_cloned(String::new()); - // @TODO perhaps replace with an element with syntax highlighter like https://github.com/WebCoder49/code-input later - TextArea::new() - .s(Background::new().color(color!("SlateBlue"))) - .s(Padding::new().x(10).y(8)) - .s(RoundedCorners::all(15)) - .s(Height::default().min(50)) - .s(Width::default().min(300)) - .s(Font::new().tracking(1).weight(FontWeight::Medium).color(color!("White")).family([FontFamily::new("Courier New"), FontFamily::Monospace])) - .s(Shadows::new([Shadow::new().inner().color(color!("DarkSlateBlue")).blur(4)])) - // @TODO to MZ API? (together with autocomplete and others?) - .update_raw_el(|raw_el| raw_el.attr("spellcheck", "false")) - .placeholder(Placeholder::new("FW.say_hello()").s(Font::new().color(color!("LightBlue")))) - .label_hidden("command editor panel") - .text_signal(script_signal) - .on_change(clone!((script, command_result) move |text| { - script.set_neq(text); - command_result.set_neq(None); - })) - .on_key_down_event_with_options(EventOptions::new().preventable(), move |event| { - if event.key() == &Key::Enter { - let RawKeyboardEvent::KeyDown(raw_event) = event.raw_event.clone(); - if raw_event.shift_key() { - // @TODO move `prevent_default` to MZ API (next to the `pass_to_parent` method?) - raw_event.prevent_default(); - let result = script_bridge::strict_eval(&script.lock_ref()); - command_result.set(Some(result)); - } - } - }) - } - - fn command_result_panel( - &self, - command_result: ReadOnlyMutable>>, - ) -> impl Element { - Column::new() - .s(Gap::new().y(10)) - .s(Align::new().top()) - .s(Scrollbars::both()) - .item( - El::new() - .child("Command result") - ) - .item(self.command_result_el(command_result)) - } - - fn command_result_el( - &self, - command_result: ReadOnlyMutable>>, - ) -> impl Element { - El::new() - .s(Font::new().tracking(1).weight(FontWeight::Medium).color(color!("White")).family([FontFamily::new("Courier New"), FontFamily::Monospace])) - .s(Scrollbars::both()) - .s(Height::default().max(100)) - .child_signal( - command_result.signal_ref(|result| match result { - Some(Ok(js_value)) => { - if let Some(string_value) = js_value.as_string() { - string_value - } else if let Some(number_value) = js_value.as_f64() { - number_value.to_string() - } else if let Some(bool_value) = js_value.as_bool() { - bool_value.to_string() - } else { - format!("{js_value:?}") - } - } - Some(Err(js_value)) => { - format!("Error: {js_value:?}") - } - None => "-".to_owned() - }), - ) - } - fn scopes_panel(&self, hierarchy: Rc) -> impl Element { Column::new() .s(Height::fill().min(150)) diff --git a/frontend/src/header_panel.rs b/frontend/src/header_panel.rs new file mode 100644 index 0000000..45ad88b --- /dev/null +++ b/frontend/src/header_panel.rs @@ -0,0 +1,280 @@ +use zoon::*; +use crate::{platform, script_bridge, Layout, Filename}; +use std::rc::Rc; + +pub struct HeaderPanel { + hierarchy: Mutable>>, + layout: Mutable, + loaded_filename: Mutable>, +} + +impl HeaderPanel { + pub fn new( + hierarchy: Mutable>>, + layout: Mutable, + loaded_filename: Mutable>, + ) -> impl Element { + Self { + hierarchy, + layout, + loaded_filename, + } + .root() + } + + fn root(&self) -> impl Element { + Row::new() + .s(Padding::new().x(20).y(15)) + .s(Gap::both(40)) + .item( + Row::new() + .s(Align::new().top()) + .s(Padding::new().top(5)) + .s(Gap::both(15)) + .item(self.load_button()) + .item(self.layout_switcher()) + ) + .item(self.command_panel()) + } + + #[cfg(FASTWAVE_PLATFORM = "TAURI")] + fn load_button(&self) -> impl Element { + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + let hierarchy = self.hierarchy.clone(); + let loaded_filename = self.loaded_filename.clone(); + Button::new() + .s(Padding::new().x(20).y(10)) + .s(Background::new().color_signal( + hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + )) + .s(Align::new().left()) + .s(RoundedCorners::all(15)) + .label(El::new().s(Font::new().no_wrap()).child_signal( + loaded_filename.signal_cloned().map_option( + |filename| format!("Unload {filename}"), + || format!("Load file.."), + ), + )) + .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) + .on_press(move || { + let mut hierarchy_lock = hierarchy.lock_mut(); + if hierarchy_lock.is_some() { + *hierarchy_lock = None; + return; + } + 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)); + hierarchy.set(Some(Rc::new(platform::get_hierarchy().await))) + } + }) + }) + } + + #[cfg(FASTWAVE_PLATFORM = "BROWSER")] + fn load_button(&self) -> impl Element { + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + let hierarchy = self.hierarchy.clone(); + let loaded_filename = self.loaded_filename.clone(); + let file_input_id = "file_input"; + Row::new() + .item( + Label::new() + .s(Padding::new().x(20).y(10)) + .s(Background::new().color_signal( + hovered_signal + .map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + )) + .s(Align::new().left()) + .s(RoundedCorners::all(15)) + .s(Cursor::new(CursorIcon::Pointer)) + .label(El::new().s(Font::new().no_wrap()).child_signal( + loaded_filename.signal_cloned().map_option( + |filename| format!("Unload {filename}"), + || format!("Load file.."), + ), + )) + .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(); + } + 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))) + } + }) + }) + }), + ) + } + + fn layout_switcher(&self) -> impl Element { + let layout = self.layout.clone(); + let (hovered, hovered_signal) = Mutable::new_and_signal(false); + Button::new() + .s(Padding::new().x(20).y(10)) + .s(Background::new().color_signal( + hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")), + )) + .s(RoundedCorners::all(15)) + .label_signal(layout.signal().map(|layout| match layout { + Layout::Tree => "Columns", + Layout::Columns => "Tree", + })) + .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered)) + .on_press(move || { + layout.update(|layout| match layout { + Layout::Tree => Layout::Columns, + Layout::Columns => Layout::Tree, + }) + }) + } + + fn command_panel(&self) -> impl Element { + let command_result: Mutable>> = <_>::default(); + Row::new() + .s(Align::new().top()) + .s(Gap::both(30)) + .s(Scrollbars::both()) + .item(self.command_editor_panel(command_result.clone())) + .item(self.command_result_panel(command_result.read_only())) + } + + fn command_editor_panel( + &self, + command_result: Mutable>>, + ) -> impl Element { + Column::new() + .s(Align::new().top()) + .s(Gap::new().y(10)) + .item( + Row::new() + .s(Gap::new().x(15)) + .s(Padding::new().x(5)) + .item(El::new().child("Javascript command")) + .item(El::new().s(Align::new().right()).child("Shift + Enter")) + ) + .item(self.command_editor(command_result)) + } + + fn command_editor( + &self, + command_result: Mutable>>, + ) -> impl Element { + let (script, script_signal) = Mutable::new_and_signal_cloned(String::new()); + // @TODO perhaps replace with an element with syntax highlighter like https://github.com/WebCoder49/code-input later + TextArea::new() + .s(Background::new().color(color!("SlateBlue"))) + .s(Padding::new().x(10).y(8)) + .s(RoundedCorners::all(15)) + .s(Height::default().min(50)) + .s(Width::default().min(300)) + .s(Font::new().tracking(1).weight(FontWeight::Medium).color(color!("White")).family([FontFamily::new("Courier New"), FontFamily::Monospace])) + .s(Shadows::new([Shadow::new().inner().color(color!("DarkSlateBlue")).blur(4)])) + // @TODO to MZ API? (together with autocomplete and others?) + .update_raw_el(|raw_el| raw_el.attr("spellcheck", "false")) + .placeholder(Placeholder::new("FW.say_hello()").s(Font::new().color(color!("LightBlue")))) + .label_hidden("command editor panel") + .text_signal(script_signal) + .on_change(clone!((script, command_result) move |text| { + script.set_neq(text); + command_result.set_neq(None); + })) + .on_key_down_event_with_options(EventOptions::new().preventable(), move |event| { + if event.key() == &Key::Enter { + let RawKeyboardEvent::KeyDown(raw_event) = event.raw_event.clone(); + if raw_event.shift_key() { + // @TODO move `prevent_default` to MZ API (next to the `pass_to_parent` method?) + raw_event.prevent_default(); + let result = script_bridge::strict_eval(&script.lock_ref()); + command_result.set(Some(result)); + } + } + }) + } + + fn command_result_panel( + &self, + command_result: ReadOnlyMutable>>, + ) -> impl Element { + Column::new() + .s(Gap::new().y(10)) + .s(Align::new().top()) + .s(Scrollbars::both()) + .s(Padding::new().x(5)) + .item( + El::new() + .child("Command result") + ) + .item(self.command_result_el(command_result)) + } + + fn command_result_el( + &self, + command_result: ReadOnlyMutable>>, + ) -> impl Element { + El::new() + .s(Font::new().tracking(1).weight(FontWeight::Medium).color(color!("White")).family([FontFamily::new("Courier New"), FontFamily::Monospace])) + .s(Scrollbars::both()) + .s(Height::default().max(100)) + .child_signal( + command_result.signal_ref(|result| match result { + Some(Ok(js_value)) => { + if let Some(string_value) = js_value.as_string() { + string_value + } else if let Some(number_value) = js_value.as_f64() { + number_value.to_string() + } else if let Some(bool_value) = js_value.as_bool() { + bool_value.to_string() + } else { + format!("{js_value:?}") + } + } + Some(Err(js_value)) => { + format!("Error: {js_value:?}") + } + None => "-".to_owned() + }), + ) + } +} diff --git a/frontend/src/main.rs b/frontend/src/main.rs index ef7859d..f459692 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -10,6 +10,9 @@ use controls_panel::ControlsPanel; mod waveform_panel; use waveform_panel::WaveformPanel; +mod header_panel; +use header_panel::HeaderPanel; + #[derive(Clone, Copy, Default)] enum Layout { Tree, @@ -17,6 +20,8 @@ enum Layout { Columns, } +type Filename = String; + #[derive(Default)] struct Store { selected_var_refs: MutableVec, @@ -37,10 +42,16 @@ fn root() -> impl Element { let hierarchy: Mutable>> = <_>::default(); let selected_var_refs = STORE.selected_var_refs.clone(); let layout: Mutable = <_>::default(); + let loaded_filename: Mutable> = <_>::default(); Column::new() .s(Height::fill()) .s(Scrollbars::y_and_clip_x()) .s(Font::new().color(color!("Lavender"))) + .item(HeaderPanel::new( + hierarchy.clone(), + layout.clone(), + loaded_filename.clone(), + )) .item( Row::new() .s(Height::fill()) @@ -49,6 +60,7 @@ fn root() -> impl Element { hierarchy.clone(), selected_var_refs.clone(), layout.clone(), + loaded_filename, )) .item_signal( layout