2024-09-14 17:12:04 +00:00
|
|
|
use crate::{platform, script_bridge, theme::*, Filename, Layout};
|
2024-06-17 16:06:36 +00:00
|
|
|
use std::sync::Arc;
|
2024-06-17 09:36:00 +00:00
|
|
|
use zoon::*;
|
2024-06-17 08:15:05 +00:00
|
|
|
|
|
|
|
pub struct HeaderPanel {
|
2024-06-17 16:06:36 +00:00
|
|
|
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
|
2024-06-17 08:15:05 +00:00
|
|
|
layout: Mutable<Layout>,
|
|
|
|
loaded_filename: Mutable<Option<Filename>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl HeaderPanel {
|
|
|
|
pub fn new(
|
2024-06-17 16:06:36 +00:00
|
|
|
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
|
2024-06-17 08:15:05 +00:00
|
|
|
layout: Mutable<Layout>,
|
|
|
|
loaded_filename: Mutable<Option<Filename>>,
|
|
|
|
) -> 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())
|
2024-06-17 09:36:00 +00:00
|
|
|
.item(self.layout_switcher()),
|
2024-06-17 08:15:05 +00:00
|
|
|
)
|
|
|
|
.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(
|
2024-09-14 16:51:03 +00:00
|
|
|
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
2024-06-17 08:15:05 +00:00
|
|
|
))
|
|
|
|
.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));
|
2024-06-17 16:06:36 +00:00
|
|
|
hierarchy.set(Some(Arc::new(platform::get_hierarchy().await)))
|
2024-06-17 08:15:05 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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();
|
2024-06-18 18:43:00 +00:00
|
|
|
let file_input_id = "file_input_for_load_waveform_button";
|
2024-06-17 08:15:05 +00:00
|
|
|
Row::new()
|
|
|
|
.item(
|
|
|
|
Label::new()
|
|
|
|
.s(Padding::new().x(20).y(10))
|
|
|
|
.s(Background::new().color_signal(
|
2024-09-14 17:12:04 +00:00
|
|
|
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
2024-06-17 08:15:05 +00:00
|
|
|
))
|
|
|
|
.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));
|
2024-06-17 17:36:44 +00:00
|
|
|
hierarchy.set(Some(Arc::new(platform::get_hierarchy().await)))
|
2024-06-17 08:15:05 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
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(
|
2024-09-14 16:51:03 +00:00
|
|
|
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
2024-06-17 08:15:05 +00:00
|
|
|
))
|
|
|
|
.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<Option<Result<JsValue, JsValue>>> = <_>::default();
|
|
|
|
Row::new()
|
|
|
|
.s(Align::new().top())
|
|
|
|
.s(Gap::both(30))
|
|
|
|
.s(Scrollbars::both())
|
2024-06-17 16:50:09 +00:00
|
|
|
.s(Width::fill())
|
2024-06-17 08:15:05 +00:00
|
|
|
.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<Option<Result<JsValue, JsValue>>>,
|
|
|
|
) -> impl Element {
|
|
|
|
Column::new()
|
|
|
|
.s(Align::new().top())
|
|
|
|
.s(Gap::new().y(10))
|
2024-06-17 16:50:09 +00:00
|
|
|
.s(Width::growable())
|
2024-06-17 08:15:05 +00:00
|
|
|
.item(
|
|
|
|
Row::new()
|
|
|
|
.s(Gap::new().x(15))
|
|
|
|
.s(Padding::new().x(5))
|
2024-06-17 09:35:13 +00:00
|
|
|
.item(El::new().child("Javascript commands"))
|
2024-06-17 09:36:00 +00:00
|
|
|
.item(El::new().s(Align::new().right()).child("Shift + Enter")),
|
2024-06-17 08:15:05 +00:00
|
|
|
)
|
|
|
|
.item(self.command_editor(command_result))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn command_editor(
|
|
|
|
&self,
|
|
|
|
command_result: Mutable<Option<Result<JsValue, JsValue>>>,
|
|
|
|
) -> 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()
|
2024-09-14 16:51:03 +00:00
|
|
|
.s(Background::new().color(COLOR_SLATE_BLUE))
|
2024-06-17 08:15:05 +00:00
|
|
|
.s(Padding::new().x(10).y(8))
|
|
|
|
.s(RoundedCorners::all(15))
|
|
|
|
.s(Height::default().min(50))
|
2024-06-17 16:50:09 +00:00
|
|
|
.s(Width::fill().min(300))
|
2024-06-17 09:36:00 +00:00
|
|
|
.s(Font::new()
|
|
|
|
.tracking(1)
|
|
|
|
.weight(FontWeight::Medium)
|
2024-09-14 16:51:03 +00:00
|
|
|
.color(COLOR_WHITE)
|
2024-06-17 09:36:00 +00:00
|
|
|
.family([FontFamily::new("Courier New"), FontFamily::Monospace]))
|
|
|
|
.s(Shadows::new([Shadow::new()
|
|
|
|
.inner()
|
2024-09-14 16:51:03 +00:00
|
|
|
.color(COLOR_DARK_SLATE_BLUE)
|
2024-06-17 09:36:00 +00:00
|
|
|
.blur(4)]))
|
2024-06-17 17:53:56 +00:00
|
|
|
// @TODO `spellcheck` and `resize` to MZ API? (together with autocomplete and others?)
|
|
|
|
.update_raw_el(|raw_el| {
|
|
|
|
raw_el
|
|
|
|
.attr("spellcheck", "false")
|
|
|
|
.style("resize", "vertical")
|
|
|
|
})
|
2024-09-14 17:12:04 +00:00
|
|
|
.placeholder(Placeholder::new("FW.say_hello()").s(Font::new().color(COLOR_LIGHT_BLUE)))
|
2024-06-17 08:15:05 +00:00
|
|
|
.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();
|
2024-06-25 16:29:33 +00:00
|
|
|
Task::start(clone!((script, command_result) async move {
|
|
|
|
let result = script_bridge::strict_eval(&script.lock_ref()).await;
|
|
|
|
command_result.set(Some(result));
|
|
|
|
}));
|
2024-06-17 08:15:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn command_result_panel(
|
|
|
|
&self,
|
|
|
|
command_result: ReadOnlyMutable<Option<Result<JsValue, JsValue>>>,
|
|
|
|
) -> impl Element {
|
|
|
|
Column::new()
|
|
|
|
.s(Gap::new().y(10))
|
|
|
|
.s(Align::new().top())
|
|
|
|
.s(Scrollbars::both())
|
|
|
|
.s(Padding::new().x(5))
|
2024-06-17 16:50:09 +00:00
|
|
|
.s(Width::growable().max(750))
|
2024-06-17 09:36:00 +00:00
|
|
|
.item(El::new().child("Command result"))
|
2024-06-17 08:15:05 +00:00
|
|
|
.item(self.command_result_el(command_result))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn command_result_el(
|
|
|
|
&self,
|
|
|
|
command_result: ReadOnlyMutable<Option<Result<JsValue, JsValue>>>,
|
|
|
|
) -> impl Element {
|
|
|
|
El::new()
|
2024-06-17 09:36:00 +00:00
|
|
|
.s(Font::new()
|
|
|
|
.tracking(1)
|
|
|
|
.weight(FontWeight::Medium)
|
2024-09-14 16:51:03 +00:00
|
|
|
.color(COLOR_WHITE)
|
2024-06-17 09:36:00 +00:00
|
|
|
.family([FontFamily::new("Courier New"), FontFamily::Monospace]))
|
2024-06-17 08:15:05 +00:00
|
|
|
.s(Scrollbars::both())
|
|
|
|
.s(Height::default().max(100))
|
2024-06-17 16:06:36 +00:00
|
|
|
.child_signal(command_result.signal_ref(|result| {
|
|
|
|
fn format_complex_js_value(js_value: &JsValue) -> String {
|
|
|
|
let value = format!("{js_value:?}");
|
|
|
|
let value = value.strip_prefix("JsValue(").unwrap_throw();
|
|
|
|
let value = value.strip_suffix(')').unwrap_throw();
|
|
|
|
value.to_owned()
|
2024-06-17 09:36:00 +00:00
|
|
|
}
|
2024-06-17 16:06:36 +00:00
|
|
|
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_complex_js_value(js_value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(Err(js_value)) => {
|
|
|
|
format!("ERROR: {}", format_complex_js_value(js_value))
|
|
|
|
}
|
|
|
|
None => "-".to_owned(),
|
2024-06-17 09:36:00 +00:00
|
|
|
}
|
|
|
|
}))
|
2024-06-17 08:15:05 +00:00
|
|
|
}
|
|
|
|
}
|