From 53cafefdef836d5193bdbe7623a16b346896b7a2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 23 Feb 2024 17:38:04 -0800 Subject: [PATCH 01/41] wip: add tests and slightly refactor interpreter code --- examples/form.rs | 17 +- packages/desktop/headless_tests/events.rs | 592 +++++++++++-------- packages/desktop/headless_tests/rendering.rs | 25 +- packages/desktop/headless_tests/utils.rs | 55 ++ packages/interpreter/src/interpreter.js | 295 ++++----- 5 files changed, 570 insertions(+), 414 deletions(-) create mode 100644 packages/desktop/headless_tests/utils.rs diff --git a/examples/form.rs b/examples/form.rs index 7da92e65c..7b305a7e7 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -4,19 +4,24 @@ //! in the "values" field. use dioxus::prelude::*; +use std::collections::HashMap; fn main() { - launch_desktop(app); + launch(app); } fn app() -> Element { + let mut values = use_signal(|| HashMap::new()); rsx! { div { h1 { "Form" } form { - onsubmit: move |ev| println!("Submitted {:?}", ev.values()), - oninput: move |ev| println!("Input {:?}", ev.values()), - input { r#type: "text", name: "username" } + oninput: move |ev| values.set(ev.values()), + input { + r#type: "text", + name: "username", + oninput: move |ev| values.set(ev.values()) + } input { r#type: "text", name: "full-name" } input { r#type: "password", name: "password" } input { r#type: "radio", name: "color", value: "red" } @@ -24,5 +29,9 @@ fn app() -> Element { button { r#type: "submit", value: "Submit", "Submit the form" } } } + div { + h1 { "Oninput Values" } + "{values:#?}" + } } } diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index f36320e7e..78d6a5568 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -1,73 +1,96 @@ +use std::{collections::HashMap, ops::Deref}; + use dioxus::html::geometry::euclid::Vector3D; use dioxus::prelude::*; use dioxus_core::prelude::consume_context; use dioxus_desktop::DesktopContext; +#[path = "./utils.rs"] +mod utils; + pub fn main() { - check_app_exits(app); + utils::check_app_exits(app); } -pub(crate) fn check_app_exits(app: fn() -> Element) { - use dioxus_desktop::tao::window::WindowBuilder; - use dioxus_desktop::Config; - // This is a deadman's switch to ensure that the app exits - let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); - let should_panic_clone = should_panic.clone(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(60)); - if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { - eprintln!("App did not exit in time"); - std::process::exit(exitcode::SOFTWARE); - } - }); +static RECEIVED_EVENTS: GlobalSignal = Signal::global(|| 0); - LaunchBuilder::desktop() - .with_cfg(Config::new().with_window(WindowBuilder::new().with_visible(true))) - .launch(app); - - // Stop deadman's switch - should_panic.store(false, std::sync::atomic::Ordering::SeqCst); -} - -fn mock_event(id: &'static str, value: &'static str) { - use_hook(move || { - spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(5000)).await; - - let js = format!( - r#" - //console.log("ran"); - // Dispatch a synthetic event - let event = {}; - let element = document.getElementById('{}'); - console.log(element, event); - element.dispatchEvent(event); - "#, - value, id - ); - - eval(&js).await.unwrap(); - }); - }) -} - -#[allow(deprecated)] fn app() -> Element { let desktop_context: DesktopContext = consume_context(); - let mut received_events = use_signal(|| 0); - // button - mock_event( + let received = RECEIVED_EVENTS(); + let expected = utils::EXPECTED_EVENTS(); + + if expected != 0 && received == expected { + println!("all events recieved"); + desktop_context.close(); + } + + rsx! { + div { + test_mounted {} + test_button {} + test_mouse_move_div {} + test_mouse_click_div {} + test_mouse_dblclick_div {} + test_mouse_down_div {} + test_mouse_up_div {} + test_mouse_scroll_div {} + test_key_down_div {} + test_key_up_div {} + test_key_press_div {} + test_focus_in_div {} + test_focus_out_div {} + test_form_input {} + } + } +} + +fn test_mounted() -> Element { + rsx! { + div { + width: "100px", + height: "100px", + onmounted: move |evt| async move { + let rect = evt.get_client_rect().await.unwrap(); + println!("rect: {:?}", rect); + assert_eq!(rect.width(), 100.0); + assert_eq!(rect.height(), 100.0); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_button() -> Element { + utils::mock_event( "button", r#"new MouseEvent("click", { - view: window, - bubbles: true, - cancelable: true, - button: 0, + view: window, + bubbles: true, + cancelable: true, + button: 0, })"#, ); - // mouse_move_div - mock_event( + + rsx! { + button { + id: "button", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_move_div() -> Element { + utils::mock_event( "mouse_move_div", r#"new MouseEvent("mousemove", { view: window, @@ -76,8 +99,27 @@ fn app() -> Element { buttons: 2, })"#, ); - // mouse_click_div - mock_event( + + rsx! { + div { + id: "mouse_move_div", + onmousemove: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_click_div() -> Element { + utils::mock_event( "mouse_click_div", r#"new MouseEvent("click", { view: window, @@ -87,8 +129,31 @@ fn app() -> Element { button: 2, })"#, ); - // mouse_dblclick_div - mock_event( + + rsx! { + div { + id: "mouse_click_div", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_dblclick_div() -> Element { + utils::mock_event( "mouse_dblclick_div", r#"new MouseEvent("dblclick", { view: window, @@ -98,8 +163,34 @@ fn app() -> Element { button: 2, })"#, ); - // mouse_down_div - mock_event( + + rsx! { + div { + id: "mouse_dblclick_div", + ondoubleclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary), + ); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_down_div() -> Element { + utils::mock_event( "mouse_down_div", r#"new MouseEvent("mousedown", { view: window, @@ -109,8 +200,31 @@ fn app() -> Element { button: 2, })"#, ); - // mouse_up_div - mock_event( + + rsx! { + div { + id: "mouse_down_div", + onmousedown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_up_div() -> Element { + utils::mock_event( "mouse_up_div", r#"new MouseEvent("mouseup", { view: window, @@ -120,8 +234,26 @@ fn app() -> Element { button: 0, })"#, ); - // wheel_div - mock_event( + + rsx! { + div { + id: "mouse_up_div", + onmouseup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_mouse_scroll_div() -> Element { + utils::mock_event( "wheel_div", r#"new WheelEvent("wheel", { view: window, @@ -132,8 +264,26 @@ fn app() -> Element { bubbles: true, })"#, ); - // key_down_div - mock_event( + + rsx! { + div { + id: "wheel_div", + width: "100px", + height: "100px", + background_color: "red", + onwheel: move |event| { + println!("{:?}", event.data); + let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else { + panic!("Expected delta to be in pixels") }; + assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} + +fn test_key_down_div() -> Element { + utils::mock_event( "key_down_div", r#"new KeyboardEvent("keydown", { key: "a", @@ -153,8 +303,24 @@ fn app() -> Element { bubbles: true, })"#, ); - // key_up_div - mock_event( + rsx! { + input { + id: "key_down_div", + onkeydown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location(), Location::Standard); + assert!(event.data.is_auto_repeating()); + assert!(event.data.is_composing()); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} +fn test_key_up_div() -> Element { + utils::mock_event( "key_up_div", r#"new KeyboardEvent("keyup", { key: "a", @@ -174,8 +340,25 @@ fn app() -> Element { bubbles: true, })"#, ); - // key_press_div - mock_event( + + rsx! { + input { + id: "key_up_div", + onkeyup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location(), Location::Standard); + assert!(!event.data.is_auto_repeating()); + assert!(!event.data.is_composing()); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + } + } + } +} +fn test_key_press_div() -> Element { + utils::mock_event( "key_press_div", r#"new KeyboardEvent("keypress", { key: "a", @@ -195,197 +378,94 @@ fn app() -> Element { bubbles: true, })"#, ); - // focus_in_div - mock_event( - "focus_in_div", - r#"new FocusEvent("focusin", {bubbles: true})"#, - ); - // focus_out_div - mock_event( - "focus_out_div", - r#"new FocusEvent("focusout",{bubbles: true})"#, - ); - - if received_events() == 13 { - println!("all events recieved"); - desktop_context.close(); - } - rsx! { - div { - div { - width: "100px", - height: "100px", - onmounted: move |evt| async move { - let rect = evt.get_client_rect().await.unwrap(); - println!("rect: {:?}", rect); - assert_eq!(rect.width(), 100.0); - assert_eq!(rect.height(), 100.0); - received_events.with_mut(|x| *x += 1); - } + input { + id: "key_press_div", + onkeypress: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location(), Location::Standard); + assert!(!event.data.is_auto_repeating()); + assert!(!event.data.is_composing()); + RECEIVED_EVENTS.with_mut(|x| *x += 1); } - button { - id: "button", - onclick: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().is_empty()); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Primary), - ); - received_events.with_mut(|x| *x += 1); - } + } + } +} + +fn test_focus_in_div() -> Element { + utils::mock_event( + "focus_in_div", + r#"new FocusEvent("focusin", {bubbles: true})"#, + ); + + rsx! { + input { + id: "focus_in_div", + onfocusin: move |event| { + println!("{:?}", event.data); + RECEIVED_EVENTS.with_mut(|x| *x += 1); } - div { - id: "mouse_move_div", - onmousemove: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!( - event - .data - .held_buttons() - .contains(dioxus_html::input_data::MouseButton::Secondary), - ); - received_events.with_mut(|x| *x += 1); - } + } + } +} + +fn test_focus_out_div() -> Element { + utils::mock_event( + "focus_out_div", + r#"new FocusEvent("focusout",{bubbles: true})"#, + ); + rsx! { + input { + id: "focus_out_div", + onfocusout: move |event| { + println!("{:?}", event.data); + RECEIVED_EVENTS.with_mut(|x| *x += 1); } - div { - id: "mouse_click_div", - onclick: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!( - event - .data - .held_buttons() - .contains(dioxus_html::input_data::MouseButton::Secondary), - ); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Secondary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "mouse_dblclick_div", - ondoubleclick: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!( - event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary), - ); - assert!( - event - .data - .held_buttons() - .contains(dioxus_html::input_data::MouseButton::Secondary), - ); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Secondary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "mouse_down_div", - onmousedown: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!( - event - .data - .held_buttons() - .contains(dioxus_html::input_data::MouseButton::Secondary), - ); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Secondary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "mouse_up_div", - onmouseup: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().is_empty()); - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Primary), - ); - received_events.with_mut(|x| *x += 1); - } - } - div { - id: "wheel_div", - width: "100px", - height: "100px", - background_color: "red", - onwheel: move |event| { - println!("{:?}", event.data); - let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else { - panic!("Expected delta to be in pixels") }; - assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "key_down_div", - onkeydown: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert_eq!(event.data.key().to_string(), "a"); - assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location(), Location::Standard); - assert!(event.data.is_auto_repeating()); - assert!(event.data.is_composing()); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "key_up_div", - onkeyup: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert_eq!(event.data.key().to_string(), "a"); - assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location(), Location::Standard); - assert!(!event.data.is_auto_repeating()); - assert!(!event.data.is_composing()); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "key_press_div", - onkeypress: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert_eq!(event.data.key().to_string(), "a"); - assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location(), Location::Standard); - assert!(!event.data.is_auto_repeating()); - assert!(!event.data.is_composing()); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "focus_in_div", - onfocusin: move |event| { - println!("{:?}", event.data); - received_events.with_mut(|x| *x += 1); - } - } - input { - id: "focus_out_div", - onfocusout: move |event| { - println!("{:?}", event.data); - received_events.with_mut(|x| *x += 1); + } + } +} + +fn test_form_input() -> Element { + let mut values = use_signal(|| HashMap::new()); + + utils::mock_event_with_extra( + "form-username", + r#"new Event("input", { + bubbles: true, + cancelable: true, + composed: true, + })"#, + r#"element.value = "hello";"#, + ); + + let set_values = move |ev: FormEvent| { + values.set(ev.values()); + eprintln!("values: {:?}", values); + values.with_mut(|x| { + assert_eq!(x.get("username").unwrap().deref(), &["hello"]); + }); + }; + + rsx! { + div { + h1 { "Form" } + form { + id: "form", + oninput: move |ev| values.set(ev.values()), + input { + r#type: "text", + name: "username", + id: "form-username", + oninput: set_values, } + input { r#type: "text", name: "full-name" } + input { r#type: "password", name: "password" } + input { r#type: "radio", name: "color", value: "red" } + input { r#type: "radio", name: "color", value: "blue" } + button { r#type: "submit", value: "Submit", "Submit the form" } } } } diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 882c70d21..15e2b9f59 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -2,28 +2,11 @@ use dioxus::prelude::*; use dioxus_core::Element; use dioxus_desktop::DesktopContext; +#[path = "./utils.rs"] +mod utils; + fn main() { - check_app_exits(check_html_renders); -} - -pub(crate) fn check_app_exits(app: fn() -> Element) { - use dioxus_desktop::Config; - use tao::window::WindowBuilder; - // This is a deadman's switch to ensure that the app exits - let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); - let should_panic_clone = should_panic.clone(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(5)); - if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { - std::process::exit(exitcode::SOFTWARE); - } - }); - - LaunchBuilder::desktop() - .with_cfg(Config::new().with_window(WindowBuilder::new().with_visible(true))) - .launch(app); - - should_panic.store(false, std::sync::atomic::Ordering::SeqCst); + utils::check_app_exits(check_html_renders); } fn use_inner_html(id: &'static str) -> Option { diff --git a/packages/desktop/headless_tests/utils.rs b/packages/desktop/headless_tests/utils.rs new file mode 100644 index 000000000..cd14a2fc1 --- /dev/null +++ b/packages/desktop/headless_tests/utils.rs @@ -0,0 +1,55 @@ +use dioxus::prelude::*; +use dioxus_core::Element; + +pub fn check_app_exits(app: fn() -> Element) { + use dioxus_desktop::tao::window::WindowBuilder; + use dioxus_desktop::Config; + // This is a deadman's switch to ensure that the app exits + let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let should_panic_clone = should_panic.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(60)); + if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { + eprintln!("App did not exit in time"); + std::process::exit(exitcode::SOFTWARE); + } + }); + + LaunchBuilder::desktop() + .with_cfg(Config::new().with_window(WindowBuilder::new().with_visible(true))) + .launch(app); + + // Stop deadman's switch + should_panic.store(false, std::sync::atomic::Ordering::SeqCst); +} + +pub static EXPECTED_EVENTS: GlobalSignal = Signal::global(|| 0); + +pub fn mock_event(id: &'static str, value: &'static str) { + mock_event_with_extra(id, value, ""); +} + +pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'static str) { + EXPECTED_EVENTS.with_mut(|x| *x += 1); + + use_hook(move || { + spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(5000)).await; + + let js = format!( + r#" + //console.log("ran"); + // Dispatch a synthetic event + let event = {}; + let element = document.getElementById('{}'); + console.log(element, event); + {} + element.dispatchEvent(event); + "#, + value, id, extra + ); + + eval(&js).await.unwrap(); + }); + }) +} diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 0e9800645..977df8ca2 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -8,155 +8,182 @@ class InterpreterConfig { // method is not used by the web implementation async function handler(event, name, bubbles, config) { let target = event.target; - if (target != null) { - let preventDefaultRequests = null; - // Some events can be triggered on text nodes, which don't have attributes - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); - } + if (target == null) { + return; + } - if (event.type === "click") { - // todo call prevent default if it's the right type of event - if (config.intercept_link_redirects) { - let a_element = target.closest("a"); - if (a_element != null) { - event.preventDefault(); + const realId = find_real_id(target); + if (realId === null) { + return; + } - let elementShouldPreventDefault = - preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - let aElementShouldPreventDefault = a_element.getAttribute( - `dioxus-prevent-default` + prevent_defaults(event, target, config); + + let contents = await serialize_event(event); + + // TODO: this should be liveview only + if ( + target.tagName === "INPUT" && + (event.type === "change" || event.type === "input") + ) { + const type = target.getAttribute("type"); + + if (type === "file") { + async function read_files() { + const files = target.files; + const file_contents = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) ); - let linkShouldPreventDefault = - aElementShouldPreventDefault && - aElementShouldPreventDefault.includes(`onclick`); - - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - window.interpreter.serializeIpcMessage("browser_open", { href }) - ); - } - } } - } + let file_engine = { + files: file_contents, + }; + contents.files = file_engine; - // also prevent buttons from submitting - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - } - - const realId = find_real_id(target); - - if ( - preventDefaultRequests && - preventDefaultRequests.includes(`on${event.type}`) - ) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - let contents = await serialize_event(event); - - // TODO: this should be liveview only - if ( - target.tagName === "INPUT" && - (event.type === "change" || event.type === "input") - ) { - const type = target.getAttribute("type"); - if (type === "file") { - async function read_files() { - const files = target.files; - const file_contents = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - let file_engine = { - files: file_contents, - }; - contents.files = file_engine; - - if (realId === null) { - return; - } - const message = window.interpreter.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }); - window.ipc.postMessage(message); + if (realId === null) { + return; } - read_files(); - return; + const message = window.interpreter.serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + window.ipc.postMessage(message); } - } - - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - const formData = new FormData(target); - - for (let name of formData.keys()) { - const fieldType = target.elements[name].type; - - switch (fieldType) { - case "select-multiple": - contents.values[name] = formData.getAll(name); - break; - - // add cases for fieldTypes that can hold multiple values here - default: - contents.values[name] = formData.get(name); - break; - } - } - } - - if ( - target.tagName === "SELECT" && - event.type === "input" - ) { - const selectData = target.options; - contents.values["options"] = []; - for (let i = 0; i < selectData.length; i++) { - let option = selectData[i]; - if (option.selected) { - contents.values["options"].push(option.value.toString()); - } - } - } - - if (realId === null) { + read_files(); return; } - window.ipc.postMessage( - window.interpreter.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }) - ); + } + + if ( + target.tagName === "FORM" && + (event.type === "submit" || event.type === "input") + ) { + const formData = new FormData(target); + + for (let name of formData.keys()) { + const fieldType = target.elements[name].type; + + switch (fieldType) { + case "select-multiple": + contents.values[name] = formData.getAll(name); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + contents.values[name] = formData.get(name); + break; + } + } + } + + if ( + target.tagName === "SELECT" && + event.type === "input" + ) { + const selectData = target.options; + contents.values["options"] = []; + for (let i = 0; i < selectData.length; i++) { + let option = selectData[i]; + if (option.selected) { + contents.values["options"].push(option.value.toString()); + } + } + } + + window.ipc.postMessage( + window.interpreter.serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }) + ); +} + +// Do our best to prevent the default action of the event +// This should: +// - prevent form submissions from navigating +// - prevent anchor tags from navigating +// - prevent buttons from submitting forms +function prevent_defaults(event, target, config) { + let preventDefaultRequests = null; + + // Some events can be triggered on text nodes, which don't have attributes + if (target instanceof Element) { + preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); + } + + if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { + event.preventDefault(); + } + + if (event.type === "submit") { + event.preventDefault(); + } + + // Attempt to intercept if the event is a click + intercept_form_submit(event, target, config, preventDefaultRequests); +} + +function intercept_form_submit(event, target, config, preventDefaultRequests) { + if (event.type !== "click") { + return; + } + + // todo call prevent default if it's the right type of event + if (!config.intercept_link_redirects) { + return; + } + + // prevent buttons in forms from submitting the form + if (target.tagName === "BUTTON" && event.type == "submit") { + event.preventDefault(); + } + + // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating + let a_element = target.closest("a"); + + if (a_element == null) { + return; + } + + event.preventDefault(); + + let elementShouldPreventDefault = + preventDefaultRequests && preventDefaultRequests.includes(`onclick`); + + let aElementShouldPreventDefault = a_element.getAttribute( + `dioxus-prevent-default` + ); + + let linkShouldPreventDefault = + aElementShouldPreventDefault && + aElementShouldPreventDefault.includes(`onclick`); + + if (!elementShouldPreventDefault && !linkShouldPreventDefault) { + const href = a_element.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.ipc.postMessage( + window.interpreter.serializeIpcMessage("browser_open", { href }) + ); + } } } function find_real_id(target) { let realId = null; + if (target instanceof Element) { realId = target.getAttribute(`data-dioxus-id`); } + // walk the tree to find the real element while (realId == null) { // we've reached the root we don't want to send an event @@ -169,6 +196,7 @@ function find_real_id(target) { realId = target.getAttribute(`data-dioxus-id`); } } + return realId; } @@ -421,7 +449,8 @@ async function serialize_event(event) { case "drop": { let files = null; if (event.dataTransfer && event.dataTransfer.files) { - files = await serializeFileList(event.dataTransfer.files); + files = ["a", "b", "c"]; + // files = await serializeFileList(event.dataTransfer.files); } return { mouse: get_mouse_data(event), files }; From 7f60010c1eec90e9b94f051cea39c3bede7f25a0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 23 Feb 2024 18:52:21 -0800 Subject: [PATCH 02/41] set up a typescript pipeline for interpreter --- packages/interpreter/build.rs | 65 +++++++++++++++ packages/interpreter/gen/README.md | 3 + .../interpreter/src/{common.js => common.ts} | 1 + packages/interpreter/src/form.ts | 0 packages/interpreter/src/gen/common.js | 81 +++++++++++++++++++ packages/interpreter/src/gen/form.js | 1 + packages/interpreter/src/gen/interpreter.js | 0 .../src/{interpreter.js => interpreter.ts} | 81 ++++++++++--------- packages/interpreter/src/lib.rs | 6 +- packages/web/src/dom.rs | 2 - packages/web/src/event.rs | 1 + 11 files changed, 197 insertions(+), 44 deletions(-) create mode 100644 packages/interpreter/build.rs create mode 100644 packages/interpreter/gen/README.md rename packages/interpreter/src/{common.js => common.ts} (99%) create mode 100644 packages/interpreter/src/form.ts create mode 100644 packages/interpreter/src/gen/common.js create mode 100644 packages/interpreter/src/gen/form.js create mode 100644 packages/interpreter/src/gen/interpreter.js rename packages/interpreter/src/{interpreter.js => interpreter.ts} (99%) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs new file mode 100644 index 000000000..776f91307 --- /dev/null +++ b/packages/interpreter/build.rs @@ -0,0 +1,65 @@ +use std::{ + fs::read_to_string, + hash::{DefaultHasher, Hash, Hasher}, + process::Command, +}; + +fn main() { + // If any TS changes, re-run the build script + println!("cargo:rerun-if-changed=src/*.ts"); + + for entry in ["common", "form", "interpreter"].iter() { + gen_bindings(entry); + } +} + +// okay...... so tsc might fail if the user doesn't have it installed +// we don't really want to fail if that's the case +// but if you started *editing* the .ts files, you're gonna have a bad time +// so..... +// we need to hash each of the .ts files and add that hash to the JS files +// if the hashes don't match, we need to fail the build +// that way we also don't need +fn gen_bindings(name: &str) { + let contents = read_to_string(&format!("src/{name}.ts")).unwrap(); + let generated = read_to_string(&format!("src/gen/{name}.js")).unwrap_or_default(); + let hashed = hash_file(&contents); + + // If the file is generated, and the hash is the same, we're good, don't do anything + if generated + .lines() + .next() + .unwrap_or_default() + .starts_with(&format!("// DO NOT EDIT THIS FILE. HASH: {}", hashed)) + { + return; + } + + // If the file is generated, and the hash is different, we need to generate it + let status = Command::new("tsc") + .arg(format!("src/{name}.ts")) + .arg("--outDir") + .arg("gen") + .arg("--target") + .arg("es6") + .status() + .unwrap(); + + if !status.success() { + panic!( + "Failed to generate bindings for {}. Make sure you have tsc installed", + name + ); + } + + // The file should exist, and now we need write the TS hash to the file + let generated = read_to_string(&format!("gen/{name}.js")).unwrap(); + let generated = format!("// DO NOT EDIT THIS FILE. HASH: {}\n{}", hashed, generated); + std::fs::write(&format!("src/gen/{name}.js"), generated).unwrap(); +} + +fn hash_file(obj: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + obj.hash(&mut hasher); + hasher.finish() +} diff --git a/packages/interpreter/gen/README.md b/packages/interpreter/gen/README.md new file mode 100644 index 000000000..e6d41cee9 --- /dev/null +++ b/packages/interpreter/gen/README.md @@ -0,0 +1,3 @@ +temporary generated code directory since tsc doesn't have a crossplatform way of generating typescript to stdout + +https://github.com/microsoft/TypeScript/issues/1226#issuecomment-523544134 diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.ts similarity index 99% rename from packages/interpreter/src/common.js rename to packages/interpreter/src/common.ts index 0c64085f9..ee16838ee 100644 --- a/packages/interpreter/src/common.js +++ b/packages/interpreter/src/common.ts @@ -1,4 +1,5 @@ export function setAttributeInner(node, field, value, ns) { + const name = field; if (ns === "style") { // ????? why do we need to do this diff --git a/packages/interpreter/src/form.ts b/packages/interpreter/src/form.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/interpreter/src/gen/common.js b/packages/interpreter/src/gen/common.js new file mode 100644 index 000000000..8f21059f3 --- /dev/null +++ b/packages/interpreter/src/gen/common.js @@ -0,0 +1,81 @@ +// DO NOT EDIT THIS FILE. HASH: 9578489549991746027 +export function setAttributeInner(node, field, value, ns) { + const name = field; + if (ns === "style") { + // ????? why do we need to do this + if (node.style === undefined) { + node.style = {}; + } + node.style[name] = value; + } + else if (!!ns) { + node.setAttributeNS(ns, name, value); + } + else { + switch (name) { + case "value": + if (value !== node.value) { + node.value = value; + } + break; + case "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "initial_checked": + node.defaultChecked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + break; + case "initial_selected": + node.defaultSelected = truthy(value); + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (!truthy(value) && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } + else { + node.setAttribute(name, value); + } + } + } +} +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, + webkitdirectory: true, +}; +function truthy(val) { + return val === "true" || val === true; +} diff --git a/packages/interpreter/src/gen/form.js b/packages/interpreter/src/gen/form.js new file mode 100644 index 000000000..0357a8736 --- /dev/null +++ b/packages/interpreter/src/gen/form.js @@ -0,0 +1 @@ +// DO NOT EDIT THIS FILE. HASH: 3476900567878811119 diff --git a/packages/interpreter/src/gen/interpreter.js b/packages/interpreter/src/gen/interpreter.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.ts similarity index 99% rename from packages/interpreter/src/interpreter.js rename to packages/interpreter/src/interpreter.ts index 977df8ca2..c8dcef095 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.ts @@ -1,5 +1,7 @@ class InterpreterConfig { - constructor(intercept_link_redirects) { + intercept_link_redirects: boolean; + + constructor(intercept_link_redirects: boolean) { this.intercept_link_redirects = intercept_link_redirects; } } @@ -21,6 +23,42 @@ async function handler(event, name, bubbles, config) { let contents = await serialize_event(event); + if ( + target.tagName === "FORM" && + (event.type === "submit" || event.type === "input") + ) { + const formData = new FormData(target); + + for (let name of formData.keys()) { + const fieldType = target.elements[name].type; + + switch (fieldType) { + case "select-multiple": + contents.values[name] = formData.getAll(name); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + contents.values[name] = formData.get(name); + break; + } + } + } + + if ( + target.tagName === "SELECT" && + event.type === "input" + ) { + const selectData = target.options; + contents.values["options"] = []; + for (let i = 0; i < selectData.length; i++) { + let option = selectData[i]; + if (option.selected) { + contents.values["options"].push(option.value.toString()); + } + } + } + // TODO: this should be liveview only if ( target.tagName === "INPUT" && @@ -61,42 +99,6 @@ async function handler(event, name, bubbles, config) { } } - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - const formData = new FormData(target); - - for (let name of formData.keys()) { - const fieldType = target.elements[name].type; - - switch (fieldType) { - case "select-multiple": - contents.values[name] = formData.getAll(name); - break; - - // add cases for fieldTypes that can hold multiple values here - default: - contents.values[name] = formData.get(name); - break; - } - } - } - - if ( - target.tagName === "SELECT" && - event.type === "input" - ) { - const selectData = target.options; - contents.values["options"] = []; - for (let i = 0; i < selectData.length; i++) { - let option = selectData[i]; - if (option.selected) { - contents.values["options"].push(option.value.toString()); - } - } - } - window.ipc.postMessage( window.interpreter.serializeIpcMessage("user_event", { name: name, @@ -324,7 +326,7 @@ window.interpreter.setFocus = function (id, focus) { return true; } -function get_mouse_data(event) { +function get_mouse_data(event: MouseEvent) { const { altKey, button, @@ -447,7 +449,8 @@ async function serialize_event(event) { case "dragover": case "dragstart": case "drop": { - let files = null; + let files = []; + if (event.dataTransfer && event.dataTransfer.files) { files = ["a", "b", "c"]; // files = await serializeFileList(event.dataTransfer.files); diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index b1aaebf04..d19ccb585 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -2,8 +2,8 @@ #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] -pub static INTERPRETER_JS: &str = include_str!("./interpreter.js"); -pub static COMMON_JS: &str = include_str!("./common.js"); +pub static INTERPRETER_JS: &str = include_str!("./gen/interpreter.js"); +pub static COMMON_JS: &str = include_str!("./gen/common.js"); #[cfg(feature = "sledgehammer")] mod sledgehammer_bindings; @@ -20,7 +20,7 @@ pub use write_native_mutations::*; #[cfg(all(feature = "minimal_bindings", feature = "webonly"))] pub mod minimal_bindings { use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; - #[wasm_bindgen(module = "/src/common.js")] + #[wasm_bindgen(module = "/src/gen/common.js")] extern "C" { pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>); } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 33f44c1d8..51aba110b 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -36,8 +36,6 @@ pub struct UiEvent { pub data: PlatformEventData, } -//fn get_document(elem: &web_sys::Element) -> - impl WebsysDom { pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender) -> Self { let (document, root) = match cfg.root { diff --git a/packages/web/src/event.rs b/packages/web/src/event.rs index 8fa2b1aea..f68487925 100644 --- a/packages/web/src/event.rs +++ b/packages/web/src/event.rs @@ -441,6 +441,7 @@ impl HasFileData for WebFormData { fn files(&self) -> Option> { #[cfg(not(feature = "file_engine"))] let files = None; + #[cfg(feature = "file_engine")] let files = self .element From b5447c162f1c42a20b76fea42934e79e3f38756a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 27 Feb 2024 12:31:11 -0800 Subject: [PATCH 03/41] extend interpreter base class with platform methods --- packages/core/src/runtime.rs | 9 + packages/desktop/js/prevent_file_upload.js | 23 + packages/desktop/src/protocol.rs | 48 +- packages/interpreter/.gitignore | 1 + packages/interpreter/NOTES.md | 7 + packages/interpreter/README.md | 10 + packages/interpreter/a.txt | 1 + packages/interpreter/build.rs | 6 +- packages/interpreter/src/common.ts | 80 -- .../src/{form.ts => gen/sledgehammer.js} | 0 packages/interpreter/src/interpreter.ts | 792 ------------------ packages/interpreter/src/lib.rs | 17 +- .../interpreter/src/sledgehammer_bindings.rs | 407 --------- packages/interpreter/src/ts/form.ts | 3 + .../interpreter/src/ts/interpreter_core.ts | 82 ++ .../interpreter/src/ts/interpreter_native.ts | 286 +++++++ .../interpreter/src/ts/interpreter_web.ts | 83 ++ packages/interpreter/src/ts/serialize.ts | 494 +++++++++++ packages/interpreter/src/ts/set_attribute.ts | 105 +++ packages/interpreter/src/unified_bindings.rs | 196 +++++ .../interpreter/src/write_native_mutations.rs | 18 +- packages/interpreter/tests/e2e.rs | 1 + packages/interpreter/tests/serialize.rs | 1 + packages/interpreter/tsconfig.json | 20 + packages/web/NOTES.md | 35 + packages/web/src/dom.rs | 1 + packages/web/src/lib.rs | 39 - 27 files changed, 1394 insertions(+), 1371 deletions(-) create mode 100644 packages/desktop/js/prevent_file_upload.js create mode 100644 packages/interpreter/.gitignore create mode 100644 packages/interpreter/NOTES.md create mode 100644 packages/interpreter/a.txt delete mode 100644 packages/interpreter/src/common.ts rename packages/interpreter/src/{form.ts => gen/sledgehammer.js} (100%) delete mode 100644 packages/interpreter/src/interpreter.ts delete mode 100644 packages/interpreter/src/sledgehammer_bindings.rs create mode 100644 packages/interpreter/src/ts/form.ts create mode 100644 packages/interpreter/src/ts/interpreter_core.ts create mode 100644 packages/interpreter/src/ts/interpreter_native.ts create mode 100644 packages/interpreter/src/ts/interpreter_web.ts create mode 100644 packages/interpreter/src/ts/serialize.ts create mode 100644 packages/interpreter/src/ts/set_attribute.ts create mode 100644 packages/interpreter/src/unified_bindings.rs create mode 100644 packages/interpreter/tests/e2e.rs create mode 100644 packages/interpreter/tests/serialize.rs create mode 100644 packages/interpreter/tsconfig.json create mode 100644 packages/web/NOTES.md diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 8493a901f..5d7d8297a 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -168,6 +168,15 @@ impl Runtime { pub(crate) fn release_flush_lock(&self) { self.flush_lock.take(); } + + /// Dispatch an event against the current runtime + /// + /// This won't do any diffing or anything, just calling event listeners syncronously + /// We expose this via the runtime so that preventDefault, stopPropagation, and other event methods can be called + /// while the event is still active. + pub fn dispatch_event(&self) { + todo!() + } } /// A guard for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime. diff --git a/packages/desktop/js/prevent_file_upload.js b/packages/desktop/js/prevent_file_upload.js new file mode 100644 index 000000000..73b2e7794 --- /dev/null +++ b/packages/desktop/js/prevent_file_upload.js @@ -0,0 +1,23 @@ +// Prevent file inputs from opening the file dialog on click + let inputs = document.querySelectorAll("input"); + for (let input of inputs) { + if (!input.getAttribute("data-dioxus-file-listener")) { + // prevent file inputs from opening the file dialog on click + const type = input.getAttribute("type"); + if (type === "file") { + input.setAttribute("data-dioxus-file-listener", true); + input.addEventListener("click", (event) => { + let target = event.target; + let target_id = find_real_id(target); + if (target_id !== null) { + const send = (event_name) => { + const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); + window.ipc.postMessage(message); + }; + send("change&input"); + } + event.preventDefault(); + }); + } + } + } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 1337f0325..e49d911f9 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,46 +1,20 @@ use crate::{assets::*, edits::EditQueue}; -use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS; +use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; use std::path::{Path, PathBuf}; use wry::{ http::{status::StatusCode, Request, Response}, RequestAsyncResponder, Result, }; -fn handle_edits_code() -> String { - const EDITS_PATH: &str = { - #[cfg(any(target_os = "android", target_os = "windows"))] - { - "http://dioxus.index.html/edits" - } - #[cfg(not(any(target_os = "android", target_os = "windows")))] - { - "dioxus://index.html/edits" - } - }; +#[cfg(any(target_os = "android", target_os = "windows"))] +const EDITS_PATH: &str = "http://dioxus.index.html/edits"; - let prevent_file_upload = r#"// Prevent file inputs from opening the file dialog on click - let inputs = document.querySelectorAll("input"); - for (let input of inputs) { - if (!input.getAttribute("data-dioxus-file-listener")) { - // prevent file inputs from opening the file dialog on click - const type = input.getAttribute("type"); - if (type === "file") { - input.setAttribute("data-dioxus-file-listener", true); - input.addEventListener("click", (event) => { - let target = event.target; - let target_id = find_real_id(target); - if (target_id !== null) { - const send = (event_name) => { - const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); - window.ipc.postMessage(message); - }; - send("change&input"); - } - event.preventDefault(); - }); - } - } - }"#; +#[cfg(not(any(target_os = "android", target_os = "windows")))] +const EDITS_PATH: &str = "dioxus://index.html/edits"; + +const PREVENT_FILE_UPLOAD: &str = include_str!("../js/prevent_file_upload.js"); + +fn handle_edits_code() -> String { let polling_request = format!( r#"// Poll for requests window.interpreter.wait_for_request = (headless) => {{ @@ -62,10 +36,12 @@ fn handle_edits_code() -> String { }}) }}"# ); + let mut interpreter = SLEDGEHAMMER_JS - .replace("/*POST_HANDLE_EDITS*/", prevent_file_upload) + .replace("/*POST_HANDLE_EDITS*/", PREVENT_FILE_UPLOAD) .replace("export", "") + &polling_request; + while let Some(import_start) = interpreter.find("import") { let import_end = interpreter[import_start..] .find(|c| c == ';' || c == '\n') diff --git a/packages/interpreter/.gitignore b/packages/interpreter/.gitignore new file mode 100644 index 000000000..27babb65e --- /dev/null +++ b/packages/interpreter/.gitignore @@ -0,0 +1 @@ +gen2/ diff --git a/packages/interpreter/NOTES.md b/packages/interpreter/NOTES.md new file mode 100644 index 000000000..db180b737 --- /dev/null +++ b/packages/interpreter/NOTES.md @@ -0,0 +1,7 @@ +# Notes on the web implementation + +Here's some useful resources if you ever need to splunk into the intricacies of how events are handled in HTML: + + +- Not all event handlers are sync: https://w3c.github.io/uievents/#sync-async +- Some attributes are truthy: https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 diff --git a/packages/interpreter/README.md b/packages/interpreter/README.md index 0c195d681..af17d1e10 100644 --- a/packages/interpreter/README.md +++ b/packages/interpreter/README.md @@ -25,6 +25,16 @@ This crate features bindings for the web and sledgehammer for increased performance. +## Architecture + +We use TypeScript to write the bindings and a very simple build.rs to convert them to javascript, minify them, and glue them into the rest of the project. + +Not every snippet of JS will be used, so we split out the snippets from the core interpreter. + +In theory, we *could* use Rust in the browser to do everything these bindings are doing. In reality, we want to stick with JS to skip the need for a WASM build step when running the LiveView and WebView renderers. We also want to use JS to prevent diverging behavior of things like canceling events, uploading files, and collecting form inputs. These details are tough to ensure 1:1 compatibility when implementing them in two languages. + +If you want to contribute to the bindings, you'll need to have the typescript compiler installed on your machine, accessible via `tsc`. + ## Contributing - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). diff --git a/packages/interpreter/a.txt b/packages/interpreter/a.txt new file mode 100644 index 000000000..db9748690 --- /dev/null +++ b/packages/interpreter/a.txt @@ -0,0 +1 @@ +pub const SLEDGEHAMMER_JS: &str = "let m,p,ls,d,t,op,i,e,z,metaflags;\n const namespace = [];\n let namespace_cache_hit, namespace_cache_idx;\n function get_namespace() {\n namespace_cache_idx = u8buf[u8bufp++];\n if(namespace_cache_idx & 128){\n namespace_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n namespace[namespace_cache_idx&4294967167]=namespace_cache_hit;\n return namespace_cache_hit;\n }\n else{\n return namespace[namespace_cache_idx&4294967167];\n }\n }let u8buf,u8bufp;const attr = [];\n let attr_cache_hit, attr_cache_idx;\n function get_attr() {\n attr_cache_idx = u8buf[u8bufp++];\n if(attr_cache_idx & 128){\n attr_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n attr[attr_cache_idx&4294967167]=attr_cache_hit;\n return attr_cache_hit;\n }\n else{\n return attr[attr_cache_idx&4294967167];\n }\n }const evt = [];\n let evt_cache_hit, evt_cache_idx;\n function get_evt() {\n evt_cache_idx = u8buf[u8bufp++];\n if(evt_cache_idx & 128){\n evt_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n evt[evt_cache_idx&4294967167]=evt_cache_hit;\n return evt_cache_hit;\n }\n else{\n return evt[evt_cache_idx&4294967167];\n }\n }const el = [];\n let el_cache_hit, el_cache_idx;\n function get_el() {\n el_cache_idx = u8buf[u8bufp++];\n if(el_cache_idx & 128){\n el_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n el[el_cache_idx&4294967167]=el_cache_hit;\n return el_cache_hit;\n }\n else{\n return el[el_cache_idx&4294967167];\n }\n }let u32buf,u32bufp;let s = \"\";let lsp,sp,sl; let c = new TextDecoder();let u16buf,u16bufp;const ns_cache = [];\n let ns_cache_cache_hit, ns_cache_cache_idx;\n function get_ns_cache() {\n ns_cache_cache_idx = u8buf[u8bufp++];\n if(ns_cache_cache_idx & 128){\n ns_cache_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n ns_cache[ns_cache_cache_idx&4294967167]=ns_cache_cache_hit;\n return ns_cache_cache_hit;\n }\n else{\n return ns_cache[ns_cache_cache_idx&4294967167];\n }\n }\n let field,ptr,array,ns,id,value,len,event,many,bubbles,event_name;\n export function create(r){\n d=r;\n }\n export function update_memory(b){\n m=new DataView(b.buffer)\n }\n export function run(){\n metaflags=m.getUint32(d,true);\n if((metaflags>>>6)&1){\n ls=m.getUint32(d+6*4,true);\n }\n p=ls;\n if ((metaflags>>>5)&1){\n t = m.getUint32(d+5*4,true);\n u8buf=new Uint8Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%1)/1);\n }\n u8bufp=0;if ((metaflags>>>3)&1){\n t = m.getUint32(d+3*4,true);\n u32buf=new Uint32Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%4)/4);\n }\n u32bufp=0;if (metaflags&1){\n lsp = m.getUint32(d+1*4,true);\n }\n if ((metaflags>>>2)&1) {\n sl = m.getUint32(d+2*4,true);\n if ((metaflags>>>1)&1) {\n sp = lsp;\n s = \"\";\n e = sp + ((sl / 4) | 0) * 4;\n while (sp < e) {\n t = m.getUint32(sp, true);\n s += String.fromCharCode(\n t & 255,\n (t & 65280) >> 8,\n (t & 16711680) >> 16,\n t >> 24\n );\n sp += 4;\n }\n while (sp < lsp + sl) {\n s += String.fromCharCode(m.getUint8(sp++));\n }\n } else {\n s = c.decode(new DataView(m.buffer, lsp, sl));\n }\n }\n sp=0;if ((metaflags>>>4)&1){\n t = m.getUint32(d+4*4,true);\n u16buf=new Uint16Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%2)/2);\n }\n u16bufp=0;\n for(;;){\n op=m.getUint32(p,true);\n p+=4;\n z=0;\n while(z++<4){\n switch(op&255){\n case 0:{AppendChildren(root, stack.length-1);}break;case 1:{stack.push(nodes[u32buf[u32bufp++]]);}break;case 2:{AppendChildren(u32buf[u32bufp++], u16buf[u16bufp++]);}break;case 3:{stack.pop();}break;case 4:{root = nodes[u32buf[u32bufp++]]; els = stack.splice(stack.length-u16buf[u16bufp++]); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}break;case 5:{nodes[u32buf[u32bufp++]].after(...stack.splice(stack.length-u16buf[u16bufp++]));}break;case 6:{nodes[u32buf[u32bufp++]].before(...stack.splice(stack.length-u16buf[u16bufp++]));}break;case 7:{node = nodes[u32buf[u32bufp++]]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}break;case 8:{stack.push(document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])));}break;case 9:{node = document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 10:{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[u32buf[u32bufp++]] = node;}break;case 11:event_name=get_evt();id=u32buf[u32bufp++];bubbles=u8buf[u8bufp++];node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `${id}`); listeners.create(event_name, node, bubbles);break;case 12:{node = nodes[u32buf[u32bufp++]]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, get_evt(), u8buf[u8bufp++]);}break;case 13:{nodes[u32buf[u32bufp++]].textContent = s.substring(sp,sp+=u32buf[u32bufp++]);}break;case 14:{node = nodes[u32buf[u32bufp++]]; setAttributeInner(node, get_attr(), s.substring(sp,sp+=u32buf[u32bufp++]), get_ns_cache());}break;case 15:id=u32buf[u32bufp++];field=get_attr();ns=get_ns_cache();{\n node = nodes[id];\n if (!ns) {\n switch (field) {\n case \"value\":\n node.value = \"\";\n break;\n case \"checked\":\n node.checked = false;\n break;\n case \"selected\":\n node.selected = false;\n break;\n case \"dangerous_inner_html\":\n node.innerHTML = \"\";\n break;\n default:\n node.removeAttribute(field);\n break;\n }\n } else if (ns == \"style\") {\n node.style.removeProperty(name);\n } else {\n node.removeAttributeNS(ns, field);\n }\n }break;case 16:{nodes[u32buf[u32bufp++]] = LoadChild(u32buf[u32bufp++], u8buf[u8bufp++]);}break;case 17:ptr=u32buf[u32bufp++];len=u8buf[u8bufp++];value=s.substring(sp,sp+=u32buf[u32bufp++]);id=u32buf[u32bufp++];{\n node = LoadChild(ptr, len);\n if (node.nodeType == Node.TEXT_NODE) {\n node.textContent = value;\n } else {\n let text = document.createTextNode(value);\n node.replaceWith(text);\n node = text;\n }\n nodes[id] = node;\n }break;case 18:{els = stack.splice(stack.length - u16buf[u16bufp++]); node = LoadChild(u32buf[u32bufp++], u8buf[u8bufp++]); node.replaceWith(...els);}break;case 19:{node = templates[u16buf[u16bufp++]][u16buf[u16bufp++]].cloneNode(true); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 20:many=u16buf[u16bufp++];{\n root = stack[stack.length-many-1];\n els = stack.splice(stack.length-many);\n for (k = 0; k < many; k++) {\n root.appendChild(els[k]);\n }\n }break;case 21:{setAttributeInner(stack[stack.length-1], get_attr(), s.substring(sp,sp+=u32buf[u32bufp++]), get_ns_cache());}break;case 22:{node = document.createElement('pre'); node.hidden = true; stack.push(node);}break;case 23:{stack.push(document.createElementNS(get_namespace(), get_el()))}break;case 24:{stack.push(document.createElement(get_el()))}break;case 25:{templates[u16buf[u16bufp++]] = stack.splice(stack.length-u16buf[u16bufp++]);}break;case 26:event=get_evt();id=u32buf[u32bufp++];bubbles=u8buf[u8bufp++];\n bubbles = bubbles == 1;\n node = nodes[id];\n if(node.listening){\n node.listening += 1;\n } else {\n node.listening = 1;\n }\n node.setAttribute('data-dioxus-id', `${id}`);\n const event_name = event;\n\n // if this is a mounted listener, we send the event immediately\n if (event_name === \"mounted\") {\n window.ipc.postMessage(\n window.interpreter.serializeIpcMessage(\"user_event\", {\n name: event_name,\n element: id,\n data: null,\n bubbles,\n })\n );\n } else {\n listeners.create(event_name, node, bubbles, (event) => {\n handler(event, event_name, bubbles, config);\n });\n }break;case 27:{nodes[u32buf[u32bufp++]] = LoadChild((()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})());}break;case 28:array=(()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})();value=s.substring(sp,sp+=u32buf[u32bufp++]);id=u32buf[u32bufp++];{\n node = LoadChild(array);\n if (node.nodeType == Node.TEXT_NODE) {\n node.textContent = value;\n } else {\n let text = document.createTextNode(value);\n node.replaceWith(text);\n node = text;\n }\n nodes[id] = node;\n }break;case 29:{els = stack.splice(stack.length - u16buf[u16bufp++]); node = LoadChild((()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})()); node.replaceWith(...els);}break;case 30:return true;\n }\n op>>>=8;\n }\n }\n }\n export function run_from_bytes(bytes){\n d = 0;\n update_memory(new Uint8Array(bytes))\n run()\n }" diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 776f91307..ee15f7ac7 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -8,9 +8,9 @@ fn main() { // If any TS changes, re-run the build script println!("cargo:rerun-if-changed=src/*.ts"); - for entry in ["common", "form", "interpreter"].iter() { - gen_bindings(entry); - } + // for entry in ["common", "form", "interpreter"].iter() { + // gen_bindings(entry); + // } } // okay...... so tsc might fail if the user doesn't have it installed diff --git a/packages/interpreter/src/common.ts b/packages/interpreter/src/common.ts deleted file mode 100644 index ee16838ee..000000000 --- a/packages/interpreter/src/common.ts +++ /dev/null @@ -1,80 +0,0 @@ -export function setAttributeInner(node, field, value, ns) { - - const name = field; - if (ns === "style") { - // ????? why do we need to do this - if (node.style === undefined) { - node.style = {}; - } - node.style[name] = value; - } else if (!!ns) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (!truthy(value) && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } -} - -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, - webkitdirectory: true, -}; - -function truthy(val) { - return val === "true" || val === true; -} diff --git a/packages/interpreter/src/form.ts b/packages/interpreter/src/gen/sledgehammer.js similarity index 100% rename from packages/interpreter/src/form.ts rename to packages/interpreter/src/gen/sledgehammer.js diff --git a/packages/interpreter/src/interpreter.ts b/packages/interpreter/src/interpreter.ts deleted file mode 100644 index c8dcef095..000000000 --- a/packages/interpreter/src/interpreter.ts +++ /dev/null @@ -1,792 +0,0 @@ -class InterpreterConfig { - intercept_link_redirects: boolean; - - constructor(intercept_link_redirects: boolean) { - this.intercept_link_redirects = intercept_link_redirects; - } -} - -// this handler is only provided on the desktop and liveview implementations since this -// method is not used by the web implementation -async function handler(event, name, bubbles, config) { - let target = event.target; - if (target == null) { - return; - } - - const realId = find_real_id(target); - if (realId === null) { - return; - } - - prevent_defaults(event, target, config); - - let contents = await serialize_event(event); - - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - const formData = new FormData(target); - - for (let name of formData.keys()) { - const fieldType = target.elements[name].type; - - switch (fieldType) { - case "select-multiple": - contents.values[name] = formData.getAll(name); - break; - - // add cases for fieldTypes that can hold multiple values here - default: - contents.values[name] = formData.get(name); - break; - } - } - } - - if ( - target.tagName === "SELECT" && - event.type === "input" - ) { - const selectData = target.options; - contents.values["options"] = []; - for (let i = 0; i < selectData.length; i++) { - let option = selectData[i]; - if (option.selected) { - contents.values["options"].push(option.value.toString()); - } - } - } - - // TODO: this should be liveview only - if ( - target.tagName === "INPUT" && - (event.type === "change" || event.type === "input") - ) { - const type = target.getAttribute("type"); - - if (type === "file") { - async function read_files() { - const files = target.files; - const file_contents = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - let file_engine = { - files: file_contents, - }; - contents.files = file_engine; - - if (realId === null) { - return; - } - const message = window.interpreter.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }); - window.ipc.postMessage(message); - } - read_files(); - return; - } - } - - window.ipc.postMessage( - window.interpreter.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }) - ); -} - -// Do our best to prevent the default action of the event -// This should: -// - prevent form submissions from navigating -// - prevent anchor tags from navigating -// - prevent buttons from submitting forms -function prevent_defaults(event, target, config) { - let preventDefaultRequests = null; - - // Some events can be triggered on text nodes, which don't have attributes - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); - } - - if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - // Attempt to intercept if the event is a click - intercept_form_submit(event, target, config, preventDefaultRequests); -} - -function intercept_form_submit(event, target, config, preventDefaultRequests) { - if (event.type !== "click") { - return; - } - - // todo call prevent default if it's the right type of event - if (!config.intercept_link_redirects) { - return; - } - - // prevent buttons in forms from submitting the form - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - - // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating - let a_element = target.closest("a"); - - if (a_element == null) { - return; - } - - event.preventDefault(); - - let elementShouldPreventDefault = - preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - - let aElementShouldPreventDefault = a_element.getAttribute( - `dioxus-prevent-default` - ); - - let linkShouldPreventDefault = - aElementShouldPreventDefault && - aElementShouldPreventDefault.includes(`onclick`); - - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - window.interpreter.serializeIpcMessage("browser_open", { href }) - ); - } - } -} - -function find_real_id(target) { - let realId = null; - - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } - - // walk the tree to find the real element - while (realId == null) { - // we've reached the root we don't want to send an event - if (target.parentElement === null) { - return; - } - - target = target.parentElement; - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } - } - - return realId; -} - -class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = null; - } - - create(event_name, element, bubbles, handler) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.root.addEventListener(event_name, handler); - } else { - this.global[event_name].active++; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.global[event_name].callback); - } - } - - removeAllNonBubbling(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } -} -function LoadChild(array) { - // iterate through each number and get that child - node = stack[stack.length - 1]; - - for (let i = 0; i < array.length; i++) { - end = array[i]; - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - return node; -} -const listeners = new ListenerMap(); -let nodes = []; -let stack = []; -let root; -const templates = {}; -let node, els, end, k; - -function AppendChildren(id, many) { - root = nodes[id]; - els = stack.splice(stack.length - many); - for (k = 0; k < many; k++) { - root.appendChild(els[k]); - } -} - -window.interpreter = {} - -window.interpreter.initialize = function (root) { - nodes = [root]; - stack = [root]; - listeners.root = root; -} - -window.interpreter.getClientRect = function (id) { - const node = nodes[id]; - if (!node) { - return; - } - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; -} - -window.interpreter.scrollTo = function (id, behavior) { - const node = nodes[id]; - if (!node) { - return false; - } - node.scrollIntoView({ - behavior: behavior, - }); - return true; -} - -/// Set the focus on the element -window.interpreter.setFocus = function (id, focus) { - const node = nodes[id]; - if (!node) { - return false; - } - if (focus) { - node.focus(); - } else { - node.blur(); - } - return true; -} - -function get_mouse_data(event: MouseEvent) { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - offsetX, - offsetY, - pageX, - pageY, - screenX, - screenY, - shiftKey, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - offset_x: offsetX, - offset_y: offsetY, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; -} - -async function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { - charCode, - isComposing, - key, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, - location, - repeat, - which, - code, - } = event; - return { - char_code: charCode, - is_composing: isComposing, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - code, - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - return { - value: value, - values: {}, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - - if (target.type === "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - values: {}, - }; - } - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": { - let files = []; - - if (event.dataTransfer && event.dataTransfer.files) { - files = ["a", "b", "c"]; - // files = await serializeFileList(event.dataTransfer.files); - } - - return { mouse: get_mouse_data(event), files }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "dblclick": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - return get_mouse_data(event); - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - pointerId, - width, - height, - pressure, - tangentialPressure, - tiltX, - tiltY, - twist, - pointerType, - isPrimary, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -window.interpreter.serializeIpcMessage = function (method, params = {}) { - return JSON.stringify({ method, params }); -} - -function is_element_node(node) { - return node.nodeType == 1; -} - -function event_bubbles(event) { - switch (event) { - case "copy": - return true; - case "cut": - return true; - case "paste": - return true; - case "compositionend": - return true; - case "compositionstart": - return true; - case "compositionupdate": - return true; - case "keydown": - return true; - case "keypress": - return true; - case "keyup": - return true; - case "focus": - return false; - case "focusout": - return true; - case "focusin": - return true; - case "blur": - return false; - case "change": - return true; - case "input": - return true; - case "invalid": - return true; - case "reset": - return true; - case "submit": - return true; - case "click": - return true; - case "contextmenu": - return true; - case "doubleclick": - return true; - case "dblclick": - return true; - case "drag": - return true; - case "dragend": - return true; - case "dragenter": - return false; - case "dragexit": - return false; - case "dragleave": - return true; - case "dragover": - return true; - case "dragstart": - return true; - case "drop": - return true; - case "mousedown": - return true; - case "mouseenter": - return false; - case "mouseleave": - return false; - case "mousemove": - return true; - case "mouseout": - return true; - case "scroll": - return false; - case "mouseover": - return true; - case "mouseup": - return true; - case "pointerdown": - return true; - case "pointermove": - return true; - case "pointerup": - return true; - case "pointercancel": - return true; - case "gotpointercapture": - return true; - case "lostpointercapture": - return true; - case "pointerenter": - return false; - case "pointerleave": - return false; - case "pointerover": - return true; - case "pointerout": - return true; - case "select": - return true; - case "touchcancel": - return true; - case "touchend": - return true; - case "touchmove": - return true; - case "touchstart": - return true; - case "wheel": - return true; - case "abort": - return false; - case "canplay": - return false; - case "canplaythrough": - return false; - case "durationchange": - return false; - case "emptied": - return false; - case "encrypted": - return true; - case "ended": - return false; - case "error": - return false; - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "load": - return false; - case "pause": - return false; - case "play": - return false; - case "playing": - return false; - case "progress": - return false; - case "ratechange": - return false; - case "seeked": - return false; - case "seeking": - return false; - case "stalled": - return false; - case "suspend": - return false; - case "timeupdate": - return false; - case "volumechange": - return false; - case "waiting": - return false; - case "animationstart": - return true; - case "animationend": - return true; - case "animationiteration": - return true; - case "transitionend": - return true; - case "toggle": - return true; - case "mounted": - return false; - } - - return true; -} diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index d19ccb585..304cc2ed3 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -5,14 +5,9 @@ pub static INTERPRETER_JS: &str = include_str!("./gen/interpreter.js"); pub static COMMON_JS: &str = include_str!("./gen/common.js"); -#[cfg(feature = "sledgehammer")] -mod sledgehammer_bindings; - -#[cfg(feature = "sledgehammer")] -pub use sledgehammer_bindings::*; - #[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))] mod write_native_mutations; + #[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))] pub use write_native_mutations::*; @@ -20,8 +15,18 @@ pub use write_native_mutations::*; #[cfg(all(feature = "minimal_bindings", feature = "webonly"))] pub mod minimal_bindings { use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + /// Some useful snippets that we use to share common functionality between the different platforms we support. + /// + /// This maintains some sort of consistency between web, desktop, and liveview #[wasm_bindgen(module = "/src/gen/common.js")] extern "C" { + /// Set the attribute of the node pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>); + + pub fn collectFormValues(node: JsValue) -> JsValue; } } + +#[cfg(feature = "sledgehammer")] +pub mod unified_bindings; diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs deleted file mode 100644 index e368d83f3..000000000 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ /dev/null @@ -1,407 +0,0 @@ -#[cfg(feature = "webonly")] -use js_sys::Function; -#[cfg(feature = "webonly")] -use sledgehammer_bindgen::bindgen; -#[cfg(feature = "webonly")] -use web_sys::Node; - -#[cfg(feature = "webonly")] -pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; - -#[cfg(feature = "webonly")] -#[bindgen(module)] -mod js { - const JS_FILE: &str = "./src/common.js"; - const JS: &str = r#" - class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = null; - this.handler = null; - } - - create(event_name, element, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.root.addEventListener(event_name, this.handler); - } else { - this.global[event_name].active++; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - } - } - - removeAllNonBubbling(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } - } - function LoadChild(ptr, len) { - // iterate through each number and get that child - node = stack[stack.length - 1]; - ptr_end = ptr + len; - for (; ptr < ptr_end; ptr++) { - end = m.getUint8(ptr); - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - return node; - } - const listeners = new ListenerMap(); - let nodes = []; - let stack = []; - let root; - const templates = {}; - let node, els, end, ptr_end, k; - export function save_template(nodes, tmpl_id) { - templates[tmpl_id] = nodes; - } - export function hydrate(ids) { - const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); - for (let i = 0; i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i]; - const hydration = hydrateNode.getAttribute('data-node-hydration'); - const split = hydration.split(','); - const id = ids[parseInt(split[0])]; - nodes[id] = hydrateNode; - if (split.length > 1) { - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute('data-dioxus-id', id); - for (let j = 1; j < split.length; j++) { - const listener = split[j]; - const split2 = listener.split(':'); - const event_name = split2[0]; - const bubbles = split2[1] === '1'; - listeners.create(event_name, hydrateNode, bubbles); - } - } - } - const treeWalker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_COMMENT, - ); - let currentNode = treeWalker.nextNode(); - while (currentNode) { - const id = currentNode.textContent; - const split = id.split('node-id'); - if (split.length > 1) { - nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - currentNode = treeWalker.nextNode(); - } - } - export function get_node(id) { - return nodes[id]; - } - export function initialize(root, handler) { - listeners.handler = handler; - nodes = [root]; - stack = [root]; - listeners.root = root; - } - function AppendChildren(id, many){ - root = nodes[id]; - els = stack.splice(stack.length-many); - for (k = 0; k < many; k++) { - root.appendChild(els[k]); - } - } - "#; - - extern "C" { - #[wasm_bindgen] - pub fn save_template(nodes: Vec, tmpl_id: u16); - - #[wasm_bindgen] - pub fn hydrate(ids: Vec); - - #[wasm_bindgen] - pub fn get_node(id: u32) -> Node; - - #[wasm_bindgen] - pub fn initialize(root: Node, handler: &Function); - } - - fn mount_to_root() { - "{AppendChildren(root, stack.length-1);}" - } - fn push_root(root: u32) { - "{stack.push(nodes[$root$]);}" - } - fn append_children(id: u32, many: u16) { - "{AppendChildren($id$, $many$);}" - } - fn pop_root() { - "{stack.pop();}" - } - fn replace_with(id: u32, n: u16) { - "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" - } - fn insert_after(id: u32, n: u16) { - "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" - } - fn insert_before(id: u32, n: u16) { - "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" - } - fn remove(id: u32) { - "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}" - } - fn create_raw_text(text: &str) { - "{stack.push(document.createTextNode($text$));}" - } - fn create_text_node(text: &str, id: u32) { - "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}" - } - fn create_placeholder(id: u32) { - "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}" - } - fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { - r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"# - } - fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { - "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}" - } - fn set_text(id: u32, text: &str) { - "{nodes[$id$].textContent = $text$;}" - } - fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { - "{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}" - } - fn remove_attribute(id: u32, field: &str, ns: &str) { - r#"{ - node = nodes[$id$]; - if (!ns) { - switch (field) { - case "value": - node.value = ""; - break; - case "checked": - node.checked = false; - break; - case "selected": - node.selected = false; - break; - case "dangerous_inner_html": - node.innerHTML = ""; - break; - default: - node.removeAttribute(field); - break; - } - } else if (ns == "style") { - node.style.removeProperty(name); - } else { - node.removeAttributeNS(ns, field); - } - }"# - } - fn assign_id(ptr: u32, len: u8, id: u32) { - "{nodes[$id$] = LoadChild($ptr$, $len$);}" - } - fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) { - r#"{ - node = LoadChild($ptr$, $len$); - if (node.nodeType == Node.TEXT_NODE) { - node.textContent = value; - } else { - let text = document.createTextNode(value); - node.replaceWith(text); - node = text; - } - nodes[$id$] = node; - }"# - } - fn replace_placeholder(ptr: u32, len: u8, n: u16) { - "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}" - } - fn load_template(tmpl_id: u16, index: u16, id: u32) { - "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" - } -} - -#[cfg(feature = "binary-protocol")] -pub mod binary_protocol { - use sledgehammer_bindgen::bindgen; - pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; - - #[bindgen] - mod protocol_js { - const JS_FILE: &str = "./src/interpreter.js"; - const JS_FILE: &str = "./src/common.js"; - - fn mount_to_root() { - "{AppendChildren(root, stack.length-1);}" - } - fn push_root(root: u32) { - "{stack.push(nodes[$root$]);}" - } - fn append_children(id: u32, many: u16) { - "{AppendChildren($id$, $many$);}" - } - fn append_children_to_top(many: u16) { - "{ - root = stack[stack.length-many-1]; - els = stack.splice(stack.length-many); - for (k = 0; k < many; k++) { - root.appendChild(els[k]); - } - }" - } - fn pop_root() { - "{stack.pop();}" - } - fn replace_with(id: u32, n: u16) { - "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" - } - fn insert_after(id: u32, n: u16) { - "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" - } - fn insert_before(id: u32, n: u16) { - "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" - } - fn remove(id: u32) { - "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}" - } - fn create_raw_text(text: &str) { - "{stack.push(document.createTextNode($text$));}" - } - fn create_text_node(text: &str, id: u32) { - "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}" - } - fn create_element(element: &'static str) { - "{stack.push(document.createElement($element$))}" - } - fn create_element_ns(element: &'static str, ns: &'static str) { - "{stack.push(document.createElementNS($ns$, $element$))}" - } - fn create_placeholder(id: u32) { - "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}" - } - fn add_placeholder() { - "{node = document.createElement('pre'); node.hidden = true; stack.push(node);}" - } - fn new_event_listener(event: &str, id: u32, bubbles: u8) { - r#" - bubbles = bubbles == 1; - node = nodes[id]; - if(node.listening){ - node.listening += 1; - } else { - node.listening = 1; - } - node.setAttribute('data-dioxus-id', `\${id}`); - const event_name = $event$; - - // if this is a mounted listener, we send the event immediately - if (event_name === "mounted") { - window.ipc.postMessage( - window.interpreter.serializeIpcMessage("user_event", { - name: event_name, - element: id, - data: null, - bubbles, - }) - ); - } else { - listeners.create(event_name, node, bubbles, (event) => { - handler(event, event_name, bubbles, config); - }); - }"# - } - fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { - "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}" - } - fn set_text(id: u32, text: &str) { - "{nodes[$id$].textContent = $text$;}" - } - fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { - "{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}" - } - fn set_top_attribute(field: &str, value: &str, ns: &str) { - "{setAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}" - } - fn remove_attribute(id: u32, field: &str, ns: &str) { - r#"{ - node = nodes[$id$]; - if (!ns) { - switch (field) { - case "value": - node.value = ""; - break; - case "checked": - node.checked = false; - break; - case "selected": - node.selected = false; - break; - case "dangerous_inner_html": - node.innerHTML = ""; - break; - default: - node.removeAttribute(field); - break; - } - } else if (ns == "style") { - node.style.removeProperty(name); - } else { - node.removeAttributeNS(ns, field); - } - }"# - } - fn assign_id(array: &[u8], id: u32) { - "{nodes[$id$] = LoadChild($array$);}" - } - fn hydrate_text(array: &[u8], value: &str, id: u32) { - r#"{ - node = LoadChild($array$); - if (node.nodeType == Node.TEXT_NODE) { - node.textContent = value; - } else { - let text = document.createTextNode(value); - node.replaceWith(text); - node = text; - } - nodes[$id$] = node; - }"# - } - fn replace_placeholder(array: &[u8], n: u16) { - "{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}" - } - fn load_template(tmpl_id: u16, index: u16, id: u32) { - "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" - } - fn add_templates(tmpl_id: u16, len: u16) { - "{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}" - } - } -} diff --git a/packages/interpreter/src/ts/form.ts b/packages/interpreter/src/ts/form.ts new file mode 100644 index 000000000..c68723b09 --- /dev/null +++ b/packages/interpreter/src/ts/form.ts @@ -0,0 +1,3 @@ +function retrieveFormValues() { + +} diff --git a/packages/interpreter/src/ts/interpreter_core.ts b/packages/interpreter/src/ts/interpreter_core.ts new file mode 100644 index 000000000..d13d71f3d --- /dev/null +++ b/packages/interpreter/src/ts/interpreter_core.ts @@ -0,0 +1,82 @@ +// The root interpreter class that holds state about the mapping between DOM and VirtualDom +// This always lives in the JS side of things, and is extended by the native and web interpreters + +// todo: we want to make these vars that the interpreter uses to be isolated so we can have multiple interpreters +export let m: DataView; // p, ls, d, t, op, i, e, z, metaflags; + +export class Interpreter { + // non bubbling events listen at the element the listener was created at + global: { + [key: string]: { active: number, callback: EventListener } + }; + // bubbling events can listen at the root element + local: { + [key: string]: { + [key: string]: EventListener + } + }; + root: Element; + handler: EventListener; + nodes: Element[]; + stack: Element[]; + templates: { + [key: string]: Element[] + }; + + constructor(root: Element, handler: EventListener) { + this.root = root; + this.nodes = [root]; + this.stack = [root]; + this.global = {}; + this.local = {}; + this.handler = handler; + } + + createListener(event_name: string, element: Element, bubbles: boolean) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = { active: 1, callback: this.handler }; + this.root.addEventListener(event_name, this.handler); + } else { + this.global[event_name].active++; + } + } else { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, this.handler); + } + } + + removeListener(element: Element, event_name: string, bubbles: boolean) { + if (bubbles) { + this.removeBubblingListener(event_name); + } else { + this.removeNonBubblingListener(element, event_name); + } + } + + removeBubblingListener(event_name: string) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + + removeNonBubblingListener(element: Element, event_name: string) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (Object.keys(this.local[id]).length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + } + + removeAllNonBubblingListeners(element: Element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } +} + diff --git a/packages/interpreter/src/ts/interpreter_native.ts b/packages/interpreter/src/ts/interpreter_native.ts new file mode 100644 index 000000000..13bb84ee4 --- /dev/null +++ b/packages/interpreter/src/ts/interpreter_native.ts @@ -0,0 +1,286 @@ +// This file provides an extended variant of the interpreter used for desktop and liveview interaction +// +// + +import { Interpreter } from "./interpreter_core"; + +export class NativeInterpreter extends Interpreter { + intercept_link_redirects: boolean; + + constructor(root: Element) { + super(root, (event) => handler(event, this, event.type, true)); + } + + serializeIpcMessage(method: string, params = {}) { + return JSON.stringify({ method, params }); + } + + scrollTo(id: number, behavior: ScrollBehavior) { + const node = this.nodes[id]; + + if (!(node instanceof HTMLElement)) { + return false; + } + + node.scrollIntoView({ + behavior: behavior, + }); + + return true; + } + + getClientRect(id: number) { + const node = this.nodes[id]; + if (!node) { + return; + } + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [rect.x, rect.y], + size: [rect.width, rect.height], + }; + } + + setFocus(id: number, focus: boolean) { + const node = this.nodes[id]; + + if (!(node instanceof HTMLElement)) { + return false; + } + + if (focus) { + node.focus(); + } else { + node.blur(); + } + + return true; + } + + LoadChild(array: number[]) { + // iterate through each number and get that child + let node = this.stack[this.stack.length - 1] as Node; + + for (let i = 0; i < array.length; i++) { + let end = array[i]; + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + + return node; + } + + AppendChildren(id: number, many: number) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + + for (let k = 0; k < many; k++) { + root.appendChild(els[k]); + } + } +} + + + + +function handler(event: Event, interpreter: NativeInterpreter, name: string, bubbles: boolean) { + const target = event.target!; + const realId = target_id(target)!; + let contents = serializeEvent(event); + + if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) { + const formData = new FormData(target); + + for (let name of formData.keys()) { + const fieldType = target.elements[name].type; + + switch (fieldType) { + case "select-multiple": + contents.values[name] = formData.getAll(name); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + contents.values[name] = formData.get(name); + break; + } + } + } + + if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) { + const selectData = target.options; + contents.values["options"] = []; + for (let i = 0; i < selectData.length; i++) { + let option = selectData[i]; + if (option.selected) { + contents.values["options"].push(option.value.toString()); + } + } + } + + // If there's files to read + if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { + if (target.getAttribute("type") === "file") { + read_files(target, contents, bubbles, realId, name); + return; + } + } + + prevents_default(event, target); + + + // Handle the event on the virtualdom and then process whatever its output was + let body = { + name: name, + data: serializeEvent(event), + element: parseInt(realId), + bubbles, + }; + + if (waitForVirtualDomPreventDefault(JSON.stringify(body))) { + event.preventDefault(); + } +} + +export function waitForVirtualDomPreventDefault(contents: string): boolean { + + + // Handle the event on the virtualdom and then process whatever its output was + const xhr = new XMLHttpRequest(); + + // Serialize the event and send it to the custom protocol in the Rust side of things + xhr.timeout = 1000; + xhr.open("GET", "/handle/event.please", false); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(contents); + + // Deserialize the response, and then prevent the default/capture the event if the virtualdom wants to + return JSON.parse(xhr.responseText).preventDefault; +} + + +async function read_files(target: HTMLInputElement, contents, bubbles, realId, name) { + let files = target.files!; + let file_contents: { [name: string]: number[] } = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + + contents.files = { files: file_contents }; + + if (realId === null) { + return; + } + + const message = window.interpreter.serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + + window.ipc.postMessage(message); +} + + +export function target_id(target: EventTarget): string | null { + if (!(target instanceof Element)) { + return null; + } + + function find_real_id(target: Element): string | null { + let realId = target.getAttribute(`data-dioxus-id`); + + // walk the tree to find the real element + while (realId == null) { + // we've reached the root we don't want to send an event + if (target.parentElement === null) { + return null; + } + + target = target.parentElement; + if (target instanceof Element) { + realId = target.getAttribute(`data-dioxus-id`); + } + } + + return realId; + } + + return find_real_id(target); +} + + + +// This should: +// - prevent form submissions from navigating +// - prevent anchor tags from navigating +// - prevent buttons from submitting forms +// - let the virtualdom attempt to prevent the event +preventDefaults(event: Event, target: EventTarget) { + let preventDefaultRequests: string | null = null; + + // Some events can be triggered on text nodes, which don't have attributes + if (target instanceof Element) { + preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); + } + + if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { + event.preventDefault(); + } + + if (event.type === "submit") { + event.preventDefault(); + } + + // Attempt to intercept if the event is a click + if (target instanceof Element && event.type === "click") { + this.preventFormNavigate(event, target); + } +} + +preventFormNavigate(event: Event, target: Element) { + // todo call prevent default if it's the right type of event + if (!this.intercept_link_redirects) { + return; + } + + // prevent buttons in forms from submitting the form + if (target.tagName === "BUTTON") { // && event.type == "submit" + event.preventDefault(); + } + + // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating + let a_element = target.closest("a"); + if (a_element == null) { + return; + } + + event.preventDefault(); + + let elementShouldPreventDefault = + preventDefaultRequests && preventDefaultRequests.includes(`onclick`); + + let aElementShouldPreventDefault = a_element.getAttribute( + `dioxus-prevent-default` + ); + + let linkShouldPreventDefault = + aElementShouldPreventDefault && + aElementShouldPreventDefault.includes(`onclick`); + + if (!elementShouldPreventDefault && !linkShouldPreventDefault) { + const href = a_element.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.ipc.postMessage( + window.interpreter.serializeIpcMessage("browser_open", { href }) + ); + } + } +} diff --git a/packages/interpreter/src/ts/interpreter_web.ts b/packages/interpreter/src/ts/interpreter_web.ts new file mode 100644 index 000000000..6a16761e7 --- /dev/null +++ b/packages/interpreter/src/ts/interpreter_web.ts @@ -0,0 +1,83 @@ +// The JS<->Rust bridge +// This file is thin, suitable just for the web +// If you want the more full-featured intrepreter, look at the native interpreter which extends this with additional functionality +// +// We're using sledgehammer directly + +import { Interpreter, m } from "./interpreter_core"; + +export class WebInterpreter extends Interpreter { + constructor(root: Element, handler: EventListener) { + super(root, handler); + } + + LoadChild(ptr: number, len: number): Node { + // iterate through each number and get that child + let node = this.stack[this.stack.length - 1] as Node; + let ptr_end = ptr + len; + + for (; ptr < ptr_end; ptr++) { + let end = m.getUint8(ptr); + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + + return node; + } + + saveTemplate(nodes: Element[], tmpl_id: string) { + this.templates[tmpl_id] = nodes; + } + + hydrateRoot(ids: { [key: number]: number }) { + const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); + + for (let i = 0; i < hydrateNodes.length; i++) { + const hydrateNode = hydrateNodes[i]; + const hydration = hydrateNode.getAttribute('data-node-hydration'); + const split = hydration!.split(','); + const id = ids[parseInt(split[0])]; + + this.nodes[id] = hydrateNode; + + if (split.length > 1) { + // @ts-ignore + hydrateNode.listening = split.length - 1; + hydrateNode.setAttribute('data-dioxus-id', id.toString()); + for (let j = 1; j < split.length; j++) { + const listener = split[j]; + const split2 = listener.split(':'); + const event_name = split2[0]; + const bubbles = split2[1] === '1'; + this.createListener(event_name, hydrateNode, bubbles); + } + } + } + const treeWalker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_COMMENT, + ); + let currentNode = treeWalker.nextNode(); + while (currentNode) { + const id = currentNode.textContent!; + const split = id.split('node-id'); + if (split.length > 1) { + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling as Element; + } + currentNode = treeWalker.nextNode(); + } + } + + getNode(id: number): Element { + return this.nodes[id]; + } + + appendChildren(id: number, many: number) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + for (let k = 0; k < many; k++) { + this.root.appendChild(els[k]); + } + } +} diff --git a/packages/interpreter/src/ts/serialize.ts b/packages/interpreter/src/ts/serialize.ts new file mode 100644 index 000000000..e7473155a --- /dev/null +++ b/packages/interpreter/src/ts/serialize.ts @@ -0,0 +1,494 @@ +// Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {}; + +type SerializedEvent = { + values?: { [key: string]: FormDataEntryValue[] } | FormDataEntryValue[]; + value?: string; + [key: string]: any; +}; + +function serializeEvent(event: Event): SerializedEvent { + + // copy, cut, paste + if (event instanceof KeyboardEvent) { + return { + char_code: event.charCode, + is_composing: event.isComposing, + key: event.key, + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + key_code: event.keyCode, + shift_key: event.shiftKey, + location: event.location, + repeat: event.repeat, + which: event.which, + code: event.code, + }; + } + + + // "pointerdown" "pointermove" "pointerup" "pointercancel" "gotpointercapture" "lostpointercapture" "pointerenter" + // "pointerleave" "pointerover" "pointerout" + if (event instanceof PointerEvent) { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + pointer_id: event.pointerId, + width: event.width, + height: event.height, + pressure: event.pressure, + tangential_pressure: event.tangentialPressure, + tilt_x: event.tiltX, + tilt_y: event.tiltY, + twist: event.twist, + pointer_type: event.pointerType, + is_primary: event.isPrimary, + }; + } + + if (event instanceof MouseEvent) { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + offset_x: event.offsetX, + offset_y: event.offsetY, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + }; + } + + if (event instanceof TouchEvent) { + return { + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + shift_key: event.shiftKey, + changed_touches: event.changedTouches, + target_touches: event.targetTouches, + touches: event.touches, + }; + } + + if (event instanceof WheelEvent) { + return { + delta_x: event.deltaX, + delta_y: event.deltaY, + delta_z: event.deltaZ, + delta_mode: event.deltaMode, + }; + } + + if (event instanceof AnimationEvent) { + return { + animation_name: event.animationName, + elapsed_time: event.elapsedTime, + pseudo_element: event.pseudoElement, + }; + } + + if (event instanceof TransitionEvent) { + return { + property_name: event.propertyName, + elapsed_time: event.elapsedTime, + pseudo_element: event.pseudoElement, + }; + } + + if (event instanceof ClipboardEvent) { + return {}; + } + + if (event instanceof CompositionEvent) { + return { + data: event.data, + }; + } + + if (event instanceof DragEvent) { + return { + mouse: { + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + shift_key: event.shiftKey, + }, + files: [], + }; + } + + if (event instanceof FocusEvent) { + return {}; + } + + return {}; +} + +function isElementNode(node: Node) { + return node.nodeType == 1; +} + +function eventBubbles(eventName: string) { + switch (eventName) { + case "copy": + return true; + case "cut": + return true; + case "paste": + return true; + case "compositionend": + return true; + case "compositionstart": + return true; + case "compositionupdate": + return true; + case "keydown": + return true; + case "keypress": + return true; + case "keyup": + return true; + case "focus": + return false; + case "focusout": + return true; + case "focusin": + return true; + case "blur": + return false; + case "change": + return true; + case "input": + return true; + case "invalid": + return true; + case "reset": + return true; + case "submit": + return true; + case "click": + return true; + case "contextmenu": + return true; + case "doubleclick": + return true; + case "dblclick": + return true; + case "drag": + return true; + case "dragend": + return true; + case "dragenter": + return false; + case "dragexit": + return false; + case "dragleave": + return true; + case "dragover": + return true; + case "dragstart": + return true; + case "drop": + return true; + case "mousedown": + return true; + case "mouseenter": + return false; + case "mouseleave": + return false; + case "mousemove": + return true; + case "mouseout": + return true; + case "scroll": + return false; + case "mouseover": + return true; + case "mouseup": + return true; + case "pointerdown": + return true; + case "pointermove": + return true; + case "pointerup": + return true; + case "pointercancel": + return true; + case "gotpointercapture": + return true; + case "lostpointercapture": + return true; + case "pointerenter": + return false; + case "pointerleave": + return false; + case "pointerover": + return true; + case "pointerout": + return true; + case "select": + return true; + case "touchcancel": + return true; + case "touchend": + return true; + case "touchmove": + return true; + case "touchstart": + return true; + case "wheel": + return true; + case "abort": + return false; + case "canplay": + return false; + case "canplaythrough": + return false; + case "durationchange": + return false; + case "emptied": + return false; + case "encrypted": + return true; + case "ended": + return false; + case "error": + return false; + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "load": + return false; + case "pause": + return false; + case "play": + return false; + case "playing": + return false; + case "progress": + return false; + case "ratechange": + return false; + case "seeked": + return false; + case "seeking": + return false; + case "stalled": + return false; + case "suspend": + return false; + case "timeupdate": + return false; + case "volumechange": + return false; + case "waiting": + return false; + case "animationstart": + return true; + case "animationend": + return true; + case "animationiteration": + return true; + case "transitionend": + return true; + case "toggle": + return true; + case "mounted": + return false; + } + + return true; +} + + + +// switch (event.type) { +// case "copy": case "cut": case "paste": { +// return {}; +// } +// case "compositionend": +// case "compositionstart": +// case "compositionupdate": { +// let { data } = event; +// return { +// data, +// }; +// } +// case "keydown": case "keypress": case "keyup": { +// if (event instanceof KeyboardEvent) { +// return { +// char_code: event.charCode, +// is_composing: event.isComposing, +// key: event.key, +// alt_key: event.altKey, +// ctrl_key: event.ctrlKey, +// meta_key: event.metaKey, +// key_code: event.keyCode, +// shift_key: event.shiftKey, +// location: event.location, +// repeat: event.repeat, +// which: event.which, +// code: event.code, +// }; +// }; +// } +// case "focus": +// case "blur": { +// return {}; +// } +// case "change": { +// let target = event.target; +// let value; +// if (target.type === "checkbox" || target.type === "radio") { +// value = target.checked ? "true" : "false"; +// } else { +// value = target.value ?? target.textContent; +// } +// return { +// value: value, +// values: {}, +// }; +// } +// case "input": +// case "invalid": +// case "reset": +// case "submit": { +// let target = event.target; +// let value = target.value ?? target.textContent; + +// if (target.type === "checkbox") { +// value = target.checked ? "true" : "false"; +// } + +// return { +// value: value, +// values: {}, +// }; +// } +// case "drag": +// case "dragend": +// case "dragenter": +// case "dragexit": +// case "dragleave": +// case "dragover": +// case "dragstart": +// case "drop": { +// let files = []; + +// if (event.dataTransfer && event.dataTransfer.files) { +// files = ["a", "b", "c"]; +// // files = await serializeFileList(event.dataTransfer.files); +// } + +// return { mouse: get_mouse_data(event), files }; +// } +// case "click": +// case "contextmenu": +// case "doubleclick": +// case "dblclick": +// case "mousedown": +// case "mouseenter": +// case "mouseleave": +// case "mousemove": +// case "mouseout": +// case "mouseover": +// case "mouseup": { +// return get_mouse_data(event); +// } +// case "select": { +// return {}; +// } +// case "touchcancel": +// case "touchend": +// case "touchmove": +// case "touchstart": { +// const { altKey, ctrlKey, metaKey, shiftKey } = event; +// return { +// // changed_touches: event.changedTouches, +// // target_touches: event.targetTouches, +// // touches: event.touches, +// alt_key: altKey, +// ctrl_key: ctrlKey, +// meta_key: metaKey, +// shift_key: shiftKey, +// }; +// } +// case "scroll": { +// return {}; +// } +// case "wheel": { +// const { deltaX, deltaY, deltaZ, deltaMode } = event; +// return { +// delta_x: deltaX, +// delta_y: deltaY, +// delta_z: deltaZ, +// delta_mode: deltaMode, +// }; +// } +// case "animationstart": +// case "animationend": +// case "animationiteration": { +// const { animationName, elapsedTime, pseudoElement } = event; +// return { +// animation_name: animationName, +// elapsed_time: elapsedTime, +// pseudo_element: pseudoElement, +// }; +// } +// case "transitionend": { +// const { propertyName, elapsedTime, pseudoElement } = event; +// return { +// property_name: propertyName, +// elapsed_time: elapsedTime, +// pseudo_element: pseudoElement, +// }; +// } +// case "abort": +// case "canplay": +// case "canplaythrough": +// case "durationchange": +// case "emptied": +// case "encrypted": +// case "ended": +// case "error": +// case "loadeddata": +// case "loadedmetadata": +// case "loadstart": +// case "pause": +// case "play": +// case "playing": +// case "progress": +// case "ratechange": +// case "seeked": +// case "seeking": +// case "stalled": +// case "suspend": +// case "timeupdate": +// case "volumechange": +// case "waiting": { +// return {}; +// } +// case "toggle": { +// return {}; +// } +// default: { +// return {}; +// } +// } diff --git a/packages/interpreter/src/ts/set_attribute.ts b/packages/interpreter/src/ts/set_attribute.ts new file mode 100644 index 000000000..cd94f5507 --- /dev/null +++ b/packages/interpreter/src/ts/set_attribute.ts @@ -0,0 +1,105 @@ +// this function should try and stay fast, if possible +export function setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { + // we support a single namespace by default: style + if (ns === "style") { + node.style.setProperty(field, value); + return; + } + + // If there's a namespace, use setAttributeNS (svg, mathml, etc.) + if (!!ns) { + node.setAttributeNS(ns, field, value); + return + } + + // A few attributes are need to be set with either boolean values or require some sort of translation + switch (field) { + case "value": + if (value !== node.getAttribute("value")) { + node.setAttribute(field, value); + } + break; + + case "initial_value": + node.setAttribute("defaultValue", value); + break; + + case "checked": + if (node instanceof HTMLInputElement) { + node.checked = truthy(value); + } + break; + + case "initial_checked": + if (node instanceof HTMLInputElement) { + node.defaultChecked = truthy(value); + } + break; + + case "selected": + if (node instanceof HTMLOptionElement) { + node.selected = truthy(value); + } + break; + + case "initial_selected": + if (node instanceof HTMLOptionElement) { + node.defaultSelected = truthy(value); + } + break; + + case "dangerous_inner_html": + node.innerHTML = value; + break; + + // The presence of a an attribute is enough to set it to true, provided the value is being set to a truthy value + // Again, kinda ugly and would prefer this logic to be baked into dioxus-html at compiile time + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (!truthy(value) && isBoolAttr(field)) { + node.removeAttribute(field); + } else { + node.setAttribute(field, value); + } + } +} + + +function truthy(val: string | boolean) { + return val === "true" || val === true; +} + +function isBoolAttr(field: string): boolean { + switch (field) { + case "allowfullscreen": + case "allowpaymentrequest": + case "async": + case "autofocus": + case "autoplay": + case "checked": + case "controls": + case "default": + case "defer": + case "disabled": + case "formnovalidate": + case "hidden": + case "ismap": + case "itemscope": + case "loop": + case "multiple": + case "muted": + case "nomodule": + case "novalidate": + case "open": + case "playsinline": + case "readonly": + case "required": + case "reversed": + case "selected": + case "truespeed": + case "webkitdirectory": + return true; + default: + return false; + } +} diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs new file mode 100644 index 000000000..194a62c48 --- /dev/null +++ b/packages/interpreter/src/unified_bindings.rs @@ -0,0 +1,196 @@ +#[cfg(feature = "webonly")] +use js_sys::Function; + +#[cfg(feature = "webonly")] +use web_sys::Node; + +use sledgehammer_bindgen::bindgen; + +pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; + +#[bindgen(module)] +mod js { + // Load in the JavaScript file with all the imports + const JS_FILE: &str = "./src/gen/interpreter.js"; + + // Boot the interpreter and attach us some bindings! + const JS: &str = r#""#; + + fn mount_to_root() { + "{AppendChildren(root, stack.length-1);}" + } + fn push_root(root: u32) { + "{stack.push(nodes[$root$]);}" + } + fn append_children(id: u32, many: u16) { + "{AppendChildren($id$, $many$);}" + } + + fn pop_root() { + "{stack.pop();}" + } + fn replace_with(id: u32, n: u16) { + "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" + } + fn insert_after(id: u32, n: u16) { + "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" + } + fn insert_before(id: u32, n: u16) { + "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" + } + fn remove(id: u32) { + "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}" + } + fn create_raw_text(text: &str) { + "{stack.push(document.createTextNode($text$));}" + } + fn create_text_node(text: &str, id: u32) { + "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}" + } + fn create_placeholder(id: u32) { + "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}" + } + fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { + r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"# + } + fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { + "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}" + } + fn set_text(id: u32, text: &str) { + "{nodes[$id$].textContent = $text$;}" + } + fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { + "{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}" + } + fn remove_attribute(id: u32, field: &str, ns: &str) { + r#"{ + node = nodes[$id$]; + if (!ns) { + switch (field) { + case "value": + node.value = ""; + break; + case "checked": + node.checked = false; + break; + case "selected": + node.selected = false; + break; + case "dangerous_inner_html": + node.innerHTML = ""; + break; + default: + node.removeAttribute(field); + break; + } + } else if (ns == "style") { + node.style.removeProperty(name); + } else { + node.removeAttributeNS(ns, field); + } + }"# + } + fn assign_id(ptr: u32, len: u8, id: u32) { + "{nodes[$id$] = LoadChild($ptr$, $len$);}" + } + fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) { + r#"{ + node = LoadChild($ptr$, $len$); + if (node.nodeType == Node.TEXT_NODE) { + node.textContent = value; + } else { + let text = document.createTextNode(value); + node.replaceWith(text); + node = text; + } + nodes[$id$] = node; + }"# + } + fn replace_placeholder(ptr: u32, len: u8, n: u16) { + "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}" + } + fn load_template(tmpl_id: u16, index: u16, id: u32) { + "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" + } + + /* + Binary protocol methods only! + + These methods let us support binary packing mutations for use on boundaries like desktop where we prefer to send + binary data instead of JSON. + + We're using native types in a number of places + */ + fn append_children_to_top(many: u16) { + "{ + root = stack[stack.length-many-1]; + els = stack.splice(stack.length-many); + for (k = 0; k < many; k++) { + root.appendChild(els[k]); + } + }" + } + fn set_top_attribute(field: &str, value: &str, ns: &str) { + "{setAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}" + } + fn add_placeholder() { + "{node = document.createElement('pre'); node.hidden = true; stack.push(node);}" + } + fn create_element_ns(element: &'static str, ns: &'static str) { + "{stack.push(document.createElementNS($ns$, $element$))}" + } + fn create_element(element: &'static str) { + "{stack.push(document.createElement($element$))}" + } + fn add_templates(tmpl_id: u16, len: u16) { + "{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}" + } + fn foreign_event_listener(event: &str, id: u32, bubbles: u8) { + r#" + bubbles = bubbles == 1; + node = nodes[id]; + if(node.listening){ + node.listening += 1; + } else { + node.listening = 1; + } + node.setAttribute('data-dioxus-id', `\${id}`); + const event_name = $event$; + + // if this is a mounted listener, we send the event immediately + if (event_name === "mounted") { + window.ipc.postMessage( + window.interpreter.serializeIpcMessage("user_event", { + name: event_name, + element: id, + data: null, + bubbles, + }) + ); + } else { + listeners.create(event_name, node, bubbles, (event) => { + handler(event, event_name, bubbles, config); + }); + }"# + } + fn assign_id_ref(array: &[u8], id: u32) { + "{nodes[$id$] = LoadChild($array$);}" + } + fn hydrate_text_ref(array: &[u8], value: &str, id: u32) { + r#"{ + node = LoadChild($array$); + if (node.nodeType == Node.TEXT_NODE) { + node.textContent = value; + } else { + let text = document.createTextNode(value); + node.replaceWith(text); + node = text; + } + nodes[$id$] = node; + }"# + } + + fn replace_placeholder_ref(array: &[u8], n: u16) { + "{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}" + } +} diff --git a/packages/interpreter/src/write_native_mutations.rs b/packages/interpreter/src/write_native_mutations.rs index 18a996240..0b3df50e8 100644 --- a/packages/interpreter/src/write_native_mutations.rs +++ b/packages/interpreter/src/write_native_mutations.rs @@ -1,17 +1,18 @@ -use dioxus_html::event_bubbles; - +// use crate::binary_protocol::Channel; +use crate::unified_bindings::Channel; use dioxus_core::{TemplateAttribute, TemplateNode, WriteMutations}; +use dioxus_html::event_bubbles; use sledgehammer_utils::rustc_hash::FxHashMap; -use crate::binary_protocol::Channel; - /// The state needed to apply mutations to a channel. This state should be kept across all mutations for the app #[derive(Default)] pub struct MutationState { /// The maximum number of templates that we have registered max_template_count: u16, + /// The currently registered templates with the template ids templates: FxHashMap, + /// The channel that we are applying mutations to channel: Channel, } @@ -100,7 +101,7 @@ impl WriteMutations for MutationState { } fn assign_node_id(&mut self, path: &'static [u8], id: dioxus_core::ElementId) { - self.channel.assign_id(path, id.0 as u32); + self.channel.assign_id_ref(path, id.0 as u32); } fn create_placeholder(&mut self, id: dioxus_core::ElementId) { @@ -112,7 +113,7 @@ impl WriteMutations for MutationState { } fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: dioxus_core::ElementId) { - self.channel.hydrate_text(path, value, id.0 as u32); + self.channel.hydrate_text_ref(path, value, id.0 as u32); } fn load_template(&mut self, name: &'static str, index: usize, id: dioxus_core::ElementId) { @@ -127,7 +128,7 @@ impl WriteMutations for MutationState { } fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) { - self.channel.replace_placeholder(path, m as u16); + self.channel.replace_placeholder_ref(path, m as u16); } fn insert_nodes_after(&mut self, id: dioxus_core::ElementId, m: usize) { @@ -181,8 +182,9 @@ impl WriteMutations for MutationState { } fn create_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) { + // note that we use the foreign event listener here self.channel - .new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); + .foreign_event_listener(name, id.0 as u32, event_bubbles(name) as u8); } fn remove_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) { diff --git a/packages/interpreter/tests/e2e.rs b/packages/interpreter/tests/e2e.rs new file mode 100644 index 000000000..81f33b00b --- /dev/null +++ b/packages/interpreter/tests/e2e.rs @@ -0,0 +1 @@ +//! Ensure that the interpreter loads, has no errors, and writes mutations diff --git a/packages/interpreter/tests/serialize.rs b/packages/interpreter/tests/serialize.rs new file mode 100644 index 000000000..4698c285a --- /dev/null +++ b/packages/interpreter/tests/serialize.rs @@ -0,0 +1 @@ +//! Ensure that events are serialized and deserialized correctly. diff --git a/packages/interpreter/tsconfig.json b/packages/interpreter/tsconfig.json new file mode 100644 index 000000000..18ce8679c --- /dev/null +++ b/packages/interpreter/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "system", + "lib": [ + "ES2015", + "DOM" + ], + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + "outFile": "gen/main.js", + "sourceMap": true + }, + "include": [ + "src/ts/*.ts" + ], + "exclude": [ + "**/*.spec.ts" + ] +} diff --git a/packages/web/NOTES.md b/packages/web/NOTES.md new file mode 100644 index 000000000..e631eaa58 --- /dev/null +++ b/packages/web/NOTES.md @@ -0,0 +1,35 @@ + +// ## RequestAnimationFrame and RequestIdleCallback +// ------------------------------------------------ +// React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long +// running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the +// main thread. +// +// React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus, +// the diff phase is non-blocking, using "work_with_deadline" to allow the browser to process other events. When the diff phase +// is finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply. +// +// Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then +// setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame +// +// From Google's guide on rAF and rIC: +// ----------------------------------- +// +// If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed, +// which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside +// of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next +// frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout, +// which is a potential performance bottleneck. +// +// Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable, +// and as such we could easily go past the deadline the browser provided. +// +// The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the +// browser with that type of work in mind. That means that our code will need to use a document fragment, which can then +// be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback +// to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback. +// +// Essentially: +// ------------ +// - Do the VDOM work during the idlecallback +// - Do DOM work in the next requestAnimationFrame callback diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 51aba110b..ab3ea3ebf 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -113,6 +113,7 @@ impl WebsysDom { ); dioxus_html::set_event_converter(Box::new(WebEventConverter)); handler.forget(); + Self { document, root, diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index e106a9dc3..d92128676 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -20,41 +20,6 @@ //! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide //! validation of websys-specific features and not the general use of Dioxus. -// ## RequestAnimationFrame and RequestIdleCallback -// ------------------------------------------------ -// React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long -// running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the -// main thread. -// -// React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus, -// the diff phase is non-blocking, using "work_with_deadline" to allow the browser to process other events. When the diff phase -// is finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply. -// -// Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then -// setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame -// -// From Google's guide on rAF and rIC: -// ----------------------------------- -// -// If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed, -// which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside -// of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next -// frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout, -// which is a potential performance bottleneck. -// -// Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable, -// and as such we could easily go past the deadline the browser provided. -// -// The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the -// browser with that type of work in mind. That means that our code will need to use a document fragment, which can then -// be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback -// to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback. -// -// Essentially: -// ------------ -// - Do the VDOM work during the idlecallback -// - Do DOM work in the next requestAnimationFrame callback - use std::rc::Rc; pub use crate::cfg::Config; @@ -78,10 +43,6 @@ mod hot_reload; #[cfg(feature = "hydrate")] mod rehydrate; -// Currently disabled since it actually slows down immediate rendering -// todo: only schedule non-immediate renders through ric/raf -// mod ric_raf; -// mod rehydrate; /// Runs the app as a future that can be scheduled around the main thread. /// From bdacca0dfb03d394c21eddc3a6bef2ccf5cf2161 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 27 Feb 2024 12:55:47 -0800 Subject: [PATCH 04/41] use this. on the unified bindings --- packages/interpreter/src/unified_bindings.rs | 146 +++++++++---------- 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index 194a62c48..236262138 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -17,100 +17,99 @@ mod js { const JS: &str = r#""#; fn mount_to_root() { - "{AppendChildren(root, stack.length-1);}" + "{this.AppendChildren(this.root, this.stack.length-1);}" } fn push_root(root: u32) { - "{stack.push(nodes[$root$]);}" + "{this.stack.push(this.nodes[$root$]);}" } fn append_children(id: u32, many: u16) { - "{AppendChildren($id$, $many$);}" + "{this.AppendChildren($id$, $many$);}" } - fn pop_root() { - "{stack.pop();}" + "{this.stack.pop();}" } fn replace_with(id: u32, n: u16) { - "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" + "{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}" } fn insert_after(id: u32, n: u16) { - "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" + "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}" } fn insert_before(id: u32, n: u16) { - "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" + "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}" } fn remove(id: u32) { - "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}" + "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}" } fn create_raw_text(text: &str) { - "{stack.push(document.createTextNode($text$));}" + "{this.stack.push(document.createTextNode($text$));}" } fn create_text_node(text: &str, id: u32) { - "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}" + "{let node = document.createTextNode($text$); this.nodes[$id$] = node; this.stack.push(node);}" } fn create_placeholder(id: u32) { - "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}" + "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}" } fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { - r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"# + r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.listeners.create($event_name$, node, $bubbles$);"# } fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { - "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}" + "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}" } fn set_text(id: u32, text: &str) { - "{nodes[$id$].textContent = $text$;}" + "{this.nodes[$id$].textContent = $text$;}" } fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { - "{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}" + "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}" } fn remove_attribute(id: u32, field: &str, ns: &str) { r#"{ - node = nodes[$id$]; - if (!ns) { - switch (field) { - case "value": - node.value = ""; - break; - case "checked": - node.checked = false; - break; - case "selected": - node.selected = false; - break; - case "dangerous_inner_html": - node.innerHTML = ""; - break; - default: - node.removeAttribute(field); - break; - } - } else if (ns == "style") { - node.style.removeProperty(name); - } else { - node.removeAttributeNS(ns, field); + let node = this.nodes[$id$]; + if (!ns) { + switch (field) { + case "value": + node.value = ""; + break; + case "checked": + node.checked = false; + break; + case "selected": + node.selected = false; + break; + case "dangerous_inner_html": + node.innerHTML = ""; + break; + default: + node.removeAttribute(field); + break; } - }"# + } else if (ns == "style") { + node.style.removeProperty(name); + } else { + node.removeAttributeNS(ns, field); + } + }"# } fn assign_id(ptr: u32, len: u8, id: u32) { - "{nodes[$id$] = LoadChild($ptr$, $len$);}" + "{this.nodes[$id$] = this.LoadChild($ptr$, $len$);}" } fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) { r#"{ - node = LoadChild($ptr$, $len$); - if (node.nodeType == Node.TEXT_NODE) { - node.textContent = value; - } else { - let text = document.createTextNode(value); - node.replaceWith(text); - node = text; - } - nodes[$id$] = node; - }"# + let node = this.LoadChild($ptr$, $len$); + if (node.nodeType == node.TEXT_NODE) { + node.textContent = value; + } else { + let text = document.createTextNode(value); + node.replaceWith(text); + node = text; + } + this.nodes[$id$] = node; + }"# } fn replace_placeholder(ptr: u32, len: u8, n: u16) { - "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}" + "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...this.els);}" } fn load_template(tmpl_id: u16, index: u16, id: u32) { - "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" + "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}" } /* @@ -123,32 +122,32 @@ mod js { */ fn append_children_to_top(many: u16) { "{ - root = stack[stack.length-many-1]; - els = stack.splice(stack.length-many); - for (k = 0; k < many; k++) { - root.appendChild(els[k]); + let root = this.stack[this.stack.length-many-1]; + this.els = this.stack.splice(this.stack.length-many); + for (let k = 0; k < many; k++) { + root.appendChild(this.els[k]); } }" } fn set_top_attribute(field: &str, value: &str, ns: &str) { - "{setAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}" + "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}" } fn add_placeholder() { - "{node = document.createElement('pre'); node.hidden = true; stack.push(node);}" - } - fn create_element_ns(element: &'static str, ns: &'static str) { - "{stack.push(document.createElementNS($ns$, $element$))}" + "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node);}" } fn create_element(element: &'static str) { - "{stack.push(document.createElement($element$))}" + "{this.stack.push(document.createElement($element$))}" + } + fn create_element_ns(element: &'static str, ns: &'static str) { + "{this.stack.push(document.createElementNS($ns$, $element$))}" } fn add_templates(tmpl_id: u16, len: u16) { - "{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}" + "{this.templates[$tmpl_id$] = this.stack.splice(this.stack.length-$len$);}" } fn foreign_event_listener(event: &str, id: u32, bubbles: u8) { r#" bubbles = bubbles == 1; - node = nodes[id]; + let node = this.nodes[id]; if(node.listening){ node.listening += 1; } else { @@ -160,7 +159,7 @@ mod js { // if this is a mounted listener, we send the event immediately if (event_name === "mounted") { window.ipc.postMessage( - window.interpreter.serializeIpcMessage("user_event", { + this.serializeIpcMessage("user_event", { name: event_name, element: id, data: null, @@ -168,29 +167,28 @@ mod js { }) ); } else { - listeners.create(event_name, node, bubbles, (event) => { - handler(event, event_name, bubbles, config); + this.listeners.create(event_name, node, bubbles, (event) => { + this.handler(event, event_name, bubbles); }); }"# } fn assign_id_ref(array: &[u8], id: u32) { - "{nodes[$id$] = LoadChild($array$);}" + "{this.nodes[$id$] = this.LoadChild($array$);}" } fn hydrate_text_ref(array: &[u8], value: &str, id: u32) { r#"{ - node = LoadChild($array$); - if (node.nodeType == Node.TEXT_NODE) { + let node = this.LoadChild($array$); + if (node.nodeType == node.TEXT_NODE) { node.textContent = value; } else { let text = document.createTextNode(value); node.replaceWith(text); node = text; } - nodes[$id$] = node; + this.nodes[$id$] = node; }"# } - fn replace_placeholder_ref(array: &[u8], n: u16) { - "{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}" + "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...this.els);}" } } From 4b64894fc52e558ed61ee9917161893e2991d00b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 27 Feb 2024 16:33:18 -0800 Subject: [PATCH 05/41] Add proper bundling to tsc --- packages/interpreter/build.rs | 4 +- packages/interpreter/src/common.js | 79 --- packages/interpreter/src/common_exported.js | 79 --- packages/interpreter/src/gen/common.js | 81 --- packages/interpreter/src/gen/form.js | 1 - packages/interpreter/src/gen/interpreter.js | 0 packages/interpreter/src/gen/sledgehammer.js | 0 packages/interpreter/src/interpreter.js | 606 ------------------ packages/interpreter/src/ts/form.ts | 52 +- .../interpreter/src/ts/interpreter_core.ts | 21 +- .../interpreter/src/ts/interpreter_native.ts | 373 ++++++----- .../interpreter/src/ts/interpreter_web.ts | 23 +- packages/interpreter/src/ts/serialize.ts | 6 +- packages/interpreter/src/ts/set_attribute.ts | 2 + packages/interpreter/tsconfig.json | 5 +- 15 files changed, 267 insertions(+), 1065 deletions(-) delete mode 100644 packages/interpreter/src/common.js delete mode 100644 packages/interpreter/src/common_exported.js delete mode 100644 packages/interpreter/src/gen/common.js delete mode 100644 packages/interpreter/src/gen/form.js delete mode 100644 packages/interpreter/src/gen/interpreter.js delete mode 100644 packages/interpreter/src/gen/sledgehammer.js diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index ee15f7ac7..a211c37c2 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -23,7 +23,7 @@ fn main() { fn gen_bindings(name: &str) { let contents = read_to_string(&format!("src/{name}.ts")).unwrap(); let generated = read_to_string(&format!("src/gen/{name}.js")).unwrap_or_default(); - let hashed = hash_file(&contents); + let hashed = hash_it(&contents); // If the file is generated, and the hash is the same, we're good, don't do anything if generated @@ -58,7 +58,7 @@ fn gen_bindings(name: &str) { std::fs::write(&format!("src/gen/{name}.js"), generated).unwrap(); } -fn hash_file(obj: &str) -> u64 { +fn hash_it(obj: impl Hash) -> u64 { let mut hasher = DefaultHasher::new(); obj.hash(&mut hasher); hasher.finish() diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.js deleted file mode 100644 index 055353fa5..000000000 --- a/packages/interpreter/src/common.js +++ /dev/null @@ -1,79 +0,0 @@ -this.setAttributeInner = function (node, field, value, ns) { - const name = field; - if (ns === "style") { - // ????? why do we need to do this - if (node.style === undefined) { - node.style = {}; - } - node.style[name] = value; - } else if (!!ns) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (!truthy(value) && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } -} - -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, - webkitdirectory: true, -}; - -function truthy(val) { - return val === "true" || val === true; -} diff --git a/packages/interpreter/src/common_exported.js b/packages/interpreter/src/common_exported.js deleted file mode 100644 index 0c64085f9..000000000 --- a/packages/interpreter/src/common_exported.js +++ /dev/null @@ -1,79 +0,0 @@ -export function setAttributeInner(node, field, value, ns) { - const name = field; - if (ns === "style") { - // ????? why do we need to do this - if (node.style === undefined) { - node.style = {}; - } - node.style[name] = value; - } else if (!!ns) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (!truthy(value) && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } -} - -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, - webkitdirectory: true, -}; - -function truthy(val) { - return val === "true" || val === true; -} diff --git a/packages/interpreter/src/gen/common.js b/packages/interpreter/src/gen/common.js deleted file mode 100644 index 8f21059f3..000000000 --- a/packages/interpreter/src/gen/common.js +++ /dev/null @@ -1,81 +0,0 @@ -// DO NOT EDIT THIS FILE. HASH: 9578489549991746027 -export function setAttributeInner(node, field, value, ns) { - const name = field; - if (ns === "style") { - // ????? why do we need to do this - if (node.style === undefined) { - node.style = {}; - } - node.style[name] = value; - } - else if (!!ns) { - node.setAttributeNS(ns, name, value); - } - else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (!truthy(value) && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } - else { - node.setAttribute(name, value); - } - } - } -} -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, - webkitdirectory: true, -}; -function truthy(val) { - return val === "true" || val === true; -} diff --git a/packages/interpreter/src/gen/form.js b/packages/interpreter/src/gen/form.js deleted file mode 100644 index 0357a8736..000000000 --- a/packages/interpreter/src/gen/form.js +++ /dev/null @@ -1 +0,0 @@ -// DO NOT EDIT THIS FILE. HASH: 3476900567878811119 diff --git a/packages/interpreter/src/gen/interpreter.js b/packages/interpreter/src/gen/interpreter.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/interpreter/src/gen/sledgehammer.js b/packages/interpreter/src/gen/sledgehammer.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 5887828c7..c3d63316e 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -145,609 +145,3 @@ this.handler = async function (event, name, bubbles) { ); } } - -function find_real_id(target) { - let realId = null; - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } - // walk the tree to find the real element - while (realId == null) { - // we've reached the root we don't want to send an event - if (target.parentElement === null) { - return; - } - - target = target.parentElement; - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } - } - return realId; -} - -class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = null; - } - - create(event_name, element, bubbles, handler) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.root.addEventListener(event_name, handler); - } else { - this.global[event_name].active++; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.global[event_name].callback); - } - } - - removeAllNonBubbling(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } -} -this.LoadChild = function (array) { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1]; - - for (let i = 0; i < array.length; i++) { - this.end = array[i]; - for (node = node.firstChild; this.end > 0; this.end--) { - node = node.nextSibling; - } - } - return node; -} -this.listeners = new ListenerMap(); -this.nodes = []; -this.stack = []; -this.root; -this.templates = {}; -this.els = null; -this.end = null; - -this.AppendChildren = function (id, many) { - this.root = this.nodes[id]; - this.els = this.stack.splice(this.stack.length - many); - for (let k = 0; k < many; k++) { - this.root.appendChild(this.els[k]); - } -} - -this.initialize = function (root) { - this.nodes = [root]; - this.stack = [root]; - this.listeners.root = root; -} - -this.getClientRect = function (id) { - const node = this.nodes[id]; - if (!node) { - return; - } - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; -} - -this.scrollTo = function (id, behavior) { - const node = this.nodes[id]; - if (!node) { - return false; - } - node.scrollIntoView({ - behavior: behavior, - }); - return true; -} - -/// Set the focus on the element -this.setFocus = function (id, focus) { - const node = this.nodes[id]; - if (!node) { - return false; - } - if (focus) { - node.focus(); - } else { - node.blur(); - } - return true; -} - -function get_mouse_data(event) { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - offsetX, - offsetY, - pageX, - pageY, - screenX, - screenY, - shiftKey, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - offset_x: offsetX, - offset_y: offsetY, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; -} - -async function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { - charCode, - isComposing, - key, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, - location, - repeat, - which, - code, - } = event; - return { - char_code: charCode, - is_composing: isComposing, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - code, - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - return { - value: value, - values: {}, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - - if (target.type === "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - values: {}, - }; - } - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": { - let files = null; - if (event.dataTransfer && event.dataTransfer.files) { - files = await serializeFileList(event.dataTransfer.files); - } - - return { mouse: get_mouse_data(event), files }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "dblclick": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - return get_mouse_data(event); - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - pointerId, - width, - height, - pressure, - tangentialPressure, - tiltX, - tiltY, - twist, - pointerType, - isPrimary, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -this.serializeIpcMessage = function (method, params = {}) { - return JSON.stringify({ method, params }); -} - -function is_element_node(node) { - return node.nodeType == 1; -} - -function event_bubbles(event) { - switch (event) { - case "copy": - return true; - case "cut": - return true; - case "paste": - return true; - case "compositionend": - return true; - case "compositionstart": - return true; - case "compositionupdate": - return true; - case "keydown": - return true; - case "keypress": - return true; - case "keyup": - return true; - case "focus": - return false; - case "focusout": - return true; - case "focusin": - return true; - case "blur": - return false; - case "change": - return true; - case "input": - return true; - case "invalid": - return true; - case "reset": - return true; - case "submit": - return true; - case "click": - return true; - case "contextmenu": - return true; - case "doubleclick": - return true; - case "dblclick": - return true; - case "drag": - return true; - case "dragend": - return true; - case "dragenter": - return false; - case "dragexit": - return false; - case "dragleave": - return true; - case "dragover": - return true; - case "dragstart": - return true; - case "drop": - return true; - case "mousedown": - return true; - case "mouseenter": - return false; - case "mouseleave": - return false; - case "mousemove": - return true; - case "mouseout": - return true; - case "scroll": - return false; - case "mouseover": - return true; - case "mouseup": - return true; - case "pointerdown": - return true; - case "pointermove": - return true; - case "pointerup": - return true; - case "pointercancel": - return true; - case "gotpointercapture": - return true; - case "lostpointercapture": - return true; - case "pointerenter": - return false; - case "pointerleave": - return false; - case "pointerover": - return true; - case "pointerout": - return true; - case "select": - return true; - case "touchcancel": - return true; - case "touchend": - return true; - case "touchmove": - return true; - case "touchstart": - return true; - case "wheel": - return true; - case "abort": - return false; - case "canplay": - return false; - case "canplaythrough": - return false; - case "durationchange": - return false; - case "emptied": - return false; - case "encrypted": - return true; - case "ended": - return false; - case "error": - return false; - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "load": - return false; - case "pause": - return false; - case "play": - return false; - case "playing": - return false; - case "progress": - return false; - case "ratechange": - return false; - case "seeked": - return false; - case "seeking": - return false; - case "stalled": - return false; - case "suspend": - return false; - case "timeupdate": - return false; - case "volumechange": - return false; - case "waiting": - return false; - case "animationstart": - return true; - case "animationend": - return true; - case "animationiteration": - return true; - case "transitionend": - return true; - case "toggle": - return true; - case "mounted": - return false; - } - - return true; -} diff --git a/packages/interpreter/src/ts/form.ts b/packages/interpreter/src/ts/form.ts index c68723b09..7f4581309 100644 --- a/packages/interpreter/src/ts/form.ts +++ b/packages/interpreter/src/ts/form.ts @@ -1,3 +1,53 @@ -function retrieveFormValues() { +// Consistently deserialize forms and form elements for use across web/desktop/mobile +type FormValues = { [key: string]: FormDataEntryValue[] }; + +export function retriveValues(event: Event, target: HTMLElement): FormValues { + const contents: FormValues = {}; + + if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) { + retrieveFormValues(target, contents); + } + + if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) { + retriveInputsValues(target, contents); + } + + return contents; +} + +export function retrieveFormValues(form: HTMLFormElement, contents: FormValues) { + const formData = new FormData(form); + + for (let name in formData.keys()) { + let element = form.elements.namedItem(name); + + // todo: this is going to be a problem for select-multiple? + if (!(element instanceof HTMLInputElement)) { + continue; + } + + switch (element.type) { + case "select-multiple": + contents[name] = formData.getAll(name); + break; + + // By default, it's just a single value + default: + contents[name] = [formData.get(name)]; + break; + } + } +} + +export function retriveInputsValues(target: HTMLSelectElement, contents: FormValues,) { + const selectData = target.options; + contents["options"] = []; + + for (let i = 0; i < selectData.length; i++) { + let option = selectData[i]; + if (option.selected) { + contents["options"].push(option.value.toString()); + } + } } diff --git a/packages/interpreter/src/ts/interpreter_core.ts b/packages/interpreter/src/ts/interpreter_core.ts index d13d71f3d..557dab3e5 100644 --- a/packages/interpreter/src/ts/interpreter_core.ts +++ b/packages/interpreter/src/ts/interpreter_core.ts @@ -1,9 +1,6 @@ // The root interpreter class that holds state about the mapping between DOM and VirtualDom // This always lives in the JS side of things, and is extended by the native and web interpreters -// todo: we want to make these vars that the interpreter uses to be isolated so we can have multiple interpreters -export let m: DataView; // p, ls, d, t, op, i, e, z, metaflags; - export class Interpreter { // non bubbling events listen at the element the listener was created at global: { @@ -15,15 +12,15 @@ export class Interpreter { [key: string]: EventListener } }; - root: Element; + root: HTMLElement; handler: EventListener; - nodes: Element[]; - stack: Element[]; + nodes: Node[]; + stack: Node[]; templates: { - [key: string]: Element[] + [key: string]: Node[] }; - constructor(root: Element, handler: EventListener) { + constructor(root: HTMLElement, handler: EventListener) { this.root = root; this.nodes = [root]; this.stack = [root]; @@ -32,7 +29,7 @@ export class Interpreter { this.handler = handler; } - createListener(event_name: string, element: Element, bubbles: boolean) { + createListener(event_name: string, element: HTMLElement, bubbles: boolean) { if (bubbles) { if (this.global[event_name] === undefined) { this.global[event_name] = { active: 1, callback: this.handler }; @@ -49,7 +46,7 @@ export class Interpreter { } } - removeListener(element: Element, event_name: string, bubbles: boolean) { + removeListener(element: HTMLElement, event_name: string, bubbles: boolean) { if (bubbles) { this.removeBubblingListener(event_name); } else { @@ -65,7 +62,7 @@ export class Interpreter { } } - removeNonBubblingListener(element: Element, event_name: string) { + removeNonBubblingListener(element: HTMLElement, event_name: string) { const id = element.getAttribute("data-dioxus-id"); delete this.local[id][event_name]; if (Object.keys(this.local[id]).length === 0) { @@ -74,7 +71,7 @@ export class Interpreter { element.removeEventListener(event_name, this.handler); } - removeAllNonBubblingListeners(element: Element) { + removeAllNonBubblingListeners(element: HTMLElement) { const id = element.getAttribute("data-dioxus-id"); delete this.local[id]; } diff --git a/packages/interpreter/src/ts/interpreter_native.ts b/packages/interpreter/src/ts/interpreter_native.ts index 13bb84ee4..e37cc3427 100644 --- a/packages/interpreter/src/ts/interpreter_native.ts +++ b/packages/interpreter/src/ts/interpreter_native.ts @@ -1,14 +1,27 @@ // This file provides an extended variant of the interpreter used for desktop and liveview interaction // -// +// This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't +// provide since it doesn't have access to the dom. +import { retriveValues } from "./form"; import { Interpreter } from "./interpreter_core"; +import { SerializedEvent, serializeEvent } from "./serialize"; export class NativeInterpreter extends Interpreter { intercept_link_redirects: boolean; + ipc: any; - constructor(root: Element) { - super(root, (event) => handler(event, this, event.type, true)); + // eventually we want to remove liveview and build it into the server-side-events of fullstack + // however, for now we need to support it since SSE in fullstack doesn't exist yet + liveview: boolean; + + constructor(root: HTMLElement) { + super(root, (event) => this.handleEvent(event, event.type, true)); + this.intercept_link_redirects = true; + this.liveview = false; + + // @ts-ignore - wry gives us this + this.ipc = window.ipc; } serializeIpcMessage(method: string, params = {}) { @@ -17,50 +30,38 @@ export class NativeInterpreter extends Interpreter { scrollTo(id: number, behavior: ScrollBehavior) { const node = this.nodes[id]; - - if (!(node instanceof HTMLElement)) { - return false; + if (node instanceof HTMLElement) { + node.scrollIntoView({ behavior }); } - - node.scrollIntoView({ - behavior: behavior, - }); - - return true; } getClientRect(id: number) { const node = this.nodes[id]; - if (!node) { - return; + if (node instanceof HTMLElement) { + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [rect.x, rect.y], + size: [rect.width, rect.height], + }; } - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; } setFocus(id: number, focus: boolean) { const node = this.nodes[id]; - if (!(node instanceof HTMLElement)) { - return false; + if (node instanceof HTMLElement) { + if (focus) { + node.focus(); + } else { + node.blur(); + } } - - if (focus) { - node.focus(); - } else { - node.blur(); - } - - return true; } LoadChild(array: number[]) { // iterate through each number and get that child - let node = this.stack[this.stack.length - 1] as Node; + let node = this.stack[this.stack.length - 1]; for (let i = 0; i < array.length; i++) { let end = array[i]; @@ -80,73 +81,162 @@ export class NativeInterpreter extends Interpreter { root.appendChild(els[k]); } } -} + + handleEvent(event: Event, name: string, bubbles: boolean) { + const target = event.target!; + const realId = targetId(target)!; + const contents = serializeEvent(event); + + // Attempt to retrive the values from the form and inputs + if (target instanceof HTMLElement) { + contents.values = retriveValues(event, target); + } + + // Handle the event on the virtualdom and then preventDefault if it also preventsDefault + // Some listeners + let body = { + name: name, + data: contents, + element: realId, + bubbles, + }; + + // Run any prevent defaults the user might've set + // This is to support the prevent_default: "onclick" attribute that dioxus has had for a while, but is not necessary + // now that we expose preventDefault to the virtualdom on desktop + // Liveview will still need to use this + this.preventDefaults(event, target); + // liveview does not have syncronous event handling, so we need to send the event to the host + if (this.liveview) { + // Okay, so the user might've requested some files to be read + if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { + if (target.getAttribute("type") === "file") { + this.readFiles(target, contents, bubbles, realId, name); + } + } + } else { + // Run the event handler on the virtualdom + // capture/prevent default of the event if the virtualdom wants to + const res = handleVirtualdomEventSync(JSON.stringify(body)); -function handler(event: Event, interpreter: NativeInterpreter, name: string, bubbles: boolean) { - const target = event.target!; - const realId = target_id(target)!; - let contents = serializeEvent(event); + if (res.preventDefault) { + event.preventDefault(); + } - if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) { - const formData = new FormData(target); - - for (let name of formData.keys()) { - const fieldType = target.elements[name].type; - - switch (fieldType) { - case "select-multiple": - contents.values[name] = formData.getAll(name); - break; - - // add cases for fieldTypes that can hold multiple values here - default: - contents.values[name] = formData.get(name); - break; + if (res.stopPropagation) { + event.stopPropagation(); } } } - if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) { - const selectData = target.options; - contents.values["options"] = []; - for (let i = 0; i < selectData.length; i++) { - let option = selectData[i]; - if (option.selected) { - contents.values["options"].push(option.value.toString()); - } + async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: number, name: string) { + let files = target.files!; + let file_contents: { [name: string]: number[] } = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + + contents.files = { files: file_contents }; + + const message = this.serializeIpcMessage("user_event", { + name: name, + element: realId, + data: contents, + bubbles, + }); + + this.ipc.postMessage(message); + } + + + // This should: + // - prevent form submissions from navigating + // - prevent anchor tags from navigating + // - prevent buttons from submitting forms + // - let the virtualdom attempt to prevent the event + preventDefaults(event: Event, target: EventTarget) { + let preventDefaultRequests: string | null = null; + + // Some events can be triggered on text nodes, which don't have attributes + if (target instanceof Element) { + preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); + } + + if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { + event.preventDefault(); + } + + if (event.type === "submit") { + event.preventDefault(); + } + + // Attempt to intercept if the event is a click + if (target instanceof Element && event.type === "click") { + this.handleClickNavigate(event, target, preventDefaultRequests); } } - // If there's files to read - if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { - if (target.getAttribute("type") === "file") { - read_files(target, contents, bubbles, realId, name); + handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) { + // todo call prevent default if it's the right type of event + if (!this.intercept_link_redirects) { return; } - } - prevents_default(event, target); + // prevent buttons in forms from submitting the form + if (target.tagName === "BUTTON" && event.type == "submit") { + event.preventDefault(); + } + // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating + let a_element = target.closest("a"); + if (a_element == null) { + return; + } - // Handle the event on the virtualdom and then process whatever its output was - let body = { - name: name, - data: serializeEvent(event), - element: parseInt(realId), - bubbles, - }; - - if (waitForVirtualDomPreventDefault(JSON.stringify(body))) { event.preventDefault(); + + let elementShouldPreventDefault = + preventDefaultRequests && preventDefaultRequests.includes(`onclick`); + + let aElementShouldPreventDefault = a_element.getAttribute( + `dioxus-prevent-default` + ); + + let linkShouldPreventDefault = + aElementShouldPreventDefault && + aElementShouldPreventDefault.includes(`onclick`); + + if (!elementShouldPreventDefault && !linkShouldPreventDefault) { + const href = a_element.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + this.ipc.postMessage( + this.serializeIpcMessage("browser_open", { href }) + ); + } + } } } -export function waitForVirtualDomPreventDefault(contents: string): boolean { - +type EventSyncResult = { + preventDefault: boolean; + stopPropagation: boolean; + stopImmediatePropagation: boolean; + filesRequested: boolean; +}; +// This function sends the event to the virtualdom and then waits for the virtualdom to process it +// +// However, it's not really suitable for liveview, because it's synchronous and will block the main thread +// We should definitely consider using a websocket if we want to block... or just not block on liveview +// Liveview is a little bit of a tricky beast +function handleVirtualdomEventSync(contents: string): EventSyncResult { // Handle the event on the virtualdom and then process whatever its output was const xhr = new XMLHttpRequest(); @@ -157,130 +247,29 @@ export function waitForVirtualDomPreventDefault(contents: string): boolean { xhr.send(contents); // Deserialize the response, and then prevent the default/capture the event if the virtualdom wants to - return JSON.parse(xhr.responseText).preventDefault; + return JSON.parse(xhr.responseText); } - -async function read_files(target: HTMLInputElement, contents, bubbles, realId, name) { - let files = target.files!; - let file_contents: { [name: string]: number[] } = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - - contents.files = { files: file_contents }; - - if (realId === null) { - return; - } - - const message = window.interpreter.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }); - - window.ipc.postMessage(message); -} - - -export function target_id(target: EventTarget): string | null { - if (!(target instanceof Element)) { +function targetId(target: EventTarget): number | null { + // Ensure that the target is a node, sometimes it's nota + if (!(target instanceof Node)) { return null; } - function find_real_id(target: Element): string | null { - let realId = target.getAttribute(`data-dioxus-id`); + let ourTarget = target; + let realId = null; - // walk the tree to find the real element - while (realId == null) { - // we've reached the root we don't want to send an event - if (target.parentElement === null) { - return null; - } - - target = target.parentElement; - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } + while (realId == null) { + if (ourTarget === null) { + return null; } - return realId; - } - - return find_real_id(target); -} - - - -// This should: -// - prevent form submissions from navigating -// - prevent anchor tags from navigating -// - prevent buttons from submitting forms -// - let the virtualdom attempt to prevent the event -preventDefaults(event: Event, target: EventTarget) { - let preventDefaultRequests: string | null = null; - - // Some events can be triggered on text nodes, which don't have attributes - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); - } - - if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - // Attempt to intercept if the event is a click - if (target instanceof Element && event.type === "click") { - this.preventFormNavigate(event, target); - } -} - -preventFormNavigate(event: Event, target: Element) { - // todo call prevent default if it's the right type of event - if (!this.intercept_link_redirects) { - return; - } - - // prevent buttons in forms from submitting the form - if (target.tagName === "BUTTON") { // && event.type == "submit" - event.preventDefault(); - } - - // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating - let a_element = target.closest("a"); - if (a_element == null) { - return; - } - - event.preventDefault(); - - let elementShouldPreventDefault = - preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - - let aElementShouldPreventDefault = a_element.getAttribute( - `dioxus-prevent-default` - ); - - let linkShouldPreventDefault = - aElementShouldPreventDefault && - aElementShouldPreventDefault.includes(`onclick`); - - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - window.interpreter.serializeIpcMessage("browser_open", { href }) - ); + if (ourTarget instanceof Element) { + realId = ourTarget.getAttribute(`data-dioxus-id`); } + + ourTarget = ourTarget.parentNode; } + + return parseInt(realId); } diff --git a/packages/interpreter/src/ts/interpreter_web.ts b/packages/interpreter/src/ts/interpreter_web.ts index 6a16761e7..628a283f8 100644 --- a/packages/interpreter/src/ts/interpreter_web.ts +++ b/packages/interpreter/src/ts/interpreter_web.ts @@ -4,10 +4,12 @@ // // We're using sledgehammer directly -import { Interpreter, m } from "./interpreter_core"; +import { Interpreter } from "./interpreter_core"; export class WebInterpreter extends Interpreter { - constructor(root: Element, handler: EventListener) { + m: any; + + constructor(root: HTMLElement, handler: EventListener) { super(root, handler); } @@ -17,7 +19,7 @@ export class WebInterpreter extends Interpreter { let ptr_end = ptr + len; for (; ptr < ptr_end; ptr++) { - let end = m.getUint8(ptr); + let end = this.m.getUint8(ptr); for (node = node.firstChild; end > 0; end--) { node = node.nextSibling; } @@ -26,7 +28,7 @@ export class WebInterpreter extends Interpreter { return node; } - saveTemplate(nodes: Element[], tmpl_id: string) { + saveTemplate(nodes: HTMLElement[], tmpl_id: string) { this.templates[tmpl_id] = nodes; } @@ -34,7 +36,7 @@ export class WebInterpreter extends Interpreter { const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); for (let i = 0; i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i]; + const hydrateNode = hydrateNodes[i] as HTMLElement; const hydration = hydrateNode.getAttribute('data-node-hydration'); const split = hydration!.split(','); const id = ids[parseInt(split[0])]; @@ -54,22 +56,27 @@ export class WebInterpreter extends Interpreter { } } } + const treeWalker = document.createTreeWalker( document.body, NodeFilter.SHOW_COMMENT, ); + let currentNode = treeWalker.nextNode(); + while (currentNode) { const id = currentNode.textContent!; const split = id.split('node-id'); + if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling as Element; + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; } + currentNode = treeWalker.nextNode(); } } - getNode(id: number): Element { + getNode(id: number): Node { return this.nodes[id]; } @@ -77,7 +84,7 @@ export class WebInterpreter extends Interpreter { const root = this.nodes[id]; const els = this.stack.splice(this.stack.length - many); for (let k = 0; k < many; k++) { - this.root.appendChild(els[k]); + root.appendChild(els[k]); } } } diff --git a/packages/interpreter/src/ts/serialize.ts b/packages/interpreter/src/ts/serialize.ts index e7473155a..03f17434a 100644 --- a/packages/interpreter/src/ts/serialize.ts +++ b/packages/interpreter/src/ts/serialize.ts @@ -1,12 +1,12 @@ // Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {}; -type SerializedEvent = { - values?: { [key: string]: FormDataEntryValue[] } | FormDataEntryValue[]; +export type SerializedEvent = { + values?: { [key: string]: FormDataEntryValue[] }; value?: string; [key: string]: any; }; -function serializeEvent(event: Event): SerializedEvent { +export function serializeEvent(event: Event): SerializedEvent { // copy, cut, paste if (event instanceof KeyboardEvent) { diff --git a/packages/interpreter/src/ts/set_attribute.ts b/packages/interpreter/src/ts/set_attribute.ts index cd94f5507..5fcd3c91f 100644 --- a/packages/interpreter/src/ts/set_attribute.ts +++ b/packages/interpreter/src/ts/set_attribute.ts @@ -1,3 +1,5 @@ +// A unified interface for setting attributes on a node + // this function should try and stay fast, if possible export function setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { // we support a single namespace by default: style diff --git a/packages/interpreter/tsconfig.json b/packages/interpreter/tsconfig.json index 18ce8679c..0f5c66032 100644 --- a/packages/interpreter/tsconfig.json +++ b/packages/interpreter/tsconfig.json @@ -3,7 +3,10 @@ "module": "system", "lib": [ "ES2015", - "DOM" + "DOM", + "dom", + "dom.iterable", + "esnext" ], "noImplicitAny": true, "removeComments": true, From f447f01403a4b43065c10699d4e900f792781a34 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 27 Feb 2024 16:33:34 -0800 Subject: [PATCH 06/41] Commit js --- packages/interpreter/a.txt | 1 - packages/interpreter/build.rs | 70 +- packages/interpreter/gen/README.md | 3 - packages/interpreter/src/js/hash.txt | 1 + packages/interpreter/src/js/native.js | 681 ++++++++++++++++++ packages/interpreter/src/js/web.js | 159 ++++ packages/interpreter/src/lib.rs | 4 +- packages/interpreter/src/ts/.gitignore | 3 + .../interpreter/src/ts/interpreter_core.ts | 1 + packages/interpreter/tsconfig.json | 5 - packages/interpreter/tsconfig.native.json | 10 + packages/interpreter/tsconfig.web.json | 10 + 12 files changed, 903 insertions(+), 45 deletions(-) delete mode 100644 packages/interpreter/a.txt delete mode 100644 packages/interpreter/gen/README.md create mode 100644 packages/interpreter/src/js/hash.txt create mode 100644 packages/interpreter/src/js/native.js create mode 100644 packages/interpreter/src/js/web.js create mode 100644 packages/interpreter/src/ts/.gitignore create mode 100644 packages/interpreter/tsconfig.native.json create mode 100644 packages/interpreter/tsconfig.web.json diff --git a/packages/interpreter/a.txt b/packages/interpreter/a.txt deleted file mode 100644 index db9748690..000000000 --- a/packages/interpreter/a.txt +++ /dev/null @@ -1 +0,0 @@ -pub const SLEDGEHAMMER_JS: &str = "let m,p,ls,d,t,op,i,e,z,metaflags;\n const namespace = [];\n let namespace_cache_hit, namespace_cache_idx;\n function get_namespace() {\n namespace_cache_idx = u8buf[u8bufp++];\n if(namespace_cache_idx & 128){\n namespace_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n namespace[namespace_cache_idx&4294967167]=namespace_cache_hit;\n return namespace_cache_hit;\n }\n else{\n return namespace[namespace_cache_idx&4294967167];\n }\n }let u8buf,u8bufp;const attr = [];\n let attr_cache_hit, attr_cache_idx;\n function get_attr() {\n attr_cache_idx = u8buf[u8bufp++];\n if(attr_cache_idx & 128){\n attr_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n attr[attr_cache_idx&4294967167]=attr_cache_hit;\n return attr_cache_hit;\n }\n else{\n return attr[attr_cache_idx&4294967167];\n }\n }const evt = [];\n let evt_cache_hit, evt_cache_idx;\n function get_evt() {\n evt_cache_idx = u8buf[u8bufp++];\n if(evt_cache_idx & 128){\n evt_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n evt[evt_cache_idx&4294967167]=evt_cache_hit;\n return evt_cache_hit;\n }\n else{\n return evt[evt_cache_idx&4294967167];\n }\n }const el = [];\n let el_cache_hit, el_cache_idx;\n function get_el() {\n el_cache_idx = u8buf[u8bufp++];\n if(el_cache_idx & 128){\n el_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n el[el_cache_idx&4294967167]=el_cache_hit;\n return el_cache_hit;\n }\n else{\n return el[el_cache_idx&4294967167];\n }\n }let u32buf,u32bufp;let s = \"\";let lsp,sp,sl; let c = new TextDecoder();let u16buf,u16bufp;const ns_cache = [];\n let ns_cache_cache_hit, ns_cache_cache_idx;\n function get_ns_cache() {\n ns_cache_cache_idx = u8buf[u8bufp++];\n if(ns_cache_cache_idx & 128){\n ns_cache_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]);\n ns_cache[ns_cache_cache_idx&4294967167]=ns_cache_cache_hit;\n return ns_cache_cache_hit;\n }\n else{\n return ns_cache[ns_cache_cache_idx&4294967167];\n }\n }\n let field,ptr,array,ns,id,value,len,event,many,bubbles,event_name;\n export function create(r){\n d=r;\n }\n export function update_memory(b){\n m=new DataView(b.buffer)\n }\n export function run(){\n metaflags=m.getUint32(d,true);\n if((metaflags>>>6)&1){\n ls=m.getUint32(d+6*4,true);\n }\n p=ls;\n if ((metaflags>>>5)&1){\n t = m.getUint32(d+5*4,true);\n u8buf=new Uint8Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%1)/1);\n }\n u8bufp=0;if ((metaflags>>>3)&1){\n t = m.getUint32(d+3*4,true);\n u32buf=new Uint32Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%4)/4);\n }\n u32bufp=0;if (metaflags&1){\n lsp = m.getUint32(d+1*4,true);\n }\n if ((metaflags>>>2)&1) {\n sl = m.getUint32(d+2*4,true);\n if ((metaflags>>>1)&1) {\n sp = lsp;\n s = \"\";\n e = sp + ((sl / 4) | 0) * 4;\n while (sp < e) {\n t = m.getUint32(sp, true);\n s += String.fromCharCode(\n t & 255,\n (t & 65280) >> 8,\n (t & 16711680) >> 16,\n t >> 24\n );\n sp += 4;\n }\n while (sp < lsp + sl) {\n s += String.fromCharCode(m.getUint8(sp++));\n }\n } else {\n s = c.decode(new DataView(m.buffer, lsp, sl));\n }\n }\n sp=0;if ((metaflags>>>4)&1){\n t = m.getUint32(d+4*4,true);\n u16buf=new Uint16Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%2)/2);\n }\n u16bufp=0;\n for(;;){\n op=m.getUint32(p,true);\n p+=4;\n z=0;\n while(z++<4){\n switch(op&255){\n case 0:{AppendChildren(root, stack.length-1);}break;case 1:{stack.push(nodes[u32buf[u32bufp++]]);}break;case 2:{AppendChildren(u32buf[u32bufp++], u16buf[u16bufp++]);}break;case 3:{stack.pop();}break;case 4:{root = nodes[u32buf[u32bufp++]]; els = stack.splice(stack.length-u16buf[u16bufp++]); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}break;case 5:{nodes[u32buf[u32bufp++]].after(...stack.splice(stack.length-u16buf[u16bufp++]));}break;case 6:{nodes[u32buf[u32bufp++]].before(...stack.splice(stack.length-u16buf[u16bufp++]));}break;case 7:{node = nodes[u32buf[u32bufp++]]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}break;case 8:{stack.push(document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])));}break;case 9:{node = document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 10:{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[u32buf[u32bufp++]] = node;}break;case 11:event_name=get_evt();id=u32buf[u32bufp++];bubbles=u8buf[u8bufp++];node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `${id}`); listeners.create(event_name, node, bubbles);break;case 12:{node = nodes[u32buf[u32bufp++]]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, get_evt(), u8buf[u8bufp++]);}break;case 13:{nodes[u32buf[u32bufp++]].textContent = s.substring(sp,sp+=u32buf[u32bufp++]);}break;case 14:{node = nodes[u32buf[u32bufp++]]; setAttributeInner(node, get_attr(), s.substring(sp,sp+=u32buf[u32bufp++]), get_ns_cache());}break;case 15:id=u32buf[u32bufp++];field=get_attr();ns=get_ns_cache();{\n node = nodes[id];\n if (!ns) {\n switch (field) {\n case \"value\":\n node.value = \"\";\n break;\n case \"checked\":\n node.checked = false;\n break;\n case \"selected\":\n node.selected = false;\n break;\n case \"dangerous_inner_html\":\n node.innerHTML = \"\";\n break;\n default:\n node.removeAttribute(field);\n break;\n }\n } else if (ns == \"style\") {\n node.style.removeProperty(name);\n } else {\n node.removeAttributeNS(ns, field);\n }\n }break;case 16:{nodes[u32buf[u32bufp++]] = LoadChild(u32buf[u32bufp++], u8buf[u8bufp++]);}break;case 17:ptr=u32buf[u32bufp++];len=u8buf[u8bufp++];value=s.substring(sp,sp+=u32buf[u32bufp++]);id=u32buf[u32bufp++];{\n node = LoadChild(ptr, len);\n if (node.nodeType == Node.TEXT_NODE) {\n node.textContent = value;\n } else {\n let text = document.createTextNode(value);\n node.replaceWith(text);\n node = text;\n }\n nodes[id] = node;\n }break;case 18:{els = stack.splice(stack.length - u16buf[u16bufp++]); node = LoadChild(u32buf[u32bufp++], u8buf[u8bufp++]); node.replaceWith(...els);}break;case 19:{node = templates[u16buf[u16bufp++]][u16buf[u16bufp++]].cloneNode(true); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 20:many=u16buf[u16bufp++];{\n root = stack[stack.length-many-1];\n els = stack.splice(stack.length-many);\n for (k = 0; k < many; k++) {\n root.appendChild(els[k]);\n }\n }break;case 21:{setAttributeInner(stack[stack.length-1], get_attr(), s.substring(sp,sp+=u32buf[u32bufp++]), get_ns_cache());}break;case 22:{node = document.createElement('pre'); node.hidden = true; stack.push(node);}break;case 23:{stack.push(document.createElementNS(get_namespace(), get_el()))}break;case 24:{stack.push(document.createElement(get_el()))}break;case 25:{templates[u16buf[u16bufp++]] = stack.splice(stack.length-u16buf[u16bufp++]);}break;case 26:event=get_evt();id=u32buf[u32bufp++];bubbles=u8buf[u8bufp++];\n bubbles = bubbles == 1;\n node = nodes[id];\n if(node.listening){\n node.listening += 1;\n } else {\n node.listening = 1;\n }\n node.setAttribute('data-dioxus-id', `${id}`);\n const event_name = event;\n\n // if this is a mounted listener, we send the event immediately\n if (event_name === \"mounted\") {\n window.ipc.postMessage(\n window.interpreter.serializeIpcMessage(\"user_event\", {\n name: event_name,\n element: id,\n data: null,\n bubbles,\n })\n );\n } else {\n listeners.create(event_name, node, bubbles, (event) => {\n handler(event, event_name, bubbles, config);\n });\n }break;case 27:{nodes[u32buf[u32bufp++]] = LoadChild((()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})());}break;case 28:array=(()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})();value=s.substring(sp,sp+=u32buf[u32bufp++]);id=u32buf[u32bufp++];{\n node = LoadChild(array);\n if (node.nodeType == Node.TEXT_NODE) {\n node.textContent = value;\n } else {\n let text = document.createTextNode(value);\n node.replaceWith(text);\n node = text;\n }\n nodes[id] = node;\n }break;case 29:{els = stack.splice(stack.length - u16buf[u16bufp++]); node = LoadChild((()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})()); node.replaceWith(...els);}break;case 30:return true;\n }\n op>>>=8;\n }\n }\n }\n export function run_from_bytes(bytes){\n d = 0;\n update_memory(new Uint8Array(bytes))\n run()\n }" diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index a211c37c2..8066cec89 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -6,11 +6,41 @@ use std::{ fn main() { // If any TS changes, re-run the build script - println!("cargo:rerun-if-changed=src/*.ts"); + println!("cargo:rerun-if-changed=src/ts/*.ts"); - // for entry in ["common", "form", "interpreter"].iter() { - // gen_bindings(entry); - // } + // Compute the hash of the ts files + let hash = hash_dir("src/ts"); + + // If the hash matches the one on disk, we're good and don't need to update bindings + if let Ok(contents) = read_to_string("src/js/hash.txt") { + if contents.trim() == hash.to_string() { + return; + } + } + + // Otherwise, generate the bindings and write the new hash to disk + // Generate the bindings for both native and web + gen_bindings("native"); + gen_bindings("web"); + + std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); +} + +/// Hashes the contents of a directory +fn hash_dir(dir: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + + for entry in std::fs::read_dir(dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + let metadata = std::fs::metadata(&path).unwrap(); + if metadata.is_file() { + let contents = std::fs::read(&path).unwrap(); + contents.hash(&mut hasher); + } + } + + hasher.finish() } // okay...... so tsc might fail if the user doesn't have it installed @@ -21,27 +51,10 @@ fn main() { // if the hashes don't match, we need to fail the build // that way we also don't need fn gen_bindings(name: &str) { - let contents = read_to_string(&format!("src/{name}.ts")).unwrap(); - let generated = read_to_string(&format!("src/gen/{name}.js")).unwrap_or_default(); - let hashed = hash_it(&contents); - - // If the file is generated, and the hash is the same, we're good, don't do anything - if generated - .lines() - .next() - .unwrap_or_default() - .starts_with(&format!("// DO NOT EDIT THIS FILE. HASH: {}", hashed)) - { - return; - } - // If the file is generated, and the hash is different, we need to generate it let status = Command::new("tsc") - .arg(format!("src/{name}.ts")) - .arg("--outDir") - .arg("gen") - .arg("--target") - .arg("es6") + .arg("--p") + .arg(format!("tsconfig.{name}.json")) .status() .unwrap(); @@ -51,15 +64,4 @@ fn gen_bindings(name: &str) { name ); } - - // The file should exist, and now we need write the TS hash to the file - let generated = read_to_string(&format!("gen/{name}.js")).unwrap(); - let generated = format!("// DO NOT EDIT THIS FILE. HASH: {}\n{}", hashed, generated); - std::fs::write(&format!("src/gen/{name}.js"), generated).unwrap(); -} - -fn hash_it(obj: impl Hash) -> u64 { - let mut hasher = DefaultHasher::new(); - obj.hash(&mut hasher); - hasher.finish() } diff --git a/packages/interpreter/gen/README.md b/packages/interpreter/gen/README.md deleted file mode 100644 index e6d41cee9..000000000 --- a/packages/interpreter/gen/README.md +++ /dev/null @@ -1,3 +0,0 @@ -temporary generated code directory since tsc doesn't have a crossplatform way of generating typescript to stdout - -https://github.com/microsoft/TypeScript/issues/1226#issuecomment-523544134 diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt new file mode 100644 index 000000000..e3bf98bdd --- /dev/null +++ b/packages/interpreter/src/js/hash.txt @@ -0,0 +1 @@ +16803007677292896021 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js new file mode 100644 index 000000000..0c59cc667 --- /dev/null +++ b/packages/interpreter/src/js/native.js @@ -0,0 +1,681 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +System.register("form", [], function (exports_1, context_1) { + "use strict"; + var __moduleName = context_1 && context_1.id; + function retriveValues(event, target) { + var contents = {}; + if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) { + retrieveFormValues(target, contents); + } + if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) { + retriveInputsValues(target, contents); + } + return contents; + } + exports_1("retriveValues", retriveValues); + function retrieveFormValues(form, contents) { + var formData = new FormData(form); + for (var name_1 in formData.keys()) { + var element = form.elements.namedItem(name_1); + if (!(element instanceof HTMLInputElement)) { + continue; + } + switch (element.type) { + case "select-multiple": + contents[name_1] = formData.getAll(name_1); + break; + default: + contents[name_1] = [formData.get(name_1)]; + break; + } + } + } + exports_1("retrieveFormValues", retrieveFormValues); + function retriveInputsValues(target, contents) { + var selectData = target.options; + contents["options"] = []; + for (var i = 0; i < selectData.length; i++) { + var option = selectData[i]; + if (option.selected) { + contents["options"].push(option.value.toString()); + } + } + } + exports_1("retriveInputsValues", retriveInputsValues); + return { + setters: [], + execute: function () { + } + }; +}); +System.register("interpreter_core", [], function (exports_2, context_2) { + "use strict"; + var Interpreter; + var __moduleName = context_2 && context_2.id; + return { + setters: [], + execute: function () { + Interpreter = (function () { + function Interpreter(root, handler) { + this.root = root; + this.nodes = [root]; + this.stack = [root]; + this.global = {}; + this.local = {}; + this.handler = handler; + } + Interpreter.prototype.createListener = function (event_name, element, bubbles) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = { active: 1, callback: this.handler }; + this.root.addEventListener(event_name, this.handler); + } + else { + this.global[event_name].active++; + } + } + else { + var id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, this.handler); + } + }; + Interpreter.prototype.removeListener = function (element, event_name, bubbles) { + if (bubbles) { + this.removeBubblingListener(event_name); + } + else { + this.removeNonBubblingListener(element, event_name); + } + }; + Interpreter.prototype.removeBubblingListener = function (event_name) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + }; + Interpreter.prototype.removeNonBubblingListener = function (element, event_name) { + var id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (Object.keys(this.local[id]).length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + }; + Interpreter.prototype.removeAllNonBubblingListeners = function (element) { + var id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + }; + return Interpreter; + }()); + exports_2("Interpreter", Interpreter); + } + }; +}); +System.register("serialize", [], function (exports_3, context_3) { + "use strict"; + var __moduleName = context_3 && context_3.id; + function serializeEvent(event) { + if (event instanceof KeyboardEvent) { + return { + char_code: event.charCode, + is_composing: event.isComposing, + key: event.key, + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + key_code: event.keyCode, + shift_key: event.shiftKey, + location: event.location, + repeat: event.repeat, + which: event.which, + code: event.code, + }; + } + if (event instanceof PointerEvent) { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + pointer_id: event.pointerId, + width: event.width, + height: event.height, + pressure: event.pressure, + tangential_pressure: event.tangentialPressure, + tilt_x: event.tiltX, + tilt_y: event.tiltY, + twist: event.twist, + pointer_type: event.pointerType, + is_primary: event.isPrimary, + }; + } + if (event instanceof MouseEvent) { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + offset_x: event.offsetX, + offset_y: event.offsetY, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + }; + } + if (event instanceof TouchEvent) { + return { + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + shift_key: event.shiftKey, + changed_touches: event.changedTouches, + target_touches: event.targetTouches, + touches: event.touches, + }; + } + if (event instanceof WheelEvent) { + return { + delta_x: event.deltaX, + delta_y: event.deltaY, + delta_z: event.deltaZ, + delta_mode: event.deltaMode, + }; + } + if (event instanceof AnimationEvent) { + return { + animation_name: event.animationName, + elapsed_time: event.elapsedTime, + pseudo_element: event.pseudoElement, + }; + } + if (event instanceof TransitionEvent) { + return { + property_name: event.propertyName, + elapsed_time: event.elapsedTime, + pseudo_element: event.pseudoElement, + }; + } + if (event instanceof ClipboardEvent) { + return {}; + } + if (event instanceof CompositionEvent) { + return { + data: event.data, + }; + } + if (event instanceof DragEvent) { + return { + mouse: { + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + shift_key: event.shiftKey, + }, + files: [], + }; + } + if (event instanceof FocusEvent) { + return {}; + } + return {}; + } + exports_3("serializeEvent", serializeEvent); + function isElementNode(node) { + return node.nodeType == 1; + } + function eventBubbles(eventName) { + switch (eventName) { + case "copy": + return true; + case "cut": + return true; + case "paste": + return true; + case "compositionend": + return true; + case "compositionstart": + return true; + case "compositionupdate": + return true; + case "keydown": + return true; + case "keypress": + return true; + case "keyup": + return true; + case "focus": + return false; + case "focusout": + return true; + case "focusin": + return true; + case "blur": + return false; + case "change": + return true; + case "input": + return true; + case "invalid": + return true; + case "reset": + return true; + case "submit": + return true; + case "click": + return true; + case "contextmenu": + return true; + case "doubleclick": + return true; + case "dblclick": + return true; + case "drag": + return true; + case "dragend": + return true; + case "dragenter": + return false; + case "dragexit": + return false; + case "dragleave": + return true; + case "dragover": + return true; + case "dragstart": + return true; + case "drop": + return true; + case "mousedown": + return true; + case "mouseenter": + return false; + case "mouseleave": + return false; + case "mousemove": + return true; + case "mouseout": + return true; + case "scroll": + return false; + case "mouseover": + return true; + case "mouseup": + return true; + case "pointerdown": + return true; + case "pointermove": + return true; + case "pointerup": + return true; + case "pointercancel": + return true; + case "gotpointercapture": + return true; + case "lostpointercapture": + return true; + case "pointerenter": + return false; + case "pointerleave": + return false; + case "pointerover": + return true; + case "pointerout": + return true; + case "select": + return true; + case "touchcancel": + return true; + case "touchend": + return true; + case "touchmove": + return true; + case "touchstart": + return true; + case "wheel": + return true; + case "abort": + return false; + case "canplay": + return false; + case "canplaythrough": + return false; + case "durationchange": + return false; + case "emptied": + return false; + case "encrypted": + return true; + case "ended": + return false; + case "error": + return false; + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "load": + return false; + case "pause": + return false; + case "play": + return false; + case "playing": + return false; + case "progress": + return false; + case "ratechange": + return false; + case "seeked": + return false; + case "seeking": + return false; + case "stalled": + return false; + case "suspend": + return false; + case "timeupdate": + return false; + case "volumechange": + return false; + case "waiting": + return false; + case "animationstart": + return true; + case "animationend": + return true; + case "animationiteration": + return true; + case "transitionend": + return true; + case "toggle": + return true; + case "mounted": + return false; + } + return true; + } + return { + setters: [], + execute: function () { + } + }; +}); +System.register("interpreter_native", ["form", "interpreter_core", "serialize"], function (exports_4, context_4) { + "use strict"; + var form_1, interpreter_core_1, serialize_1, NativeInterpreter; + var __moduleName = context_4 && context_4.id; + function handleVirtualdomEventSync(contents) { + var xhr = new XMLHttpRequest(); + xhr.timeout = 1000; + xhr.open("GET", "/handle/event.please", false); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(contents); + return JSON.parse(xhr.responseText); + } + function targetId(target) { + if (!(target instanceof Node)) { + return null; + } + var ourTarget = target; + var realId = null; + while (realId == null) { + if (ourTarget === null) { + return null; + } + if (ourTarget instanceof Element) { + realId = ourTarget.getAttribute("data-dioxus-id"); + } + ourTarget = ourTarget.parentNode; + } + return parseInt(realId); + } + return { + setters: [ + function (form_1_1) { + form_1 = form_1_1; + }, + function (interpreter_core_1_1) { + interpreter_core_1 = interpreter_core_1_1; + }, + function (serialize_1_1) { + serialize_1 = serialize_1_1; + } + ], + execute: function () { + NativeInterpreter = (function (_super) { + __extends(NativeInterpreter, _super); + function NativeInterpreter(root) { + var _this = _super.call(this, root, function (event) { return _this.handleEvent(event, event.type, true); }) || this; + _this.intercept_link_redirects = true; + _this.liveview = false; + _this.ipc = window.ipc; + return _this; + } + NativeInterpreter.prototype.serializeIpcMessage = function (method, params) { + if (params === void 0) { params = {}; } + return JSON.stringify({ method: method, params: params }); + }; + NativeInterpreter.prototype.scrollTo = function (id, behavior) { + var node = this.nodes[id]; + if (node instanceof HTMLElement) { + node.scrollIntoView({ behavior: behavior }); + } + }; + NativeInterpreter.prototype.getClientRect = function (id) { + var node = this.nodes[id]; + if (node instanceof HTMLElement) { + var rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [rect.x, rect.y], + size: [rect.width, rect.height], + }; + } + }; + NativeInterpreter.prototype.setFocus = function (id, focus) { + var node = this.nodes[id]; + if (node instanceof HTMLElement) { + if (focus) { + node.focus(); + } + else { + node.blur(); + } + } + }; + NativeInterpreter.prototype.LoadChild = function (array) { + var node = this.stack[this.stack.length - 1]; + for (var i = 0; i < array.length; i++) { + var end = array[i]; + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + return node; + }; + NativeInterpreter.prototype.AppendChildren = function (id, many) { + var root = this.nodes[id]; + var els = this.stack.splice(this.stack.length - many); + for (var k = 0; k < many; k++) { + root.appendChild(els[k]); + } + }; + NativeInterpreter.prototype.handleEvent = function (event, name, bubbles) { + var target = event.target; + var realId = targetId(target); + var contents = serialize_1.serializeEvent(event); + if (target instanceof HTMLElement) { + contents.values = form_1.retriveValues(event, target); + } + var body = { + name: name, + data: contents, + element: realId, + bubbles: bubbles, + }; + this.preventDefaults(event, target); + if (this.liveview) { + if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { + if (target.getAttribute("type") === "file") { + this.readFiles(target, contents, bubbles, realId, name); + } + } + } + else { + var res = handleVirtualdomEventSync(JSON.stringify(body)); + if (res.preventDefault) { + event.preventDefault(); + } + if (res.stopPropagation) { + event.stopPropagation(); + } + } + }; + NativeInterpreter.prototype.readFiles = function (target, contents, bubbles, realId, name) { + return __awaiter(this, void 0, void 0, function () { + var files, file_contents, i, file, _a, _b, _c, _d, _e, message; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + files = target.files; + file_contents = {}; + i = 0; + _f.label = 1; + case 1: + if (!(i < files.length)) return [3, 4]; + file = files[i]; + _a = file_contents; + _b = file.name; + _d = (_c = Array).from; + _e = Uint8Array.bind; + return [4, file.arrayBuffer()]; + case 2: + _a[_b] = _d.apply(_c, [new (_e.apply(Uint8Array, [void 0, _f.sent()]))()]); + _f.label = 3; + case 3: + i++; + return [3, 1]; + case 4: + contents.files = { files: file_contents }; + message = this.serializeIpcMessage("user_event", { + name: name, + element: realId, + data: contents, + bubbles: bubbles, + }); + this.ipc.postMessage(message); + return [2]; + } + }); + }); + }; + NativeInterpreter.prototype.preventDefaults = function (event, target) { + var preventDefaultRequests = null; + if (target instanceof Element) { + preventDefaultRequests = target.getAttribute("dioxus-prevent-default"); + } + if (preventDefaultRequests && preventDefaultRequests.includes("on".concat(event.type))) { + event.preventDefault(); + } + if (event.type === "submit") { + event.preventDefault(); + } + if (target instanceof Element && event.type === "click") { + this.handleClickNavigate(event, target, preventDefaultRequests); + } + }; + NativeInterpreter.prototype.handleClickNavigate = function (event, target, preventDefaultRequests) { + if (!this.intercept_link_redirects) { + return; + } + if (target.tagName === "BUTTON" && event.type == "submit") { + event.preventDefault(); + } + var a_element = target.closest("a"); + if (a_element == null) { + return; + } + event.preventDefault(); + var elementShouldPreventDefault = preventDefaultRequests && preventDefaultRequests.includes("onclick"); + var aElementShouldPreventDefault = a_element.getAttribute("dioxus-prevent-default"); + var linkShouldPreventDefault = aElementShouldPreventDefault && + aElementShouldPreventDefault.includes("onclick"); + if (!elementShouldPreventDefault && !linkShouldPreventDefault) { + var href = a_element.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + this.ipc.postMessage(this.serializeIpcMessage("browser_open", { href: href })); + } + } + }; + return NativeInterpreter; + }(interpreter_core_1.Interpreter)); + exports_4("NativeInterpreter", NativeInterpreter); + } + }; +}); diff --git a/packages/interpreter/src/js/web.js b/packages/interpreter/src/js/web.js new file mode 100644 index 000000000..bb4a724d4 --- /dev/null +++ b/packages/interpreter/src/js/web.js @@ -0,0 +1,159 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +System.register("interpreter_core", [], function (exports_1, context_1) { + "use strict"; + var Interpreter; + var __moduleName = context_1 && context_1.id; + return { + setters: [], + execute: function () { + Interpreter = (function () { + function Interpreter(root, handler) { + this.root = root; + this.nodes = [root]; + this.stack = [root]; + this.global = {}; + this.local = {}; + this.handler = handler; + } + Interpreter.prototype.createListener = function (event_name, element, bubbles) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = { active: 1, callback: this.handler }; + this.root.addEventListener(event_name, this.handler); + } + else { + this.global[event_name].active++; + } + } + else { + var id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, this.handler); + } + }; + Interpreter.prototype.removeListener = function (element, event_name, bubbles) { + if (bubbles) { + this.removeBubblingListener(event_name); + } + else { + this.removeNonBubblingListener(element, event_name); + } + }; + Interpreter.prototype.removeBubblingListener = function (event_name) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + }; + Interpreter.prototype.removeNonBubblingListener = function (element, event_name) { + var id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (Object.keys(this.local[id]).length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + }; + Interpreter.prototype.removeAllNonBubblingListeners = function (element) { + var id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + }; + return Interpreter; + }()); + exports_1("Interpreter", Interpreter); + } + }; +}); +System.register("interpreter_web", ["interpreter_core"], function (exports_2, context_2) { + "use strict"; + var interpreter_core_1, WebInterpreter; + var __moduleName = context_2 && context_2.id; + return { + setters: [ + function (interpreter_core_1_1) { + interpreter_core_1 = interpreter_core_1_1; + } + ], + execute: function () { + WebInterpreter = (function (_super) { + __extends(WebInterpreter, _super); + function WebInterpreter(root, handler) { + return _super.call(this, root, handler) || this; + } + WebInterpreter.prototype.LoadChild = function (ptr, len) { + var node = this.stack[this.stack.length - 1]; + var ptr_end = ptr + len; + for (; ptr < ptr_end; ptr++) { + var end = this.m.getUint8(ptr); + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + return node; + }; + WebInterpreter.prototype.saveTemplate = function (nodes, tmpl_id) { + this.templates[tmpl_id] = nodes; + }; + WebInterpreter.prototype.hydrateRoot = function (ids) { + var hydrateNodes = document.querySelectorAll('[data-node-hydration]'); + for (var i = 0; i < hydrateNodes.length; i++) { + var hydrateNode = hydrateNodes[i]; + var hydration = hydrateNode.getAttribute('data-node-hydration'); + var split = hydration.split(','); + var id = ids[parseInt(split[0])]; + this.nodes[id] = hydrateNode; + if (split.length > 1) { + hydrateNode.listening = split.length - 1; + hydrateNode.setAttribute('data-dioxus-id', id.toString()); + for (var j = 1; j < split.length; j++) { + var listener = split[j]; + var split2 = listener.split(':'); + var event_name = split2[0]; + var bubbles = split2[1] === '1'; + this.createListener(event_name, hydrateNode, bubbles); + } + } + } + var treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT); + var currentNode = treeWalker.nextNode(); + while (currentNode) { + var id = currentNode.textContent; + var split = id.split('node-id'); + if (split.length > 1) { + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; + } + currentNode = treeWalker.nextNode(); + } + }; + WebInterpreter.prototype.getNode = function (id) { + return this.nodes[id]; + }; + WebInterpreter.prototype.appendChildren = function (id, many) { + var root = this.nodes[id]; + var els = this.stack.splice(this.stack.length - many); + for (var k = 0; k < many; k++) { + root.appendChild(els[k]); + } + }; + return WebInterpreter; + }(interpreter_core_1.Interpreter)); + exports_2("WebInterpreter", WebInterpreter); + } + }; +}); diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 685e51af3..b50f2dfeb 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -2,8 +2,8 @@ #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] -pub static INTERPRETER_JS: &str = include_str!("./gen/interpreter.js"); -pub static COMMON_JS: &str = include_str!("./gen/common.js"); +pub static INTERPRETER_JS: &str = include_str!("./js/native.js"); +pub static COMMON_JS: &str = include_str!("./js/web.js"); #[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))] mod write_native_mutations; diff --git a/packages/interpreter/src/ts/.gitignore b/packages/interpreter/src/ts/.gitignore new file mode 100644 index 000000000..68b79b0d3 --- /dev/null +++ b/packages/interpreter/src/ts/.gitignore @@ -0,0 +1,3 @@ +# please dont accidentally run tsc and commit your js in this dir. +*.js + diff --git a/packages/interpreter/src/ts/interpreter_core.ts b/packages/interpreter/src/ts/interpreter_core.ts index 557dab3e5..57ec4c5b8 100644 --- a/packages/interpreter/src/ts/interpreter_core.ts +++ b/packages/interpreter/src/ts/interpreter_core.ts @@ -12,6 +12,7 @@ export class Interpreter { [key: string]: EventListener } }; + root: HTMLElement; handler: EventListener; nodes: Node[]; diff --git a/packages/interpreter/tsconfig.json b/packages/interpreter/tsconfig.json index 0f5c66032..fe0cdc8ab 100644 --- a/packages/interpreter/tsconfig.json +++ b/packages/interpreter/tsconfig.json @@ -11,12 +11,7 @@ "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, - "outFile": "gen/main.js", - "sourceMap": true }, - "include": [ - "src/ts/*.ts" - ], "exclude": [ "**/*.spec.ts" ] diff --git a/packages/interpreter/tsconfig.native.json b/packages/interpreter/tsconfig.native.json new file mode 100644 index 000000000..57feec085 --- /dev/null +++ b/packages/interpreter/tsconfig.native.json @@ -0,0 +1,10 @@ +{ + // extends the base + "extends": "./tsconfig.json", + "compilerOptions": { + "outFile": "src/js/native.js" + }, + "files": [ + "src/ts/interpreter_native.ts" + ], +} diff --git a/packages/interpreter/tsconfig.web.json b/packages/interpreter/tsconfig.web.json new file mode 100644 index 000000000..ac1961b32 --- /dev/null +++ b/packages/interpreter/tsconfig.web.json @@ -0,0 +1,10 @@ +{ + // extends the base + "extends": "./tsconfig.json", + "compilerOptions": { + "outFile": "src/js/web.js" + }, + "files": [ + "src/ts/interpreter_web.ts" + ], +} From 774fba85e4a87a62f002f068c3d4d6e07bb3c10d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 27 Feb 2024 16:36:25 -0800 Subject: [PATCH 07/41] track the configs too --- packages/interpreter/build.rs | 1 + packages/interpreter/src/js/README.md | 1 + packages/interpreter/tsconfig.json | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 packages/interpreter/src/js/README.md diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 8066cec89..236467eaf 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -7,6 +7,7 @@ use std::{ fn main() { // If any TS changes, re-run the build script println!("cargo:rerun-if-changed=src/ts/*.ts"); + println!("cargo:rerun-if-changed=*.json"); // Compute the hash of the ts files let hash = hash_dir("src/ts"); diff --git a/packages/interpreter/src/js/README.md b/packages/interpreter/src/js/README.md new file mode 100644 index 000000000..851ef6751 --- /dev/null +++ b/packages/interpreter/src/js/README.md @@ -0,0 +1 @@ +this files are generated - do not edit them! diff --git a/packages/interpreter/tsconfig.json b/packages/interpreter/tsconfig.json index fe0cdc8ab..0207f8ca7 100644 --- a/packages/interpreter/tsconfig.json +++ b/packages/interpreter/tsconfig.json @@ -5,8 +5,7 @@ "ES2015", "DOM", "dom", - "dom.iterable", - "esnext" + "dom.iterable" ], "noImplicitAny": true, "removeComments": true, From 925d0617dab119ad69054a8062b546e143cba0e3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 27 Feb 2024 16:39:40 -0800 Subject: [PATCH 08/41] add glue to webonly interpreter --- packages/interpreter/src/unified_bindings.rs | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index 236262138..ce3cf2866 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -8,6 +8,31 @@ use sledgehammer_bindgen::bindgen; pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; +#[cfg(feature = "webonly")] +#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#" +export function save_template(channel, nodes, tmpl_id) { + channel.save_template(nodes, tmpl_id); +} +export function hydrate(channel, ids) { + channel.hydrate(ids); +} +export function get_node(channel, id) { + return channel.get_node(id); +} +export function initialize(channel, root, handler) { + channel.initialize(root, handler); +} +"#)] +extern "C" { + pub fn save_template(channel: &JSChannel, nodes: Vec, tmpl_id: u16); + + pub fn hydrate(channel: &JSChannel, ids: Vec); + + pub fn get_node(channel: &JSChannel, id: u32) -> Node; + + pub fn initialize(channel: &JSChannel, root: Node, handler: &Function); +} + #[bindgen(module)] mod js { // Load in the JavaScript file with all the imports From f196514b4f5271ad8ae43618d104ed355b7a6dd0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 27 Feb 2024 19:23:36 -0800 Subject: [PATCH 09/41] change serialization --- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 224 ++---------- packages/interpreter/src/ts/serialize.ts | 411 +++-------------------- 3 files changed, 72 insertions(+), 565 deletions(-) diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index e3bf98bdd..d29edbd8e 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -16803007677292896021 \ No newline at end of file +12494364440840058490 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 0c59cc667..883285eda 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -169,6 +169,21 @@ System.register("serialize", [], function (exports_3, context_3) { "use strict"; var __moduleName = context_3 && context_3.id; function serializeEvent(event) { + var _a; + if (event instanceof InputEvent) { + if (event.target instanceof HTMLInputElement) { + var target = event.target; + var value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; + if (target.type === "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + values: {}, + }; + } + return {}; + } if (event instanceof KeyboardEvent) { return { char_code: event.charCode, @@ -185,6 +200,24 @@ System.register("serialize", [], function (exports_3, context_3) { code: event.code, }; } + if (event instanceof MouseEvent) { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + offset_x: event.offsetX, + offset_y: event.offsetY, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + }; + } if (event instanceof PointerEvent) { return { alt_key: event.altKey, @@ -211,24 +244,6 @@ System.register("serialize", [], function (exports_3, context_3) { is_primary: event.isPrimary, }; } - if (event instanceof MouseEvent) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - offset_x: event.offsetX, - offset_y: event.offsetY, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - }; - } if (event instanceof TouchEvent) { return { alt_key: event.altKey, @@ -287,179 +302,6 @@ System.register("serialize", [], function (exports_3, context_3) { return {}; } exports_3("serializeEvent", serializeEvent); - function isElementNode(node) { - return node.nodeType == 1; - } - function eventBubbles(eventName) { - switch (eventName) { - case "copy": - return true; - case "cut": - return true; - case "paste": - return true; - case "compositionend": - return true; - case "compositionstart": - return true; - case "compositionupdate": - return true; - case "keydown": - return true; - case "keypress": - return true; - case "keyup": - return true; - case "focus": - return false; - case "focusout": - return true; - case "focusin": - return true; - case "blur": - return false; - case "change": - return true; - case "input": - return true; - case "invalid": - return true; - case "reset": - return true; - case "submit": - return true; - case "click": - return true; - case "contextmenu": - return true; - case "doubleclick": - return true; - case "dblclick": - return true; - case "drag": - return true; - case "dragend": - return true; - case "dragenter": - return false; - case "dragexit": - return false; - case "dragleave": - return true; - case "dragover": - return true; - case "dragstart": - return true; - case "drop": - return true; - case "mousedown": - return true; - case "mouseenter": - return false; - case "mouseleave": - return false; - case "mousemove": - return true; - case "mouseout": - return true; - case "scroll": - return false; - case "mouseover": - return true; - case "mouseup": - return true; - case "pointerdown": - return true; - case "pointermove": - return true; - case "pointerup": - return true; - case "pointercancel": - return true; - case "gotpointercapture": - return true; - case "lostpointercapture": - return true; - case "pointerenter": - return false; - case "pointerleave": - return false; - case "pointerover": - return true; - case "pointerout": - return true; - case "select": - return true; - case "touchcancel": - return true; - case "touchend": - return true; - case "touchmove": - return true; - case "touchstart": - return true; - case "wheel": - return true; - case "abort": - return false; - case "canplay": - return false; - case "canplaythrough": - return false; - case "durationchange": - return false; - case "emptied": - return false; - case "encrypted": - return true; - case "ended": - return false; - case "error": - return false; - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "load": - return false; - case "pause": - return false; - case "play": - return false; - case "playing": - return false; - case "progress": - return false; - case "ratechange": - return false; - case "seeked": - return false; - case "seeking": - return false; - case "stalled": - return false; - case "suspend": - return false; - case "timeupdate": - return false; - case "volumechange": - return false; - case "waiting": - return false; - case "animationstart": - return true; - case "animationend": - return true; - case "animationiteration": - return true; - case "transitionend": - return true; - case "toggle": - return true; - case "mounted": - return false; - } - return true; - } return { setters: [], execute: function () { diff --git a/packages/interpreter/src/ts/serialize.ts b/packages/interpreter/src/ts/serialize.ts index 03f17434a..1cfd5990c 100644 --- a/packages/interpreter/src/ts/serialize.ts +++ b/packages/interpreter/src/ts/serialize.ts @@ -7,8 +7,21 @@ export type SerializedEvent = { }; export function serializeEvent(event: Event): SerializedEvent { + if (event instanceof InputEvent) { + if (event.target instanceof HTMLInputElement) { + let target = event.target; + let value = target.value ?? target.textContent; + if (target.type === "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + values: {}, + }; + } + return {}; + } - // copy, cut, paste if (event instanceof KeyboardEvent) { return { char_code: event.charCode, @@ -26,9 +39,25 @@ export function serializeEvent(event: Event): SerializedEvent { }; } + if (event instanceof MouseEvent) { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + offset_x: event.offsetX, + offset_y: event.offsetY, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + }; + } - // "pointerdown" "pointermove" "pointerup" "pointercancel" "gotpointercapture" "lostpointercapture" "pointerenter" - // "pointerleave" "pointerover" "pointerout" if (event instanceof PointerEvent) { return { alt_key: event.altKey, @@ -56,24 +85,6 @@ export function serializeEvent(event: Event): SerializedEvent { }; } - if (event instanceof MouseEvent) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - offset_x: event.offsetX, - offset_y: event.offsetY, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - }; - } if (event instanceof TouchEvent) { return { @@ -123,6 +134,12 @@ export function serializeEvent(event: Event): SerializedEvent { } if (event instanceof DragEvent) { + // let files = []; + // if (event.dataTransfer && event.dataTransfer.files) { + // files = ["a", "b", "c"]; + // // files = await serializeFileList(event.dataTransfer.files); + // } + // return { mouse: get_mouse_data(event), files }; return { mouse: { alt_key: event.altKey, @@ -140,355 +157,3 @@ export function serializeEvent(event: Event): SerializedEvent { return {}; } - -function isElementNode(node: Node) { - return node.nodeType == 1; -} - -function eventBubbles(eventName: string) { - switch (eventName) { - case "copy": - return true; - case "cut": - return true; - case "paste": - return true; - case "compositionend": - return true; - case "compositionstart": - return true; - case "compositionupdate": - return true; - case "keydown": - return true; - case "keypress": - return true; - case "keyup": - return true; - case "focus": - return false; - case "focusout": - return true; - case "focusin": - return true; - case "blur": - return false; - case "change": - return true; - case "input": - return true; - case "invalid": - return true; - case "reset": - return true; - case "submit": - return true; - case "click": - return true; - case "contextmenu": - return true; - case "doubleclick": - return true; - case "dblclick": - return true; - case "drag": - return true; - case "dragend": - return true; - case "dragenter": - return false; - case "dragexit": - return false; - case "dragleave": - return true; - case "dragover": - return true; - case "dragstart": - return true; - case "drop": - return true; - case "mousedown": - return true; - case "mouseenter": - return false; - case "mouseleave": - return false; - case "mousemove": - return true; - case "mouseout": - return true; - case "scroll": - return false; - case "mouseover": - return true; - case "mouseup": - return true; - case "pointerdown": - return true; - case "pointermove": - return true; - case "pointerup": - return true; - case "pointercancel": - return true; - case "gotpointercapture": - return true; - case "lostpointercapture": - return true; - case "pointerenter": - return false; - case "pointerleave": - return false; - case "pointerover": - return true; - case "pointerout": - return true; - case "select": - return true; - case "touchcancel": - return true; - case "touchend": - return true; - case "touchmove": - return true; - case "touchstart": - return true; - case "wheel": - return true; - case "abort": - return false; - case "canplay": - return false; - case "canplaythrough": - return false; - case "durationchange": - return false; - case "emptied": - return false; - case "encrypted": - return true; - case "ended": - return false; - case "error": - return false; - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "load": - return false; - case "pause": - return false; - case "play": - return false; - case "playing": - return false; - case "progress": - return false; - case "ratechange": - return false; - case "seeked": - return false; - case "seeking": - return false; - case "stalled": - return false; - case "suspend": - return false; - case "timeupdate": - return false; - case "volumechange": - return false; - case "waiting": - return false; - case "animationstart": - return true; - case "animationend": - return true; - case "animationiteration": - return true; - case "transitionend": - return true; - case "toggle": - return true; - case "mounted": - return false; - } - - return true; -} - - - -// switch (event.type) { -// case "copy": case "cut": case "paste": { -// return {}; -// } -// case "compositionend": -// case "compositionstart": -// case "compositionupdate": { -// let { data } = event; -// return { -// data, -// }; -// } -// case "keydown": case "keypress": case "keyup": { -// if (event instanceof KeyboardEvent) { -// return { -// char_code: event.charCode, -// is_composing: event.isComposing, -// key: event.key, -// alt_key: event.altKey, -// ctrl_key: event.ctrlKey, -// meta_key: event.metaKey, -// key_code: event.keyCode, -// shift_key: event.shiftKey, -// location: event.location, -// repeat: event.repeat, -// which: event.which, -// code: event.code, -// }; -// }; -// } -// case "focus": -// case "blur": { -// return {}; -// } -// case "change": { -// let target = event.target; -// let value; -// if (target.type === "checkbox" || target.type === "radio") { -// value = target.checked ? "true" : "false"; -// } else { -// value = target.value ?? target.textContent; -// } -// return { -// value: value, -// values: {}, -// }; -// } -// case "input": -// case "invalid": -// case "reset": -// case "submit": { -// let target = event.target; -// let value = target.value ?? target.textContent; - -// if (target.type === "checkbox") { -// value = target.checked ? "true" : "false"; -// } - -// return { -// value: value, -// values: {}, -// }; -// } -// case "drag": -// case "dragend": -// case "dragenter": -// case "dragexit": -// case "dragleave": -// case "dragover": -// case "dragstart": -// case "drop": { -// let files = []; - -// if (event.dataTransfer && event.dataTransfer.files) { -// files = ["a", "b", "c"]; -// // files = await serializeFileList(event.dataTransfer.files); -// } - -// return { mouse: get_mouse_data(event), files }; -// } -// case "click": -// case "contextmenu": -// case "doubleclick": -// case "dblclick": -// case "mousedown": -// case "mouseenter": -// case "mouseleave": -// case "mousemove": -// case "mouseout": -// case "mouseover": -// case "mouseup": { -// return get_mouse_data(event); -// } -// case "select": { -// return {}; -// } -// case "touchcancel": -// case "touchend": -// case "touchmove": -// case "touchstart": { -// const { altKey, ctrlKey, metaKey, shiftKey } = event; -// return { -// // changed_touches: event.changedTouches, -// // target_touches: event.targetTouches, -// // touches: event.touches, -// alt_key: altKey, -// ctrl_key: ctrlKey, -// meta_key: metaKey, -// shift_key: shiftKey, -// }; -// } -// case "scroll": { -// return {}; -// } -// case "wheel": { -// const { deltaX, deltaY, deltaZ, deltaMode } = event; -// return { -// delta_x: deltaX, -// delta_y: deltaY, -// delta_z: deltaZ, -// delta_mode: deltaMode, -// }; -// } -// case "animationstart": -// case "animationend": -// case "animationiteration": { -// const { animationName, elapsedTime, pseudoElement } = event; -// return { -// animation_name: animationName, -// elapsed_time: elapsedTime, -// pseudo_element: pseudoElement, -// }; -// } -// case "transitionend": { -// const { propertyName, elapsedTime, pseudoElement } = event; -// return { -// property_name: propertyName, -// elapsed_time: elapsedTime, -// pseudo_element: pseudoElement, -// }; -// } -// case "abort": -// case "canplay": -// case "canplaythrough": -// case "durationchange": -// case "emptied": -// case "encrypted": -// case "ended": -// case "error": -// case "loadeddata": -// case "loadedmetadata": -// case "loadstart": -// case "pause": -// case "play": -// case "playing": -// case "progress": -// case "ratechange": -// case "seeked": -// case "seeking": -// case "stalled": -// case "suspend": -// case "timeupdate": -// case "volumechange": -// case "waiting": { -// return {}; -// } -// case "toggle": { -// return {}; -// } -// default: { -// return {}; -// } -// } From 8a969a26dc1bb46cc24d0c914ca098cdfce61181 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 28 Feb 2024 11:22:15 -0600 Subject: [PATCH 10/41] make interpreter extend the interpreter core class --- Cargo.lock | 306 +++++++++---------- packages/interpreter/Cargo.toml | 2 +- packages/interpreter/src/unified_bindings.rs | 121 ++++---- packages/web/src/lib.rs | 1 - 4 files changed, 211 insertions(+), 219 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de7afb9eb..bc1064e8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" +checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" dependencies = [ "cfg-if", "const-random", @@ -154,9 +154,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -256,7 +256,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -278,7 +278,7 @@ dependencies = [ "argh_shared", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -463,7 +463,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -498,7 +498,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -737,7 +737,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -1012,7 +1012,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", "syn_derive", ] @@ -1048,9 +1048,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "regex-automata", @@ -1083,9 +1083,9 @@ checksum = "38d17f4d6e4dc36d1a02fbedc2753a096848e7c1b0772f7654eab8e2c927dd53" [[package]] name = "bumpalo" -version = "3.15.2" +version = "3.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b1be7772ee4501dba05acbe66bb1e8760f6a6c474a36035631638e4415f130" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" [[package]] name = "bytecheck" @@ -1332,9 +1332,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.86" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730" +checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" dependencies = [ "libc", ] @@ -1426,7 +1426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half 2.3.1", + "half 2.4.0", ] [[package]] @@ -1481,7 +1481,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -1858,9 +1858,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ "crossbeam-utils", ] @@ -2003,7 +2003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -2029,12 +2029,12 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" +checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" dependencies = [ "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -2067,9 +2067,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.6" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c376d08ea6aa96aafe61237c7200d1241cb177b7d3a542d791f2d118e9cbb955" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ "darling_core", "darling_macro", @@ -2077,27 +2077,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.6" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33043dcd19068b8192064c704b3f83eb464f91f1ff527b44a4e2b08d9cdb8855" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] name = "darling_macro" -version = "0.20.6" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -2243,7 +2243,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -2307,7 +2307,7 @@ dependencies = [ "serde", "serde_json", "subprocess", - "syn 2.0.50", + "syn 2.0.51", "tar", "tauri-bundler", "tempfile", @@ -2375,7 +2375,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.50", + "syn 2.0.51", "trybuild", ] @@ -2554,7 +2554,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", "trybuild", ] @@ -2651,7 +2651,7 @@ dependencies = [ "quote", "rustc-hash", "smallvec", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -2729,7 +2729,7 @@ dependencies = [ "proc-macro2", "quote", "slab", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -2742,7 +2742,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.50", + "syn 2.0.51", "tracing", ] @@ -2856,7 +2856,7 @@ dependencies = [ "proc-macro2", "quote", "server_fn_macro", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -2979,7 +2979,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -3000,7 +3000,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -3021,7 +3021,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -3161,7 +3161,7 @@ checksum = "ce8cd46a041ad005ab9c71263f9a0ff5b529eac0fe4cc9b4a20f4f0765d8cf4b" dependencies = [ "execute-command-tokens", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -3184,7 +3184,7 @@ checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" dependencies = [ "bit_field", "flume", - "half 2.3.1", + "half 2.4.0", "lebe", "miniz_oxide", "rayon-core", @@ -3329,7 +3329,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -3512,7 +3512,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -3717,9 +3717,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" dependencies = [ "color_quant", "weezl", @@ -3795,7 +3795,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eadca029ef716b4378f7afb19f7ee101fde9e58ba1f1445971315ac866db417" dependencies = [ - "bstr 1.9.0", + "bstr 1.9.1", "btoi", "gix-date", "itoa 1.0.10", @@ -3809,7 +3809,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0341471d55d8676e98b88e121d7065dfa4c9c5acea4b6d6ecdd2846e85cce0c3" dependencies = [ - "bstr 1.9.0", + "bstr 1.9.1", "gix-config-value", "gix-features", "gix-glob", @@ -3826,12 +3826,12 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e7bfb37a46ed0b8468db37a6d8a0a61d56bdbe4603ae492cb322e5f3958" +checksum = "74ab5d22bc21840f4be0ba2e78df947ba14d8ba6999ea798f86b5bdb999edd0c" dependencies = [ "bitflags 2.4.2", - "bstr 1.9.0", + "bstr 1.9.1", "gix-path", "libc", "thiserror", @@ -3839,11 +3839,11 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7f3dfb72bebe3449b5e642be64e3c6ccbe9821c8b8f19f487cf5bfbbf4067e" +checksum = "17077f0870ac12b55d2eed9cb3f56549e40def514c8a783a0a79177a8a76b7c5" dependencies = [ - "bstr 1.9.0", + "bstr 1.9.1", "itoa 1.0.10", "thiserror", "time", @@ -3879,7 +3879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5db19298c5eeea2961e5b3bf190767a2d1f09b8802aeb5f258e42276350aff19" dependencies = [ "bitflags 2.4.2", - "bstr 1.9.0", + "bstr 1.9.1", "gix-features", "gix-path", ] @@ -3911,7 +3911,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "febf79c5825720c1c63fe974c7bbe695d0cb54aabad73f45671c60ce0e501e33" dependencies = [ - "bstr 1.9.0", + "bstr 1.9.1", "btoi", "gix-actor", "gix-date", @@ -3926,11 +3926,11 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97e9ad649bf5e109562d6acba657ca428661ec08e77eaf3a755d8fa55485be9c" +checksum = "69e0b521a5c345b7cd6a81e3e6f634407360a038c8b74ba14c621124304251b8" dependencies = [ - "bstr 1.9.0", + "bstr 1.9.1", "gix-trace", "home", "once_cell", @@ -3960,9 +3960,9 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8d9bf462feaf05f2121cba7399dbc6c34d88a9cad58fc1e95027791d6a3c6d2" +checksum = "022592a0334bdf77c18c06e12a7c0eaff28845c37e73c51a3e37d56dd495fb35" dependencies = [ "bitflags 2.4.2", "gix-path", @@ -3991,9 +3991,9 @@ checksum = "02b202d766a7fefc596e2cc6a89cda8ad8ad733aed82da635ac120691112a9b1" [[package]] name = "gix-utils" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e839f3d0798b296411263da6bee780a176ef8008a5dfc31287f7eda9266ab8" +checksum = "60157a15b9f14b11af1c6817ad7a93b10b50b4e5136d98a127c46a37ff16eeb6" dependencies = [ "fastrand 2.0.1", "unicode-normalization", @@ -4005,7 +4005,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac7cc36f496bd5d96cdca0f9289bb684480725d40db60f48194aa7723b883854" dependencies = [ - "bstr 1.9.0", + "bstr 1.9.1", "thiserror", ] @@ -4043,7 +4043,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -4083,7 +4083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick 1.1.2", - "bstr 1.9.0", + "bstr 1.9.1", "log", "regex-automata", "regex-syntax", @@ -4355,7 +4355,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -4398,15 +4398,15 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" [[package]] name = "half" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" dependencies = [ "cfg-if", "crunchy", @@ -4441,7 +4441,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.9", + "ahash 0.8.10", "bumpalo", ] @@ -4451,7 +4451,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.9", + "ahash 0.8.10", "allocator-api2", ] @@ -4541,9 +4541,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" [[package]] name = "hex" @@ -4721,7 +4721,7 @@ dependencies = [ "httpdate", "itoa 1.0.10", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -4808,7 +4808,7 @@ dependencies = [ "http-body 1.0.0", "hyper 1.2.0", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tower", "tower-service", @@ -4888,9 +4888,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.8" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ "bytemuck", "byteorder", @@ -5041,7 +5041,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -5082,7 +5082,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.6", + "hermit-abi 0.3.8", "libc", "windows-sys 0.48.0", ] @@ -5093,7 +5093,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.5", + "socket2 0.5.6", "widestring", "windows-sys 0.48.0", "winreg", @@ -5139,7 +5139,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.3.6", + "hermit-abi 0.3.8", "libc", "windows-sys 0.52.0", ] @@ -5507,11 +5507,11 @@ dependencies = [ [[package]] name = "lightningcss" -version = "1.0.0-alpha.53" +version = "1.0.0-alpha.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae8ba2b1b450cefc6a7a6ee93868992f56f662869b90404de2a7319da18640f" +checksum = "07d306844e5af1753490c420c0d6ae3d814b00725092d106332762827ca8f0fe" dependencies = [ - "ahash 0.8.9", + "ahash 0.8.10", "bitflags 2.4.2", "const-str", "cssparser 0.33.0", @@ -5602,7 +5602,7 @@ checksum = "fc2fb41a9bb4257a3803154bdf7e2df7d45197d1941c9b1a90ad815231630721" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -5656,9 +5656,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ "hashbrown 0.14.3", ] @@ -5777,7 +5777,7 @@ dependencies = [ "manganis-common", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -6255,7 +6255,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -6306,7 +6306,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.6", + "hermit-abi 0.3.8", "libc", ] @@ -6414,9 +6414,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "5.0.1" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349" +checksum = "eedff767bc49d336bff300224f73307ae36963c843e38dc9312a22171b012cbc" dependencies = [ "is-wsl", "libc", @@ -6446,7 +6446,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -6729,7 +6729,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -6857,7 +6857,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -6904,7 +6904,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -7107,7 +7107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22020dfcf177fcc7bf5deaf7440af371400c67c0de14c399938d8ed4fb4645d3" dependencies = [ "proc-macro2", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -7137,7 +7137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -7229,7 +7229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -7516,9 +7516,9 @@ checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" [[package]] name = "rayon" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -7704,7 +7704,7 @@ version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3625f343d89990133d013e39c46e350915178cf94f1bec9f49b0cbef98a3e3c" dependencies = [ - "ahash 0.8.9", + "ahash 0.8.10", "bitflags 2.4.2", "instant", "num-traits", @@ -7722,7 +7722,7 @@ checksum = "853977598f084a492323fe2f7896b4100a86284ee8473612de60021ea341310f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -7820,7 +7820,7 @@ dependencies = [ "pretty_assertions", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -7969,9 +7969,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" +checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" [[package]] name = "rustls-webpki" @@ -8171,7 +8171,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" dependencies = [ - "half 1.8.2", + "half 1.8.3", "serde", ] @@ -8183,7 +8183,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -8236,7 +8236,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -8287,7 +8287,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -8335,7 +8335,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", "xxhash-rust", ] @@ -8346,7 +8346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89732cbf095803f0a23dff6a9d2f469049d48affdaa80edee0d826c986330ced" dependencies = [ "server_fn_macro", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -8525,8 +8525,7 @@ dependencies = [ [[package]] name = "sledgehammer_bindgen" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1ca40134578bf7cf17973defcd4eb8d7d2adf7868b29892481722957bd543e" +source = "git+https://github.com/ealmloff/sledgehammer_bindgen#6f90290e1655014f22623eb8b2ea41fb098e1164" dependencies = [ "sledgehammer_bindgen_macro", "wasm-bindgen", @@ -8535,11 +8534,10 @@ dependencies = [ [[package]] name = "sledgehammer_bindgen_macro" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04559ded3de5c62f08457cadcb6c44649c4d90e72fdc0804c6c30ce1bc526304" +source = "git+https://github.com/ealmloff/sledgehammer_bindgen#6f90290e1655014f22623eb8b2ea41fb098e1164" dependencies = [ "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -8591,12 +8589,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -8689,7 +8687,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" dependencies = [ - "ahash 0.8.9", + "ahash 0.8.10", "atoi", "bigdecimal", "bit-vec", @@ -8981,7 +8979,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -9023,9 +9021,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.50" +version = "2.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" +checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" dependencies = [ "proc-macro2", "quote", @@ -9041,7 +9039,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -9326,7 +9324,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -9437,7 +9435,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2 0.5.6", "tokio-macros", "windows-sys 0.48.0", ] @@ -9450,7 +9448,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -9612,7 +9610,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.2", + "winnow 0.6.3", ] [[package]] @@ -9633,9 +9631,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "async-compression", "base64", @@ -9694,7 +9692,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -10141,7 +10139,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", "wasm-bindgen-shared", ] @@ -10207,7 +10205,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -10250,7 +10248,7 @@ checksum = "a5211b7550606857312bba1d978a8ec75692eae187becc5e680444fffc5e6f89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -10442,7 +10440,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -10546,7 +10544,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -10557,7 +10555,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -10778,9 +10776,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178" +checksum = "44e19b97e00a4d3db3cdb9b53c8c5f87151b5689b82cc86c2848cbdcccb2689b" dependencies = [ "memchr", ] @@ -10905,9 +10903,9 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zbus" -version = "3.15.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c45d06ae3b0f9ba1fb2671268b975557d8f5a84bb5ec6e43964f87e763d8bca8" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" dependencies = [ "async-broadcast", "async-executor", @@ -10946,9 +10944,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.15.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a1ba45ed0ad344b85a2bb5a1fe9830aed23d67812ea39a586e7d0136439c7d" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", @@ -10986,7 +10984,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.51", ] [[package]] @@ -11073,9 +11071,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.15.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" dependencies = [ "byteorder", "enumflags2", @@ -11087,9 +11085,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.15.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index ba813a538..1cb573911 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -17,7 +17,7 @@ web-sys = { version = "0.3.56", optional = true, features = [ "Element", "Node", ] } -sledgehammer_bindgen = { version = "0.4.0", default-features = false, optional = true } +sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } sledgehammer_utils = { version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index ce3cf2866..20c4b6ca2 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -4,42 +4,37 @@ use js_sys::Function; #[cfg(feature = "webonly")] use web_sys::Node; +#[cfg(feature = "webonly")] +use wasm_bindgen::prelude::wasm_bindgen; + use sledgehammer_bindgen::bindgen; pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; #[cfg(feature = "webonly")] -#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#" -export function save_template(channel, nodes, tmpl_id) { - channel.save_template(nodes, tmpl_id); -} -export function hydrate(channel, ids) { - channel.hydrate(ids); -} -export function get_node(channel, id) { - return channel.get_node(id); -} -export function initialize(channel, root, handler) { - channel.initialize(root, handler); -} -"#)] +#[wasm_bindgen(module = "src/js/web.js")] extern "C" { - pub fn save_template(channel: &JSChannel, nodes: Vec, tmpl_id: u16); + #[wasm_bindgen] + pub type Interpreter; - pub fn hydrate(channel: &JSChannel, ids: Vec); + #[wasm_bindgen(method)] + pub fn save_template(this: &Interpreter, nodes: Vec, tmpl_id: u16); - pub fn get_node(channel: &JSChannel, id: u32) -> Node; + #[wasm_bindgen(method)] + pub fn hydrate(this: &Interpreter, ids: Vec); - pub fn initialize(channel: &JSChannel, root: Node, handler: &Function); + #[wasm_bindgen(method)] + pub fn get_node(this: &Interpreter, id: u32) -> Node; + + #[wasm_bindgen(method)] + pub fn initialize(this: &Interpreter, root: Node, handler: &Function); } #[bindgen(module)] mod js { - // Load in the JavaScript file with all the imports - const JS_FILE: &str = "./src/gen/interpreter.js"; - - // Boot the interpreter and attach us some bindings! - const JS: &str = r#""#; + /// The interpreter extends the core interpreter which contains the state for the interpreter along with some functions that all platforms use like `AppendChildren`. + #[extends(Interpreter)] + pub struct InterpreterInterface; fn mount_to_root() { "{this.AppendChildren(this.root, this.stack.length-1);}" @@ -147,12 +142,12 @@ mod js { */ fn append_children_to_top(many: u16) { "{ - let root = this.stack[this.stack.length-many-1]; - this.els = this.stack.splice(this.stack.length-many); - for (let k = 0; k < many; k++) { - root.appendChild(this.els[k]); - } - }" + let root = this.stack[this.stack.length-many-1]; + this.els = this.stack.splice(this.stack.length-many); + for (let k = 0; k < many; k++) { + root.appendChild(this.els[k]); + } + }" } fn set_top_attribute(field: &str, value: &str, ns: &str) { "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}" @@ -171,47 +166,47 @@ mod js { } fn foreign_event_listener(event: &str, id: u32, bubbles: u8) { r#" - bubbles = bubbles == 1; - let node = this.nodes[id]; - if(node.listening){ - node.listening += 1; - } else { - node.listening = 1; - } - node.setAttribute('data-dioxus-id', `\${id}`); - const event_name = $event$; + bubbles = bubbles == 1; + let node = this.nodes[id]; + if(node.listening){ + node.listening += 1; + } else { + node.listening = 1; + } + node.setAttribute('data-dioxus-id', `\${id}`); + const event_name = $event$; - // if this is a mounted listener, we send the event immediately - if (event_name === "mounted") { - window.ipc.postMessage( - this.serializeIpcMessage("user_event", { - name: event_name, - element: id, - data: null, - bubbles, - }) - ); - } else { - this.listeners.create(event_name, node, bubbles, (event) => { - this.handler(event, event_name, bubbles); - }); - }"# + // if this is a mounted listener, we send the event immediately + if (event_name === "mounted") { + window.ipc.postMessage( + this.serializeIpcMessage("user_event", { + name: event_name, + element: id, + data: null, + bubbles, + }) + ); + } else { + this.listeners.create(event_name, node, bubbles, (event) => { + this.handler(event, event_name, bubbles); + }); + }"# } fn assign_id_ref(array: &[u8], id: u32) { "{this.nodes[$id$] = this.LoadChild($array$);}" } fn hydrate_text_ref(array: &[u8], value: &str, id: u32) { r#"{ - let node = this.LoadChild($array$); - if (node.nodeType == node.TEXT_NODE) { - node.textContent = value; - } else { - let text = document.createTextNode(value); - node.replaceWith(text); - node = text; - } - this.nodes[$id$] = node; - }"# + let node = this.LoadChild($array$); + if (node.nodeType == node.TEXT_NODE) { + node.textContent = value; + } else { + let text = document.createTextNode(value); + node.replaceWith(text); + node = text; + } + this.nodes[$id$] = node; + }"# } fn replace_placeholder_ref(array: &[u8], n: u16) { "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...this.els);}" diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index d92128676..1acbdeb87 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -43,7 +43,6 @@ mod hot_reload; #[cfg(feature = "hydrate")] mod rehydrate; - /// Runs the app as a future that can be scheduled around the main thread. /// /// Polls futures internal to the VirtualDOM, hence the async nature of this function. From 0ff0eb78467d16682244239c00d20c423761ec6c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 1 Mar 2024 14:23:53 -0800 Subject: [PATCH 11/41] Use the platform as the base interpreter instead of trying to extend it --- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 10 ++++++ packages/interpreter/src/js/web.js | 20 +++++------ packages/interpreter/src/lib.rs | 5 ++- .../interpreter/src/ts/interpreter_core.ts | 12 +++++++ .../interpreter/src/ts/interpreter_native.ts | 1 + .../interpreter/src/ts/interpreter_web.ts | 12 ------- packages/interpreter/src/unified_bindings.rs | 34 +++++++++++-------- .../interpreter/src/write_native_mutations.rs | 3 +- packages/web/src/dom.rs | 11 +++--- 10 files changed, 65 insertions(+), 45 deletions(-) diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index d29edbd8e..f79c5a54f 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -12494364440840058490 \ No newline at end of file +13493771420133770074 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 883285eda..91c33d9d5 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -159,6 +159,16 @@ System.register("interpreter_core", [], function (exports_2, context_2) { var id = element.getAttribute("data-dioxus-id"); delete this.local[id]; }; + Interpreter.prototype.getNode = function (id) { + return this.nodes[id]; + }; + Interpreter.prototype.appendChildren = function (id, many) { + var root = this.nodes[id]; + var els = this.stack.splice(this.stack.length - many); + for (var k = 0; k < many; k++) { + root.appendChild(els[k]); + } + }; return Interpreter; }()); exports_2("Interpreter", Interpreter); diff --git a/packages/interpreter/src/js/web.js b/packages/interpreter/src/js/web.js index bb4a724d4..287bc6b8d 100644 --- a/packages/interpreter/src/js/web.js +++ b/packages/interpreter/src/js/web.js @@ -74,6 +74,16 @@ System.register("interpreter_core", [], function (exports_1, context_1) { var id = element.getAttribute("data-dioxus-id"); delete this.local[id]; }; + Interpreter.prototype.getNode = function (id) { + return this.nodes[id]; + }; + Interpreter.prototype.appendChildren = function (id, many) { + var root = this.nodes[id]; + var els = this.stack.splice(this.stack.length - many); + for (var k = 0; k < many; k++) { + root.appendChild(els[k]); + } + }; return Interpreter; }()); exports_1("Interpreter", Interpreter); @@ -141,16 +151,6 @@ System.register("interpreter_web", ["interpreter_core"], function (exports_2, co currentNode = treeWalker.nextNode(); } }; - WebInterpreter.prototype.getNode = function (id) { - return this.nodes[id]; - }; - WebInterpreter.prototype.appendChildren = function (id, many) { - var root = this.nodes[id]; - var els = this.stack.splice(this.stack.length - many); - for (var k = 0; k < many; k++) { - root.appendChild(els[k]); - } - }; return WebInterpreter; }(interpreter_core_1.Interpreter)); exports_2("WebInterpreter", WebInterpreter); diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index b50f2dfeb..f3a87dff7 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -19,7 +19,7 @@ pub mod minimal_bindings { /// Some useful snippets that we use to share common functionality between the different platforms we support. /// /// This maintains some sort of consistency between web, desktop, and liveview - #[wasm_bindgen(module = "/src/common_exported.js")] + #[wasm_bindgen(module = "/src/js/web.js")] extern "C" { /// Set the attribute of the node pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>); @@ -30,3 +30,6 @@ pub mod minimal_bindings { #[cfg(feature = "sledgehammer")] pub mod unified_bindings; + +#[cfg(feature = "sledgehammer")] +pub use unified_bindings::*; diff --git a/packages/interpreter/src/ts/interpreter_core.ts b/packages/interpreter/src/ts/interpreter_core.ts index 57ec4c5b8..ba8213954 100644 --- a/packages/interpreter/src/ts/interpreter_core.ts +++ b/packages/interpreter/src/ts/interpreter_core.ts @@ -76,5 +76,17 @@ export class Interpreter { const id = element.getAttribute("data-dioxus-id"); delete this.local[id]; } + + getNode(id: number): Node { + return this.nodes[id]; + } + + appendChildren(id: number, many: number) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + for (let k = 0; k < many; k++) { + root.appendChild(els[k]); + } + } } diff --git a/packages/interpreter/src/ts/interpreter_native.ts b/packages/interpreter/src/ts/interpreter_native.ts index e37cc3427..ffdab3567 100644 --- a/packages/interpreter/src/ts/interpreter_native.ts +++ b/packages/interpreter/src/ts/interpreter_native.ts @@ -7,6 +7,7 @@ import { retriveValues } from "./form"; import { Interpreter } from "./interpreter_core"; import { SerializedEvent, serializeEvent } from "./serialize"; + export class NativeInterpreter extends Interpreter { intercept_link_redirects: boolean; ipc: any; diff --git a/packages/interpreter/src/ts/interpreter_web.ts b/packages/interpreter/src/ts/interpreter_web.ts index 628a283f8..b393e3b1f 100644 --- a/packages/interpreter/src/ts/interpreter_web.ts +++ b/packages/interpreter/src/ts/interpreter_web.ts @@ -75,16 +75,4 @@ export class WebInterpreter extends Interpreter { currentNode = treeWalker.nextNode(); } } - - getNode(id: number): Node { - return this.nodes[id]; - } - - appendChildren(id: number, many: number) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - for (let k = 0; k < many; k++) { - root.appendChild(els[k]); - } - } } diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index 20c4b6ca2..e4e2e0aeb 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -1,6 +1,3 @@ -#[cfg(feature = "webonly")] -use js_sys::Function; - #[cfg(feature = "webonly")] use web_sys::Node; @@ -11,30 +8,39 @@ use sledgehammer_bindgen::bindgen; pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; +/// Extensions to the interpreter that are specific to the web platform. #[cfg(feature = "webonly")] #[wasm_bindgen(module = "src/js/web.js")] extern "C" { - #[wasm_bindgen] - pub type Interpreter; + pub type WebInterpreter; + + #[wasm_bindgen(method, js_name = "saveTemplate")] + pub fn save_template(this: &WebInterpreter, nodes: Vec, tmpl_id: u16); #[wasm_bindgen(method)] - pub fn save_template(this: &Interpreter, nodes: Vec, tmpl_id: u16); + pub fn hydrate(this: &WebInterpreter, ids: Vec); - #[wasm_bindgen(method)] - pub fn hydrate(this: &Interpreter, ids: Vec); + #[wasm_bindgen(method, js_name = "getNode")] + pub fn get_node(this: &WebInterpreter, id: u32) -> Node; +} - #[wasm_bindgen(method)] - pub fn get_node(this: &Interpreter, id: u32) -> Node; +#[cfg(feature = "webonly")] +type PlatformInterpreter = WebInterpreter; - #[wasm_bindgen(method)] - pub fn initialize(this: &Interpreter, root: Node, handler: &Function); +#[cfg(feature = "webonly")] +impl Interpreter { + /// Convert the interpreter to a web interpreter, enabling methods like hydrate and save_template. + pub fn as_web(&self) -> &WebInterpreter { + use wasm_bindgen::prelude::JsCast; + &self.js_channel().unchecked_ref() + } } #[bindgen(module)] mod js { /// The interpreter extends the core interpreter which contains the state for the interpreter along with some functions that all platforms use like `AppendChildren`. - #[extends(Interpreter)] - pub struct InterpreterInterface; + #[extends(PlatformInterpreter)] + pub struct Interpreter; fn mount_to_root() { "{this.AppendChildren(this.root, this.stack.length-1);}" diff --git a/packages/interpreter/src/write_native_mutations.rs b/packages/interpreter/src/write_native_mutations.rs index 0b3df50e8..8f07080a2 100644 --- a/packages/interpreter/src/write_native_mutations.rs +++ b/packages/interpreter/src/write_native_mutations.rs @@ -1,5 +1,4 @@ -// use crate::binary_protocol::Channel; -use crate::unified_bindings::Channel; +use crate::unified_bindings::Interpreter as Channel; use dioxus_core::{TemplateAttribute, TemplateNode, WriteMutations}; use dioxus_html::event_bubbles; use sledgehammer_utils::rustc_hash::FxHashMap; diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 428b8d8b9..f6add78a4 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -8,7 +8,7 @@ use dioxus_core::ElementId; use dioxus_html::PlatformEventData; -use dioxus_interpreter_js::Channel; +use dioxus_interpreter_js::unified_bindings::{Interpreter, InterpreterInterface}; use futures_channel::mpsc; use rustc_hash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; @@ -22,7 +22,7 @@ pub struct WebsysDom { pub(crate) root: Element, pub(crate) templates: FxHashMap, pub(crate) max_template_id: u16, - pub(crate) interpreter: Channel, + pub(crate) interpreter: InterpreterInterface, #[cfg(feature = "mounted")] pub(crate) event_channel: mpsc::UnboundedSender, #[cfg(feature = "mounted")] @@ -64,7 +64,7 @@ impl WebsysDom { } }; - let interpreter = Channel::default(); + let interpreter = InterpreterInterface::default(); let handler: Closure = Closure::wrap(Box::new({ let event_channel = event_channel.clone(); @@ -107,11 +107,12 @@ impl WebsysDom { } })); - dioxus_interpreter_js::initialize( - interpreter.js_channel(), + let _interpreter: &Interpreter = interpreter.as_ref(); + _interpreter.initialize( root.clone().unchecked_into(), handler.as_ref().unchecked_ref(), ); + dioxus_html::set_event_converter(Box::new(WebEventConverter)); handler.forget(); From 199173a409e1c4a413ff84a40b919e976e24d13a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 1 Mar 2024 23:37:46 -0800 Subject: [PATCH 12/41] fix navigating when files are dropped --- examples/file_upload.rs | 7 +- examples/form.rs | 77 +- packages/desktop/headless_tests/events.rs | 109 +- packages/desktop/headless_tests/utils.rs | 16 +- packages/desktop/src/protocol.rs | 4 +- packages/html/src/events/drag.rs | 2 + packages/html/src/events/form.rs | 84 +- packages/html/src/transit.rs | 15 +- packages/interpreter/README.md | 6 +- packages/interpreter/build.rs | 20 +- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 1065 +++++++++-------- packages/interpreter/src/js/web.js | 287 ++--- packages/interpreter/src/lib.rs | 1 + packages/interpreter/src/ts/form.ts | 77 +- .../interpreter/src/ts/interpreter_core.ts | 20 +- .../interpreter/src/ts/interpreter_native.ts | 70 +- .../interpreter/src/ts/interpreter_web.ts | 2 +- packages/interpreter/src/ts/serialize.ts | 346 +++--- packages/interpreter/src/ts/set_attribute.ts | 29 +- packages/interpreter/src/unified_bindings.rs | 24 +- .../interpreter/src/write_native_mutations.rs | 3 +- packages/interpreter/tsconfig.json | 2 +- 23 files changed, 1242 insertions(+), 1026 deletions(-) diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 280144e44..89669a248 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -43,10 +43,8 @@ fn app() -> Element { checked: enable_directory_upload, oninput: move |evt| enable_directory_upload.set(evt.checked()), }, - label { - r#for: "directory-upload", - "Enable directory upload" - } + + label { r#for: "directory-upload", "Enable directory upload" } input { r#type: "file", @@ -60,7 +58,6 @@ fn app() -> Element { // cheating with a little bit of JS... "ondragover": "this.style.backgroundColor='#88FF88';", "ondragleave": "this.style.backgroundColor='#FFFFFF';", - id: "drop-zone", prevent_default: "ondrop dragover dragenter", ondrop: handle_file_drop, diff --git a/examples/form.rs b/examples/form.rs index 7b305a7e7..3442d4d73 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -15,23 +15,92 @@ fn app() -> Element { rsx! { div { h1 { "Form" } + + // The form element is used to create an HTML form for user input + // You can attach regular attributes to it form { - oninput: move |ev| values.set(ev.values()), + id: "cool-form", + style: "display: flex; flex-direction: column;", + + // You can attach a handler to the entire form + oninput: move |ev| { + println!("Input event: {:#?}", ev); + values.set(ev.values()); + }, + + // On desktop/liveview, the form will not navigate the page - the expectation is that you handle + // The form event. + // Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview. + // We suggest always attaching a submit handler to the form. + onsubmit: move |ev| { + println!("Submit event: {:#?}", ev); + }, + + // Regular text inputs with handlers + label { r#for: "username", "Username" } input { r#type: "text", name: "username", - oninput: move |ev| values.set(ev.values()) + oninput: move |ev| { + println!("setting username"); + values.set(ev.values()); + } } + + // And then the various inputs that might exist + // Note for a value to be returned in .values(), it must be named! + + label { r#for: "full-name", "Full Name" } input { r#type: "text", name: "full-name" } + + label { r#for: "email", "Email" } + input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" } + + label { r#for: "password", "Password" } input { r#type: "password", name: "password" } - input { r#type: "radio", name: "color", value: "red" } + + label { r#for: "color", "Color" } + input { r#type: "radio", checked: true, name: "color", value: "red" } input { r#type: "radio", name: "color", value: "blue" } + input { r#type: "radio", name: "color", value: "green" } + + // Select multiple comes in as a comma separated list of selected values + // You should split them on the comma to get the values manually + label { r#for: "country", "Country" } + select { + name: "country", + multiple: true, + oninput: move |ev| { + println!("Input event: {:#?}", ev); + println!("Values: {:#?}", ev.value().split(',').collect::>()); + }, + option { value: "usa", "USA" } + option { value: "canada", "Canada" } + option { value: "mexico", "Mexico" } + } + + // Safari can be quirky with color inputs on mac. + // We recommend always providing a text input for color as a fallback. + label { r#for: "color", "Color" } + input { r#type: "color", value: "#000002", name: "head", id: "head" } + + // Dates! + input { + min: "2018-01-01", + value: "2018-07-22", + r#type: "date", + name: "trip-start", + max: "2025-12-31", + id: "start" + } + + // Buttons will submit your form by default. button { r#type: "submit", value: "Submit", "Submit the form" } } } div { h1 { "Oninput Values" } - "{values:#?}" + pre { "{values:#?}" } } } } diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index 78d6a5568..870e43057 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -20,6 +20,10 @@ fn app() -> Element { let received = RECEIVED_EVENTS(); let expected = utils::EXPECTED_EVENTS(); + use_effect(move || { + println!("expecting {} events", utils::EXPECTED_EVENTS()); + }); + if expected != 0 && received == expected { println!("all events recieved"); desktop_context.close(); @@ -41,11 +45,15 @@ fn app() -> Element { test_focus_in_div {} test_focus_out_div {} test_form_input {} + test_form_submit {} + test_select_multiple_options {} } } } fn test_mounted() -> Element { + use_hook(|| utils::EXPECTED_EVENTS.with_mut(|x| *x += 1)); + rsx! { div { width: "100px", @@ -433,20 +441,24 @@ fn test_form_input() -> Element { utils::mock_event_with_extra( "form-username", - r#"new Event("input", { - bubbles: true, - cancelable: true, - composed: true, - })"#, + r#"new Event("input", { bubbles: true, cancelable: true, composed: true })"#, r#"element.value = "hello";"#, ); - let set_values = move |ev: FormEvent| { + let set_username = move |ev: FormEvent| { values.set(ev.values()); - eprintln!("values: {:?}", values); + + // The value of the input should match + assert_eq!(ev.value(), "hello"); + + // And then the value the form gives us should also match values.with_mut(|x| { - assert_eq!(x.get("username").unwrap().deref(), &["hello"]); + assert_eq!(x.get("username").unwrap().deref(), "hello"); + assert_eq!(x.get("full-name").unwrap().deref(), "lorem"); + assert_eq!(x.get("password").unwrap().deref(), "ipsum"); + assert_eq!(x.get("color").unwrap().deref(), "red"); }); + RECEIVED_EVENTS.with_mut(|x| *x += 1); }; rsx! { @@ -454,19 +466,90 @@ fn test_form_input() -> Element { h1 { "Form" } form { id: "form", - oninput: move |ev| values.set(ev.values()), + oninput: move |ev| { + values.set(ev.values()); + }, + onsubmit: move |ev| { + println!("{:?}", ev); + }, input { r#type: "text", name: "username", id: "form-username", - oninput: set_values, + oninput: set_username, } - input { r#type: "text", name: "full-name" } - input { r#type: "password", name: "password" } - input { r#type: "radio", name: "color", value: "red" } + input { r#type: "text", name: "full-name", value: "lorem" } + input { r#type: "password", name: "password", value: "ipsum" } + input { r#type: "radio", name: "color", value: "red", checked: true } input { r#type: "radio", name: "color", value: "blue" } button { r#type: "submit", value: "Submit", "Submit the form" } } } } } + +fn test_form_submit() -> Element { + let mut values = use_signal(|| HashMap::new()); + + utils::mock_event_with_extra( + "form-submitter", + r#"new Event("submit", { bubbles: true, cancelable: true, composed: true })"#, + r#"element.submit();"#, + ); + + let set_values = move |ev: FormEvent| { + values.set(ev.values()); + values.with_mut(|x| { + assert_eq!(x.get("username").unwrap().deref(), "goodbye"); + assert_eq!(x.get("full-name").unwrap().deref(), "lorem"); + assert_eq!(x.get("password").unwrap().deref(), "ipsum"); + assert_eq!(x.get("color").unwrap().deref(), "red"); + }); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + }; + + rsx! { + div { + h1 { "Form" } + form { + id: "form-submitter", + onsubmit: set_values, + input { r#type: "text", name: "username", id: "username", value: "goodbye" } + input { r#type: "text", name: "full-name", value: "lorem" } + input { r#type: "password", name: "password", value: "ipsum" } + input { r#type: "radio", name: "color", value: "red", checked: true } + input { r#type: "radio", name: "color", value: "blue" } + button { r#type: "submit", value: "Submit", "Submit the form" } + } + } + } +} + +fn test_select_multiple_options() -> Element { + utils::mock_event_with_extra( + "select-many", + r#"new Event("input", { bubbles: true, cancelable: true, composed: true })"#, + r#" + document.getElementById('usa').selected = true; + document.getElementById('canada').selected = true; + document.getElementById('mexico').selected = false; + "#, + ); + + rsx! { + select { + id: "select-many", + name: "country", + multiple: true, + oninput: move |ev| { + let values = ev.value(); + let values = values.split(',').collect::>(); + assert_eq!(values, vec!["usa", "canada"]); + RECEIVED_EVENTS.with_mut(|x| *x += 1); + }, + option { id: "usa", value: "usa", "USA" } + option { id: "canada", value: "canada", "Canada" } + option { id: "mexico", value: "mexico", selected: true, "Mexico" } + } + } +} diff --git a/packages/desktop/headless_tests/utils.rs b/packages/desktop/headless_tests/utils.rs index cd14a2fc1..d8bd3fb2d 100644 --- a/packages/desktop/headless_tests/utils.rs +++ b/packages/desktop/headless_tests/utils.rs @@ -30,23 +30,19 @@ pub fn mock_event(id: &'static str, value: &'static str) { } pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'static str) { - EXPECTED_EVENTS.with_mut(|x| *x += 1); - use_hook(move || { + EXPECTED_EVENTS.with_mut(|x| *x += 1); + spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(5000)).await; let js = format!( r#" - //console.log("ran"); - // Dispatch a synthetic event - let event = {}; - let element = document.getElementById('{}'); - console.log(element, event); - {} + let event = {value}; + let element = document.getElementById('{id}'); + {extra} element.dispatchEvent(event); - "#, - value, id, extra + "# ); eval(&js).await.unwrap(); diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index e9d4125b2..ba349f61c 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,5 +1,5 @@ use crate::{assets::*, edits::EditQueue}; -use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; +use dioxus_interpreter_js::unified_bindings::{native_js, SLEDGEHAMMER_JS}; use std::path::{Path, PathBuf}; use wry::{ http::{status::StatusCode, Request, Response}, @@ -38,7 +38,7 @@ fn handle_edits_code() -> String { }}"# ); - let mut interpreter = SLEDGEHAMMER_JS + let mut interpreter = native_js() .replace("/*POST_HANDLE_EDITS*/", PREVENT_FILE_UPLOAD) .replace("export", "") + &polling_request; diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index bbedf9a2b..6e31334a1 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -110,6 +110,8 @@ impl PointerInteraction for DragData { #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] pub struct SerializedDragData { mouse: crate::point_interaction::SerializedPointInteraction, + + #[serde(default)] files: Option, } diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 01d5793ec..bd8c45fec 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -1,56 +1,11 @@ use crate::file_data::FileEngine; use crate::file_data::HasFileData; -use std::ops::Deref; use std::{collections::HashMap, fmt::Debug}; use dioxus_core::Event; pub type FormEvent = Event; -/// A form value that may either be a list of values or a single value -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - // this will serialize Text(String) -> String and VecText(Vec) to Vec - serde(untagged) -)] -#[derive(Debug, Clone, PartialEq)] -pub enum FormValue { - Text(String), - VecText(Vec), -} - -impl From for Vec { - fn from(value: FormValue) -> Self { - match value { - FormValue::Text(s) => vec![s], - FormValue::VecText(vec) => vec, - } - } -} - -impl Deref for FormValue { - type Target = [String]; - - fn deref(&self) -> &Self::Target { - self.as_slice() - } -} - -impl FormValue { - /// Convenient way to represent Value as slice - pub fn as_slice(&self) -> &[String] { - match self { - FormValue::Text(s) => std::slice::from_ref(s), - FormValue::VecText(vec) => vec.as_slice(), - } - } - /// Convert into Vec - pub fn to_vec(self) -> Vec { - self.into() - } -} - /* DOMEvent: Send + SyncTarget relatedTarget */ pub struct FormData { inner: Box, @@ -73,6 +28,7 @@ impl Debug for FormData { f.debug_struct("FormEvent") .field("value", &self.value()) .field("values", &self.values()) + .field("valid", &self.valid()) .finish() } } @@ -106,8 +62,10 @@ impl FormData { self.value().parse().unwrap_or(false) } - /// Get the values of the form event - pub fn values(&self) -> HashMap { + /// Collect all the named form values from the containing form. + /// + /// Every input must be named! + pub fn values(&self) -> HashMap { self.inner.values() } @@ -120,6 +78,11 @@ impl FormData { pub fn downcast(&self) -> Option<&T> { self.inner.as_any().downcast_ref::() } + + /// Did this form pass its own validation? + pub fn valid(&self) -> bool { + self.inner.value().is_empty() + } } /// An object that has all the data for a form event @@ -128,7 +91,11 @@ pub trait HasFormData: HasFileData + std::any::Any { Default::default() } - fn values(&self) -> HashMap { + fn valid(&self) -> bool { + true + } + + fn values(&self) -> HashMap { Default::default() } @@ -164,8 +131,16 @@ impl FormData { /// A serialized form data object #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] pub struct SerializedFormData { + #[serde(default)] value: String, - values: HashMap, + + #[serde(default)] + values: HashMap, + + #[serde(default)] + valid: bool, + + #[serde(default)] files: Option, } @@ -174,13 +149,14 @@ impl SerializedFormData { /// Create a new serialized form data object pub fn new( value: String, - values: HashMap, + values: HashMap, files: Option, ) -> Self { Self { value, values, files, + valid: true, } } @@ -189,6 +165,7 @@ impl SerializedFormData { Self { value: data.value(), values: data.values(), + valid: data.valid(), files: match data.files() { Some(files) => { let mut resolved_files = HashMap::new(); @@ -211,6 +188,7 @@ impl SerializedFormData { Self { value: data.value(), values: data.values(), + valid: data.valid(), files: None, } } @@ -222,10 +200,14 @@ impl HasFormData for SerializedFormData { self.value.clone() } - fn values(&self) -> HashMap { + fn values(&self) -> HashMap { self.values.clone() } + fn valid(&self) -> bool { + self.valid + } + fn as_any(&self) -> &dyn std::any::Any { self } diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 17a8a5f2d..c1b561014 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -34,8 +34,19 @@ impl<'de> Deserialize<'de> for HtmlEvent { data, } = Inner::deserialize(deserializer)?; + // in debug mode let's try and be helpful as to why the deserialization failed + #[cfg(debug_assertions)] + { + _ = deserialize_raw(&name, data.clone()).unwrap_or_else(|e| { + panic!( + "Failed to deserialize event data for event {}: {:#?}\n'{:#?}'", + name, e, data, + ) + }); + } + Ok(HtmlEvent { - data: fun_name(&name, data).unwrap(), + data: deserialize_raw(&name, data).unwrap(), element, bubbles, name, @@ -44,7 +55,7 @@ impl<'de> Deserialize<'de> for HtmlEvent { } #[cfg(feature = "serialize")] -fn fun_name( +fn deserialize_raw( name: &str, data: serde_value::Value, ) -> Result { diff --git a/packages/interpreter/README.md b/packages/interpreter/README.md index af17d1e10..a53e4b470 100644 --- a/packages/interpreter/README.md +++ b/packages/interpreter/README.md @@ -27,13 +27,15 @@ This crate features bindings for the web and sledgehammer for increased performa ## Architecture -We use TypeScript to write the bindings and a very simple build.rs to convert them to javascript, minify them, and glue them into the rest of the project. +We use TypeScript to write the bindings and a very simple build.rs along with bun to convert them to javascript, minify them, and glue them into the rest of the project. Not every snippet of JS will be used, so we split out the snippets from the core interpreter. In theory, we *could* use Rust in the browser to do everything these bindings are doing. In reality, we want to stick with JS to skip the need for a WASM build step when running the LiveView and WebView renderers. We also want to use JS to prevent diverging behavior of things like canceling events, uploading files, and collecting form inputs. These details are tough to ensure 1:1 compatibility when implementing them in two languages. -If you want to contribute to the bindings, you'll need to have the typescript compiler installed on your machine, accessible via `tsc`. +If you want to contribute to the bindings, you'll need to have the typescript compiler installed on your machine as well as bun: + +https://bun.sh/docs/installation ## Contributing diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 236467eaf..75c070d80 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -6,8 +6,7 @@ use std::{ fn main() { // If any TS changes, re-run the build script - println!("cargo:rerun-if-changed=src/ts/*.ts"); - println!("cargo:rerun-if-changed=*.json"); + println!("cargo:rerun-if-changed=src/ts/*.ts,*.json"); // Compute the hash of the ts files let hash = hash_dir("src/ts"); @@ -21,8 +20,8 @@ fn main() { // Otherwise, generate the bindings and write the new hash to disk // Generate the bindings for both native and web - gen_bindings("native"); - gen_bindings("web"); + gen_bindings("interpreter_native", "native"); + gen_bindings("interpreter_web", "web"); std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); } @@ -51,18 +50,21 @@ fn hash_dir(dir: &str) -> u64 { // we need to hash each of the .ts files and add that hash to the JS files // if the hashes don't match, we need to fail the build // that way we also don't need -fn gen_bindings(name: &str) { +fn gen_bindings(input_name: &str, output_name: &str) { // If the file is generated, and the hash is different, we need to generate it - let status = Command::new("tsc") - .arg("--p") - .arg(format!("tsconfig.{name}.json")) + let status = Command::new("bun") + .arg("build") + .arg(format!("src/ts/{input_name}.ts")) + .arg("--outfile") + .arg(format!("src/js/{output_name}.js")) + // .arg("--minify") .status() .unwrap(); if !status.success() { panic!( "Failed to generate bindings for {}. Make sure you have tsc installed", - name + input_name ); } } diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index f79c5a54f..134f6fe87 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -13493771420133770074 \ No newline at end of file +4429706825984325407 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 91c33d9d5..7a03ae59f 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1,533 +1,538 @@ -var __extends = (this && this.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; - return extendStatics(d, b); +// src/ts/interpreter_core.ts +class Interpreter { + global; + local; + root; + handler; + nodes; + stack; + templates; + constructor(root, handler) { + this.handler = handler; + this.initialize(root); + } + initialize(root) { + this.global = {}; + this.local = {}; + this.root = root; + this.nodes = [root]; + this.stack = [root]; + this.templates = {}; + } + createListener(event_name, element, bubbles) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = { active: 1, callback: this.handler }; + this.root.addEventListener(event_name, this.handler); + } else { + this.global[event_name].active++; + } + } else { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, this.handler); + } + } + removeListener(element, event_name, bubbles) { + if (bubbles) { + this.removeBubblingListener(event_name); + } else { + this.removeNonBubblingListener(element, event_name); + } + } + removeBubblingListener(event_name) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + removeNonBubblingListener(element, event_name) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (Object.keys(this.local[id]).length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + } + removeAllNonBubblingListeners(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } + getNode(id) { + return this.nodes[id]; + } + appendChildren(id, many) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + for (let k = 0;k < many; k++) { + root.appendChild(els[k]); + } + } +} + +// src/ts/form.ts +function retriveValues(event, target) { + let contents = { + values: {} + }; + let form = target.closest("form"); + if (form) { + if (event.type === "input" || event.type === "change" || event.type === "submit" || event.type === "reset" || event.type === "click") { + contents = retrieveFormValues(form); + } + } + return contents; +} +function retrieveFormValues(form) { + const formData = new FormData(form); + const contents = {}; + formData.forEach((value, key) => { + if (contents[key]) { + contents[key] += "," + value; + } else { + contents[key] = value; + } + }); + return { + valid: form.checkValidity(), + values: contents + }; +} +function retriveSelectValue(target) { + let options = target.selectedOptions; + let values = []; + for (let i = 0;i < options.length; i++) { + values.push(options[i].value); + } + return values; +} + +// src/ts/serialize.ts +function serializeEvent(event, target) { + let contents = {}; + let extend = (obj) => contents = { ...contents, ...obj }; + if (event instanceof WheelEvent) { + extend(serializeWheelEvent(event)); + } + if (event instanceof MouseEvent) { + extend(serializeMouseEvent(event)); + } + if (event instanceof KeyboardEvent) { + extend(serializeKeyboardEvent(event)); + } + if (event instanceof InputEvent) { + extend(serializeInputEvent(event, target)); + } + if (event instanceof PointerEvent) { + extend(serializePointerEvent(event)); + } + if (event instanceof AnimationEvent) { + extend(serializeAnimationEvent(event)); + } + if (event instanceof TransitionEvent) { + extend({ property_name: event.propertyName, elapsed_time: event.elapsedTime, pseudo_element: event.pseudoElement }); + } + if (event instanceof CompositionEvent) { + extend({ data: event.data }); + } + if (event instanceof DragEvent) { + extend(serializeDragEvent(event)); + } + if (event instanceof FocusEvent) { + extend({}); + } + if (event instanceof ClipboardEvent) { + extend({}); + } + if (typeof TouchEvent !== "undefined" && event instanceof TouchEvent) { + extend(serializeTouchEvent(event)); + } + if (event.type === "submit" || event.type === "reset" || event.type === "click" || event.type === "change" || event.type === "input") { + extend(serializeInputEvent(event, target)); + } + if (event instanceof DragEvent) { + } + return contents; +} +var serializeInputEvent = function(event, target) { + let contents = {}; + if (target instanceof HTMLElement) { + let values = retriveValues(event, target); + contents.values = values.values; + contents.valid = values.valid; + } + if (event.target instanceof HTMLInputElement) { + let target2 = event.target; + let value = target2.value ?? target2.textContent ?? ""; + if (target2.type === "checkbox") { + value = target2.checked ? "true" : "false"; + } else if (target2.type === "radio") { + value = target2.value; + } + contents.value = value; + } + if (event.target instanceof HTMLTextAreaElement) { + contents.value = event.target.value; + } + if (event.target instanceof HTMLSelectElement) { + contents.value = retriveSelectValue(event.target).join(","); + } + if (contents.value === undefined) { + contents.value = ""; + } + return contents; +}; +var serializeWheelEvent = function(event) { + return { + delta_x: event.deltaX, + delta_y: event.deltaY, + delta_z: event.deltaZ, + delta_mode: event.deltaMode + }; +}; +var serializeTouchEvent = function(event) { + return { + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + shift_key: event.shiftKey, + changed_touches: event.changedTouches, + target_touches: event.targetTouches, + touches: event.touches + }; +}; +var serializePointerEvent = function(event) { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + pointer_id: event.pointerId, + width: event.width, + height: event.height, + pressure: event.pressure, + tangential_pressure: event.tangentialPressure, + tilt_x: event.tiltX, + tilt_y: event.tiltY, + twist: event.twist, + pointer_type: event.pointerType, + is_primary: event.isPrimary + }; +}; +var serializeMouseEvent = function(event) { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + offset_x: event.offsetX, + offset_y: event.offsetY, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey + }; +}; +var serializeKeyboardEvent = function(event) { + return { + char_code: event.charCode, + is_composing: event.isComposing, + key: event.key, + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + key_code: event.keyCode, + shift_key: event.shiftKey, + location: event.location, + repeat: event.repeat, + which: event.which, + code: event.code + }; +}; +var serializeAnimationEvent = function(event) { + return { + animation_name: event.animationName, + elapsed_time: event.elapsedTime, + pseudo_element: event.pseudoElement + }; +}; +var serializeDragEvent = function(event) { + return { + mouse: { + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + shift_key: event.shiftKey, + ...serializeMouseEvent(event) + }, + files: { + files: { + a: [1, 2, 3] + } + } + }; +}; + +// src/ts/set_attribute.ts +function setAttributeInner(node, field, value, ns) { + if (ns === "style") { + node.style.setProperty(field, value); + return; + } + if (!!ns) { + node.setAttributeNS(ns, field, value); + return; + } + switch (field) { + case "value": + if (node.value !== value) { + node.value = value; + } + break; + case "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "initial_checked": + node.defaultChecked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + break; + case "initial_selected": + node.defaultSelected = truthy(value); + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + if (!truthy(value) && isBoolAttr(field)) { + node.removeAttribute(field); + } else { + node.setAttribute(field, value); + } + } +} +var truthy = function(val) { + return val === "true" || val === true; +}; +var isBoolAttr = function(field) { + switch (field) { + case "allowfullscreen": + case "allowpaymentrequest": + case "async": + case "autofocus": + case "autoplay": + case "checked": + case "controls": + case "default": + case "defer": + case "disabled": + case "formnovalidate": + case "hidden": + case "ismap": + case "itemscope": + case "loop": + case "multiple": + case "muted": + case "nomodule": + case "novalidate": + case "open": + case "playsinline": + case "readonly": + case "required": + case "reversed": + case "selected": + case "truespeed": + case "webkitdirectory": + return true; + default: + return false; + } +}; + +// src/ts/interpreter_native.ts +var targetId = function(target) { + if (!(target instanceof Node)) { + return null; + } + let ourTarget = target; + let realId = null; + while (realId == null) { + if (ourTarget === null) { + return null; + } + if (ourTarget instanceof Element) { + realId = ourTarget.getAttribute(`data-dioxus-id`); + } + ourTarget = ourTarget.parentNode; + } + return parseInt(realId); +}; + +class PlatformInterpreter extends Interpreter { + intercept_link_redirects; + ipc; + liveview; + constructor(root) { + super(root, (event) => this.handleEvent(event, event.type, true)); + this.intercept_link_redirects = true; + this.liveview = false; + window.addEventListener("dragover", function(e) { + if (e.target instanceof Element && e.target.tagName != "INPUT") { + e.preventDefault(); + } + }, false); + window.addEventListener("drop", function(e) { + if (e.target instanceof Element && e.target.tagName != "INPUT") { + e.preventDefault(); + } + }, false); + this.ipc = window.ipc; + } + serializeIpcMessage(method, params = {}) { + return JSON.stringify({ method, params }); + } + setAttributeInner(node, field, value, ns) { + setAttributeInner(node, field, value, ns); + } + scrollTo(id, behavior) { + const node = this.nodes[id]; + if (node instanceof HTMLElement) { + node.scrollIntoView({ behavior }); + } + } + getClientRect(id) { + const node = this.nodes[id]; + if (node instanceof HTMLElement) { + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [rect.x, rect.y], + size: [rect.width, rect.height] + }; + } + } + setFocus(id, focus) { + const node = this.nodes[id]; + if (node instanceof HTMLElement) { + if (focus) { + node.focus(); + } else { + node.blur(); + } + } + } + LoadChild(array) { + let node = this.stack[this.stack.length - 1]; + for (let i = 0;i < array.length; i++) { + let end = array[i]; + for (node = node.firstChild;end > 0; end--) { + node = node.nextSibling; + } + } + return node; + } + AppendChildren(id, many) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + for (let k = 0;k < many; k++) { + root.appendChild(els[k]); + } + } + handleEvent(event, name, bubbles) { + const target = event.target; + const realId = targetId(target); + const contents = serializeEvent(event, target); + let body = { + name, + data: contents, + element: realId, + bubbles }; - return function (d, b) { - if (typeof b !== "function" && b !== null) - throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); + this.preventDefaults(event, target); + if (this.liveview) { + if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { + if (target.getAttribute("type") === "file") { + this.readFiles(target, contents, bubbles, realId, name); + } + } + } else { + const message = this.serializeIpcMessage("user_event", body); + this.ipc.postMessage(message); + } + } + async readFiles(target, contents, bubbles, realId, name) { + let files = target.files; + let file_contents = {}; + for (let i = 0;i < files.length; i++) { + const file = files[i]; + file_contents[file.name] = Array.from(new Uint8Array(await file.arrayBuffer())); + } + contents.files = { files: file_contents }; + const message = this.serializeIpcMessage("user_event", { + name, + element: realId, + data: contents, + bubbles }); + this.ipc.postMessage(message); + } + preventDefaults(event, target) { + let preventDefaultRequests = null; + if (target instanceof Element) { + preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); + } + if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { + event.preventDefault(); + } + if (event.type === "submit") { + event.preventDefault(); + } + if (target instanceof Element && event.type === "click") { + this.handleClickNavigate(event, target, preventDefaultRequests); + } + } + handleClickNavigate(event, target, preventDefaultRequests) { + if (!this.intercept_link_redirects) { + return; + } + if (target.tagName === "BUTTON" && event.type == "submit") { + event.preventDefault(); + } + let a_element = target.closest("a"); + if (a_element == null) { + return; + } + event.preventDefault(); + let elementShouldPreventDefault = preventDefaultRequests && preventDefaultRequests.includes(`onclick`); + let aElementShouldPreventDefault = a_element.getAttribute(`dioxus-prevent-default`); + let linkShouldPreventDefault = aElementShouldPreventDefault && aElementShouldPreventDefault.includes(`onclick`); + if (!elementShouldPreventDefault && !linkShouldPreventDefault) { + const href = a_element.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + this.ipc.postMessage(this.serializeIpcMessage("browser_open", { href })); + } + } + } +} +export { + PlatformInterpreter }; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -System.register("form", [], function (exports_1, context_1) { - "use strict"; - var __moduleName = context_1 && context_1.id; - function retriveValues(event, target) { - var contents = {}; - if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) { - retrieveFormValues(target, contents); - } - if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) { - retriveInputsValues(target, contents); - } - return contents; - } - exports_1("retriveValues", retriveValues); - function retrieveFormValues(form, contents) { - var formData = new FormData(form); - for (var name_1 in formData.keys()) { - var element = form.elements.namedItem(name_1); - if (!(element instanceof HTMLInputElement)) { - continue; - } - switch (element.type) { - case "select-multiple": - contents[name_1] = formData.getAll(name_1); - break; - default: - contents[name_1] = [formData.get(name_1)]; - break; - } - } - } - exports_1("retrieveFormValues", retrieveFormValues); - function retriveInputsValues(target, contents) { - var selectData = target.options; - contents["options"] = []; - for (var i = 0; i < selectData.length; i++) { - var option = selectData[i]; - if (option.selected) { - contents["options"].push(option.value.toString()); - } - } - } - exports_1("retriveInputsValues", retriveInputsValues); - return { - setters: [], - execute: function () { - } - }; -}); -System.register("interpreter_core", [], function (exports_2, context_2) { - "use strict"; - var Interpreter; - var __moduleName = context_2 && context_2.id; - return { - setters: [], - execute: function () { - Interpreter = (function () { - function Interpreter(root, handler) { - this.root = root; - this.nodes = [root]; - this.stack = [root]; - this.global = {}; - this.local = {}; - this.handler = handler; - } - Interpreter.prototype.createListener = function (event_name, element, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = { active: 1, callback: this.handler }; - this.root.addEventListener(event_name, this.handler); - } - else { - this.global[event_name].active++; - } - } - else { - var id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - }; - Interpreter.prototype.removeListener = function (element, event_name, bubbles) { - if (bubbles) { - this.removeBubblingListener(event_name); - } - else { - this.removeNonBubblingListener(element, event_name); - } - }; - Interpreter.prototype.removeBubblingListener = function (event_name) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - }; - Interpreter.prototype.removeNonBubblingListener = function (element, event_name) { - var id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (Object.keys(this.local[id]).length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - }; - Interpreter.prototype.removeAllNonBubblingListeners = function (element) { - var id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - }; - Interpreter.prototype.getNode = function (id) { - return this.nodes[id]; - }; - Interpreter.prototype.appendChildren = function (id, many) { - var root = this.nodes[id]; - var els = this.stack.splice(this.stack.length - many); - for (var k = 0; k < many; k++) { - root.appendChild(els[k]); - } - }; - return Interpreter; - }()); - exports_2("Interpreter", Interpreter); - } - }; -}); -System.register("serialize", [], function (exports_3, context_3) { - "use strict"; - var __moduleName = context_3 && context_3.id; - function serializeEvent(event) { - var _a; - if (event instanceof InputEvent) { - if (event.target instanceof HTMLInputElement) { - var target = event.target; - var value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; - if (target.type === "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - values: {}, - }; - } - return {}; - } - if (event instanceof KeyboardEvent) { - return { - char_code: event.charCode, - is_composing: event.isComposing, - key: event.key, - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - key_code: event.keyCode, - shift_key: event.shiftKey, - location: event.location, - repeat: event.repeat, - which: event.which, - code: event.code, - }; - } - if (event instanceof MouseEvent) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - offset_x: event.offsetX, - offset_y: event.offsetY, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - }; - } - if (event instanceof PointerEvent) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - pointer_id: event.pointerId, - width: event.width, - height: event.height, - pressure: event.pressure, - tangential_pressure: event.tangentialPressure, - tilt_x: event.tiltX, - tilt_y: event.tiltY, - twist: event.twist, - pointer_type: event.pointerType, - is_primary: event.isPrimary, - }; - } - if (event instanceof TouchEvent) { - return { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - changed_touches: event.changedTouches, - target_touches: event.targetTouches, - touches: event.touches, - }; - } - if (event instanceof WheelEvent) { - return { - delta_x: event.deltaX, - delta_y: event.deltaY, - delta_z: event.deltaZ, - delta_mode: event.deltaMode, - }; - } - if (event instanceof AnimationEvent) { - return { - animation_name: event.animationName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - } - if (event instanceof TransitionEvent) { - return { - property_name: event.propertyName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - } - if (event instanceof ClipboardEvent) { - return {}; - } - if (event instanceof CompositionEvent) { - return { - data: event.data, - }; - } - if (event instanceof DragEvent) { - return { - mouse: { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - }, - files: [], - }; - } - if (event instanceof FocusEvent) { - return {}; - } - return {}; - } - exports_3("serializeEvent", serializeEvent); - return { - setters: [], - execute: function () { - } - }; -}); -System.register("interpreter_native", ["form", "interpreter_core", "serialize"], function (exports_4, context_4) { - "use strict"; - var form_1, interpreter_core_1, serialize_1, NativeInterpreter; - var __moduleName = context_4 && context_4.id; - function handleVirtualdomEventSync(contents) { - var xhr = new XMLHttpRequest(); - xhr.timeout = 1000; - xhr.open("GET", "/handle/event.please", false); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.send(contents); - return JSON.parse(xhr.responseText); - } - function targetId(target) { - if (!(target instanceof Node)) { - return null; - } - var ourTarget = target; - var realId = null; - while (realId == null) { - if (ourTarget === null) { - return null; - } - if (ourTarget instanceof Element) { - realId = ourTarget.getAttribute("data-dioxus-id"); - } - ourTarget = ourTarget.parentNode; - } - return parseInt(realId); - } - return { - setters: [ - function (form_1_1) { - form_1 = form_1_1; - }, - function (interpreter_core_1_1) { - interpreter_core_1 = interpreter_core_1_1; - }, - function (serialize_1_1) { - serialize_1 = serialize_1_1; - } - ], - execute: function () { - NativeInterpreter = (function (_super) { - __extends(NativeInterpreter, _super); - function NativeInterpreter(root) { - var _this = _super.call(this, root, function (event) { return _this.handleEvent(event, event.type, true); }) || this; - _this.intercept_link_redirects = true; - _this.liveview = false; - _this.ipc = window.ipc; - return _this; - } - NativeInterpreter.prototype.serializeIpcMessage = function (method, params) { - if (params === void 0) { params = {}; } - return JSON.stringify({ method: method, params: params }); - }; - NativeInterpreter.prototype.scrollTo = function (id, behavior) { - var node = this.nodes[id]; - if (node instanceof HTMLElement) { - node.scrollIntoView({ behavior: behavior }); - } - }; - NativeInterpreter.prototype.getClientRect = function (id) { - var node = this.nodes[id]; - if (node instanceof HTMLElement) { - var rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; - } - }; - NativeInterpreter.prototype.setFocus = function (id, focus) { - var node = this.nodes[id]; - if (node instanceof HTMLElement) { - if (focus) { - node.focus(); - } - else { - node.blur(); - } - } - }; - NativeInterpreter.prototype.LoadChild = function (array) { - var node = this.stack[this.stack.length - 1]; - for (var i = 0; i < array.length; i++) { - var end = array[i]; - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - return node; - }; - NativeInterpreter.prototype.AppendChildren = function (id, many) { - var root = this.nodes[id]; - var els = this.stack.splice(this.stack.length - many); - for (var k = 0; k < many; k++) { - root.appendChild(els[k]); - } - }; - NativeInterpreter.prototype.handleEvent = function (event, name, bubbles) { - var target = event.target; - var realId = targetId(target); - var contents = serialize_1.serializeEvent(event); - if (target instanceof HTMLElement) { - contents.values = form_1.retriveValues(event, target); - } - var body = { - name: name, - data: contents, - element: realId, - bubbles: bubbles, - }; - this.preventDefaults(event, target); - if (this.liveview) { - if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { - if (target.getAttribute("type") === "file") { - this.readFiles(target, contents, bubbles, realId, name); - } - } - } - else { - var res = handleVirtualdomEventSync(JSON.stringify(body)); - if (res.preventDefault) { - event.preventDefault(); - } - if (res.stopPropagation) { - event.stopPropagation(); - } - } - }; - NativeInterpreter.prototype.readFiles = function (target, contents, bubbles, realId, name) { - return __awaiter(this, void 0, void 0, function () { - var files, file_contents, i, file, _a, _b, _c, _d, _e, message; - return __generator(this, function (_f) { - switch (_f.label) { - case 0: - files = target.files; - file_contents = {}; - i = 0; - _f.label = 1; - case 1: - if (!(i < files.length)) return [3, 4]; - file = files[i]; - _a = file_contents; - _b = file.name; - _d = (_c = Array).from; - _e = Uint8Array.bind; - return [4, file.arrayBuffer()]; - case 2: - _a[_b] = _d.apply(_c, [new (_e.apply(Uint8Array, [void 0, _f.sent()]))()]); - _f.label = 3; - case 3: - i++; - return [3, 1]; - case 4: - contents.files = { files: file_contents }; - message = this.serializeIpcMessage("user_event", { - name: name, - element: realId, - data: contents, - bubbles: bubbles, - }); - this.ipc.postMessage(message); - return [2]; - } - }); - }); - }; - NativeInterpreter.prototype.preventDefaults = function (event, target) { - var preventDefaultRequests = null; - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute("dioxus-prevent-default"); - } - if (preventDefaultRequests && preventDefaultRequests.includes("on".concat(event.type))) { - event.preventDefault(); - } - if (event.type === "submit") { - event.preventDefault(); - } - if (target instanceof Element && event.type === "click") { - this.handleClickNavigate(event, target, preventDefaultRequests); - } - }; - NativeInterpreter.prototype.handleClickNavigate = function (event, target, preventDefaultRequests) { - if (!this.intercept_link_redirects) { - return; - } - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - var a_element = target.closest("a"); - if (a_element == null) { - return; - } - event.preventDefault(); - var elementShouldPreventDefault = preventDefaultRequests && preventDefaultRequests.includes("onclick"); - var aElementShouldPreventDefault = a_element.getAttribute("dioxus-prevent-default"); - var linkShouldPreventDefault = aElementShouldPreventDefault && - aElementShouldPreventDefault.includes("onclick"); - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - var href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - this.ipc.postMessage(this.serializeIpcMessage("browser_open", { href: href })); - } - } - }; - return NativeInterpreter; - }(interpreter_core_1.Interpreter)); - exports_4("NativeInterpreter", NativeInterpreter); - } - }; -}); diff --git a/packages/interpreter/src/js/web.js b/packages/interpreter/src/js/web.js index 287bc6b8d..77bc50da9 100644 --- a/packages/interpreter/src/js/web.js +++ b/packages/interpreter/src/js/web.js @@ -1,159 +1,130 @@ -var __extends = (this && this.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - if (typeof b !== "function" && b !== null) - throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -System.register("interpreter_core", [], function (exports_1, context_1) { - "use strict"; - var Interpreter; - var __moduleName = context_1 && context_1.id; - return { - setters: [], - execute: function () { - Interpreter = (function () { - function Interpreter(root, handler) { - this.root = root; - this.nodes = [root]; - this.stack = [root]; - this.global = {}; - this.local = {}; - this.handler = handler; - } - Interpreter.prototype.createListener = function (event_name, element, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = { active: 1, callback: this.handler }; - this.root.addEventListener(event_name, this.handler); - } - else { - this.global[event_name].active++; - } - } - else { - var id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - }; - Interpreter.prototype.removeListener = function (element, event_name, bubbles) { - if (bubbles) { - this.removeBubblingListener(event_name); - } - else { - this.removeNonBubblingListener(element, event_name); - } - }; - Interpreter.prototype.removeBubblingListener = function (event_name) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - }; - Interpreter.prototype.removeNonBubblingListener = function (element, event_name) { - var id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (Object.keys(this.local[id]).length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - }; - Interpreter.prototype.removeAllNonBubblingListeners = function (element) { - var id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - }; - Interpreter.prototype.getNode = function (id) { - return this.nodes[id]; - }; - Interpreter.prototype.appendChildren = function (id, many) { - var root = this.nodes[id]; - var els = this.stack.splice(this.stack.length - many); - for (var k = 0; k < many; k++) { - root.appendChild(els[k]); - } - }; - return Interpreter; - }()); - exports_1("Interpreter", Interpreter); +// src/ts/interpreter_core.ts +class Interpreter { + global; + local; + root; + handler; + nodes; + stack; + templates; + constructor(root, handler) { + this.handler = handler; + this.initialize(root); + } + initialize(root) { + this.global = {}; + this.local = {}; + this.root = root; + this.nodes = [root]; + this.stack = [root]; + this.templates = {}; + } + createListener(event_name, element, bubbles) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = { active: 1, callback: this.handler }; + this.root.addEventListener(event_name, this.handler); + } else { + this.global[event_name].active++; + } + } else { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, this.handler); + } + } + removeListener(element, event_name, bubbles) { + if (bubbles) { + this.removeBubblingListener(event_name); + } else { + this.removeNonBubblingListener(element, event_name); + } + } + removeBubblingListener(event_name) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + removeNonBubblingListener(element, event_name) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (Object.keys(this.local[id]).length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + } + removeAllNonBubblingListeners(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } + getNode(id) { + return this.nodes[id]; + } + appendChildren(id, many) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + for (let k = 0;k < many; k++) { + root.appendChild(els[k]); + } + } +} + +// src/ts/interpreter_web.ts +class PlatformInterpreter extends Interpreter { + m; + constructor(root, handler) { + super(root, handler); + } + LoadChild(ptr, len) { + let node = this.stack[this.stack.length - 1]; + let ptr_end = ptr + len; + for (;ptr < ptr_end; ptr++) { + let end = this.m.getUint8(ptr); + for (node = node.firstChild;end > 0; end--) { + node = node.nextSibling; + } + } + return node; + } + saveTemplate(nodes, tmpl_id) { + this.templates[tmpl_id] = nodes; + } + hydrateRoot(ids) { + const hydrateNodes = document.querySelectorAll("[data-node-hydration]"); + for (let i = 0;i < hydrateNodes.length; i++) { + const hydrateNode = hydrateNodes[i]; + const hydration = hydrateNode.getAttribute("data-node-hydration"); + const split = hydration.split(","); + const id = ids[parseInt(split[0])]; + this.nodes[id] = hydrateNode; + if (split.length > 1) { + hydrateNode.listening = split.length - 1; + hydrateNode.setAttribute("data-dioxus-id", id.toString()); + for (let j = 1;j < split.length; j++) { + const listener = split[j]; + const split2 = listener.split(":"); + const event_name = split2[0]; + const bubbles = split2[1] === "1"; + this.createListener(event_name, hydrateNode, bubbles); } - }; -}); -System.register("interpreter_web", ["interpreter_core"], function (exports_2, context_2) { - "use strict"; - var interpreter_core_1, WebInterpreter; - var __moduleName = context_2 && context_2.id; - return { - setters: [ - function (interpreter_core_1_1) { - interpreter_core_1 = interpreter_core_1_1; - } - ], - execute: function () { - WebInterpreter = (function (_super) { - __extends(WebInterpreter, _super); - function WebInterpreter(root, handler) { - return _super.call(this, root, handler) || this; - } - WebInterpreter.prototype.LoadChild = function (ptr, len) { - var node = this.stack[this.stack.length - 1]; - var ptr_end = ptr + len; - for (; ptr < ptr_end; ptr++) { - var end = this.m.getUint8(ptr); - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - return node; - }; - WebInterpreter.prototype.saveTemplate = function (nodes, tmpl_id) { - this.templates[tmpl_id] = nodes; - }; - WebInterpreter.prototype.hydrateRoot = function (ids) { - var hydrateNodes = document.querySelectorAll('[data-node-hydration]'); - for (var i = 0; i < hydrateNodes.length; i++) { - var hydrateNode = hydrateNodes[i]; - var hydration = hydrateNode.getAttribute('data-node-hydration'); - var split = hydration.split(','); - var id = ids[parseInt(split[0])]; - this.nodes[id] = hydrateNode; - if (split.length > 1) { - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute('data-dioxus-id', id.toString()); - for (var j = 1; j < split.length; j++) { - var listener = split[j]; - var split2 = listener.split(':'); - var event_name = split2[0]; - var bubbles = split2[1] === '1'; - this.createListener(event_name, hydrateNode, bubbles); - } - } - } - var treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT); - var currentNode = treeWalker.nextNode(); - while (currentNode) { - var id = currentNode.textContent; - var split = id.split('node-id'); - if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - currentNode = treeWalker.nextNode(); - } - }; - return WebInterpreter; - }(interpreter_core_1.Interpreter)); - exports_2("WebInterpreter", WebInterpreter); - } - }; -}); + } + } + const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT); + let currentNode = treeWalker.nextNode(); + while (currentNode) { + const id = currentNode.textContent; + const split = id.split("node-id"); + if (split.length > 1) { + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; + } + currentNode = treeWalker.nextNode(); + } + } +} +export { + PlatformInterpreter +}; diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index f3a87dff7..d7112aec2 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -24,6 +24,7 @@ pub mod minimal_bindings { /// Set the attribute of the node pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>); + /// Roll up all the values from the node into a JS object that we can deserialize pub fn collectFormValues(node: JsValue) -> JsValue; } } diff --git a/packages/interpreter/src/ts/form.ts b/packages/interpreter/src/ts/form.ts index 7f4581309..9f76e3c1b 100644 --- a/packages/interpreter/src/ts/form.ts +++ b/packages/interpreter/src/ts/form.ts @@ -1,53 +1,58 @@ // Consistently deserialize forms and form elements for use across web/desktop/mobile -type FormValues = { [key: string]: FormDataEntryValue[] }; +type FormValues = { + valid?: boolean; + values: { [key: string]: FormDataEntryValue }; +} export function retriveValues(event: Event, target: HTMLElement): FormValues { - const contents: FormValues = {}; + let contents: FormValues = { + values: {} + }; - if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) { - retrieveFormValues(target, contents); - } + // If there's a form... + let form = target.closest("form"); - if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) { - retriveInputsValues(target, contents); + // If the target is an input, and the event is input or change, we want to get the value without going through the form + if (form) { + if ( + event.type === "input" + || event.type === "change" + || event.type === "submit" + || event.type === "reset" + || event.type === "click" + ) { + contents = retrieveFormValues(form); + } } return contents; } -export function retrieveFormValues(form: HTMLFormElement, contents: FormValues) { +// todo: maybe encode spaces or something? +// We encode select multiple as a comma separated list which breaks... when there's commas in the values +export function retrieveFormValues(form: HTMLFormElement): FormValues { const formData = new FormData(form); - - for (let name in formData.keys()) { - let element = form.elements.namedItem(name); - - // todo: this is going to be a problem for select-multiple? - if (!(element instanceof HTMLInputElement)) { - continue; + const contents: { [key: string]: FormDataEntryValue } = {}; + formData.forEach((value, key) => { + if (contents[key]) { + contents[key] += "," + value; + } else { + contents[key] = value; } - - switch (element.type) { - case "select-multiple": - contents[name] = formData.getAll(name); - break; - - // By default, it's just a single value - default: - contents[name] = [formData.get(name)]; - break; - } - } + }); + return { + valid: form.checkValidity(), + values: contents + }; } -export function retriveInputsValues(target: HTMLSelectElement, contents: FormValues,) { - const selectData = target.options; - contents["options"] = []; - - for (let i = 0; i < selectData.length; i++) { - let option = selectData[i]; - if (option.selected) { - contents["options"].push(option.value.toString()); - } +export function retriveSelectValue(target: HTMLSelectElement): string[] { + // there might be multiple... + let options = target.selectedOptions; + let values = []; + for (let i = 0; i < options.length; i++) { + values.push(options[i].value); } + return values; } diff --git a/packages/interpreter/src/ts/interpreter_core.ts b/packages/interpreter/src/ts/interpreter_core.ts index ba8213954..ade777fb7 100644 --- a/packages/interpreter/src/ts/interpreter_core.ts +++ b/packages/interpreter/src/ts/interpreter_core.ts @@ -1,6 +1,8 @@ // The root interpreter class that holds state about the mapping between DOM and VirtualDom // This always lives in the JS side of things, and is extended by the native and web interpreters +export type NodeId = number; + export class Interpreter { // non bubbling events listen at the element the listener was created at global: { @@ -22,12 +24,18 @@ export class Interpreter { }; constructor(root: HTMLElement, handler: EventListener) { - this.root = root; - this.nodes = [root]; - this.stack = [root]; + this.handler = handler; + this.initialize(root); + } + + initialize(root: HTMLElement) { this.global = {}; this.local = {}; - this.handler = handler; + this.root = root; + + this.nodes = [root]; + this.stack = [root]; + this.templates = {}; } createListener(event_name: string, element: HTMLElement, bubbles: boolean) { @@ -77,11 +85,11 @@ export class Interpreter { delete this.local[id]; } - getNode(id: number): Node { + getNode(id: NodeId): Node { return this.nodes[id]; } - appendChildren(id: number, many: number) { + appendChildren(id: NodeId, many: number) { const root = this.nodes[id]; const els = this.stack.splice(this.stack.length - many); for (let k = 0; k < many; k++) { diff --git a/packages/interpreter/src/ts/interpreter_native.ts b/packages/interpreter/src/ts/interpreter_native.ts index ffdab3567..968bad631 100644 --- a/packages/interpreter/src/ts/interpreter_native.ts +++ b/packages/interpreter/src/ts/interpreter_native.ts @@ -3,12 +3,11 @@ // This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't // provide since it doesn't have access to the dom. -import { retriveValues } from "./form"; -import { Interpreter } from "./interpreter_core"; +import { Interpreter, NodeId } from "./interpreter_core"; import { SerializedEvent, serializeEvent } from "./serialize"; +import { setAttributeInner } from "./set_attribute"; - -export class NativeInterpreter extends Interpreter { +export class PlatformInterpreter extends Interpreter { intercept_link_redirects: boolean; ipc: any; @@ -21,6 +20,23 @@ export class NativeInterpreter extends Interpreter { this.intercept_link_redirects = true; this.liveview = false; + // attach an event listener on the body that prevents file drops from navigating + // this is because the browser will try to navigate to the file if it's dropped on the window + window.addEventListener("dragover", function (e) { + // check which element is our target + if (e.target instanceof Element && e.target.tagName != "INPUT") { + e.preventDefault(); + } + }, false); + + window.addEventListener("drop", function (e) { + // check which element is our target + if (e.target instanceof Element && e.target.tagName != "INPUT") { + e.preventDefault(); + } + }, false); + + // @ts-ignore - wry gives us this this.ipc = window.ipc; } @@ -29,14 +45,18 @@ export class NativeInterpreter extends Interpreter { return JSON.stringify({ method, params }); } - scrollTo(id: number, behavior: ScrollBehavior) { + setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { + setAttributeInner(node, field, value, ns); + } + + scrollTo(id: NodeId, behavior: ScrollBehavior) { const node = this.nodes[id]; if (node instanceof HTMLElement) { node.scrollIntoView({ behavior }); } } - getClientRect(id: number) { + getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined { const node = this.nodes[id]; if (node instanceof HTMLElement) { const rect = node.getBoundingClientRect(); @@ -48,7 +68,7 @@ export class NativeInterpreter extends Interpreter { } } - setFocus(id: number, focus: boolean) { + setFocus(id: NodeId, focus: boolean) { const node = this.nodes[id]; if (node instanceof HTMLElement) { @@ -74,7 +94,7 @@ export class NativeInterpreter extends Interpreter { return node; } - AppendChildren(id: number, many: number) { + AppendChildren(id: NodeId, many: number) { const root = this.nodes[id]; const els = this.stack.splice(this.stack.length - many); @@ -86,12 +106,7 @@ export class NativeInterpreter extends Interpreter { handleEvent(event: Event, name: string, bubbles: boolean) { const target = event.target!; const realId = targetId(target)!; - const contents = serializeEvent(event); - - // Attempt to retrive the values from the form and inputs - if (target instanceof HTMLElement) { - contents.values = retriveValues(event, target); - } + const contents = serializeEvent(event, target); // Handle the event on the virtualdom and then preventDefault if it also preventsDefault // Some listeners @@ -119,21 +134,26 @@ export class NativeInterpreter extends Interpreter { } } else { - // Run the event handler on the virtualdom - // capture/prevent default of the event if the virtualdom wants to - const res = handleVirtualdomEventSync(JSON.stringify(body)); + const message = this.serializeIpcMessage("user_event", body); + this.ipc.postMessage(message); - if (res.preventDefault) { - event.preventDefault(); - } + // // Run the event handler on the virtualdom + // // capture/prevent default of the event if the virtualdom wants to + // const res = handleVirtualdomEventSync(JSON.stringify(body)); - if (res.stopPropagation) { - event.stopPropagation(); - } + // if (res.preventDefault) { + // event.preventDefault(); + // } + + // if (res.stopPropagation) { + // event.stopPropagation(); + // } } } - async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: number, name: string) { + // A liveview only function + // Desktop will intercept the event before it hits this + async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) { let files = target.files!; let file_contents: { [name: string]: number[] } = {}; @@ -251,7 +271,7 @@ function handleVirtualdomEventSync(contents: string): EventSyncResult { return JSON.parse(xhr.responseText); } -function targetId(target: EventTarget): number | null { +function targetId(target: EventTarget): NodeId | null { // Ensure that the target is a node, sometimes it's nota if (!(target instanceof Node)) { return null; diff --git a/packages/interpreter/src/ts/interpreter_web.ts b/packages/interpreter/src/ts/interpreter_web.ts index b393e3b1f..3f4eade17 100644 --- a/packages/interpreter/src/ts/interpreter_web.ts +++ b/packages/interpreter/src/ts/interpreter_web.ts @@ -6,7 +6,7 @@ import { Interpreter } from "./interpreter_core"; -export class WebInterpreter extends Interpreter { +export class PlatformInterpreter extends Interpreter { m: any; constructor(root: HTMLElement, handler: EventListener) { diff --git a/packages/interpreter/src/ts/serialize.ts b/packages/interpreter/src/ts/serialize.ts index 1cfd5990c..c9288ac3f 100644 --- a/packages/interpreter/src/ts/serialize.ts +++ b/packages/interpreter/src/ts/serialize.ts @@ -1,159 +1,217 @@ // Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {}; +import { retriveSelectValue, retriveValues } from "./form"; + +export type AppTouchEvent = TouchEvent; + export type SerializedEvent = { - values?: { [key: string]: FormDataEntryValue[] }; + values?: { [key: string]: FormDataEntryValue }; value?: string; [key: string]: any; }; -export function serializeEvent(event: Event): SerializedEvent { - if (event instanceof InputEvent) { - if (event.target instanceof HTMLInputElement) { - let target = event.target; - let value = target.value ?? target.textContent; - if (target.type === "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - values: {}, - }; - } - return {}; - } - - if (event instanceof KeyboardEvent) { - return { - char_code: event.charCode, - is_composing: event.isComposing, - key: event.key, - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - key_code: event.keyCode, - shift_key: event.shiftKey, - location: event.location, - repeat: event.repeat, - which: event.which, - code: event.code, - }; - } - - if (event instanceof MouseEvent) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - offset_x: event.offsetX, - offset_y: event.offsetY, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - }; - } - - if (event instanceof PointerEvent) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - pointer_id: event.pointerId, - width: event.width, - height: event.height, - pressure: event.pressure, - tangential_pressure: event.tangentialPressure, - tilt_x: event.tiltX, - tilt_y: event.tiltY, - twist: event.twist, - pointer_type: event.pointerType, - is_primary: event.isPrimary, - }; - } - - - if (event instanceof TouchEvent) { - return { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - changed_touches: event.changedTouches, - target_touches: event.targetTouches, - touches: event.touches, - }; - } - - if (event instanceof WheelEvent) { - return { - delta_x: event.deltaX, - delta_y: event.deltaY, - delta_z: event.deltaZ, - delta_mode: event.deltaMode, - }; - } - - if (event instanceof AnimationEvent) { - return { - animation_name: event.animationName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - } - - if (event instanceof TransitionEvent) { - return { - property_name: event.propertyName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - } - - if (event instanceof ClipboardEvent) { - return {}; - } - - if (event instanceof CompositionEvent) { - return { - data: event.data, - }; +export function serializeEvent(event: Event, target: EventTarget): SerializedEvent { + let contents = {}; + + // merge the object into the contents + let extend = (obj: any) => (contents = { ...contents, ...obj }); + + if (event instanceof WheelEvent) { extend(serializeWheelEvent(event)) }; + if (event instanceof MouseEvent) { extend(serializeMouseEvent(event)) } + if (event instanceof KeyboardEvent) { extend(serializeKeyboardEvent(event)) } + + if (event instanceof InputEvent) { extend(serializeInputEvent(event, target)) } + if (event instanceof PointerEvent) { extend(serializePointerEvent(event)) } + if (event instanceof AnimationEvent) { extend(serializeAnimationEvent(event)) } + if (event instanceof TransitionEvent) { extend({ property_name: event.propertyName, elapsed_time: event.elapsedTime, pseudo_element: event.pseudoElement, }) } + if (event instanceof CompositionEvent) { extend({ data: event.data, }) } + if (event instanceof DragEvent) { extend(serializeDragEvent(event)) } + if (event instanceof FocusEvent) { extend({}) } + if (event instanceof ClipboardEvent) { extend({}) } + + // safari is quirky and doesn't have TouchEvent + if (typeof TouchEvent !== 'undefined' && event instanceof TouchEvent) { extend(serializeTouchEvent(event)); } + + if (event.type === "submit" || event.type === "reset" || event.type === "click" || event.type === "change" || event.type === "input") { + extend(serializeInputEvent(event as InputEvent, target)); } + // If there's any files, we need to serialize them if (event instanceof DragEvent) { - // let files = []; - // if (event.dataTransfer && event.dataTransfer.files) { - // files = ["a", "b", "c"]; - // // files = await serializeFileList(event.dataTransfer.files); - // } - // return { mouse: get_mouse_data(event), files }; - return { - mouse: { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - }, - files: [], - }; + // let files: { [key: string]: Uint8Array } = {}; + // if (event.dataTransfer && event.dataTransfer.files) { + // files["a"] = new Uint8Array(0); + // // files = { + // // entries: Array.from(event.dataTransfer.files).map((file) => { + // // return { + // // name: file.name, + // // type: file.type, + // // size: file.size, + // // last_modified: file.lastModified, + // // }; + // // } + // // }; + // // files = await serializeFileList(event.dataTransfer.files); + // } + // extend({ files: files }); } - if (event instanceof FocusEvent) { - return {}; + return contents; +} + +function serializeInputEvent(event: InputEvent, target: EventTarget): SerializedEvent { + let contents: SerializedEvent = {}; + + // Attempt to retrieve the values from the form + if (target instanceof HTMLElement) { + let values = retriveValues(event, target); + contents.values = values.values; + contents.valid = values.valid; + } + + if (event.target instanceof HTMLInputElement) { + let target = event.target; + let value = target.value ?? target.textContent ?? ""; + + if (target.type === "checkbox") { + value = target.checked ? "true" : "false"; + } else if (target.type === "radio") { + value = target.value; + } + + contents.value = value; + } + + if (event.target instanceof HTMLTextAreaElement) { + contents.value = event.target.value; + } + + if (event.target instanceof HTMLSelectElement) { + contents.value = retriveSelectValue(event.target).join(","); + } + + // Ensure the serializer isn't quirky + if (contents.value === undefined) { + contents.value = ""; } + + + return contents; +} + + + +function serializeWheelEvent(event: WheelEvent): SerializedEvent { + return { + delta_x: event.deltaX, + delta_y: event.deltaY, + delta_z: event.deltaZ, + delta_mode: event.deltaMode, + }; +} + +function serializeTouchEvent(event: TouchEvent): SerializedEvent { + return { + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + shift_key: event.shiftKey, + changed_touches: event.changedTouches, + target_touches: event.targetTouches, + touches: event.touches, + }; +} + +function serializePointerEvent(event: PointerEvent): SerializedEvent { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + pointer_id: event.pointerId, + width: event.width, + height: event.height, + pressure: event.pressure, + tangential_pressure: event.tangentialPressure, + tilt_x: event.tiltX, + tilt_y: event.tiltY, + twist: event.twist, + pointer_type: event.pointerType, + is_primary: event.isPrimary, + }; +} + +function serializeMouseEvent(event: MouseEvent): SerializedEvent { + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + offset_x: event.offsetX, + offset_y: event.offsetY, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + }; +} + +function serializeKeyboardEvent(event: KeyboardEvent): SerializedEvent { + return { + char_code: event.charCode, + is_composing: event.isComposing, + key: event.key, + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + key_code: event.keyCode, + shift_key: event.shiftKey, + location: event.location, + repeat: event.repeat, + which: event.which, + code: event.code, + }; +} + +function serializeAnimationEvent(event: AnimationEvent): SerializedEvent { + return { + animation_name: event.animationName, + elapsed_time: event.elapsedTime, + pseudo_element: event.pseudoElement, + }; +} - return {}; +function serializeDragEvent(event: DragEvent): SerializedEvent { + // let files = []; + // if (event.dataTransfer && event.dataTransfer.files) { + // files = ["a", "b", "c"]; + // // files = await serializeFileList(event.dataTransfer.files); + // } + // return { mouse: get_mouse_data(event), files }; + return { + mouse: { + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + shift_key: event.shiftKey, + ...serializeMouseEvent(event), + }, + files: { + files: { + "a": [1, 2, 3], + } + }, + }; } diff --git a/packages/interpreter/src/ts/set_attribute.ts b/packages/interpreter/src/ts/set_attribute.ts index 5fcd3c91f..378fd479b 100644 --- a/packages/interpreter/src/ts/set_attribute.ts +++ b/packages/interpreter/src/ts/set_attribute.ts @@ -17,37 +17,36 @@ export function setAttributeInner(node: HTMLElement, field: string, value: strin // A few attributes are need to be set with either boolean values or require some sort of translation switch (field) { case "value": - if (value !== node.getAttribute("value")) { - node.setAttribute(field, value); + // @ts-ignore + if (node.value !== value) { + // @ts-ignore + node.value = value; } break; case "initial_value": - node.setAttribute("defaultValue", value); + // @ts-ignore + node.defaultValue = value; break; case "checked": - if (node instanceof HTMLInputElement) { - node.checked = truthy(value); - } + // @ts-ignore + node.checked = truthy(value); break; case "initial_checked": - if (node instanceof HTMLInputElement) { - node.defaultChecked = truthy(value); - } + // @ts-ignore + node.defaultChecked = truthy(value); break; case "selected": - if (node instanceof HTMLOptionElement) { - node.selected = truthy(value); - } + // @ts-ignore + node.selected = truthy(value); break; case "initial_selected": - if (node instanceof HTMLOptionElement) { - node.defaultSelected = truthy(value); - } + // @ts-ignore + node.defaultSelected = truthy(value); break; case "dangerous_inner_html": diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index e4e2e0aeb..36e28b5b9 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -6,6 +6,10 @@ use wasm_bindgen::prelude::wasm_bindgen; use sledgehammer_bindgen::bindgen; +pub fn native_js() -> String { + format!("{}\n{}", include_str!("./js/native.js"), GENERATED_JS,) +} + pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; /// Extensions to the interpreter that are specific to the web platform. @@ -55,7 +59,7 @@ mod js { "{this.stack.pop();}" } fn replace_with(id: u32, n: u16) { - "{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}" + "{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.removeAllNonBubblingListeners(root); } root.replaceWith(...this.els);}" } fn insert_after(id: u32, n: u16) { "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}" @@ -64,7 +68,7 @@ mod js { "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}" } fn remove(id: u32) { - "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}" + "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.removeAllNonBubblingListeners(node); } node.remove(); }}" } fn create_raw_text(text: &str) { "{this.stack.push(document.createTextNode($text$));}" @@ -76,10 +80,10 @@ mod js { "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}" } fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { - r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.listeners.create($event_name$, node, $bubbles$);"# + r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.createListener($event_name$, node, $bubbles$);"# } fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { - "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}" + "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.removeListener(node, $event_name$, $bubbles$);}" } fn set_text(id: u32, text: &str) { "{this.nodes[$id$].textContent = $text$;}" @@ -173,13 +177,13 @@ mod js { fn foreign_event_listener(event: &str, id: u32, bubbles: u8) { r#" bubbles = bubbles == 1; - let node = this.nodes[id]; - if(node.listening){ - node.listening += 1; + let this_node = this.nodes[id]; + if(this_node.listening){ + this_node.listening += 1; } else { - node.listening = 1; + this_node.listening = 1; } - node.setAttribute('data-dioxus-id', `\${id}`); + this_node.setAttribute('data-dioxus-id', `\${id}`); const event_name = $event$; // if this is a mounted listener, we send the event immediately @@ -193,7 +197,7 @@ mod js { }) ); } else { - this.listeners.create(event_name, node, bubbles, (event) => { + this.createListener(event_name, this_node, bubbles, (event) => { this.handler(event, event_name, bubbles); }); }"# diff --git a/packages/interpreter/src/write_native_mutations.rs b/packages/interpreter/src/write_native_mutations.rs index 8f07080a2..1f4adbd13 100644 --- a/packages/interpreter/src/write_native_mutations.rs +++ b/packages/interpreter/src/write_native_mutations.rs @@ -181,7 +181,8 @@ impl WriteMutations for MutationState { } fn create_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) { - // note that we use the foreign event listener here + // note that we use the foreign event listener here instead of the native one + // the native method assumes we have direct access to the dom, which we don't. self.channel .foreign_event_listener(name, id.0 as u32, event_bubbles(name) as u8); } diff --git a/packages/interpreter/tsconfig.json b/packages/interpreter/tsconfig.json index 0207f8ca7..6a9dd215a 100644 --- a/packages/interpreter/tsconfig.json +++ b/packages/interpreter/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "system", + "module": "CommonJS", "lib": [ "ES2015", "DOM", From 22266cc5608a6b34d3db5e49904fba70cb816fd3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 4 Mar 2024 15:23:48 -0800 Subject: [PATCH 13/41] Switch to bun, clean up web implementation --- packages/desktop/headless_tests/utils.rs | 2 + packages/desktop/js/prevent_file_upload.js | 2 +- packages/desktop/src/ipc.rs | 2 +- packages/desktop/src/protocol.rs | 2 +- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 5 +- packages/interpreter/src/js/web.js | 86 ++++++++++++++++++- packages/interpreter/src/lib.rs | 12 +-- .../interpreter/src/ts/interpreter_core.ts | 6 +- .../interpreter/src/ts/interpreter_web.ts | 1 + packages/interpreter/src/unified_bindings.rs | 60 +++++++------ packages/interpreter/tsconfig.native.json | 10 --- packages/interpreter/tsconfig.web.json | 10 --- packages/liveview/src/lib.rs | 2 +- packages/plasmo/src/hooks.rs | 5 +- packages/web/ric_raf/README.md | 3 + packages/web/{src => ric_raf}/ric_raf.rs | 0 packages/web/{src => ric_raf}/ricpolyfill.js | 0 packages/web/src/dom.rs | 71 ++++++++------- packages/web/src/event.rs | 27 ++---- packages/web/src/lib.rs | 22 ++--- packages/web/src/mutations.rs | 68 +++++++-------- packages/web/src/rehydrate.rs | 15 ++-- 23 files changed, 240 insertions(+), 173 deletions(-) delete mode 100644 packages/interpreter/tsconfig.native.json delete mode 100644 packages/interpreter/tsconfig.web.json create mode 100644 packages/web/ric_raf/README.md rename packages/web/{src => ric_raf}/ric_raf.rs (100%) rename packages/web/{src => ric_raf}/ricpolyfill.js (100%) diff --git a/packages/desktop/headless_tests/utils.rs b/packages/desktop/headless_tests/utils.rs index d8bd3fb2d..343e76467 100644 --- a/packages/desktop/headless_tests/utils.rs +++ b/packages/desktop/headless_tests/utils.rs @@ -1,3 +1,5 @@ +#![allow(unused)] // for whatever reason, the compiler is not recognizing the use of these functions + use dioxus::prelude::*; use dioxus_core::Element; diff --git a/packages/desktop/js/prevent_file_upload.js b/packages/desktop/js/prevent_file_upload.js index 73b2e7794..935415178 100644 --- a/packages/desktop/js/prevent_file_upload.js +++ b/packages/desktop/js/prevent_file_upload.js @@ -11,7 +11,7 @@ let target_id = find_real_id(target); if (target_id !== null) { const send = (event_name) => { - const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); + const message = window.interpreter.serializeIpcMessage("file_dialog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); window.ipc.postMessage(message); }; send("change&input"); diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs index c8810c63a..464b80423 100644 --- a/packages/desktop/src/ipc.rs +++ b/packages/desktop/src/ipc.rs @@ -49,7 +49,7 @@ impl IpcMessage { pub(crate) fn method(&self) -> IpcMethod { match self.method.as_str() { // todo: this is a misspelling, needs to be fixed - "file_diolog" => IpcMethod::FileDialog, + "file_dialog" => IpcMethod::FileDialog, "user_event" => IpcMethod::UserEvent, "query" => IpcMethod::Query, "browser_open" => IpcMethod::BrowserOpen, diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index ba349f61c..033d44fbd 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,5 +1,5 @@ use crate::{assets::*, edits::EditQueue}; -use dioxus_interpreter_js::unified_bindings::{native_js, SLEDGEHAMMER_JS}; +use dioxus_interpreter_js::unified_bindings::native_js; use std::path::{Path, PathBuf}; use wry::{ http::{status::StatusCode, Request, Response}, diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 134f6fe87..a6b01b52c 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -4429706825984325407 \ No newline at end of file +3599957386864841107 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 7a03ae59f..20ab039a9 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -11,13 +11,16 @@ class Interpreter { this.handler = handler; this.initialize(root); } - initialize(root) { + initialize(root, handler = null) { this.global = {}; this.local = {}; this.root = root; this.nodes = [root]; this.stack = [root]; this.templates = {}; + if (handler) { + this.handler = handler; + } } createListener(event_name, element, bubbles) { if (bubbles) { diff --git a/packages/interpreter/src/js/web.js b/packages/interpreter/src/js/web.js index 77bc50da9..1de671893 100644 --- a/packages/interpreter/src/js/web.js +++ b/packages/interpreter/src/js/web.js @@ -11,13 +11,16 @@ class Interpreter { this.handler = handler; this.initialize(root); } - initialize(root) { + initialize(root, handler = null) { this.global = {}; this.local = {}; this.root = root; this.nodes = [root]; this.stack = [root]; this.templates = {}; + if (handler) { + this.handler = handler; + } } createListener(event_name, element, bubbles) { if (bubbles) { @@ -73,6 +76,86 @@ class Interpreter { } } +// src/ts/set_attribute.ts +function setAttributeInner(node, field, value, ns) { + if (ns === "style") { + node.style.setProperty(field, value); + return; + } + if (!!ns) { + node.setAttributeNS(ns, field, value); + return; + } + switch (field) { + case "value": + if (node.value !== value) { + node.value = value; + } + break; + case "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "initial_checked": + node.defaultChecked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + break; + case "initial_selected": + node.defaultSelected = truthy(value); + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + if (!truthy(value) && isBoolAttr(field)) { + node.removeAttribute(field); + } else { + node.setAttribute(field, value); + } + } +} +var truthy = function(val) { + return val === "true" || val === true; +}; +var isBoolAttr = function(field) { + switch (field) { + case "allowfullscreen": + case "allowpaymentrequest": + case "async": + case "autofocus": + case "autoplay": + case "checked": + case "controls": + case "default": + case "defer": + case "disabled": + case "formnovalidate": + case "hidden": + case "ismap": + case "itemscope": + case "loop": + case "multiple": + case "muted": + case "nomodule": + case "novalidate": + case "open": + case "playsinline": + case "readonly": + case "required": + case "reversed": + case "selected": + case "truespeed": + case "webkitdirectory": + return true; + default: + return false; + } +}; + // src/ts/interpreter_web.ts class PlatformInterpreter extends Interpreter { m; @@ -126,5 +209,6 @@ class PlatformInterpreter extends Interpreter { } } export { + setAttributeInner, PlatformInterpreter }; diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index d7112aec2..1db7cda86 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -11,6 +11,12 @@ mod write_native_mutations; #[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))] pub use write_native_mutations::*; +#[cfg(feature = "sledgehammer")] +pub mod unified_bindings; + +#[cfg(feature = "sledgehammer")] +pub use unified_bindings::*; + // Common bindings for minimal usage. #[cfg(all(feature = "minimal_bindings", feature = "webonly"))] pub mod minimal_bindings { @@ -28,9 +34,3 @@ pub mod minimal_bindings { pub fn collectFormValues(node: JsValue) -> JsValue; } } - -#[cfg(feature = "sledgehammer")] -pub mod unified_bindings; - -#[cfg(feature = "sledgehammer")] -pub use unified_bindings::*; diff --git a/packages/interpreter/src/ts/interpreter_core.ts b/packages/interpreter/src/ts/interpreter_core.ts index ade777fb7..11b0c83f8 100644 --- a/packages/interpreter/src/ts/interpreter_core.ts +++ b/packages/interpreter/src/ts/interpreter_core.ts @@ -28,7 +28,7 @@ export class Interpreter { this.initialize(root); } - initialize(root: HTMLElement) { + initialize(root: HTMLElement, handler: EventListener | null = null) { this.global = {}; this.local = {}; this.root = root; @@ -36,6 +36,10 @@ export class Interpreter { this.nodes = [root]; this.stack = [root]; this.templates = {}; + + if (handler) { + this.handler = handler; + } } createListener(event_name: string, element: HTMLElement, bubbles: boolean) { diff --git a/packages/interpreter/src/ts/interpreter_web.ts b/packages/interpreter/src/ts/interpreter_web.ts index 3f4eade17..86f195446 100644 --- a/packages/interpreter/src/ts/interpreter_web.ts +++ b/packages/interpreter/src/ts/interpreter_web.ts @@ -5,6 +5,7 @@ // We're using sledgehammer directly import { Interpreter } from "./interpreter_core"; +export { setAttributeInner } from "./set_attribute"; export class PlatformInterpreter extends Interpreter { m: any; diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index 36e28b5b9..d1e3b2b0d 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -6,40 +6,13 @@ use wasm_bindgen::prelude::wasm_bindgen; use sledgehammer_bindgen::bindgen; +/// Combine the interpreter class with the sledgehammer_bindgen generated methods. pub fn native_js() -> String { format!("{}\n{}", include_str!("./js/native.js"), GENERATED_JS,) } pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; -/// Extensions to the interpreter that are specific to the web platform. -#[cfg(feature = "webonly")] -#[wasm_bindgen(module = "src/js/web.js")] -extern "C" { - pub type WebInterpreter; - - #[wasm_bindgen(method, js_name = "saveTemplate")] - pub fn save_template(this: &WebInterpreter, nodes: Vec, tmpl_id: u16); - - #[wasm_bindgen(method)] - pub fn hydrate(this: &WebInterpreter, ids: Vec); - - #[wasm_bindgen(method, js_name = "getNode")] - pub fn get_node(this: &WebInterpreter, id: u32) -> Node; -} - -#[cfg(feature = "webonly")] -type PlatformInterpreter = WebInterpreter; - -#[cfg(feature = "webonly")] -impl Interpreter { - /// Convert the interpreter to a web interpreter, enabling methods like hydrate and save_template. - pub fn as_web(&self) -> &WebInterpreter { - use wasm_bindgen::prelude::JsCast; - &self.js_channel().unchecked_ref() - } -} - #[bindgen(module)] mod js { /// The interpreter extends the core interpreter which contains the state for the interpreter along with some functions that all platforms use like `AppendChildren`. @@ -222,3 +195,34 @@ mod js { "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...this.els);}" } } + +/// Extensions to the interpreter that are specific to the web platform. +#[cfg(feature = "webonly")] +#[wasm_bindgen(module = "src/js/web.js")] +extern "C" { + pub type WebInterpreter; + + #[wasm_bindgen(method)] + pub fn initialize(this: &WebInterpreter, root: Node, handler: &js_sys::Function); + + #[wasm_bindgen(method, js_name = "saveTemplate")] + pub fn save_template(this: &WebInterpreter, nodes: Vec, tmpl_id: u16); + + #[wasm_bindgen(method)] + pub fn hydrate(this: &WebInterpreter, ids: Vec); + + #[wasm_bindgen(method, js_name = "getNode")] + pub fn get_node(this: &WebInterpreter, id: u32) -> Node; +} + +#[cfg(feature = "webonly")] +type PlatformInterpreter = WebInterpreter; + +#[cfg(feature = "webonly")] +impl Interpreter { + /// Convert the interpreter to a web interpreter, enabling methods like hydrate and save_template. + pub fn as_web(&self) -> &WebInterpreter { + use wasm_bindgen::prelude::JsCast; + &self.js_channel().unchecked_ref() + } +} diff --git a/packages/interpreter/tsconfig.native.json b/packages/interpreter/tsconfig.native.json deleted file mode 100644 index 57feec085..000000000 --- a/packages/interpreter/tsconfig.native.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - // extends the base - "extends": "./tsconfig.json", - "compilerOptions": { - "outFile": "src/js/native.js" - }, - "files": [ - "src/ts/interpreter_native.ts" - ], -} diff --git a/packages/interpreter/tsconfig.web.json b/packages/interpreter/tsconfig.web.json deleted file mode 100644 index ac1961b32..000000000 --- a/packages/interpreter/tsconfig.web.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - // extends the base - "extends": "./tsconfig.json", - "compilerOptions": { - "outFile": "src/js/web.js" - }, - "files": [ - "src/ts/interpreter_web.ts" - ], -} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 9ed7b85e8..121f0245c 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -31,7 +31,7 @@ pub enum LiveViewError { } fn handle_edits_code() -> String { - use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS; + use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; use minify_js::{minify, Session, TopLevelMode}; let serialize_file_uploads = r#"if ( diff --git a/packages/plasmo/src/hooks.rs b/packages/plasmo/src/hooks.rs index 81f6558b6..0c0ca1c21 100644 --- a/packages/plasmo/src/hooks.rs +++ b/packages/plasmo/src/hooks.rs @@ -18,7 +18,6 @@ use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; use dioxus_html::input_data::{ MouseButton as DioxusMouseButton, MouseButtonSet as DioxusMouseButtons, }; -use dioxus_html::FormValue; use dioxus_html::{event_bubbles, prelude::*}; use std::any::Any; use std::collections::HashMap; @@ -67,7 +66,7 @@ impl EventData { pub struct FormData { pub(crate) value: String, - pub values: HashMap, + pub values: HashMap, pub(crate) files: Option, } @@ -77,7 +76,7 @@ impl HasFormData for FormData { self.value.clone() } - fn values(&self) -> HashMap { + fn values(&self) -> HashMap { self.values.clone() } diff --git a/packages/web/ric_raf/README.md b/packages/web/ric_raf/README.md new file mode 100644 index 000000000..a1bc20e82 --- /dev/null +++ b/packages/web/ric_raf/README.md @@ -0,0 +1,3 @@ +requestIdleCallback and requestAnimationFrame implemenation + +These currently actually slow down our DOM patching and thus are temporarily removed. Technically we can schedule around rIC and rAF but choose not to. diff --git a/packages/web/src/ric_raf.rs b/packages/web/ric_raf/ric_raf.rs similarity index 100% rename from packages/web/src/ric_raf.rs rename to packages/web/ric_raf/ric_raf.rs diff --git a/packages/web/src/ricpolyfill.js b/packages/web/ric_raf/ricpolyfill.js similarity index 100% rename from packages/web/src/ricpolyfill.js rename to packages/web/ric_raf/ricpolyfill.js diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index f6add78a4..ab5521053 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -8,7 +8,7 @@ use dioxus_core::ElementId; use dioxus_html::PlatformEventData; -use dioxus_interpreter_js::unified_bindings::{Interpreter, InterpreterInterface}; +use dioxus_interpreter_js::{unified_bindings::Interpreter, WebInterpreter}; use futures_channel::mpsc; use rustc_hash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; @@ -17,14 +17,16 @@ use web_sys::{Document, Element, Event}; use crate::{load_document, virtual_event_from_websys_event, Config, WebEventConverter}; pub struct WebsysDom { - pub(crate) document: Document, #[allow(dead_code)] pub(crate) root: Element, + pub(crate) document: Document, pub(crate) templates: FxHashMap, pub(crate) max_template_id: u16, - pub(crate) interpreter: InterpreterInterface, + pub(crate) interpreter: Interpreter, + #[cfg(feature = "mounted")] pub(crate) event_channel: mpsc::UnboundedSender, + #[cfg(feature = "mounted")] pub(crate) queued_mounted_events: Vec, } @@ -64,7 +66,7 @@ impl WebsysDom { } }; - let interpreter = InterpreterInterface::default(); + let interpreter = Interpreter::default(); let handler: Closure = Closure::wrap(Box::new({ let event_channel = event_channel.clone(); @@ -72,42 +74,45 @@ impl WebsysDom { let name = event.type_(); let element = walk_event_for_id(event); let bubbles = dioxus_html::event_bubbles(name.as_str()); - if let Some((element, target)) = element { - let prevent_event; - if let Some(prevent_requests) = target - .get_attribute("dioxus-prevent-default") - .as_deref() - .map(|f| f.split_whitespace()) - { - prevent_event = prevent_requests - .map(|f| f.trim_start_matches("on")) - .any(|f| f == name); - } else { - prevent_event = false; - } - // Prevent forms from submitting and redirecting - if name == "submit" { - // On forms the default behavior is not to submit, if prevent default is set then we submit the form - if !prevent_event { - event.prevent_default(); - } - } else if prevent_event { + let Some((element, target)) = element else { + return; + }; + + let prevent_event; + if let Some(prevent_requests) = target + .get_attribute("dioxus-prevent-default") + .as_deref() + .map(|f| f.split_whitespace()) + { + prevent_event = prevent_requests + .map(|f| f.trim_start_matches("on")) + .any(|f| f == name); + } else { + prevent_event = false; + } + + // Prevent forms from submitting and redirecting + if name == "submit" { + // On forms the default behavior is not to submit, if prevent default is set then we submit the form + if !prevent_event { event.prevent_default(); } - - let data = virtual_event_from_websys_event(event.clone(), target); - let _ = event_channel.unbounded_send(UiEvent { - name, - bubbles, - element, - data, - }); + } else if prevent_event { + event.prevent_default(); } + + let data = virtual_event_from_websys_event(event.clone(), target); + let _ = event_channel.unbounded_send(UiEvent { + name, + bubbles, + element, + data, + }); } })); - let _interpreter: &Interpreter = interpreter.as_ref(); + let _interpreter: &WebInterpreter = interpreter.as_web(); _interpreter.initialize( root.clone().unchecked_into(), handler.as_ref().unchecked_ref(), diff --git a/packages/web/src/event.rs b/packages/web/src/event.rs index f68487925..1765fd38a 100644 --- a/packages/web/src/event.rs +++ b/packages/web/src/event.rs @@ -4,7 +4,6 @@ use dioxus_html::{ point_interaction::{ InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction, }, - prelude::FormValue, DragData, FileEngine, FormData, HasDragData, HasFileData, HasFormData, HasImageData, HasMouseData, HtmlEventConverter, ImageData, MountedData, PlatformEventData, ScrollData, }; @@ -385,24 +384,14 @@ impl HasFormData for WebFormData { .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener") } - fn values(&self) -> HashMap { + fn values(&self) -> HashMap { let mut values = HashMap::new(); - fn insert_value(map: &mut HashMap, key: String, new_value: String) { - match map.entry(key) { - std::collections::hash_map::Entry::Occupied(mut o) => { - let first_value = match o.get_mut() { - FormValue::Text(data) => std::mem::take(data), - FormValue::VecText(vec) => { - vec.push(new_value); - return; - } - }; - let _ = o.insert(FormValue::VecText(vec![first_value, new_value])); - } - std::collections::hash_map::Entry::Vacant(v) => { - let _ = v.insert(FormValue::Text(new_value)); - } + fn insert_value(map: &mut HashMap, key: String, new_value: String) { + if let Some(value) = map.get(&key) { + map.insert(key, format!("{},{}", value, new_value)); + } else { + map.insert(key, new_value); } } @@ -425,8 +414,8 @@ impl HasFormData for WebFormData { } } else if let Some(select) = self.element.dyn_ref::() { // try to fill in select element values - let options = get_select_data(select); - values.insert("options".to_string(), FormValue::VecText(options)); + let options = get_select_data(select).join(","); + values.insert("options".to_string(), options); } values diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 1acbdeb87..d949263f5 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -30,16 +30,21 @@ use futures_util::{pin_mut, select, FutureExt, StreamExt}; mod cfg; mod dom; -#[cfg(feature = "eval")] -mod eval; + mod event; pub mod launch; mod mutations; pub use event::*; + +#[cfg(feature = "eval")] +mod eval; + #[cfg(feature = "file_engine")] mod file_engine; + #[cfg(all(feature = "hot_reload", debug_assertions))] mod hot_reload; + #[cfg(feature = "hydrate")] mod rehydrate; @@ -49,7 +54,7 @@ mod rehydrate; /// /// # Example /// -/// ```ignore +/// ```rust, ignore /// fn main() { /// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("joe") }); /// wasm_bindgen_futures::spawn_local(app_fut); @@ -61,12 +66,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) { let mut dom = virtual_dom; #[cfg(feature = "eval")] - { - // Eval - dom.in_runtime(|| { - eval::init_eval(); - }); - } + dom.in_runtime(|| eval::init_eval()); #[cfg(feature = "panic_hook")] if web_config.default_panic_hook { @@ -109,13 +109,12 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) { websys_dom.mount(); loop { - tracing::trace!("waiting for work"); - // if virtual dom has nothing, wait for it to have something before requesting idle time // if there is work then this future resolves immediately. let (mut res, template) = { let work = dom.wait_for_work().fuse(); pin_mut!(work); + let mut rx_next = rx.select_next_some(); #[cfg(all(feature = "hot_reload", debug_assertions))] @@ -127,6 +126,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) { evt = rx_next => (Some(evt), None), } } + #[cfg(not(all(feature = "hot_reload", debug_assertions)))] select! { _ = work => (None, None), diff --git a/packages/web/src/mutations.rs b/packages/web/src/mutations.rs index c3c2a8147..19139e1ed 100644 --- a/packages/web/src/mutations.rs +++ b/packages/web/src/mutations.rs @@ -5,9 +5,7 @@ use dioxus_core::WriteMutations; use dioxus_core::{AttributeValue, ElementId}; use dioxus_html::event_bubbles; use dioxus_html::PlatformEventData; -use dioxus_interpreter_js::get_node; use dioxus_interpreter_js::minimal_bindings; -use dioxus_interpreter_js::save_template; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; @@ -58,19 +56,23 @@ impl WebsysDom { pub fn flush_edits(&mut self) { self.interpreter.flush(); - #[cfg(feature = "mounted")] + // Now that we've flushed the edits and the dom nodes exist, we can send the mounted events. - { - for id in self.queued_mounted_events.drain(..) { - let node = get_node(self.interpreter.js_channel(), id.0 as u32); - if let Some(element) = node.dyn_ref::() { - let _ = self.event_channel.unbounded_send(UiEvent { - name: "mounted".to_string(), - bubbles: false, - element: id, - data: PlatformEventData::new(Box::new(element.clone())), - }); - } + #[cfg(feature = "mounted")] + self.flush_queued_mounted_events(); + } + + #[cfg(feature = "mounted")] + fn flush_queued_mounted_events(&mut self) { + for id in self.queued_mounted_events.drain(..) { + let node = self.interpreter.as_web().get_node(id.0 as u32); + if let Some(element) = node.dyn_ref::() { + let _ = self.event_channel.unbounded_send(UiEvent { + name: "mounted".to_string(), + bubbles: false, + element: id, + data: PlatformEventData::new(Box::new(element.clone())), + }); } } } @@ -84,14 +86,14 @@ impl WebsysDom { impl WriteMutations for WebsysDom { fn register_template(&mut self, template: Template) { let mut roots = vec![]; - for root in template.roots { roots.push(self.create_template_node(root)) } - self.templates .insert(template.name.to_owned(), self.max_template_id); - save_template(self.interpreter.js_channel(), roots, self.max_template_id); + self.interpreter + .as_web() + .save_template(roots, self.max_template_id); self.max_template_id += 1 } @@ -184,30 +186,24 @@ impl WriteMutations for WebsysDom { } fn create_event_listener(&mut self, name: &'static str, id: ElementId) { - match name { - // mounted events are fired immediately after the element is mounted. - "mounted" => { - #[cfg(feature = "mounted")] - self.send_mount_event(id); - } - _ => { - self.interpreter - .new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); - } + // mounted events are fired immediately after the element is mounted. + if name == "mounted" { + #[cfg(feature = "mounted")] + self.send_mount_event(id); + return; } + + self.interpreter + .new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); } fn remove_event_listener(&mut self, name: &'static str, id: ElementId) { - match name { - "mounted" => {} - _ => { - self.interpreter.remove_event_listener( - name, - id.0 as u32, - event_bubbles(name) as u8, - ); - } + if name == "mounted" { + return; } + + self.interpreter + .remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8); } fn remove_node(&mut self, id: ElementId) { diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 271b3daab..3627ad0aa 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -3,7 +3,6 @@ use dioxus_core::prelude::*; use dioxus_core::AttributeValue; use dioxus_core::WriteMutations; use dioxus_core::{DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom}; -use dioxus_interpreter_js::save_template; #[derive(Debug)] pub enum RehydrationError { @@ -23,7 +22,7 @@ impl WebsysDom { // Recursively rehydrate the dom from the VirtualDom self.rehydrate_scope(root_scope, dom, &mut ids, &mut to_mount)?; - dioxus_interpreter_js::hydrate(self.interpreter.js_channel(), ids); + self.interpreter.as_web().hydrate(ids); #[cfg(feature = "mounted")] for id in to_mount { @@ -40,8 +39,7 @@ impl WebsysDom { ids: &mut Vec, to_mount: &mut Vec, ) -> Result<(), RehydrationError> { - let vnode = scope.root_node(); - self.rehydrate_vnode(dom, vnode, ids, to_mount) + self.rehydrate_vnode(dom, scope.root_node(), ids, to_mount) } fn rehydrate_vnode( @@ -168,11 +166,10 @@ impl WriteMutations for OnlyWriteTemplates<'_> { self.0 .templates .insert(template.name.to_owned(), self.0.max_template_id); - save_template( - self.0.interpreter.js_channel(), - roots, - self.0.max_template_id, - ); + self.0 + .interpreter + .as_web() + .save_template(roots, self.0.max_template_id); self.0.max_template_id += 1 } From b7516743541e35b2b3ac9b60b89e4c50cd65b7b9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 11:16:34 -0800 Subject: [PATCH 14/41] Web works with the js structure --- Cargo.lock | 2 - packages/interpreter/Cargo.toml | 3 +- packages/interpreter/build.rs | 5 +- packages/interpreter/src/js/common.js | 99 ++++++++ .../interpreter/src/js/{web.js => core.js} | 163 +++++++------ packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 217 +++++++++++------- packages/interpreter/src/lib.rs | 9 +- packages/interpreter/src/ts/common.ts | 2 + .../src/ts/{interpreter_core.ts => core.ts} | 74 +++++- .../interpreter/src/ts/interpreter_web.ts | 79 ------- .../ts/{interpreter_native.ts => native.ts} | 4 +- packages/interpreter/src/unified_bindings.rs | 124 +++++----- packages/web/src/dom.rs | 4 +- packages/web/src/mutations.rs | 4 +- packages/web/src/rehydrate.rs | 4 +- 16 files changed, 470 insertions(+), 325 deletions(-) create mode 100644 packages/interpreter/src/js/common.js rename packages/interpreter/src/js/{web.js => core.js} (95%) create mode 100644 packages/interpreter/src/ts/common.ts rename packages/interpreter/src/ts/{interpreter_core.ts => core.ts} (57%) delete mode 100644 packages/interpreter/src/ts/interpreter_web.ts rename packages/interpreter/src/ts/{interpreter_native.ts => native.ts} (98%) diff --git a/Cargo.lock b/Cargo.lock index f037f0a10..ae75345b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8527,7 +8527,6 @@ dependencies = [ [[package]] name = "sledgehammer_bindgen" version = "0.4.0" -source = "git+https://github.com/ealmloff/sledgehammer_bindgen#6f90290e1655014f22623eb8b2ea41fb098e1164" dependencies = [ "sledgehammer_bindgen_macro", "wasm-bindgen", @@ -8536,7 +8535,6 @@ dependencies = [ [[package]] name = "sledgehammer_bindgen_macro" version = "0.4.0" -source = "git+https://github.com/ealmloff/sledgehammer_bindgen#6f90290e1655014f22623eb8b2ea41fb098e1164" dependencies = [ "quote", "syn 2.0.51", diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 1cb573911..64edf911a 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -17,7 +17,8 @@ web-sys = { version = "0.3.56", optional = true, features = [ "Element", "Node", ] } -sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } +sledgehammer_bindgen = { path = "../../../Tinkering/sledgehammer_bindgen", default-features = false, optional = true } +# sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } sledgehammer_utils = { version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 75c070d80..90e7823a6 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -20,8 +20,9 @@ fn main() { // Otherwise, generate the bindings and write the new hash to disk // Generate the bindings for both native and web - gen_bindings("interpreter_native", "native"); - gen_bindings("interpreter_web", "web"); + gen_bindings("common", "common"); + gen_bindings("native", "native"); + gen_bindings("core", "core"); std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); } diff --git a/packages/interpreter/src/js/common.js b/packages/interpreter/src/js/common.js new file mode 100644 index 000000000..25d0ee691 --- /dev/null +++ b/packages/interpreter/src/js/common.js @@ -0,0 +1,99 @@ +// src/ts/set_attribute.ts +function setAttributeInner(node, field, value, ns) { + if (ns === "style") { + node.style.setProperty(field, value); + return; + } + if (!!ns) { + node.setAttributeNS(ns, field, value); + return; + } + switch (field) { + case "value": + if (node.value !== value) { + node.value = value; + } + break; + case "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "initial_checked": + node.defaultChecked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + break; + case "initial_selected": + node.defaultSelected = truthy(value); + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + if (!truthy(value) && isBoolAttr(field)) { + node.removeAttribute(field); + } else { + node.setAttribute(field, value); + } + } +} +var truthy = function(val) { + return val === "true" || val === true; +}; +var isBoolAttr = function(field) { + switch (field) { + case "allowfullscreen": + case "allowpaymentrequest": + case "async": + case "autofocus": + case "autoplay": + case "checked": + case "controls": + case "default": + case "defer": + case "disabled": + case "formnovalidate": + case "hidden": + case "ismap": + case "itemscope": + case "loop": + case "multiple": + case "muted": + case "nomodule": + case "novalidate": + case "open": + case "playsinline": + case "readonly": + case "required": + case "reversed": + case "selected": + case "truespeed": + case "webkitdirectory": + return true; + default: + return false; + } +}; +// src/ts/form.ts +function retrieveFormValues(form) { + const formData = new FormData(form); + const contents = {}; + formData.forEach((value, key) => { + if (contents[key]) { + contents[key] += "," + value; + } else { + contents[key] = value; + } + }); + return { + valid: form.checkValidity(), + values: contents + }; +} +export { + setAttributeInner, + retrieveFormValues +}; diff --git a/packages/interpreter/src/js/web.js b/packages/interpreter/src/js/core.js similarity index 95% rename from packages/interpreter/src/js/web.js rename to packages/interpreter/src/js/core.js index 1de671893..8487578c1 100644 --- a/packages/interpreter/src/js/web.js +++ b/packages/interpreter/src/js/core.js @@ -1,81 +1,3 @@ -// src/ts/interpreter_core.ts -class Interpreter { - global; - local; - root; - handler; - nodes; - stack; - templates; - constructor(root, handler) { - this.handler = handler; - this.initialize(root); - } - initialize(root, handler = null) { - this.global = {}; - this.local = {}; - this.root = root; - this.nodes = [root]; - this.stack = [root]; - this.templates = {}; - if (handler) { - this.handler = handler; - } - } - createListener(event_name, element, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = { active: 1, callback: this.handler }; - this.root.addEventListener(event_name, this.handler); - } else { - this.global[event_name].active++; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - } - removeListener(element, event_name, bubbles) { - if (bubbles) { - this.removeBubblingListener(event_name); - } else { - this.removeNonBubblingListener(element, event_name); - } - } - removeBubblingListener(event_name) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - removeNonBubblingListener(element, event_name) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (Object.keys(this.local[id]).length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - } - removeAllNonBubblingListeners(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } - getNode(id) { - return this.nodes[id]; - } - appendChildren(id, many) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - for (let k = 0;k < many; k++) { - root.appendChild(els[k]); - } - } -} - // src/ts/set_attribute.ts function setAttributeInner(node, field, value, ns) { if (ns === "style") { @@ -156,13 +78,84 @@ var isBoolAttr = function(field) { } }; -// src/ts/interpreter_web.ts -class PlatformInterpreter extends Interpreter { +// src/ts/core.ts +class BaseInterpreter { + global; + local; + root; + handler; + nodes; + stack; + templates; m; constructor(root, handler) { - super(root, handler); + this.handler = handler; + this.initialize(root); } - LoadChild(ptr, len) { + initialize(root, handler = null) { + this.global = {}; + this.local = {}; + this.root = root; + this.nodes = [root]; + this.stack = [root]; + this.templates = {}; + if (handler) { + this.handler = handler; + } + } + createListener(event_name, element, bubbles) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = { active: 1, callback: this.handler }; + this.root.addEventListener(event_name, this.handler); + } else { + this.global[event_name].active++; + } + } else { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, this.handler); + } + } + removeListener(element, event_name, bubbles) { + if (bubbles) { + this.removeBubblingListener(event_name); + } else { + this.removeNonBubblingListener(element, event_name); + } + } + removeBubblingListener(event_name) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + removeNonBubblingListener(element, event_name) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (Object.keys(this.local[id]).length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + } + removeAllNonBubblingListeners(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } + getNode(id) { + return this.nodes[id]; + } + appendChildren(id, many) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + for (let k = 0;k < many; k++) { + root.appendChild(els[k]); + } + } + loadChild(ptr, len) { let node = this.stack[this.stack.length - 1]; let ptr_end = ptr + len; for (;ptr < ptr_end; ptr++) { @@ -207,8 +200,10 @@ class PlatformInterpreter extends Interpreter { currentNode = treeWalker.nextNode(); } } + setAttributeInner(node, field, value, ns) { + setAttributeInner(node, field, value, ns); + } } export { - setAttributeInner, - PlatformInterpreter + BaseInterpreter }; diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index a6b01b52c..cba33cfe2 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -3599957386864841107 \ No newline at end of file +7906913262979346544 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 20ab039a9..012e5f1ce 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1,5 +1,85 @@ -// src/ts/interpreter_core.ts -class Interpreter { +// src/ts/set_attribute.ts +function setAttributeInner(node, field, value, ns) { + if (ns === "style") { + node.style.setProperty(field, value); + return; + } + if (!!ns) { + node.setAttributeNS(ns, field, value); + return; + } + switch (field) { + case "value": + if (node.value !== value) { + node.value = value; + } + break; + case "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "initial_checked": + node.defaultChecked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + break; + case "initial_selected": + node.defaultSelected = truthy(value); + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + if (!truthy(value) && isBoolAttr(field)) { + node.removeAttribute(field); + } else { + node.setAttribute(field, value); + } + } +} +var truthy = function(val) { + return val === "true" || val === true; +}; +var isBoolAttr = function(field) { + switch (field) { + case "allowfullscreen": + case "allowpaymentrequest": + case "async": + case "autofocus": + case "autoplay": + case "checked": + case "controls": + case "default": + case "defer": + case "disabled": + case "formnovalidate": + case "hidden": + case "ismap": + case "itemscope": + case "loop": + case "multiple": + case "muted": + case "nomodule": + case "novalidate": + case "open": + case "playsinline": + case "readonly": + case "required": + case "reversed": + case "selected": + case "truespeed": + case "webkitdirectory": + return true; + default: + return false; + } +}; + +// src/ts/core.ts +class BaseInterpreter { global; local; root; @@ -7,6 +87,7 @@ class Interpreter { nodes; stack; templates; + m; constructor(root, handler) { this.handler = handler; this.initialize(root); @@ -74,6 +155,54 @@ class Interpreter { root.appendChild(els[k]); } } + loadChild(ptr, len) { + let node = this.stack[this.stack.length - 1]; + let ptr_end = ptr + len; + for (;ptr < ptr_end; ptr++) { + let end = this.m.getUint8(ptr); + for (node = node.firstChild;end > 0; end--) { + node = node.nextSibling; + } + } + return node; + } + saveTemplate(nodes, tmpl_id) { + this.templates[tmpl_id] = nodes; + } + hydrateRoot(ids) { + const hydrateNodes = document.querySelectorAll("[data-node-hydration]"); + for (let i = 0;i < hydrateNodes.length; i++) { + const hydrateNode = hydrateNodes[i]; + const hydration = hydrateNode.getAttribute("data-node-hydration"); + const split = hydration.split(","); + const id = ids[parseInt(split[0])]; + this.nodes[id] = hydrateNode; + if (split.length > 1) { + hydrateNode.listening = split.length - 1; + hydrateNode.setAttribute("data-dioxus-id", id.toString()); + for (let j = 1;j < split.length; j++) { + const listener = split[j]; + const split2 = listener.split(":"); + const event_name = split2[0]; + const bubbles = split2[1] === "1"; + this.createListener(event_name, hydrateNode, bubbles); + } + } + } + const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT); + let currentNode = treeWalker.nextNode(); + while (currentNode) { + const id = currentNode.textContent; + const split = id.split("node-id"); + if (split.length > 1) { + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; + } + currentNode = treeWalker.nextNode(); + } + } + setAttributeInner(node, field, value, ns) { + setAttributeInner(node, field, value, ns); + } } // src/ts/form.ts @@ -291,87 +420,7 @@ var serializeDragEvent = function(event) { }; }; -// src/ts/set_attribute.ts -function setAttributeInner(node, field, value, ns) { - if (ns === "style") { - node.style.setProperty(field, value); - return; - } - if (!!ns) { - node.setAttributeNS(ns, field, value); - return; - } - switch (field) { - case "value": - if (node.value !== value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - if (!truthy(value) && isBoolAttr(field)) { - node.removeAttribute(field); - } else { - node.setAttribute(field, value); - } - } -} -var truthy = function(val) { - return val === "true" || val === true; -}; -var isBoolAttr = function(field) { - switch (field) { - case "allowfullscreen": - case "allowpaymentrequest": - case "async": - case "autofocus": - case "autoplay": - case "checked": - case "controls": - case "default": - case "defer": - case "disabled": - case "formnovalidate": - case "hidden": - case "ismap": - case "itemscope": - case "loop": - case "multiple": - case "muted": - case "nomodule": - case "novalidate": - case "open": - case "playsinline": - case "readonly": - case "required": - case "reversed": - case "selected": - case "truespeed": - case "webkitdirectory": - return true; - default: - return false; - } -}; - -// src/ts/interpreter_native.ts +// src/ts/native.ts var targetId = function(target) { if (!(target instanceof Node)) { return null; @@ -390,7 +439,7 @@ var targetId = function(target) { return parseInt(realId); }; -class PlatformInterpreter extends Interpreter { +class PlatformInterpreter extends BaseInterpreter { intercept_link_redirects; ipc; liveview; diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 1db7cda86..6d21f0b5b 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -2,8 +2,11 @@ #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] -pub static INTERPRETER_JS: &str = include_str!("./js/native.js"); -pub static COMMON_JS: &str = include_str!("./js/web.js"); +/// The base class that the JS channel will extend +pub static INTERPRETER_JS: &str = include_str!("./js/core.js"); + +/// The code explicitly for desktop/liveview that bridges the eval gap between the two +pub static NATIVE_JS: &str = include_str!("./js/native.js"); #[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))] mod write_native_mutations; @@ -25,7 +28,7 @@ pub mod minimal_bindings { /// Some useful snippets that we use to share common functionality between the different platforms we support. /// /// This maintains some sort of consistency between web, desktop, and liveview - #[wasm_bindgen(module = "/src/js/web.js")] + #[wasm_bindgen(module = "/src/js/common.js")] extern "C" { /// Set the attribute of the node pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>); diff --git a/packages/interpreter/src/ts/common.ts b/packages/interpreter/src/ts/common.ts new file mode 100644 index 000000000..643f36436 --- /dev/null +++ b/packages/interpreter/src/ts/common.ts @@ -0,0 +1,2 @@ +export { setAttributeInner } from "./set_attribute"; +export { retrieveFormValues } from "./form"; diff --git a/packages/interpreter/src/ts/interpreter_core.ts b/packages/interpreter/src/ts/core.ts similarity index 57% rename from packages/interpreter/src/ts/interpreter_core.ts rename to packages/interpreter/src/ts/core.ts index 11b0c83f8..f5e1cd94a 100644 --- a/packages/interpreter/src/ts/interpreter_core.ts +++ b/packages/interpreter/src/ts/core.ts @@ -1,9 +1,11 @@ // The root interpreter class that holds state about the mapping between DOM and VirtualDom // This always lives in the JS side of things, and is extended by the native and web interpreters +import { setAttributeInner } from "./set_attribute"; + export type NodeId = number; -export class Interpreter { +export class BaseInterpreter { // non bubbling events listen at the element the listener was created at global: { [key: string]: { active: number, callback: EventListener } @@ -23,6 +25,9 @@ export class Interpreter { [key: string]: Node[] }; + // sledgehammer is generating this... + m: any; + constructor(root: HTMLElement, handler: EventListener) { this.handler = handler; this.initialize(root); @@ -100,5 +105,72 @@ export class Interpreter { root.appendChild(els[k]); } } + + loadChild(ptr: number, len: number): Node { + // iterate through each number and get that child + let node = this.stack[this.stack.length - 1] as Node; + let ptr_end = ptr + len; + + for (; ptr < ptr_end; ptr++) { + let end = this.m.getUint8(ptr); + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + + return node; + } + + saveTemplate(nodes: HTMLElement[], tmpl_id: string) { + this.templates[tmpl_id] = nodes; + } + + hydrateRoot(ids: { [key: number]: number }) { + const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); + + for (let i = 0; i < hydrateNodes.length; i++) { + const hydrateNode = hydrateNodes[i] as HTMLElement; + const hydration = hydrateNode.getAttribute('data-node-hydration'); + const split = hydration!.split(','); + const id = ids[parseInt(split[0])]; + + this.nodes[id] = hydrateNode; + + if (split.length > 1) { + // @ts-ignore + hydrateNode.listening = split.length - 1; + hydrateNode.setAttribute('data-dioxus-id', id.toString()); + for (let j = 1; j < split.length; j++) { + const listener = split[j]; + const split2 = listener.split(':'); + const event_name = split2[0]; + const bubbles = split2[1] === '1'; + this.createListener(event_name, hydrateNode, bubbles); + } + } + } + + const treeWalker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_COMMENT, + ); + + let currentNode = treeWalker.nextNode(); + + while (currentNode) { + const id = currentNode.textContent!; + const split = id.split('node-id'); + + if (split.length > 1) { + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; + } + + currentNode = treeWalker.nextNode(); + } + } + + setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { + setAttributeInner(node, field, value, ns); + } } diff --git a/packages/interpreter/src/ts/interpreter_web.ts b/packages/interpreter/src/ts/interpreter_web.ts deleted file mode 100644 index 86f195446..000000000 --- a/packages/interpreter/src/ts/interpreter_web.ts +++ /dev/null @@ -1,79 +0,0 @@ -// The JS<->Rust bridge -// This file is thin, suitable just for the web -// If you want the more full-featured intrepreter, look at the native interpreter which extends this with additional functionality -// -// We're using sledgehammer directly - -import { Interpreter } from "./interpreter_core"; -export { setAttributeInner } from "./set_attribute"; - -export class PlatformInterpreter extends Interpreter { - m: any; - - constructor(root: HTMLElement, handler: EventListener) { - super(root, handler); - } - - LoadChild(ptr: number, len: number): Node { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1] as Node; - let ptr_end = ptr + len; - - for (; ptr < ptr_end; ptr++) { - let end = this.m.getUint8(ptr); - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - - return node; - } - - saveTemplate(nodes: HTMLElement[], tmpl_id: string) { - this.templates[tmpl_id] = nodes; - } - - hydrateRoot(ids: { [key: number]: number }) { - const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); - - for (let i = 0; i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i] as HTMLElement; - const hydration = hydrateNode.getAttribute('data-node-hydration'); - const split = hydration!.split(','); - const id = ids[parseInt(split[0])]; - - this.nodes[id] = hydrateNode; - - if (split.length > 1) { - // @ts-ignore - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute('data-dioxus-id', id.toString()); - for (let j = 1; j < split.length; j++) { - const listener = split[j]; - const split2 = listener.split(':'); - const event_name = split2[0]; - const bubbles = split2[1] === '1'; - this.createListener(event_name, hydrateNode, bubbles); - } - } - } - - const treeWalker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_COMMENT, - ); - - let currentNode = treeWalker.nextNode(); - - while (currentNode) { - const id = currentNode.textContent!; - const split = id.split('node-id'); - - if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - - currentNode = treeWalker.nextNode(); - } - } -} diff --git a/packages/interpreter/src/ts/interpreter_native.ts b/packages/interpreter/src/ts/native.ts similarity index 98% rename from packages/interpreter/src/ts/interpreter_native.ts rename to packages/interpreter/src/ts/native.ts index 968bad631..ddce3239a 100644 --- a/packages/interpreter/src/ts/interpreter_native.ts +++ b/packages/interpreter/src/ts/native.ts @@ -3,11 +3,11 @@ // This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't // provide since it doesn't have access to the dom. -import { Interpreter, NodeId } from "./interpreter_core"; +import { BaseInterpreter, NodeId } from "./core"; import { SerializedEvent, serializeEvent } from "./serialize"; import { setAttributeInner } from "./set_attribute"; -export class PlatformInterpreter extends Interpreter { +export class PlatformInterpreter extends BaseInterpreter { intercept_link_redirects: boolean; ipc: any; diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index d1e3b2b0d..9ab7cfd00 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -1,32 +1,54 @@ #[cfg(feature = "webonly")] use web_sys::Node; -#[cfg(feature = "webonly")] -use wasm_bindgen::prelude::wasm_bindgen; - -use sledgehammer_bindgen::bindgen; - -/// Combine the interpreter class with the sledgehammer_bindgen generated methods. -pub fn native_js() -> String { - format!("{}\n{}", include_str!("./js/native.js"), GENERATED_JS,) -} - pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; -#[bindgen(module)] +#[cfg(feature = "webonly")] +#[wasm_bindgen::prelude::wasm_bindgen] +extern "C" { + pub type BaseInterpreter; + + #[wasm_bindgen(method)] + pub fn initialize(this: &BaseInterpreter, root: Node, handler: &js_sys::Function); + + #[wasm_bindgen(method, js_name = "saveTemplate")] + pub fn save_template(this: &BaseInterpreter, nodes: Vec, tmpl_id: u16); + + #[wasm_bindgen(method)] + pub fn hydrate(this: &BaseInterpreter, ids: Vec); + + #[wasm_bindgen(method, js_name = "getNode")] + pub fn get_node(this: &BaseInterpreter, id: u32) -> Node; +} + +// Note that this impl is for the sledgehammer interpreter to allow us dropping down to the base interpreter +// During hydration and initialization we need to the base interpreter methods +#[cfg(feature = "webonly")] +impl Interpreter { + /// Convert the interpreter to its baseclass, giving + pub fn base(&self) -> &BaseInterpreter { + use wasm_bindgen::prelude::JsCast; + &self.js_channel().unchecked_ref() + } +} + +#[sledgehammer_bindgen::bindgen(module)] mod js { + // Extend the web base class + const BASE: &str = "./src/js/core.js"; + /// The interpreter extends the core interpreter which contains the state for the interpreter along with some functions that all platforms use like `AppendChildren`. - #[extends(PlatformInterpreter)] + #[extends(BaseInterpreter)] pub struct Interpreter; fn mount_to_root() { - "{this.AppendChildren(this.root, this.stack.length-1);}" + "{this.appendChildren(this.root, this.stack.length-1);}" } fn push_root(root: u32) { "{this.stack.push(this.nodes[$root$]);}" } fn append_children(id: u32, many: u16) { - "{this.AppendChildren($id$, $many$);}" + "{this.appendChildren($id$, $many$);}" } fn pop_root() { "{this.stack.pop();}" @@ -93,11 +115,11 @@ mod js { }"# } fn assign_id(ptr: u32, len: u8, id: u32) { - "{this.nodes[$id$] = this.LoadChild($ptr$, $len$);}" + "{this.nodes[$id$] = this.loadChild($ptr$, $len$);}" } fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) { r#"{ - let node = this.LoadChild($ptr$, $len$); + let node = this.loadChild($ptr$, $len$); if (node.nodeType == node.TEXT_NODE) { node.textContent = value; } else { @@ -109,20 +131,13 @@ mod js { }"# } fn replace_placeholder(ptr: u32, len: u8, n: u16) { - "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...this.els);}" + "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($ptr$, $len$); node.replaceWith(...this.els);}" } fn load_template(tmpl_id: u16, index: u16, id: u32) { "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}" } - /* - Binary protocol methods only! - - These methods let us support binary packing mutations for use on boundaries like desktop where we prefer to send - binary data instead of JSON. - - We're using native types in a number of places - */ + #[cfg(feature = "binary-protocol")] fn append_children_to_top(many: u16) { "{ let root = this.stack[this.stack.length-many-1]; @@ -130,23 +145,35 @@ mod js { for (let k = 0; k < many; k++) { root.appendChild(this.els[k]); } - }" + }" } + + #[cfg(feature = "binary-protocol")] fn set_top_attribute(field: &str, value: &str, ns: &str) { "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}" } + + #[cfg(feature = "binary-protocol")] fn add_placeholder() { "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node);}" } + + #[cfg(feature = "binary-protocol")] fn create_element(element: &'static str) { "{this.stack.push(document.createElement($element$))}" } + + #[cfg(feature = "binary-protocol")] fn create_element_ns(element: &'static str, ns: &'static str) { "{this.stack.push(document.createElementNS($ns$, $element$))}" } + + #[cfg(feature = "binary-protocol")] fn add_templates(tmpl_id: u16, len: u16) { "{this.templates[$tmpl_id$] = this.stack.splice(this.stack.length-$len$);}" } + + #[cfg(feature = "binary-protocol")] fn foreign_event_listener(event: &str, id: u32, bubbles: u8) { r#" bubbles = bubbles == 1; @@ -175,12 +202,18 @@ mod js { }); }"# } + + /// Assign the ID + #[cfg(feature = "binary-protocol")] fn assign_id_ref(array: &[u8], id: u32) { - "{this.nodes[$id$] = this.LoadChild($array$);}" + "{this.nodes[$id$] = this.loadChild($array$);}" } + + /// The coolest ID ever! + #[cfg(feature = "binary-protocol")] fn hydrate_text_ref(array: &[u8], value: &str, id: u32) { r#"{ - let node = this.LoadChild($array$); + let node = this.loadChild($array$); if (node.nodeType == node.TEXT_NODE) { node.textContent = value; } else { @@ -191,38 +224,9 @@ mod js { this.nodes[$id$] = node; }"# } + + #[cfg(feature = "binary-protocol")] fn replace_placeholder_ref(array: &[u8], n: u16) { - "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...this.els);}" - } -} - -/// Extensions to the interpreter that are specific to the web platform. -#[cfg(feature = "webonly")] -#[wasm_bindgen(module = "src/js/web.js")] -extern "C" { - pub type WebInterpreter; - - #[wasm_bindgen(method)] - pub fn initialize(this: &WebInterpreter, root: Node, handler: &js_sys::Function); - - #[wasm_bindgen(method, js_name = "saveTemplate")] - pub fn save_template(this: &WebInterpreter, nodes: Vec, tmpl_id: u16); - - #[wasm_bindgen(method)] - pub fn hydrate(this: &WebInterpreter, ids: Vec); - - #[wasm_bindgen(method, js_name = "getNode")] - pub fn get_node(this: &WebInterpreter, id: u32) -> Node; -} - -#[cfg(feature = "webonly")] -type PlatformInterpreter = WebInterpreter; - -#[cfg(feature = "webonly")] -impl Interpreter { - /// Convert the interpreter to a web interpreter, enabling methods like hydrate and save_template. - pub fn as_web(&self) -> &WebInterpreter { - use wasm_bindgen::prelude::JsCast; - &self.js_channel().unchecked_ref() + "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($array$); node.replaceWith(...this.els);}" } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ab5521053..f975fd738 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -8,7 +8,7 @@ use dioxus_core::ElementId; use dioxus_html::PlatformEventData; -use dioxus_interpreter_js::{unified_bindings::Interpreter, WebInterpreter}; +use dioxus_interpreter_js::unified_bindings::Interpreter; use futures_channel::mpsc; use rustc_hash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; @@ -112,7 +112,7 @@ impl WebsysDom { } })); - let _interpreter: &WebInterpreter = interpreter.as_web(); + let _interpreter = interpreter.base(); _interpreter.initialize( root.clone().unchecked_into(), handler.as_ref().unchecked_ref(), diff --git a/packages/web/src/mutations.rs b/packages/web/src/mutations.rs index 19139e1ed..dc7d91e75 100644 --- a/packages/web/src/mutations.rs +++ b/packages/web/src/mutations.rs @@ -65,7 +65,7 @@ impl WebsysDom { #[cfg(feature = "mounted")] fn flush_queued_mounted_events(&mut self) { for id in self.queued_mounted_events.drain(..) { - let node = self.interpreter.as_web().get_node(id.0 as u32); + let node = self.interpreter.base().get_node(id.0 as u32); if let Some(element) = node.dyn_ref::() { let _ = self.event_channel.unbounded_send(UiEvent { name: "mounted".to_string(), @@ -92,7 +92,7 @@ impl WriteMutations for WebsysDom { self.templates .insert(template.name.to_owned(), self.max_template_id); self.interpreter - .as_web() + .base() .save_template(roots, self.max_template_id); self.max_template_id += 1 } diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 3627ad0aa..de137873a 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -22,7 +22,7 @@ impl WebsysDom { // Recursively rehydrate the dom from the VirtualDom self.rehydrate_scope(root_scope, dom, &mut ids, &mut to_mount)?; - self.interpreter.as_web().hydrate(ids); + self.interpreter.base().hydrate(ids); #[cfg(feature = "mounted")] for id in to_mount { @@ -168,7 +168,7 @@ impl WriteMutations for OnlyWriteTemplates<'_> { .insert(template.name.to_owned(), self.0.max_template_id); self.0 .interpreter - .as_web() + .base() .save_template(roots, self.0.max_template_id); self.0.max_template_id += 1 } From 56a365742cc00e770d43091628233dcc0380379f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 12:08:33 -0800 Subject: [PATCH 15/41] clean up native js more --- examples/form.rs | 167 +++++++++--------- packages/desktop/src/protocol.rs | 47 +---- packages/interpreter/src/js/core.js | 4 +- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 244 +++----------------------- packages/interpreter/src/ts/core.ts | 5 +- packages/interpreter/src/ts/native.ts | 55 ++++-- 7 files changed, 173 insertions(+), 351 deletions(-) diff --git a/examples/form.rs b/examples/form.rs index 3442d4d73..b49072292 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -12,95 +12,104 @@ fn main() { fn app() -> Element { let mut values = use_signal(|| HashMap::new()); + let mut submitted_values = use_signal(|| HashMap::new()); + rsx! { - div { - h1 { "Form" } + div { style: "display: flex", + div { style: "width: 50%", + h1 { "Form" } - // The form element is used to create an HTML form for user input - // You can attach regular attributes to it - form { - id: "cool-form", - style: "display: flex; flex-direction: column;", - - // You can attach a handler to the entire form - oninput: move |ev| { - println!("Input event: {:#?}", ev); - values.set(ev.values()); - }, - - // On desktop/liveview, the form will not navigate the page - the expectation is that you handle - // The form event. - // Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview. - // We suggest always attaching a submit handler to the form. - onsubmit: move |ev| { - println!("Submit event: {:#?}", ev); - }, - - // Regular text inputs with handlers - label { r#for: "username", "Username" } - input { - r#type: "text", - name: "username", - oninput: move |ev| { - println!("setting username"); - values.set(ev.values()); - } + if !submitted_values.read().is_empty() { + h2 { "Submitted! ✅" } } - // And then the various inputs that might exist - // Note for a value to be returned in .values(), it must be named! + // The form element is used to create an HTML form for user input + // You can attach regular attributes to it + form { + id: "cool-form", + style: "display: flex; flex-direction: column;", - label { r#for: "full-name", "Full Name" } - input { r#type: "text", name: "full-name" } - - label { r#for: "email", "Email" } - input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" } - - label { r#for: "password", "Password" } - input { r#type: "password", name: "password" } - - label { r#for: "color", "Color" } - input { r#type: "radio", checked: true, name: "color", value: "red" } - input { r#type: "radio", name: "color", value: "blue" } - input { r#type: "radio", name: "color", value: "green" } - - // Select multiple comes in as a comma separated list of selected values - // You should split them on the comma to get the values manually - label { r#for: "country", "Country" } - select { - name: "country", - multiple: true, + // You can attach a handler to the entire form oninput: move |ev| { println!("Input event: {:#?}", ev); - println!("Values: {:#?}", ev.value().split(',').collect::>()); + values.set(ev.values()); }, - option { value: "usa", "USA" } - option { value: "canada", "Canada" } - option { value: "mexico", "Mexico" } + + // On desktop/liveview, the form will not navigate the page - the expectation is that you handle + // The form event. + // Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview. + // We suggest always attaching a submit handler to the form. + onsubmit: move |ev| { + println!("Submit event: {:#?}", ev); + submitted_values.set(ev.values()); + }, + + // Regular text inputs with handlers + label { r#for: "username", "Username" } + input { + r#type: "text", + name: "username", + oninput: move |ev| { + println!("setting username"); + values.set(ev.values()); + } + } + + // And then the various inputs that might exist + // Note for a value to be returned in .values(), it must be named! + + label { r#for: "full-name", "Full Name" } + input { r#type: "text", name: "full-name" } + + label { r#for: "email", "Email" } + input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" } + + label { r#for: "password", "Password" } + input { r#type: "password", name: "password" } + + label { r#for: "color", "Color" } + input { r#type: "radio", checked: true, name: "color", value: "red" } + input { r#type: "radio", name: "color", value: "blue" } + input { r#type: "radio", name: "color", value: "green" } + + // Select multiple comes in as a comma separated list of selected values + // You should split them on the comma to get the values manually + label { r#for: "country", "Country" } + select { + name: "country", + multiple: true, + oninput: move |ev| { + println!("Input event: {:#?}", ev); + println!("Values: {:#?}", ev.value().split(',').collect::>()); + }, + option { value: "usa", "USA" } + option { value: "canada", "Canada" } + option { value: "mexico", "Mexico" } + } + + // Safari can be quirky with color inputs on mac. + // We recommend always providing a text input for color as a fallback. + label { r#for: "color", "Color" } + input { r#type: "color", value: "#000002", name: "head", id: "head" } + + // Dates! + input { + min: "2018-01-01", + value: "2018-07-22", + r#type: "date", + name: "trip-start", + max: "2025-12-31", + id: "start" + } + + // Buttons will submit your form by default. + button { r#type: "submit", value: "Submit", "Submit the form" } } - - // Safari can be quirky with color inputs on mac. - // We recommend always providing a text input for color as a fallback. - label { r#for: "color", "Color" } - input { r#type: "color", value: "#000002", name: "head", id: "head" } - - // Dates! - input { - min: "2018-01-01", - value: "2018-07-22", - r#type: "date", - name: "trip-start", - max: "2025-12-31", - id: "start" - } - - // Buttons will submit your form by default. - button { r#type: "submit", value: "Submit", "Submit the form" } } - } - div { - h1 { "Oninput Values" } - pre { "{values:#?}" } + div { style: "width: 50%", + h1 { "Oninput Values" } + pre { "{values:#?}" } + } } } } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 033d44fbd..ae3da5ba4 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,5 +1,6 @@ use crate::{assets::*, edits::EditQueue}; -use dioxus_interpreter_js::unified_bindings::native_js; +use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; +use dioxus_interpreter_js::NATIVE_JS; use std::path::{Path, PathBuf}; use wry::{ http::{status::StatusCode, Request, Response}, @@ -12,46 +13,16 @@ const EDITS_PATH: &str = "http://dioxus.index.html/edits"; #[cfg(not(any(target_os = "android", target_os = "windows")))] const EDITS_PATH: &str = "dioxus://index.html/edits"; -const PREVENT_FILE_UPLOAD: &str = include_str!("../js/prevent_file_upload.js"); +// const PREVENT_FILE_UPLOAD: &str = include_str!("../js/prevent_file_upload.js"); fn handle_edits_code() -> String { - let polling_request = format!( + format!( r#"// Poll for requests - window.interpreter = new JSChannel(); - window.interpreter.wait_for_request = (headless) => {{ - fetch(new Request("{EDITS_PATH}")) - .then(response => {{ - response.arrayBuffer() - .then(bytes => {{ - // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly - if (headless) {{ - window.interpreter.run_from_bytes(bytes); - }} - else {{ - requestAnimationFrame(() => {{ - window.interpreter.run_from_bytes(bytes); - }}); - }} - window.interpreter.wait_for_request(headless); - }}); - }}) - }}"# - ); - - let mut interpreter = native_js() - .replace("/*POST_HANDLE_EDITS*/", PREVENT_FILE_UPLOAD) - .replace("export", "") - + &polling_request; - - while let Some(import_start) = interpreter.find("import") { - let import_end = interpreter[import_start..] - .find(|c| c == ';' || c == '\n') - .map(|i| i + import_start) - .unwrap_or_else(|| interpreter.len()); - interpreter.replace_range(import_start..import_end, ""); - } - - format!("{interpreter}\nconst intercept_link_redirects = true;") + {SLEDGEHAMMER_JS} + {NATIVE_JS} + window.interpreter = new NativeInterpreter("{EDITS_PATH}"); + "# + ) } static DEFAULT_INDEX: &str = include_str!("./index.html"); diff --git a/packages/interpreter/src/js/core.js b/packages/interpreter/src/js/core.js index 8487578c1..48cc3315d 100644 --- a/packages/interpreter/src/js/core.js +++ b/packages/interpreter/src/js/core.js @@ -88,9 +88,7 @@ class BaseInterpreter { stack; templates; m; - constructor(root, handler) { - this.handler = handler; - this.initialize(root); + constructor() { } initialize(root, handler = null) { this.global = {}; diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index cba33cfe2..00ba413f1 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -7906913262979346544 \ No newline at end of file +5236313184333634830 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 012e5f1ce..376eac052 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1,210 +1,3 @@ -// src/ts/set_attribute.ts -function setAttributeInner(node, field, value, ns) { - if (ns === "style") { - node.style.setProperty(field, value); - return; - } - if (!!ns) { - node.setAttributeNS(ns, field, value); - return; - } - switch (field) { - case "value": - if (node.value !== value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - if (!truthy(value) && isBoolAttr(field)) { - node.removeAttribute(field); - } else { - node.setAttribute(field, value); - } - } -} -var truthy = function(val) { - return val === "true" || val === true; -}; -var isBoolAttr = function(field) { - switch (field) { - case "allowfullscreen": - case "allowpaymentrequest": - case "async": - case "autofocus": - case "autoplay": - case "checked": - case "controls": - case "default": - case "defer": - case "disabled": - case "formnovalidate": - case "hidden": - case "ismap": - case "itemscope": - case "loop": - case "multiple": - case "muted": - case "nomodule": - case "novalidate": - case "open": - case "playsinline": - case "readonly": - case "required": - case "reversed": - case "selected": - case "truespeed": - case "webkitdirectory": - return true; - default: - return false; - } -}; - -// src/ts/core.ts -class BaseInterpreter { - global; - local; - root; - handler; - nodes; - stack; - templates; - m; - constructor(root, handler) { - this.handler = handler; - this.initialize(root); - } - initialize(root, handler = null) { - this.global = {}; - this.local = {}; - this.root = root; - this.nodes = [root]; - this.stack = [root]; - this.templates = {}; - if (handler) { - this.handler = handler; - } - } - createListener(event_name, element, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = { active: 1, callback: this.handler }; - this.root.addEventListener(event_name, this.handler); - } else { - this.global[event_name].active++; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - } - removeListener(element, event_name, bubbles) { - if (bubbles) { - this.removeBubblingListener(event_name); - } else { - this.removeNonBubblingListener(element, event_name); - } - } - removeBubblingListener(event_name) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - removeNonBubblingListener(element, event_name) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (Object.keys(this.local[id]).length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - } - removeAllNonBubblingListeners(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } - getNode(id) { - return this.nodes[id]; - } - appendChildren(id, many) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - for (let k = 0;k < many; k++) { - root.appendChild(els[k]); - } - } - loadChild(ptr, len) { - let node = this.stack[this.stack.length - 1]; - let ptr_end = ptr + len; - for (;ptr < ptr_end; ptr++) { - let end = this.m.getUint8(ptr); - for (node = node.firstChild;end > 0; end--) { - node = node.nextSibling; - } - } - return node; - } - saveTemplate(nodes, tmpl_id) { - this.templates[tmpl_id] = nodes; - } - hydrateRoot(ids) { - const hydrateNodes = document.querySelectorAll("[data-node-hydration]"); - for (let i = 0;i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i]; - const hydration = hydrateNode.getAttribute("data-node-hydration"); - const split = hydration.split(","); - const id = ids[parseInt(split[0])]; - this.nodes[id] = hydrateNode; - if (split.length > 1) { - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute("data-dioxus-id", id.toString()); - for (let j = 1;j < split.length; j++) { - const listener = split[j]; - const split2 = listener.split(":"); - const event_name = split2[0]; - const bubbles = split2[1] === "1"; - this.createListener(event_name, hydrateNode, bubbles); - } - } - } - const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT); - let currentNode = treeWalker.nextNode(); - while (currentNode) { - const id = currentNode.textContent; - const split = id.split("node-id"); - if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - currentNode = treeWalker.nextNode(); - } - } - setAttributeInner(node, field, value, ns) { - setAttributeInner(node, field, value, ns); - } -} - // src/ts/form.ts function retriveValues(event, target) { let contents = { @@ -438,13 +231,21 @@ var targetId = function(target) { } return parseInt(realId); }; +var JSChannel_; +if (RawInterpreter) { + JSChannel_ = RawInterpreter; +} -class PlatformInterpreter extends BaseInterpreter { +class NativeInterpreter extends JSChannel_ { intercept_link_redirects; ipc; + editsPath; liveview; - constructor(root) { - super(root, (event) => this.handleEvent(event, event.type, true)); + constructor(editsPath) { + super(); + this.editsPath = editsPath; + } + initialize(root) { this.intercept_link_redirects = true; this.liveview = false; window.addEventListener("dragover", function(e) { @@ -458,13 +259,12 @@ class PlatformInterpreter extends BaseInterpreter { } }, false); this.ipc = window.ipc; + const handler = (event) => this.handleEvent(event, event.type, true); + super.initialize(root, handler); } serializeIpcMessage(method, params = {}) { return JSON.stringify({ method, params }); } - setAttributeInner(node, field, value, ns) { - setAttributeInner(node, field, value, ns); - } scrollTo(id, behavior) { const node = this.nodes[id]; if (node instanceof HTMLElement) { @@ -492,7 +292,7 @@ class PlatformInterpreter extends BaseInterpreter { } } } - LoadChild(array) { + loadChild(array) { let node = this.stack[this.stack.length - 1]; for (let i = 0;i < array.length; i++) { let end = array[i]; @@ -502,7 +302,7 @@ class PlatformInterpreter extends BaseInterpreter { } return node; } - AppendChildren(id, many) { + appendChildren(id, many) { const root = this.nodes[id]; const els = this.stack.splice(this.stack.length - many); for (let k = 0;k < many; k++) { @@ -510,6 +310,7 @@ class PlatformInterpreter extends BaseInterpreter { } } handleEvent(event, name, bubbles) { + console.log("handling event: ", event); const target = event.target; const realId = targetId(target); const contents = serializeEvent(event, target); @@ -529,6 +330,7 @@ class PlatformInterpreter extends BaseInterpreter { } else { const message = this.serializeIpcMessage("user_event", body); this.ipc.postMessage(message); + console.log("sent message to host: ", message); } } async readFiles(target, contents, bubbles, realId, name) { @@ -584,7 +386,17 @@ class PlatformInterpreter extends BaseInterpreter { } } } + wait_for_request(headless) { + fetch(new Request(this.editsPath)).then((response) => response.arrayBuffer()).then((bytes) => { + if (headless) { + this.run_from_bytes(bytes); + } else { + requestAnimationFrame(() => this.run_from_bytes(bytes)); + } + this.wait_for_request(headless); + }); + } } export { - PlatformInterpreter + NativeInterpreter }; diff --git a/packages/interpreter/src/ts/core.ts b/packages/interpreter/src/ts/core.ts index f5e1cd94a..6f78e9ce3 100644 --- a/packages/interpreter/src/ts/core.ts +++ b/packages/interpreter/src/ts/core.ts @@ -28,10 +28,7 @@ export class BaseInterpreter { // sledgehammer is generating this... m: any; - constructor(root: HTMLElement, handler: EventListener) { - this.handler = handler; - this.initialize(root); - } + constructor() { } initialize(root: HTMLElement, handler: EventListener | null = null) { this.global = {}; diff --git a/packages/interpreter/src/ts/native.ts b/packages/interpreter/src/ts/native.ts index ddce3239a..13bb152e2 100644 --- a/packages/interpreter/src/ts/native.ts +++ b/packages/interpreter/src/ts/native.ts @@ -5,18 +5,31 @@ import { BaseInterpreter, NodeId } from "./core"; import { SerializedEvent, serializeEvent } from "./serialize"; -import { setAttributeInner } from "./set_attribute"; -export class PlatformInterpreter extends BaseInterpreter { +// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope +// we want to extend it, and it technically extends base intepreter. To make typescript happy, +// we're going to bind the JSChannel_ object to the JSChannel object, and then extend it +var JSChannel_: typeof BaseInterpreter; + +// @ts-ignore - this is coming from the host +if (RawInterpreter) { JSChannel_ = RawInterpreter; } + +export class NativeInterpreter extends JSChannel_ { intercept_link_redirects: boolean; ipc: any; + editsPath: string; // eventually we want to remove liveview and build it into the server-side-events of fullstack // however, for now we need to support it since SSE in fullstack doesn't exist yet liveview: boolean; - constructor(root: HTMLElement) { - super(root, (event) => this.handleEvent(event, event.type, true)); + constructor(editsPath: string) { + super(); + this.editsPath = editsPath; + } + + initialize(root: HTMLElement): void { + this.intercept_link_redirects = true; this.liveview = false; @@ -39,16 +52,16 @@ export class PlatformInterpreter extends BaseInterpreter { // @ts-ignore - wry gives us this this.ipc = window.ipc; + + // make sure we pass the handler to the base interpreter + const handler: EventListener = (event) => this.handleEvent(event, event.type, true); + super.initialize(root, handler); } serializeIpcMessage(method: string, params = {}) { return JSON.stringify({ method, params }); } - setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { - setAttributeInner(node, field, value, ns); - } - scrollTo(id: NodeId, behavior: ScrollBehavior) { const node = this.nodes[id]; if (node instanceof HTMLElement) { @@ -80,7 +93,9 @@ export class PlatformInterpreter extends BaseInterpreter { } } - LoadChild(array: number[]) { + // ignore the fact the base interpreter uses ptr + len but we use array + // @ts-ignore + loadChild(array: number[]) { // iterate through each number and get that child let node = this.stack[this.stack.length - 1]; @@ -94,7 +109,7 @@ export class PlatformInterpreter extends BaseInterpreter { return node; } - AppendChildren(id: NodeId, many: number) { + appendChildren(id: NodeId, many: number) { const root = this.nodes[id]; const els = this.stack.splice(this.stack.length - many); @@ -104,6 +119,8 @@ export class PlatformInterpreter extends BaseInterpreter { } handleEvent(event: Event, name: string, bubbles: boolean) { + console.log("handling event: ", event); + const target = event.target!; const realId = targetId(target)!; const contents = serializeEvent(event, target); @@ -137,6 +154,8 @@ export class PlatformInterpreter extends BaseInterpreter { const message = this.serializeIpcMessage("user_event", body); this.ipc.postMessage(message); + console.log("sent message to host: ", message); + // // Run the event handler on the virtualdom // // capture/prevent default of the event if the virtualdom wants to // const res = handleVirtualdomEventSync(JSON.stringify(body)); @@ -243,6 +262,22 @@ export class PlatformInterpreter extends BaseInterpreter { } } } + + wait_for_request(headless: boolean) { + fetch(new Request(this.editsPath)) + .then(response => response.arrayBuffer()) + .then(bytes => { + // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly + if (headless) { + // @ts-ignore + this.run_from_bytes(bytes); + } else { + // @ts-ignore + requestAnimationFrame(() => this.run_from_bytes(bytes)); + } + this.wait_for_request(headless); + }); + } } type EventSyncResult = { From 403e8e2f4908d094eb25501dfc3218329aeb8482 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 13:00:27 -0800 Subject: [PATCH 16/41] Clean up file uploads in desktop/liveview, remove minify --- .vscode/settings.json | 3 +- Cargo.lock | 49 +------------- examples/file_upload.rs | 35 ++++++---- packages/desktop/js/prevent_file_upload.js | 23 ------- packages/desktop/src/app.rs | 31 ++------- packages/desktop/src/events.rs | 10 ++- packages/desktop/src/file_upload.rs | 20 +++++- packages/desktop/src/protocol.rs | 30 ++++----- packages/html/src/events.rs | 4 ++ packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 33 ++++++++-- packages/interpreter/src/ts/native.ts | 75 +++++++++++++++++++--- packages/liveview/Cargo.toml | 1 - packages/liveview/src/lib.rs | 9 +-- 14 files changed, 169 insertions(+), 156 deletions(-) delete mode 100644 packages/desktop/js/prevent_file_upload.js diff --git a/.vscode/settings.json b/.vscode/settings.json index e386d4b4c..1ebfd79ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,8 @@ "[toml]": { "editor.formatOnSave": false }, - "rust-analyzer.check.workspace": true, + "rust-analyzer.check.workspace": false, + // "rust-analyzer.check.workspace": true, "rust-analyzer.check.features": "all", "rust-analyzer.cargo.features": "all", "rust-analyzer.check.allTargets": true diff --git a/Cargo.lock b/Cargo.lock index ae75345b4..d928facd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,15 +83,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - [[package]] name = "aho-corasick" version = "1.1.2" @@ -2601,7 +2592,6 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "minify-js", "pretty_env_logger", "rustc-hash", "serde", @@ -4084,7 +4074,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ - "aho-corasick 1.1.2", + "aho-corasick", "bstr 1.9.1", "log", "regex-automata", @@ -4437,16 +4427,6 @@ dependencies = [ "ahash 0.7.8", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.10", - "bumpalo", -] - [[package]] name = "hashbrown" version = "0.14.3" @@ -5898,16 +5878,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minify-js" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22d6c512a82abddbbc13b70609cb2beff01be2c7afff534d6e5e1c85e438fc8b" -dependencies = [ - "lazy_static", - "parse-js", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -6619,19 +6589,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "parse-js" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ec3b11d443640ec35165ee8f6f0559f1c6f41878d70330fe9187012b5935f02" -dependencies = [ - "aho-corasick 0.7.20", - "bumpalo", - "hashbrown 0.13.2", - "lazy_static", - "memchr", -] - [[package]] name = "password-hash" version = "0.4.2" @@ -7562,7 +7519,7 @@ version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ - "aho-corasick 1.1.2", + "aho-corasick", "memchr", "regex-automata", "regex-syntax", @@ -7574,7 +7531,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ - "aho-corasick 1.1.2", + "aho-corasick", "memchr", "regex-syntax", ] diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 89669a248..4fe2a5a5b 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -3,9 +3,10 @@ //! Dioxus intercepts these events and provides a Rusty interface to the file data. Since we want this interface to //! be crossplatform, -use dioxus::html::HasFileData; +use std::sync::Arc; + use dioxus::prelude::*; -use tokio::time::sleep; +use dioxus::{html::HasFileData, prelude::dioxus_elements::FileEngine}; fn main() { launch(app); @@ -15,22 +16,24 @@ fn app() -> Element { let mut enable_directory_upload = use_signal(|| false); let mut files_uploaded = use_signal(|| Vec::new() as Vec); + let read_files = move |file_engine: Arc| async move { + let files = file_engine.files(); + for file_name in &files { + if let Some(file) = file_engine.read_file_to_string(file_name).await { + files_uploaded.write().push(file); + } + } + }; + let upload_files = move |evt: FormEvent| async move { - for file_name in evt.files().unwrap().files() { - // no files on form inputs? - sleep(std::time::Duration::from_secs(1)).await; - files_uploaded.write().push(file_name); + if let Some(file_engine) = evt.files() { + read_files(file_engine).await; } }; let handle_file_drop = move |evt: DragEvent| async move { - if let Some(file_engine) = &evt.files() { - let files = file_engine.files(); - for file_name in &files { - if let Some(file) = file_engine.read_file_to_string(file_name).await { - files_uploaded.write().push(file); - } - } + if let Some(file_engine) = evt.files() { + read_files(file_engine).await; } }; @@ -50,20 +53,24 @@ fn app() -> Element { r#type: "file", accept: ".txt,.rs", multiple: true, + name: "textreader", directory: enable_directory_upload, onchange: upload_files, } + label { r#for: "textreader", "Upload text/rust files and read them" } + div { // cheating with a little bit of JS... "ondragover": "this.style.backgroundColor='#88FF88';", "ondragleave": "this.style.backgroundColor='#FFFFFF';", id: "drop-zone", - prevent_default: "ondrop dragover dragenter", + // prevent_default: "ondrop dragover dragenter", ondrop: handle_file_drop, ondragover: move |event| event.stop_propagation(), "Drop files here" } + ul { for file in files_uploaded.read().iter() { li { "{file}" } diff --git a/packages/desktop/js/prevent_file_upload.js b/packages/desktop/js/prevent_file_upload.js deleted file mode 100644 index 935415178..000000000 --- a/packages/desktop/js/prevent_file_upload.js +++ /dev/null @@ -1,23 +0,0 @@ -// Prevent file inputs from opening the file dialog on click - let inputs = document.querySelectorAll("input"); - for (let input of inputs) { - if (!input.getAttribute("data-dioxus-file-listener")) { - // prevent file inputs from opening the file dialog on click - const type = input.getAttribute("type"); - if (type === "file") { - input.setAttribute("data-dioxus-file-listener", true); - input.addEventListener("click", (event) => { - let target = event.target; - let target_id = find_real_id(target); - if (target_id !== null) { - const send = (event_name) => { - const message = window.interpreter.serializeIpcMessage("file_dialog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); - window.ipc.postMessage(message); - }; - send("change&input"); - } - event.preventDefault(); - }); - } - } - } diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 9adf6bc96..2927ff6c8 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -2,9 +2,8 @@ use crate::{ config::{Config, WindowCloseBehaviour}, element::DesktopElement, event_handlers::WindowEventHandlers, - file_upload::FileDialogRequest, - ipc::IpcMessage, - ipc::{EventData, UserWindowEvent}, + file_upload::{DesktopFileUploadForm, FileDialogRequest}, + ipc::{EventData, IpcMessage, UserWindowEvent}, query::QueryResult, shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, webview::WebviewInstance, @@ -12,10 +11,7 @@ use crate::{ use crossbeam_channel::Receiver; use dioxus_core::ElementId; use dioxus_core::VirtualDom; -use dioxus_html::{ - native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData, HtmlEvent, - PlatformEventData, -}; +use dioxus_html::{native_bind::NativeFileEngine, HtmlEvent, PlatformEventData}; use std::{ cell::{Cell, RefCell}, collections::HashMap, @@ -270,30 +266,17 @@ impl App { let Ok(file_dialog) = serde_json::from_value::(msg.params()) else { return; }; - struct DesktopFileUploadForm { - files: Arc, - } - - impl HasFileData for DesktopFileUploadForm { - fn files(&self) -> Option> { - Some(self.files.clone()) - } - } - - impl HasFormData for DesktopFileUploadForm { - fn as_any(&self) -> &dyn std::any::Any { - self - } - } let id = ElementId(file_dialog.target); let event_name = &file_dialog.event; let event_bubbles = file_dialog.bubbles; let files = file_dialog.get_file_event(); - let data = Rc::new(PlatformEventData::new(Box::new(DesktopFileUploadForm { + let as_any = Box::new(DesktopFileUploadForm { files: Arc::new(NativeFileEngine::new(files)), - }))); + }); + + let data = Rc::new(PlatformEventData::new(as_any)); let view = self.webviews.get_mut(&window).unwrap(); diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index ac5a3df0d..e14c04a01 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -1,6 +1,6 @@ //! Convert a serialized event to an event trigger -use crate::element::DesktopElement; +use crate::{element::DesktopElement, file_upload::DesktopFileUploadForm}; use dioxus_html::*; pub(crate) struct SerializedHtmlEventConverter; @@ -47,8 +47,14 @@ impl HtmlEventConverter for SerializedHtmlEventConverter { } fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + // Attempt a simple serialized form data conversion + if let Some(_data) = event.downcast::() { + return _data.clone().into(); + } + + // If that failed then it's a file upload form event - .downcast::() + .downcast::() .cloned() .unwrap() .into() diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 98dbec437..c2bd651c5 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -1,7 +1,8 @@ #![allow(unused)] +use dioxus_html::{native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData}; use serde::Deserialize; -use std::{path::PathBuf, str::FromStr}; +use std::{path::PathBuf, str::FromStr, sync::Arc}; #[derive(Debug, Deserialize)] pub(crate) struct FileDialogRequest { @@ -124,3 +125,20 @@ impl FromStr for Filters { } } } + +#[derive(Clone)] +pub(crate) struct DesktopFileUploadForm { + pub files: Arc, +} + +impl HasFileData for DesktopFileUploadForm { + fn files(&self) -> Option> { + Some(self.files.clone()) + } +} + +impl HasFormData for DesktopFileUploadForm { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index ae3da5ba4..870c6cabb 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -13,18 +13,6 @@ const EDITS_PATH: &str = "http://dioxus.index.html/edits"; #[cfg(not(any(target_os = "android", target_os = "windows")))] const EDITS_PATH: &str = "dioxus://index.html/edits"; -// const PREVENT_FILE_UPLOAD: &str = include_str!("../js/prevent_file_upload.js"); - -fn handle_edits_code() -> String { - format!( - r#"// Poll for requests - {SLEDGEHAMMER_JS} - {NATIVE_JS} - window.interpreter = new NativeInterpreter("{EDITS_PATH}"); - "# - ) -} - static DEFAULT_INDEX: &str = include_str!("./index.html"); /// Build the index.html file we use for bootstrapping a new app @@ -184,20 +172,26 @@ fn serve_from_fs(path: PathBuf) -> Result>> { /// - headless: is this page being loaded but invisible? Important because not all windows are visible and the /// interpreter can't connect until the window is ready. fn module_loader(root_id: &str, headless: bool) -> String { - let js = handle_edits_code(); format!( r#" "# diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 9b58fc11c..0a761f719 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -59,6 +59,10 @@ impl PlatformEventData { Self { event } } + pub fn inner(&self) -> &Box { + &self.event + } + pub fn downcast(&self) -> Option<&T> { self.event.downcast_ref::() } diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 00ba413f1..31196d748 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -5236313184333634830 \ No newline at end of file +12023679989252671232 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 376eac052..b47df8f8a 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -214,7 +214,7 @@ var serializeDragEvent = function(event) { }; // src/ts/native.ts -var targetId = function(target) { +var getTargetId = function(target) { if (!(target instanceof Node)) { return null; } @@ -254,10 +254,30 @@ class NativeInterpreter extends JSChannel_ { } }, false); window.addEventListener("drop", function(e) { - if (e.target instanceof Element && e.target.tagName != "INPUT") { - e.preventDefault(); + let target = e.target; + if (!(target instanceof Element)) { + return; } + e.preventDefault(); }, false); + window.addEventListener("click", (event) => { + const target = event.target; + if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") { + let target_id = getTargetId(target); + if (target_id !== null) { + const message = this.serializeIpcMessage("file_dialog", { + event: "change&input", + accept: target.getAttribute("accept"), + directory: target.getAttribute("webkitdirectory") === "true", + multiple: target.hasAttribute("multiple"), + target: target_id, + bubbles: event.bubbles + }); + this.ipc.postMessage(message); + } + event.preventDefault(); + } + }); this.ipc = window.ipc; const handler = (event) => this.handleEvent(event, event.type, true); super.initialize(root, handler); @@ -310,9 +330,8 @@ class NativeInterpreter extends JSChannel_ { } } handleEvent(event, name, bubbles) { - console.log("handling event: ", event); const target = event.target; - const realId = targetId(target); + const realId = getTargetId(target); const contents = serializeEvent(event, target); let body = { name, @@ -386,14 +405,14 @@ class NativeInterpreter extends JSChannel_ { } } } - wait_for_request(headless) { + waitForRequest(headless) { fetch(new Request(this.editsPath)).then((response) => response.arrayBuffer()).then((bytes) => { if (headless) { this.run_from_bytes(bytes); } else { requestAnimationFrame(() => this.run_from_bytes(bytes)); } - this.wait_for_request(headless); + this.waitForRequest(headless); }); } } diff --git a/packages/interpreter/src/ts/native.ts b/packages/interpreter/src/ts/native.ts index 13bb152e2..9755421a8 100644 --- a/packages/interpreter/src/ts/native.ts +++ b/packages/interpreter/src/ts/native.ts @@ -36,19 +36,49 @@ export class NativeInterpreter extends JSChannel_ { // attach an event listener on the body that prevents file drops from navigating // this is because the browser will try to navigate to the file if it's dropped on the window window.addEventListener("dragover", function (e) { - // check which element is our target + // // check which element is our target if (e.target instanceof Element && e.target.tagName != "INPUT") { e.preventDefault(); } }, false); window.addEventListener("drop", function (e) { - // check which element is our target - if (e.target instanceof Element && e.target.tagName != "INPUT") { - e.preventDefault(); + let target = e.target; + + if (!(target instanceof Element)) { + return; } + + // Dropping a file on the window will navigate to the file, which we don't want + e.preventDefault(); + + // if the element has a drop listener on it, we should send a message to the host with the contents of the drop instead + }, false); + // attach a listener to the route that listens for clicks and prevents the default file dialog + window.addEventListener("click", (event) => { + const target = event.target; + if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") { + // Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it + let target_id = getTargetId(target); + if (target_id !== null) { + const message = this.serializeIpcMessage("file_dialog", { + event: "change&input", + accept: target.getAttribute("accept"), + directory: target.getAttribute("webkitdirectory") === "true", + multiple: target.hasAttribute("multiple"), + target: target_id, + bubbles: event.bubbles, + }); + this.ipc.postMessage(message); + } + + // Prevent default regardless - we don't want file dialogs and we don't want the browser to navigate + event.preventDefault(); + } + }); + // @ts-ignore - wry gives us this this.ipc = window.ipc; @@ -119,10 +149,8 @@ export class NativeInterpreter extends JSChannel_ { } handleEvent(event: Event, name: string, bubbles: boolean) { - console.log("handling event: ", event); - const target = event.target!; - const realId = targetId(target)!; + const realId = getTargetId(target)!; const contents = serializeEvent(event, target); // Handle the event on the virtualdom and then preventDefault if it also preventsDefault @@ -141,6 +169,8 @@ export class NativeInterpreter extends JSChannel_ { this.preventDefaults(event, target); + + // liveview does not have syncronous event handling, so we need to send the event to the host if (this.liveview) { // Okay, so the user might've requested some files to be read @@ -263,7 +293,7 @@ export class NativeInterpreter extends JSChannel_ { } } - wait_for_request(headless: boolean) { + waitForRequest(headless: boolean) { fetch(new Request(this.editsPath)) .then(response => response.arrayBuffer()) .then(bytes => { @@ -275,7 +305,7 @@ export class NativeInterpreter extends JSChannel_ { // @ts-ignore requestAnimationFrame(() => this.run_from_bytes(bytes)); } - this.wait_for_request(headless); + this.waitForRequest(headless); }); } } @@ -306,7 +336,7 @@ function handleVirtualdomEventSync(contents: string): EventSyncResult { return JSON.parse(xhr.responseText); } -function targetId(target: EventTarget): NodeId | null { +function getTargetId(target: EventTarget): NodeId | null { // Ensure that the target is a node, sometimes it's nota if (!(target instanceof Node)) { return null; @@ -329,3 +359,28 @@ function targetId(target: EventTarget): NodeId | null { return parseInt(realId); } + + +// function applyFileUpload() { +// let inputs = document.querySelectorAll("input"); +// for (let input of inputs) { +// if (!input.getAttribute("data-dioxus-file-listener")) { +// // prevent file inputs from opening the file dialog on click +// const type = input.getAttribute("type"); +// if (type === "file") { +// input.setAttribute("data-dioxus-file-listener", true); +// input.addEventListener("click", (event) => { +// let target = event.target; +// let target_id = find_real_id(target); +// if (target_id !== null) { +// const send = (event_name) => { +// const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); +// window.ipc.postMessage(message); +// }; +// send("change&input"); +// } +// event.preventDefault(); +// }); +// } +// } +// } diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 6e7173e18..dac3ea6b6 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -24,7 +24,6 @@ serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" dioxus-html = { workspace = true, features = ["serialize", "eval", "mounted"] } rustc-hash = { workspace = true } -minify-js = "0.5.6" dioxus-core = { workspace = true, features = ["serialize"] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 121f0245c..33f916785 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -32,7 +32,6 @@ pub enum LiveViewError { fn handle_edits_code() -> String { use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; - use minify_js::{minify, Session, TopLevelMode}; let serialize_file_uploads = r#"if ( target.tagName === "INPUT" && @@ -81,15 +80,9 @@ fn handle_edits_code() -> String { .unwrap_or_else(|| interpreter.len()); interpreter.replace_range(import_start..import_end, ""); } - let main_js = include_str!("./main.js"); - let js = format!("{interpreter}\n{main_js}"); - - let session = Session::new(); - let mut out = Vec::new(); - minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap(); - String::from_utf8(out).unwrap() + js } /// This script that gets injected into your app connects this page to the websocket endpoint From 78d16536a783cddcc192d2fe19c821ee5ac87c1b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 13:57:28 -0800 Subject: [PATCH 17/41] =?UTF-8?q?wip:=20native=20file=20handles=20when=20d?= =?UTF-8?q?ropping=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/file_upload.rs | 7 +- packages/desktop/src/app.rs | 2 + packages/desktop/src/config.rs | 9 --- packages/desktop/src/desktop_context.rs | 14 +++- packages/desktop/src/file_upload.rs | 13 ++- packages/desktop/src/ipc.rs | 2 + packages/desktop/src/launch.rs | 1 + packages/desktop/src/webview.rs | 23 ++++-- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 35 ++++---- packages/interpreter/src/ts/native.ts | 102 ++++++++++++++++-------- 11 files changed, 134 insertions(+), 76 deletions(-) diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 4fe2a5a5b..f26b45477 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; use dioxus::{html::HasFileData, prelude::dioxus_elements::FileEngine}; fn main() { - launch(app); + LaunchBuilder::desktop().launch(app); } fn app() -> Element { @@ -51,7 +51,7 @@ fn app() -> Element { input { r#type: "file", - accept: ".txt,.rs", + accept: ".txt,.rs,.js", multiple: true, name: "textreader", directory: enable_directory_upload, @@ -64,10 +64,9 @@ fn app() -> Element { // cheating with a little bit of JS... "ondragover": "this.style.backgroundColor='#88FF88';", "ondragleave": "this.style.backgroundColor='#FFFFFF';", + "ondrop": "this.style.backgroundColor='#FFFFFF';", id: "drop-zone", - // prevent_default: "ondrop dragover dragenter", ondrop: handle_file_drop, - ondragover: move |event| event.stop_propagation(), "Drop files here" } diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 2927ff6c8..b05bb0946 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -262,6 +262,8 @@ impl App { } } + pub fn handle_file_drag(&mut self, msg: IpcMessage, window: WindowId) {} + pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) { let Ok(file_dialog) = serde_json::from_value::(msg.params()) else { return; diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index f84baf6ea..feedf00da 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -118,15 +118,6 @@ impl Config { self } - /// Set a file drop handler. If this is enabled, html drag events will be disabled. - pub fn with_file_drop_handler( - mut self, - handler: impl Fn(WindowId, FileDropEvent) -> bool + 'static, - ) -> Self { - self.file_drop_handler = Some(Box::new(handler)); - self - } - /// Set a custom protocol pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self where diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 867516a64..55f3c6417 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -2,6 +2,7 @@ use crate::{ app::SharedContext, assets::AssetHandlerRegistry, edits::EditQueue, + file_upload::NativeFileHover, ipc::{EventData, UserWindowEvent}, query::QueryEngine, shortcut::{HotKey, ShortcutHandle, ShortcutRegistryError}, @@ -13,13 +14,17 @@ use dioxus_core::{ VirtualDom, }; use dioxus_interpreter_js::MutationState; -use std::{cell::RefCell, rc::Rc, rc::Weak}; +use std::{ + cell::RefCell, + rc::{Rc, Weak}, + sync::Arc, +}; use tao::{ event::Event, event_loop::EventLoopWindowTarget, window::{Fullscreen as WryFullscreen, Window, WindowId}, }; -use wry::{RequestAsyncResponder, WebView}; +use wry::{FileDropEvent, RequestAsyncResponder, WebView}; #[cfg(target_os = "ios")] use tao::platform::ios::WindowExtIOS; @@ -62,6 +67,7 @@ pub struct DesktopService { pub(crate) edit_queue: EditQueue, pub(crate) mutation_state: RefCell, pub(crate) asset_handlers: AssetHandlerRegistry, + pub(crate) file_hover: NativeFileHover, #[cfg(target_os = "ios")] pub(crate) views: Rc>>, @@ -83,14 +89,16 @@ impl DesktopService { shared: Rc, edit_queue: EditQueue, asset_handlers: AssetHandlerRegistry, + file_hover: NativeFileHover, ) -> Self { Self { window, webview, shared, edit_queue, - mutation_state: Default::default(), asset_handlers, + file_hover, + mutation_state: Default::default(), query: Default::default(), #[cfg(target_os = "ios")] views: Default::default(), diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index c2bd651c5..af6643b7a 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -2,7 +2,8 @@ use dioxus_html::{native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData}; use serde::Deserialize; -use std::{path::PathBuf, str::FromStr, sync::Arc}; +use std::{cell::Cell, path::PathBuf, rc::Rc, str::FromStr, sync::Arc}; +use wry::FileDropEvent; #[derive(Debug, Deserialize)] pub(crate) struct FileDialogRequest { @@ -142,3 +143,13 @@ impl HasFormData for DesktopFileUploadForm { self } } + +#[derive(Default, Clone)] +pub struct NativeFileHover { + event: Rc>>, +} +impl NativeFileHover { + pub fn set(&self, event: FileDropEvent) { + self.event.set(Some(event)); + } +} diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs index 464b80423..69d8eb5a3 100644 --- a/packages/desktop/src/ipc.rs +++ b/packages/desktop/src/ipc.rs @@ -42,6 +42,7 @@ pub enum IpcMethod<'a> { Query, BrowserOpen, Initialize, + FileDrag, Other(&'a str), } @@ -50,6 +51,7 @@ impl IpcMessage { match self.method.as_str() { // todo: this is a misspelling, needs to be fixed "file_dialog" => IpcMethod::FileDialog, + "file_drag" => IpcMethod::FileDrag, "user_event" => IpcMethod::UserEvent, "query" => IpcMethod::Query, "browser_open" => IpcMethod::BrowserOpen, diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index b5602d10e..40029d87d 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -34,6 +34,7 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), EventData::Ipc(msg) => match msg.method() { IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), + IpcMethod::FileDrag => app.handle_file_drag(msg, id), IpcMethod::UserEvent => app.handle_user_event_msg(msg, id), IpcMethod::Query => app.handle_query_msg(msg, id), IpcMethod::BrowserOpen => app.handle_browser_open(msg), diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index ade023c2f..276327da4 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -3,8 +3,9 @@ use crate::{ assets::AssetHandlerRegistry, edits::EditQueue, eval::DesktopEvalProvider, + file_upload::NativeFileHover, ipc::{EventData, UserWindowEvent}, - protocol::{self}, + protocol, waker::tao_waker, Config, DesktopContext, DesktopService, }; @@ -65,7 +66,6 @@ impl WebviewInstance { // Rust :( let window_id = window.id(); - let file_handler = cfg.file_drop_handler.take(); let custom_head = cfg.custom_head.clone(); let index_file = cfg.custom_index.clone(); let root_name = cfg.root_name.clone(); @@ -102,6 +102,8 @@ impl WebviewInstance { } }; + let file_hover = NativeFileHover::default(); + #[cfg(any( target_os = "windows", target_os = "macos", @@ -128,12 +130,18 @@ impl WebviewInstance { .with_url("dioxus://index.html/") .unwrap() .with_ipc_handler(ipc_handler) + .with_navigation_handler(|var| var.contains("dioxus")) // prevent all navigations .with_asynchronous_custom_protocol(String::from("dioxus"), request_handler) - .with_web_context(&mut web_context); - - if let Some(handler) = file_handler { - webview = webview.with_file_drop_handler(move |evt| handler(window_id, evt)) - } + .with_web_context(&mut web_context) + .with_file_drop_handler({ + let file_hover = file_hover.clone(); + move |evt| { + println!("file drop: {:?}", evt); + // Update the most recent file hover status + file_hover.set(evt); + false + } + }); if let Some(color) = cfg.background_color { webview = webview.with_background_color(color); @@ -178,6 +186,7 @@ impl WebviewInstance { shared.clone(), edit_queue, asset_handlers, + file_hover, )); let provider: Rc = diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 31196d748..4bfd228e6 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -12023679989252671232 \ No newline at end of file +2617078454438840343 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index b47df8f8a..0bd145a16 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -248,6 +248,8 @@ class NativeInterpreter extends JSChannel_ { initialize(root) { this.intercept_link_redirects = true; this.liveview = false; + const dragEventHandler = (e) => { + }; window.addEventListener("dragover", function(e) { if (e.target instanceof Element && e.target.tagName != "INPUT") { e.preventDefault(); @@ -349,25 +351,8 @@ class NativeInterpreter extends JSChannel_ { } else { const message = this.serializeIpcMessage("user_event", body); this.ipc.postMessage(message); - console.log("sent message to host: ", message); } } - async readFiles(target, contents, bubbles, realId, name) { - let files = target.files; - let file_contents = {}; - for (let i = 0;i < files.length; i++) { - const file = files[i]; - file_contents[file.name] = Array.from(new Uint8Array(await file.arrayBuffer())); - } - contents.files = { files: file_contents }; - const message = this.serializeIpcMessage("user_event", { - name, - element: realId, - data: contents, - bubbles - }); - this.ipc.postMessage(message); - } preventDefaults(event, target) { let preventDefaultRequests = null; if (target instanceof Element) { @@ -415,6 +400,22 @@ class NativeInterpreter extends JSChannel_ { this.waitForRequest(headless); }); } + async readFiles(target, contents, bubbles, realId, name) { + let files = target.files; + let file_contents = {}; + for (let i = 0;i < files.length; i++) { + const file = files[i]; + file_contents[file.name] = Array.from(new Uint8Array(await file.arrayBuffer())); + } + contents.files = { files: file_contents }; + const message = this.serializeIpcMessage("user_event", { + name, + element: realId, + data: contents, + bubbles + }); + this.ipc.postMessage(message); + } } export { NativeInterpreter diff --git a/packages/interpreter/src/ts/native.ts b/packages/interpreter/src/ts/native.ts index 9755421a8..bcc810940 100644 --- a/packages/interpreter/src/ts/native.ts +++ b/packages/interpreter/src/ts/native.ts @@ -29,10 +29,50 @@ export class NativeInterpreter extends JSChannel_ { } initialize(root: HTMLElement): void { - this.intercept_link_redirects = true; this.liveview = false; + + const dragEventHandler = (e: DragEvent) => { + // e.preventDefault(); + // e.dataTransfer.effectAllowed = 'none'; + // e.dataTransfer.dropEffect = 'none'; + + // + + // we need to signal to the host to provide a native file path, + // not the one coming from here + + // We can't get native + // if (e.type === "drop") { + // let target = e.target; + + // if (target instanceof Element) { + // let target_id = getTargetId(target); + // if (target_id !== null) { + // const message = this.serializeIpcMessage("file_drop", { + // event: "drop", + // target: target_id, + // data: Array.from(e.dataTransfer.files).map(file => { + // return { + // name: file.name, + // type: file.type, + // size: file.size, + // }; + // }), + // }); + + // this.ipc.postMessage(message); + // } + // } + // } + }; + + + // ["dragenter", "dragover", "drop"].forEach((event) => { + // window.addEventListener(event, dragEventHandler, false) + // }); + // attach an event listener on the body that prevents file drops from navigating // this is because the browser will try to navigate to the file if it's dropped on the window window.addEventListener("dragover", function (e) { @@ -51,9 +91,6 @@ export class NativeInterpreter extends JSChannel_ { // Dropping a file on the window will navigate to the file, which we don't want e.preventDefault(); - - // if the element has a drop listener on it, we should send a message to the host with the contents of the drop instead - }, false); // attach a listener to the route that listens for clicks and prevents the default file dialog @@ -123,7 +160,7 @@ export class NativeInterpreter extends JSChannel_ { } } - // ignore the fact the base interpreter uses ptr + len but we use array + // ignore the fact the base interpreter uses ptr + len but we use array... // @ts-ignore loadChild(array: number[]) { // iterate through each number and get that child @@ -168,9 +205,6 @@ export class NativeInterpreter extends JSChannel_ { // Liveview will still need to use this this.preventDefaults(event, target); - - - // liveview does not have syncronous event handling, so we need to send the event to the host if (this.liveview) { // Okay, so the user might've requested some files to be read @@ -184,8 +218,6 @@ export class NativeInterpreter extends JSChannel_ { const message = this.serializeIpcMessage("user_event", body); this.ipc.postMessage(message); - console.log("sent message to host: ", message); - // // Run the event handler on the virtualdom // // capture/prevent default of the event if the virtualdom wants to // const res = handleVirtualdomEventSync(JSON.stringify(body)); @@ -200,30 +232,6 @@ export class NativeInterpreter extends JSChannel_ { } } - // A liveview only function - // Desktop will intercept the event before it hits this - async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) { - let files = target.files!; - let file_contents: { [name: string]: number[] } = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - - contents.files = { files: file_contents }; - - const message = this.serializeIpcMessage("user_event", { - name: name, - element: realId, - data: contents, - bubbles, - }); - - this.ipc.postMessage(message); - } // This should: @@ -308,6 +316,32 @@ export class NativeInterpreter extends JSChannel_ { this.waitForRequest(headless); }); } + + + // A liveview only function + // Desktop will intercept the event before it hits this + async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) { + let files = target.files!; + let file_contents: { [name: string]: number[] } = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + + contents.files = { files: file_contents }; + + const message = this.serializeIpcMessage("user_event", { + name: name, + element: realId, + data: contents, + bubbles, + }); + + this.ipc.postMessage(message); + } } type EventSyncResult = { From 624e58bd789097160f9dcd9dd316290fc9e3e511 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 14:11:12 -0800 Subject: [PATCH 18/41] native file drop --- packages/desktop/src/app.rs | 24 +++++++- packages/desktop/src/events.rs | 13 ++++- packages/desktop/src/file_upload.rs | 80 +++++++++++++++++++++++++- packages/html/src/events/drag.rs | 2 +- packages/html/src/point_interaction.rs | 2 +- packages/interpreter/src/js/hash.txt | 2 +- 6 files changed, 115 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index b05bb0946..ed3528508 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -2,7 +2,7 @@ use crate::{ config::{Config, WindowCloseBehaviour}, element::DesktopElement, event_handlers::WindowEventHandlers, - file_upload::{DesktopFileUploadForm, FileDialogRequest}, + file_upload::{DesktopFileDragEvent, DesktopFileUploadForm, FileDialogRequest}, ipc::{EventData, IpcMessage, UserWindowEvent}, query::QueryResult, shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, @@ -11,7 +11,7 @@ use crate::{ use crossbeam_channel::Receiver; use dioxus_core::ElementId; use dioxus_core::VirtualDom; -use dioxus_html::{native_bind::NativeFileEngine, HtmlEvent, PlatformEventData}; +use dioxus_html::{native_bind::NativeFileEngine, HasFileData, HtmlEvent, PlatformEventData}; use std::{ cell::{Cell, RefCell}, collections::HashMap, @@ -231,6 +231,7 @@ impl App { let view = self.webviews.get_mut(&id).unwrap(); let query = view.desktop_context.query.clone(); + let recent_file = view.desktop_context.file_hover.clone(); // check for a mounted event placeholder and replace it with a desktop specific element let as_any = match data { @@ -238,6 +239,25 @@ impl App { let element = DesktopElement::new(element, view.desktop_context.clone(), query); Rc::new(PlatformEventData::new(Box::new(element))) } + dioxus_html::EventData::Drag(ref drag) => { + // we want to override this with a native file engine, provided by the most recent drag event + if drag.files().is_some() { + let file_event = recent_file.current().unwrap(); + let paths = match file_event { + wry::FileDropEvent::Hovered { paths, position } => paths, + wry::FileDropEvent::Dropped { paths, position } => paths, + _ => vec![], + }; + let files = Arc::new(NativeFileEngine::new(paths)); + let event = DesktopFileDragEvent { + mouse: drag.mouse.clone(), + files, + }; + Rc::new(PlatformEventData::new(Box::new(event))) + } else { + data.into_any() + } + } _ => data.into_any(), }; diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index e14c04a01..5e772320b 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -1,6 +1,9 @@ //! Convert a serialized event to an event trigger -use crate::{element::DesktopElement, file_upload::DesktopFileUploadForm}; +use crate::{ + element::DesktopElement, + file_upload::{DesktopFileDragEvent, DesktopFileUploadForm}, +}; use dioxus_html::*; pub(crate) struct SerializedHtmlEventConverter; @@ -31,8 +34,14 @@ impl HtmlEventConverter for SerializedHtmlEventConverter { } fn convert_drag_data(&self, event: &PlatformEventData) -> DragData { + // Attempt a simple serialized data conversion + if let Some(_data) = event.downcast::() { + return _data.clone().into(); + } + + // If that failed then it's a file drag form event - .downcast::() + .downcast::() .cloned() .unwrap() .into() diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index af6643b7a..a7edef30d 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -1,6 +1,16 @@ #![allow(unused)] -use dioxus_html::{native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData}; +use dioxus_html::{ + geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}, + input_data::{MouseButton, MouseButtonSet}, + native_bind::NativeFileEngine, + point_interaction::{ + InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction, + }, + prelude::{SerializedMouseData, SerializedPointInteraction}, + FileEngine, HasDragData, HasFileData, HasFormData, HasMouseData, +}; +use muda::accelerator::Modifiers; use serde::Deserialize; use std::{cell::Cell, path::PathBuf, rc::Rc, str::FromStr, sync::Arc}; use wry::FileDropEvent; @@ -152,4 +162,72 @@ impl NativeFileHover { pub fn set(&self, event: FileDropEvent) { self.event.set(Some(event)); } + + pub fn current(&self) -> Option { + self.event.as_ref().clone().take() + } +} + +#[derive(Clone)] +pub(crate) struct DesktopFileDragEvent { + pub mouse: SerializedPointInteraction, + pub files: Arc, +} + +impl HasFileData for DesktopFileDragEvent { + fn files(&self) -> Option> { + Some(self.files.clone()) + } +} + +impl HasDragData for DesktopFileDragEvent { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasMouseData for DesktopFileDragEvent { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl InteractionLocation for DesktopFileDragEvent { + fn client_coordinates(&self) -> ClientPoint { + self.mouse.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.mouse.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.mouse.screen_coordinates() + } +} + +impl InteractionElementOffset for DesktopFileDragEvent { + fn element_coordinates(&self) -> ElementPoint { + self.mouse.element_coordinates() + } + + fn coordinates(&self) -> Coordinates { + self.mouse.coordinates() + } +} + +impl ModifiersInteraction for DesktopFileDragEvent { + fn modifiers(&self) -> Modifiers { + self.mouse.modifiers() + } +} + +impl PointerInteraction for DesktopFileDragEvent { + fn held_buttons(&self) -> MouseButtonSet { + self.mouse.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.mouse.trigger_button() + } } diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index 6e31334a1..4cd8119db 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -109,7 +109,7 @@ impl PointerInteraction for DragData { /// A serialized version of DragData #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] pub struct SerializedDragData { - mouse: crate::point_interaction::SerializedPointInteraction, + pub mouse: crate::point_interaction::SerializedPointInteraction, #[serde(default)] files: Option, diff --git a/packages/html/src/point_interaction.rs b/packages/html/src/point_interaction.rs index 9aa4e0f31..b0e34f780 100644 --- a/packages/html/src/point_interaction.rs +++ b/packages/html/src/point_interaction.rs @@ -50,7 +50,7 @@ pub trait ModifiersInteraction { #[cfg(feature = "serialize")] #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Default)] -pub(crate) struct SerializedPointInteraction { +pub struct SerializedPointInteraction { pub alt_key: bool, /// The button number that was pressed (if applicable) when the mouse event was fired. diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 4bfd228e6..bfd4a2268 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -2617078454438840343 \ No newline at end of file +13140749608285001344 \ No newline at end of file From 39d6580eca12e71160140369651be9329b566ca6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 15:14:07 -0800 Subject: [PATCH 19/41] use git version of sledgehammer --- Cargo.lock | 2 ++ packages/interpreter/Cargo.toml | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d928facd3..9ffbb194b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8484,6 +8484,7 @@ dependencies = [ [[package]] name = "sledgehammer_bindgen" version = "0.4.0" +source = "git+https://github.com/ealmloff/sledgehammer_bindgen#91331b3f380883b8883cf5d5b81424c1846340cf" dependencies = [ "sledgehammer_bindgen_macro", "wasm-bindgen", @@ -8492,6 +8493,7 @@ dependencies = [ [[package]] name = "sledgehammer_bindgen_macro" version = "0.4.0" +source = "git+https://github.com/ealmloff/sledgehammer_bindgen#91331b3f380883b8883cf5d5b81424c1846340cf" dependencies = [ "quote", "syn 2.0.51", diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 64edf911a..1cb573911 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -17,8 +17,7 @@ web-sys = { version = "0.3.56", optional = true, features = [ "Element", "Node", ] } -sledgehammer_bindgen = { path = "../../../Tinkering/sledgehammer_bindgen", default-features = false, optional = true } -# sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } +sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } sledgehammer_utils = { version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } From 4b2e42695898d9adf6108a9ef4c8ce8571041fdf Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 22:38:38 -0800 Subject: [PATCH 20/41] clippy, and don't hash invisible files for ts generation --- Cargo.lock | 1 - examples/assets/file_upload.css | 9 +- examples/file_upload.rs | 57 ++- examples/form.rs | 4 +- packages/desktop/Cargo.toml | 2 - packages/desktop/headless_tests/events.rs | 4 +- packages/desktop/headless_tests/rendering.rs | 2 +- packages/desktop/headless_tests/utils.rs | 2 +- packages/desktop/src/app.rs | 70 +-- packages/desktop/src/config.rs | 12 +- packages/desktop/src/desktop_context.rs | 16 +- packages/desktop/src/file_upload.rs | 14 +- packages/desktop/src/ipc.rs | 18 +- packages/desktop/src/launch.rs | 20 +- packages/desktop/src/mobile_shortcut.rs | 6 - packages/desktop/src/protocol.rs | 1 + packages/desktop/src/query.rs | 9 +- packages/desktop/src/waker.rs | 4 +- packages/desktop/src/webview.rs | 49 +-- packages/interpreter/build.rs | 23 +- packages/interpreter/src/js/common.js | 100 +---- packages/interpreter/src/js/core.js | 208 +-------- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 423 +------------------ packages/interpreter/src/ts/native.ts | 41 -- packages/interpreter/src/unified_bindings.rs | 2 +- packages/web/src/lib.rs | 10 +- 27 files changed, 173 insertions(+), 936 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ffbb194b..3d1ebad8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2384,7 +2384,6 @@ version = "0.5.0-alpha.0" dependencies = [ "async-trait", "core-foundation", - "crossbeam-channel", "dioxus", "dioxus-cli-config", "dioxus-core", diff --git a/examples/assets/file_upload.css b/examples/assets/file_upload.css index 81aba2638..58d1bb3dc 100644 --- a/examples/assets/file_upload.css +++ b/examples/assets/file_upload.css @@ -1,14 +1,11 @@ body { font-family: Arial, sans-serif; - margin: 0; - padding: 0; + margin: 10px; + padding: 10px; background-color: #f4f4f4; - display: flex; justify-content: center; align-items: center; - height: 100vh; - flex-direction: column; - gap: 20px; + min-height: 100vh; } #drop-zone { diff --git a/examples/file_upload.rs b/examples/file_upload.rs index f26b45477..3a433250e 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -12,15 +12,23 @@ fn main() { LaunchBuilder::desktop().launch(app); } +struct UploadedFile { + name: String, + contents: String, +} + fn app() -> Element { let mut enable_directory_upload = use_signal(|| false); - let mut files_uploaded = use_signal(|| Vec::new() as Vec); + let mut files_uploaded = use_signal(|| Vec::new() as Vec); let read_files = move |file_engine: Arc| async move { let files = file_engine.files(); for file_name in &files { - if let Some(file) = file_engine.read_file_to_string(file_name).await { - files_uploaded.write().push(file); + if let Some(contents) = file_engine.read_file_to_string(file_name).await { + files_uploaded.write().push(UploadedFile { + name: file_name.clone(), + contents, + }); } } }; @@ -40,25 +48,31 @@ fn app() -> Element { rsx! { style { {include_str!("./assets/file_upload.css")} } - input { - r#type: "checkbox", - id: "directory-upload", - checked: enable_directory_upload, - oninput: move |evt| enable_directory_upload.set(evt.checked()), - }, + h1 { "File Upload Example" } + p { "Drop a .txt, .rs, or .js file here to read it" } - label { r#for: "directory-upload", "Enable directory upload" } - input { - r#type: "file", - accept: ".txt,.rs,.js", - multiple: true, - name: "textreader", - directory: enable_directory_upload, - onchange: upload_files, + div { + label { r#for: "directory-upload", "Enable directory upload" } + input { + r#type: "checkbox", + id: "directory-upload", + checked: enable_directory_upload, + oninput: move |evt| enable_directory_upload.set(evt.checked()), + }, } - label { r#for: "textreader", "Upload text/rust files and read them" } + div { + label { r#for: "textreader", "Upload text/rust files and read them" } + input { + r#type: "file", + accept: ".txt,.rs,.js", + multiple: true, + name: "textreader", + directory: enable_directory_upload, + onchange: upload_files, + } + } div { // cheating with a little bit of JS... @@ -71,8 +85,11 @@ fn app() -> Element { } ul { - for file in files_uploaded.read().iter() { - li { "{file}" } + for file in files_uploaded.read().iter().rev() { + li { + span { "{file.name}" } + pre { "{file.contents}" } + } } } } diff --git a/examples/form.rs b/examples/form.rs index b49072292..9f6215226 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -11,8 +11,8 @@ fn main() { } fn app() -> Element { - let mut values = use_signal(|| HashMap::new()); - let mut submitted_values = use_signal(|| HashMap::new()); + let mut values = use_signal(HashMap::new); + let mut submitted_values = use_signal(HashMap::new); rsx! { div { style: "display: flex", diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index fdb68d734..dfca27c58 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -46,11 +46,9 @@ dunce = "1.0.2" slab = { workspace = true } rustc-hash = { workspace = true } dioxus-hooks = { workspace = true } - futures-util = { workspace = true } urlencoding = "2.1.2" async-trait = "0.1.68" -crossbeam-channel = "0.5.8" tao = { version = "0.24.0", features = ["rwh_05"] } [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index 870e43057..c981144d5 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -437,7 +437,7 @@ fn test_focus_out_div() -> Element { } fn test_form_input() -> Element { - let mut values = use_signal(|| HashMap::new()); + let mut values = use_signal(HashMap::new); utils::mock_event_with_extra( "form-username", @@ -489,7 +489,7 @@ fn test_form_input() -> Element { } fn test_form_submit() -> Element { - let mut values = use_signal(|| HashMap::new()); + let mut values = use_signal(HashMap::new); utils::mock_event_with_extra( "form-submitter", diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 15e2b9f59..df050ed10 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -14,7 +14,7 @@ fn use_inner_html(id: &'static str) -> Option { use_effect(move || { spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(2000)).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; let res = eval(&format!( r#"let element = document.getElementById('{}'); diff --git a/packages/desktop/headless_tests/utils.rs b/packages/desktop/headless_tests/utils.rs index 343e76467..419f5c8cd 100644 --- a/packages/desktop/headless_tests/utils.rs +++ b/packages/desktop/headless_tests/utils.rs @@ -36,7 +36,7 @@ pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'sta EXPECTED_EVENTS.with_mut(|x| *x += 1); spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(5000)).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; let js = format!( r#" diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index ed3528508..d70788bd3 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -3,12 +3,11 @@ use crate::{ element::DesktopElement, event_handlers::WindowEventHandlers, file_upload::{DesktopFileDragEvent, DesktopFileUploadForm, FileDialogRequest}, - ipc::{EventData, IpcMessage, UserWindowEvent}, + ipc::{IpcMessage, UserWindowEvent}, query::QueryResult, shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, webview::WebviewInstance, }; -use crossbeam_channel::Receiver; use dioxus_core::ElementId; use dioxus_core::VirtualDom; use dioxus_html::{native_bind::NativeFileEngine, HasFileData, HtmlEvent, PlatformEventData}; @@ -50,7 +49,6 @@ pub(crate) struct SharedContext { pub(crate) event_handlers: WindowEventHandlers, pub(crate) pending_webviews: RefCell>, pub(crate) shortcut_manager: ShortcutRegistry, - pub(crate) global_hotkey_channel: Receiver, pub(crate) proxy: EventLoopProxy, pub(crate) target: EventLoopWindowTarget, } @@ -70,7 +68,6 @@ impl App { event_handlers: WindowEventHandlers::default(), pending_webviews: Default::default(), shortcut_manager: ShortcutRegistry::new(), - global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(), proxy: event_loop.create_proxy(), target: event_loop.clone(), }), @@ -79,6 +76,9 @@ impl App { // Set the event converter dioxus_html::set_event_converter(Box::new(crate::events::SerializedHtmlEventConverter)); + // Wire up the global hotkey handler + app.set_global_hotkey_handler(); + // Allow hotreloading to work - but only in debug mode #[cfg(all(feature = "hot-reload", debug_assertions))] app.connect_hotreload(); @@ -88,14 +88,13 @@ impl App { pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) { self.control_flow = ControlFlow::Wait; - self.shared .event_handlers .apply_event(window_event, &self.shared.target); + } - if let Ok(event) = self.shared.global_hotkey_channel.try_recv() { - self.shared.shortcut_manager.call_handlers(event); - } + pub fn handle_global_hotkey(&self, event: GlobalHotKeyEvent) { + self.shared.shortcut_manager.call_handlers(event); } #[cfg(all(feature = "hot-reload", debug_assertions))] @@ -103,10 +102,7 @@ impl App { dioxus_hot_reload::connect({ let proxy = self.shared.proxy.clone(); move |template| { - let _ = proxy.send_event(UserWindowEvent( - EventData::HotReloadEvent(template), - unsafe { WindowId::dummy() }, - )); + let _ = proxy.send_event(UserWindowEvent::HotReloadEvent(template)); } }); } @@ -115,10 +111,7 @@ impl App { for handler in self.shared.pending_webviews.borrow_mut().drain(..) { let id = handler.desktop_context.window.id(); self.webviews.insert(id, handler); - _ = self - .shared - .proxy - .send_event(UserWindowEvent(EventData::Poll, id)); + _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id)); } } @@ -168,11 +161,6 @@ impl App { let id = webview.desktop_context.window.id(); self.webviews.insert(id, webview); - - _ = self - .shared - .proxy - .send_event(UserWindowEvent(EventData::Poll, id)); } pub fn handle_browser_open(&mut self, msg: IpcMessage) { @@ -186,19 +174,29 @@ impl App { } } + /// The webview is finally loaded + /// + /// Let's rebuild it and then start polling it pub fn handle_initialize_msg(&mut self, id: WindowId) { let view = self.webviews.get_mut(&id).unwrap(); + view.dom .rebuild(&mut *view.desktop_context.mutation_state.borrow_mut()); + view.desktop_context.send_edits(); + view.desktop_context .window .set_visible(self.is_visible_before_start); + + _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id)); } + /// Todo: maybe we should poll the virtualdom asking if it has any final actions to apply before closing the webview + /// + /// Technically you can handle this with the use_window_event hook pub fn handle_close_msg(&mut self, id: WindowId) { self.webviews.remove(&id); - if self.webviews.is_empty() { self.control_flow = ControlFlow::Exit } @@ -244,16 +242,14 @@ impl App { if drag.files().is_some() { let file_event = recent_file.current().unwrap(); let paths = match file_event { - wry::FileDropEvent::Hovered { paths, position } => paths, - wry::FileDropEvent::Dropped { paths, position } => paths, + wry::FileDropEvent::Hovered { paths, .. } => paths, + wry::FileDropEvent::Dropped { paths, .. } => paths, _ => vec![], }; - let files = Arc::new(NativeFileEngine::new(paths)); - let event = DesktopFileDragEvent { + Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent { mouse: drag.mouse.clone(), - files, - }; - Rc::new(PlatformEventData::new(Box::new(event))) + files: Arc::new(NativeFileEngine::new(paths)), + }))) } else { data.into_any() } @@ -282,8 +278,6 @@ impl App { } } - pub fn handle_file_drag(&mut self, msg: IpcMessage, window: WindowId) {} - pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) { let Ok(file_dialog) = serde_json::from_value::(msg.params()) else { return; @@ -327,6 +321,19 @@ impl App { view.poll_vdom(); } + + fn set_global_hotkey_handler(&self) { + let receiver = self.shared.proxy.clone(); + + // The event loop becomes the hotkey receiver + // This means we don't need to poll the receiver on every tick - we just get the events as they come in + // This is a bit more efficient than the previous implementation, but if someone else sets a handler, the + // receiver will become inert. + global_hotkey::GlobalHotKeyEvent::set_event_handler(Some(move |t| { + // todo: should we unset the event handler when the app shuts down? + _ = receiver.send_event(UserWindowEvent::GlobalHotKeyEvent(t)); + })); + } } /// Different hide implementations per platform @@ -336,7 +343,6 @@ pub fn hide_app_window(window: &wry::WebView) { { use tao::platform::windows::WindowExtWindows; window.set_visible(false); - // window.set_skip_taskbar(true); } #[cfg(target_os = "linux")] diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index feedf00da..6178dbf93 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -1,11 +1,7 @@ use std::borrow::Cow; use std::path::PathBuf; - -use tao::window::{Icon, WindowBuilder, WindowId}; -use wry::{ - http::{Request as HttpRequest, Response as HttpResponse}, - FileDropEvent, -}; +use tao::window::{Icon, WindowBuilder}; +use wry::http::{Request as HttpRequest, Response as HttpResponse}; /// The behaviour of the application when the last window is closed. #[derive(Copy, Clone, Eq, PartialEq)] @@ -21,7 +17,6 @@ pub enum WindowCloseBehaviour { /// The configuration for the desktop application. pub struct Config { pub(crate) window: WindowBuilder, - pub(crate) file_drop_handler: Option, pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) disable_context_menu: bool, @@ -35,8 +30,6 @@ pub struct Config { pub(crate) enable_default_menu_bar: bool, } -type DropHandler = Box bool>; - pub(crate) type WryProtocol = ( String, Box>) -> HttpResponse> + 'static>, @@ -56,7 +49,6 @@ impl Config { Self { window, protocols: Vec::new(), - file_drop_handler: None, pre_rendered: None, disable_context_menu: !cfg!(debug_assertions), resource_dir: None, diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 55f3c6417..a32436f0e 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,7 +3,7 @@ use crate::{ assets::AssetHandlerRegistry, edits::EditQueue, file_upload::NativeFileHover, - ipc::{EventData, UserWindowEvent}, + ipc::UserWindowEvent, query::QueryEngine, shortcut::{HotKey, ShortcutHandle, ShortcutRegistryError}, webview::WebviewInstance, @@ -17,14 +17,13 @@ use dioxus_interpreter_js::MutationState; use std::{ cell::RefCell, rc::{Rc, Weak}, - sync::Arc, }; use tao::{ event::Event, event_loop::EventLoopWindowTarget, window::{Fullscreen as WryFullscreen, Window, WindowId}, }; -use wry::{FileDropEvent, RequestAsyncResponder, WebView}; +use wry::{RequestAsyncResponder, WebView}; #[cfg(target_os = "ios")] use tao::platform::ios::WindowExtIOS; @@ -130,12 +129,7 @@ impl DesktopService { self.shared .proxy - .send_event(UserWindowEvent(EventData::NewWindow, cx.id())) - .unwrap(); - - self.shared - .proxy - .send_event(UserWindowEvent(EventData::Poll, cx.id())) + .send_event(UserWindowEvent::NewWindow) .unwrap(); self.shared.pending_webviews.borrow_mut().push(window); @@ -167,7 +161,7 @@ impl DesktopService { let _ = self .shared .proxy - .send_event(UserWindowEvent(EventData::CloseWindow, self.id())); + .send_event(UserWindowEvent::CloseWindow(self.id())); } /// Close a particular window, given its ID @@ -175,7 +169,7 @@ impl DesktopService { let _ = self .shared .proxy - .send_event(UserWindowEvent(EventData::CloseWindow, id)); + .send_event(UserWindowEvent::CloseWindow(id)); } /// change window to fullscreen diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index a7edef30d..e31352814 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -12,7 +12,13 @@ use dioxus_html::{ }; use muda::accelerator::Modifiers; use serde::Deserialize; -use std::{cell::Cell, path::PathBuf, rc::Rc, str::FromStr, sync::Arc}; +use std::{ + cell::{Cell, RefCell}, + path::PathBuf, + rc::Rc, + str::FromStr, + sync::Arc, +}; use wry::FileDropEvent; #[derive(Debug, Deserialize)] @@ -156,15 +162,15 @@ impl HasFormData for DesktopFileUploadForm { #[derive(Default, Clone)] pub struct NativeFileHover { - event: Rc>>, + event: Rc>>, } impl NativeFileHover { pub fn set(&self, event: FileDropEvent) { - self.event.set(Some(event)); + self.event.borrow_mut().replace(event); } pub fn current(&self) -> Option { - self.event.as_ref().clone().take() + self.event.borrow_mut().clone() } } diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs index 69d8eb5a3..785f0a4e7 100644 --- a/packages/desktop/src/ipc.rs +++ b/packages/desktop/src/ipc.rs @@ -1,18 +1,17 @@ +use global_hotkey::GlobalHotKeyEvent; use serde::{Deserialize, Serialize}; use tao::window::WindowId; -/// A pair of data #[derive(Debug, Clone)] -pub struct UserWindowEvent(pub EventData, pub WindowId); +pub enum UserWindowEvent { + /// A global hotkey event + GlobalHotKeyEvent(GlobalHotKeyEvent), -/// The data that might eminate from any window/webview -#[derive(Debug, Clone)] -pub enum EventData { /// Poll the virtualdom - Poll, + Poll(WindowId), /// Handle an ipc message eminating from the window.postMessage of a given webview - Ipc(IpcMessage), + Ipc { id: WindowId, msg: IpcMessage }, /// Handle a hotreload event, basically telling us to update our templates #[cfg(all(feature = "hot-reload", debug_assertions))] @@ -22,7 +21,7 @@ pub enum EventData { NewWindow, /// Close a given window (could be any window!) - CloseWindow, + CloseWindow(WindowId), } /// A message struct that manages the communication between the webview and the eventloop code @@ -42,16 +41,13 @@ pub enum IpcMethod<'a> { Query, BrowserOpen, Initialize, - FileDrag, Other(&'a str), } impl IpcMessage { pub(crate) fn method(&self) -> IpcMethod { match self.method.as_str() { - // todo: this is a misspelling, needs to be fixed "file_dialog" => IpcMethod::FileDialog, - "file_drag" => IpcMethod::FileDrag, "user_event" => IpcMethod::UserEvent, "query" => IpcMethod::Query, "browser_open" => IpcMethod::BrowserOpen, diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index 40029d87d..ed75163fa 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -1,7 +1,7 @@ pub use crate::Config; use crate::{ app::App, - ipc::{EventData, IpcMethod, UserWindowEvent}, + ipc::{IpcMethod, UserWindowEvent}, }; use dioxus_core::*; use std::any::Any; @@ -15,6 +15,7 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf let (event_loop, mut app) = App::new(desktop_config, virtual_dom); event_loop.run(move |window_event, _, control_flow| { + // Set the control flow and check if any events need to be handled in the app itself app.tick(&window_event); match window_event { @@ -26,19 +27,20 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf WindowEvent::Destroyed { .. } => app.window_destroyed(window_id), _ => {} }, - Event::UserEvent(UserWindowEvent(event, id)) => match event { - EventData::Poll => app.poll_vdom(id), - EventData::NewWindow => app.handle_new_window(), - EventData::CloseWindow => app.handle_close_msg(id), + + Event::UserEvent(event) => match event { + UserWindowEvent::Poll(id) => app.poll_vdom(id), + UserWindowEvent::NewWindow => app.handle_new_window(), + UserWindowEvent::CloseWindow(id) => app.handle_close_msg(id), + UserWindowEvent::GlobalHotKeyEvent(evnt) => app.handle_global_hotkey(evnt), #[cfg(all(feature = "hot-reload", debug_assertions))] - EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), - EventData::Ipc(msg) => match msg.method() { + UserWindowEvent::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), + UserWindowEvent::Ipc { id, msg } => match msg.method() { + IpcMethod::Initialize => app.handle_initialize_msg(id), IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), - IpcMethod::FileDrag => app.handle_file_drag(msg, id), IpcMethod::UserEvent => app.handle_user_event_msg(msg, id), IpcMethod::Query => app.handle_query_msg(msg, id), IpcMethod::BrowserOpen => app.handle_browser_open(msg), - IpcMethod::Initialize => app.handle_initialize_msg(id), IpcMethod::Other(_) => {} }, }, diff --git a/packages/desktop/src/mobile_shortcut.rs b/packages/desktop/src/mobile_shortcut.rs index 03566bf36..5ff0683a1 100644 --- a/packages/desktop/src/mobile_shortcut.rs +++ b/packages/desktop/src/mobile_shortcut.rs @@ -80,10 +80,4 @@ pub struct GlobalHotKeyEvent { pub id: u32, } -impl GlobalHotKeyEvent { - pub fn receiver() -> crossbeam_channel::Receiver { - crossbeam_channel::unbounded().1 - } -} - pub(crate) type Code = dioxus_html::input_data::keyboard_types::Code; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 870c6cabb..1e615aa6e 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -50,6 +50,7 @@ pub(super) fn index_request( } None => assets_head(), }; + if let Some(head) = head { index.insert_str(index.find("").expect("Head element to exist"), &head); } diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs index 0e5ecbe98..d6ab3f07b 100644 --- a/packages/desktop/src/query.rs +++ b/packages/desktop/src/query.rs @@ -1,13 +1,17 @@ -use std::{cell::RefCell, rc::Rc}; - use crate::DesktopContext; use futures_util::{FutureExt, StreamExt}; use generational_box::Owner; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use slab::Slab; +use std::{cell::RefCell, rc::Rc}; use thiserror::Error; +/* +todo: +- write this in the interpreter itself rather than in blobs of inline javascript... +- it could also be simpler, probably? +*/ const DIOXUS_CODE: &str = r#" let dioxus = { recv: function () { @@ -43,7 +47,6 @@ let dioxus = { }"#; /// Tracks what query ids are currently active - pub(crate) struct SharedSlab { pub slab: Rc>>, } diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index 5c3e915e3..fd031afb3 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,4 +1,4 @@ -use crate::ipc::{EventData, UserWindowEvent}; +use crate::ipc::UserWindowEvent; use futures_util::task::ArcWake; use std::sync::Arc; use tao::{event_loop::EventLoopProxy, window::WindowId}; @@ -24,7 +24,7 @@ pub fn tao_waker(proxy: EventLoopProxy, id: WindowId) -> std::t fn wake_by_ref(arc_self: &Arc) { _ = arc_self .proxy - .send_event(UserWindowEvent(EventData::Poll, arc_self.id)); + .send_event(UserWindowEvent::Poll(arc_self.id)); } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 276327da4..ae3d48260 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,13 +1,7 @@ use crate::{ - app::SharedContext, - assets::AssetHandlerRegistry, - edits::EditQueue, - eval::DesktopEvalProvider, - file_upload::NativeFileHover, - ipc::{EventData, UserWindowEvent}, - protocol, - waker::tao_waker, - Config, DesktopContext, DesktopService, + app::SharedContext, assets::AssetHandlerRegistry, edits::EditQueue, eval::DesktopEvalProvider, + file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config, + DesktopContext, DesktopService, }; use dioxus_core::{ScopeId, VirtualDom}; use dioxus_html::prelude::EvalProvider; @@ -61,6 +55,7 @@ impl WebviewInstance { let mut web_context = WebContext::new(cfg.data_dir.clone()); let edit_queue = EditQueue::default(); + let file_hover = NativeFileHover::default(); let asset_handlers = AssetHandlerRegistry::new(dom.runtime()); let headless = !cfg.window.window.visible; @@ -72,6 +67,7 @@ impl WebviewInstance { let asset_handlers_ = asset_handlers.clone(); let edit_queue_ = edit_queue.clone(); let proxy_ = shared.proxy.clone(); + let file_hover_ = file_hover.clone(); let request_handler = move |request, responder: RequestAsyncResponder| { // Try to serve the index file first @@ -97,12 +93,17 @@ impl WebviewInstance { let ipc_handler = move |payload: String| { // defer the event to the main thread - if let Ok(message) = serde_json::from_str(&payload) { - _ = proxy_.send_event(UserWindowEvent(EventData::Ipc(message), window_id)); + if let Ok(msg) = serde_json::from_str(&payload) { + _ = proxy_.send_event(UserWindowEvent::Ipc { id: window_id, msg }); } }; - let file_hover = NativeFileHover::default(); + let file_drop_handler = move |evt| { + // Update the most recent file drop event - when the event comes in from the webview we can use the + // most recent event to build a new event with the files in it. + file_hover_.set(evt); + false + }; #[cfg(any( target_os = "windows", @@ -133,15 +134,7 @@ impl WebviewInstance { .with_navigation_handler(|var| var.contains("dioxus")) // prevent all navigations .with_asynchronous_custom_protocol(String::from("dioxus"), request_handler) .with_web_context(&mut web_context) - .with_file_drop_handler({ - let file_hover = file_hover.clone(); - move |evt| { - println!("file drop: {:?}", evt); - // Update the most recent file hover status - file_hover.set(evt); - false - } - }); + .with_file_drop_handler(file_drop_handler); if let Some(color) = cfg.background_color { webview = webview.with_background_color(color); @@ -153,15 +146,15 @@ impl WebviewInstance { const INITIALIZATION_SCRIPT: &str = r#" if (document.addEventListener) { - document.addEventListener('contextmenu', function(e) { - e.preventDefault(); - }, false); + document.addEventListener('contextmenu', function(e) { + e.preventDefault(); + }, false); } else { - document.attachEvent('oncontextmenu', function() { - window.event.returnValue = false; - }); + document.attachEvent('oncontextmenu', function() { + window.event.returnValue = false; + }); } - "#; + "#; if cfg.disable_context_menu { // in release mode, we don't want to show the dev tool or reload menus diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 90e7823a6..f3cf0b7de 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -6,13 +6,13 @@ use std::{ fn main() { // If any TS changes, re-run the build script - println!("cargo:rerun-if-changed=src/ts/*.ts,*.json"); + println!("cargo:rerun-if-changed=src/ts/*.ts"); // Compute the hash of the ts files - let hash = hash_dir("src/ts"); + let hash = hash_dir("./src/ts"); // If the hash matches the one on disk, we're good and don't need to update bindings - if let Ok(contents) = read_to_string("src/js/hash.txt") { + if let Ok(contents) = read_to_string("./src/js/hash.txt") { if contents.trim() == hash.to_string() { return; } @@ -34,11 +34,17 @@ fn hash_dir(dir: &str) -> u64 { for entry in std::fs::read_dir(dir).unwrap() { let entry = entry.unwrap(); let path = entry.path(); - let metadata = std::fs::metadata(&path).unwrap(); - if metadata.is_file() { - let contents = std::fs::read(&path).unwrap(); - contents.hash(&mut hasher); + + let Some(ext) = path.extension() else { + continue; + }; + + if ext != "ts" { + continue; } + + let contents = std::fs::read(&path).unwrap(); + contents.hash(&mut hasher); } hasher.finish() @@ -58,7 +64,8 @@ fn gen_bindings(input_name: &str, output_name: &str) { .arg(format!("src/ts/{input_name}.ts")) .arg("--outfile") .arg(format!("src/js/{output_name}.js")) - // .arg("--minify") + .arg("--minify-whitespace") + .arg("--minify-syntax") .status() .unwrap(); diff --git a/packages/interpreter/src/js/common.js b/packages/interpreter/src/js/common.js index 25d0ee691..9e4964594 100644 --- a/packages/interpreter/src/js/common.js +++ b/packages/interpreter/src/js/common.js @@ -1,99 +1 @@ -// src/ts/set_attribute.ts -function setAttributeInner(node, field, value, ns) { - if (ns === "style") { - node.style.setProperty(field, value); - return; - } - if (!!ns) { - node.setAttributeNS(ns, field, value); - return; - } - switch (field) { - case "value": - if (node.value !== value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - if (!truthy(value) && isBoolAttr(field)) { - node.removeAttribute(field); - } else { - node.setAttribute(field, value); - } - } -} -var truthy = function(val) { - return val === "true" || val === true; -}; -var isBoolAttr = function(field) { - switch (field) { - case "allowfullscreen": - case "allowpaymentrequest": - case "async": - case "autofocus": - case "autoplay": - case "checked": - case "controls": - case "default": - case "defer": - case "disabled": - case "formnovalidate": - case "hidden": - case "ismap": - case "itemscope": - case "loop": - case "multiple": - case "muted": - case "nomodule": - case "novalidate": - case "open": - case "playsinline": - case "readonly": - case "required": - case "reversed": - case "selected": - case "truespeed": - case "webkitdirectory": - return true; - default: - return false; - } -}; -// src/ts/form.ts -function retrieveFormValues(form) { - const formData = new FormData(form); - const contents = {}; - formData.forEach((value, key) => { - if (contents[key]) { - contents[key] += "," + value; - } else { - contents[key] = value; - } - }); - return { - valid: form.checkValidity(), - values: contents - }; -} -export { - setAttributeInner, - retrieveFormValues -}; +function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key]+=","+value;else contents[key]=value}),{valid:form.checkValidity(),values:contents}}export{setAttributeInner,retrieveFormValues}; diff --git a/packages/interpreter/src/js/core.js b/packages/interpreter/src/js/core.js index 48cc3315d..1940d5195 100644 --- a/packages/interpreter/src/js/core.js +++ b/packages/interpreter/src/js/core.js @@ -1,207 +1 @@ -// src/ts/set_attribute.ts -function setAttributeInner(node, field, value, ns) { - if (ns === "style") { - node.style.setProperty(field, value); - return; - } - if (!!ns) { - node.setAttributeNS(ns, field, value); - return; - } - switch (field) { - case "value": - if (node.value !== value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - if (!truthy(value) && isBoolAttr(field)) { - node.removeAttribute(field); - } else { - node.setAttribute(field, value); - } - } -} -var truthy = function(val) { - return val === "true" || val === true; -}; -var isBoolAttr = function(field) { - switch (field) { - case "allowfullscreen": - case "allowpaymentrequest": - case "async": - case "autofocus": - case "autoplay": - case "checked": - case "controls": - case "default": - case "defer": - case "disabled": - case "formnovalidate": - case "hidden": - case "ismap": - case "itemscope": - case "loop": - case "multiple": - case "muted": - case "nomodule": - case "novalidate": - case "open": - case "playsinline": - case "readonly": - case "required": - case "reversed": - case "selected": - case "truespeed": - case "webkitdirectory": - return true; - default: - return false; - } -}; - -// src/ts/core.ts -class BaseInterpreter { - global; - local; - root; - handler; - nodes; - stack; - templates; - m; - constructor() { - } - initialize(root, handler = null) { - this.global = {}; - this.local = {}; - this.root = root; - this.nodes = [root]; - this.stack = [root]; - this.templates = {}; - if (handler) { - this.handler = handler; - } - } - createListener(event_name, element, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = { active: 1, callback: this.handler }; - this.root.addEventListener(event_name, this.handler); - } else { - this.global[event_name].active++; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - } - removeListener(element, event_name, bubbles) { - if (bubbles) { - this.removeBubblingListener(event_name); - } else { - this.removeNonBubblingListener(element, event_name); - } - } - removeBubblingListener(event_name) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - removeNonBubblingListener(element, event_name) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (Object.keys(this.local[id]).length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - } - removeAllNonBubblingListeners(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } - getNode(id) { - return this.nodes[id]; - } - appendChildren(id, many) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - for (let k = 0;k < many; k++) { - root.appendChild(els[k]); - } - } - loadChild(ptr, len) { - let node = this.stack[this.stack.length - 1]; - let ptr_end = ptr + len; - for (;ptr < ptr_end; ptr++) { - let end = this.m.getUint8(ptr); - for (node = node.firstChild;end > 0; end--) { - node = node.nextSibling; - } - } - return node; - } - saveTemplate(nodes, tmpl_id) { - this.templates[tmpl_id] = nodes; - } - hydrateRoot(ids) { - const hydrateNodes = document.querySelectorAll("[data-node-hydration]"); - for (let i = 0;i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i]; - const hydration = hydrateNode.getAttribute("data-node-hydration"); - const split = hydration.split(","); - const id = ids[parseInt(split[0])]; - this.nodes[id] = hydrateNode; - if (split.length > 1) { - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute("data-dioxus-id", id.toString()); - for (let j = 1;j < split.length; j++) { - const listener = split[j]; - const split2 = listener.split(":"); - const event_name = split2[0]; - const bubbles = split2[1] === "1"; - this.createListener(event_name, hydrateNode, bubbles); - } - } - } - const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT); - let currentNode = treeWalker.nextNode(); - while (currentNode) { - const id = currentNode.textContent; - const split = id.split("node-id"); - if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - currentNode = treeWalker.nextNode(); - } - } - setAttributeInner(node, field, value, ns) { - setAttributeInner(node, field, value, ns); - } -} -export { - BaseInterpreter -}; +function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){if(this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},handler)this.handler=handler}createListener(event_name,element,bubbles){if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}}removeListener(element,event_name,bubbles){if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrateRoot(ids){const hydrateNodes=document.querySelectorAll("[data-node-hydration]");for(let i=0;i1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j1)this.nodes[ids[parseInt(split[1])]]=currentNode.nextSibling;currentNode=treeWalker.nextNode()}}setAttributeInner(node,field,value,ns){setAttributeInner(node,field,value,ns)}}export{BaseInterpreter}; diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index bfd4a2268..4ab3240ae 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -13140749608285001344 \ No newline at end of file +6653358969285642234 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 0bd145a16..a281625d9 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1,422 +1 @@ -// src/ts/form.ts -function retriveValues(event, target) { - let contents = { - values: {} - }; - let form = target.closest("form"); - if (form) { - if (event.type === "input" || event.type === "change" || event.type === "submit" || event.type === "reset" || event.type === "click") { - contents = retrieveFormValues(form); - } - } - return contents; -} -function retrieveFormValues(form) { - const formData = new FormData(form); - const contents = {}; - formData.forEach((value, key) => { - if (contents[key]) { - contents[key] += "," + value; - } else { - contents[key] = value; - } - }); - return { - valid: form.checkValidity(), - values: contents - }; -} -function retriveSelectValue(target) { - let options = target.selectedOptions; - let values = []; - for (let i = 0;i < options.length; i++) { - values.push(options[i].value); - } - return values; -} - -// src/ts/serialize.ts -function serializeEvent(event, target) { - let contents = {}; - let extend = (obj) => contents = { ...contents, ...obj }; - if (event instanceof WheelEvent) { - extend(serializeWheelEvent(event)); - } - if (event instanceof MouseEvent) { - extend(serializeMouseEvent(event)); - } - if (event instanceof KeyboardEvent) { - extend(serializeKeyboardEvent(event)); - } - if (event instanceof InputEvent) { - extend(serializeInputEvent(event, target)); - } - if (event instanceof PointerEvent) { - extend(serializePointerEvent(event)); - } - if (event instanceof AnimationEvent) { - extend(serializeAnimationEvent(event)); - } - if (event instanceof TransitionEvent) { - extend({ property_name: event.propertyName, elapsed_time: event.elapsedTime, pseudo_element: event.pseudoElement }); - } - if (event instanceof CompositionEvent) { - extend({ data: event.data }); - } - if (event instanceof DragEvent) { - extend(serializeDragEvent(event)); - } - if (event instanceof FocusEvent) { - extend({}); - } - if (event instanceof ClipboardEvent) { - extend({}); - } - if (typeof TouchEvent !== "undefined" && event instanceof TouchEvent) { - extend(serializeTouchEvent(event)); - } - if (event.type === "submit" || event.type === "reset" || event.type === "click" || event.type === "change" || event.type === "input") { - extend(serializeInputEvent(event, target)); - } - if (event instanceof DragEvent) { - } - return contents; -} -var serializeInputEvent = function(event, target) { - let contents = {}; - if (target instanceof HTMLElement) { - let values = retriveValues(event, target); - contents.values = values.values; - contents.valid = values.valid; - } - if (event.target instanceof HTMLInputElement) { - let target2 = event.target; - let value = target2.value ?? target2.textContent ?? ""; - if (target2.type === "checkbox") { - value = target2.checked ? "true" : "false"; - } else if (target2.type === "radio") { - value = target2.value; - } - contents.value = value; - } - if (event.target instanceof HTMLTextAreaElement) { - contents.value = event.target.value; - } - if (event.target instanceof HTMLSelectElement) { - contents.value = retriveSelectValue(event.target).join(","); - } - if (contents.value === undefined) { - contents.value = ""; - } - return contents; -}; -var serializeWheelEvent = function(event) { - return { - delta_x: event.deltaX, - delta_y: event.deltaY, - delta_z: event.deltaZ, - delta_mode: event.deltaMode - }; -}; -var serializeTouchEvent = function(event) { - return { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - changed_touches: event.changedTouches, - target_touches: event.targetTouches, - touches: event.touches - }; -}; -var serializePointerEvent = function(event) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - pointer_id: event.pointerId, - width: event.width, - height: event.height, - pressure: event.pressure, - tangential_pressure: event.tangentialPressure, - tilt_x: event.tiltX, - tilt_y: event.tiltY, - twist: event.twist, - pointer_type: event.pointerType, - is_primary: event.isPrimary - }; -}; -var serializeMouseEvent = function(event) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - offset_x: event.offsetX, - offset_y: event.offsetY, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey - }; -}; -var serializeKeyboardEvent = function(event) { - return { - char_code: event.charCode, - is_composing: event.isComposing, - key: event.key, - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - key_code: event.keyCode, - shift_key: event.shiftKey, - location: event.location, - repeat: event.repeat, - which: event.which, - code: event.code - }; -}; -var serializeAnimationEvent = function(event) { - return { - animation_name: event.animationName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement - }; -}; -var serializeDragEvent = function(event) { - return { - mouse: { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - ...serializeMouseEvent(event) - }, - files: { - files: { - a: [1, 2, 3] - } - } - }; -}; - -// src/ts/native.ts -var getTargetId = function(target) { - if (!(target instanceof Node)) { - return null; - } - let ourTarget = target; - let realId = null; - while (realId == null) { - if (ourTarget === null) { - return null; - } - if (ourTarget instanceof Element) { - realId = ourTarget.getAttribute(`data-dioxus-id`); - } - ourTarget = ourTarget.parentNode; - } - return parseInt(realId); -}; -var JSChannel_; -if (RawInterpreter) { - JSChannel_ = RawInterpreter; -} - -class NativeInterpreter extends JSChannel_ { - intercept_link_redirects; - ipc; - editsPath; - liveview; - constructor(editsPath) { - super(); - this.editsPath = editsPath; - } - initialize(root) { - this.intercept_link_redirects = true; - this.liveview = false; - const dragEventHandler = (e) => { - }; - window.addEventListener("dragover", function(e) { - if (e.target instanceof Element && e.target.tagName != "INPUT") { - e.preventDefault(); - } - }, false); - window.addEventListener("drop", function(e) { - let target = e.target; - if (!(target instanceof Element)) { - return; - } - e.preventDefault(); - }, false); - window.addEventListener("click", (event) => { - const target = event.target; - if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") { - let target_id = getTargetId(target); - if (target_id !== null) { - const message = this.serializeIpcMessage("file_dialog", { - event: "change&input", - accept: target.getAttribute("accept"), - directory: target.getAttribute("webkitdirectory") === "true", - multiple: target.hasAttribute("multiple"), - target: target_id, - bubbles: event.bubbles - }); - this.ipc.postMessage(message); - } - event.preventDefault(); - } - }); - this.ipc = window.ipc; - const handler = (event) => this.handleEvent(event, event.type, true); - super.initialize(root, handler); - } - serializeIpcMessage(method, params = {}) { - return JSON.stringify({ method, params }); - } - scrollTo(id, behavior) { - const node = this.nodes[id]; - if (node instanceof HTMLElement) { - node.scrollIntoView({ behavior }); - } - } - getClientRect(id) { - const node = this.nodes[id]; - if (node instanceof HTMLElement) { - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height] - }; - } - } - setFocus(id, focus) { - const node = this.nodes[id]; - if (node instanceof HTMLElement) { - if (focus) { - node.focus(); - } else { - node.blur(); - } - } - } - loadChild(array) { - let node = this.stack[this.stack.length - 1]; - for (let i = 0;i < array.length; i++) { - let end = array[i]; - for (node = node.firstChild;end > 0; end--) { - node = node.nextSibling; - } - } - return node; - } - appendChildren(id, many) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - for (let k = 0;k < many; k++) { - root.appendChild(els[k]); - } - } - handleEvent(event, name, bubbles) { - const target = event.target; - const realId = getTargetId(target); - const contents = serializeEvent(event, target); - let body = { - name, - data: contents, - element: realId, - bubbles - }; - this.preventDefaults(event, target); - if (this.liveview) { - if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { - if (target.getAttribute("type") === "file") { - this.readFiles(target, contents, bubbles, realId, name); - } - } - } else { - const message = this.serializeIpcMessage("user_event", body); - this.ipc.postMessage(message); - } - } - preventDefaults(event, target) { - let preventDefaultRequests = null; - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); - } - if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { - event.preventDefault(); - } - if (event.type === "submit") { - event.preventDefault(); - } - if (target instanceof Element && event.type === "click") { - this.handleClickNavigate(event, target, preventDefaultRequests); - } - } - handleClickNavigate(event, target, preventDefaultRequests) { - if (!this.intercept_link_redirects) { - return; - } - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - let a_element = target.closest("a"); - if (a_element == null) { - return; - } - event.preventDefault(); - let elementShouldPreventDefault = preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - let aElementShouldPreventDefault = a_element.getAttribute(`dioxus-prevent-default`); - let linkShouldPreventDefault = aElementShouldPreventDefault && aElementShouldPreventDefault.includes(`onclick`); - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - this.ipc.postMessage(this.serializeIpcMessage("browser_open", { href })); - } - } - } - waitForRequest(headless) { - fetch(new Request(this.editsPath)).then((response) => response.arrayBuffer()).then((bytes) => { - if (headless) { - this.run_from_bytes(bytes); - } else { - requestAnimationFrame(() => this.run_from_bytes(bytes)); - } - this.waitForRequest(headless); - }); - } - async readFiles(target, contents, bubbles, realId, name) { - let files = target.files; - let file_contents = {}; - for (let i = 0;i < files.length; i++) { - const file = files[i]; - file_contents[file.name] = Array.from(new Uint8Array(await file.arrayBuffer())); - } - contents.files = { files: file_contents }; - const message = this.serializeIpcMessage("user_event", { - name, - element: realId, - data: contents, - bubbles - }); - this.ipc.postMessage(message); - } -} -export { - NativeInterpreter -}; +function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key]+=","+value;else contents[key]=value}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files:{files:{a:[1,2,3]}}}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;liveview;constructor(editsPath){super();this.editsPath=editsPath}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message)}event.preventDefault()}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;kresponse.arrayBuffer()).then((bytes)=>{if(headless)this.run_from_bytes(bytes);else requestAnimationFrame(()=>this.run_from_bytes(bytes));this.waitForRequest(headless)})}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i { - // e.preventDefault(); - // e.dataTransfer.effectAllowed = 'none'; - // e.dataTransfer.dropEffect = 'none'; - - // - - // we need to signal to the host to provide a native file path, - // not the one coming from here - - // We can't get native - // if (e.type === "drop") { - // let target = e.target; - - // if (target instanceof Element) { - // let target_id = getTargetId(target); - // if (target_id !== null) { - // const message = this.serializeIpcMessage("file_drop", { - // event: "drop", - // target: target_id, - // data: Array.from(e.dataTransfer.files).map(file => { - // return { - // name: file.name, - // type: file.type, - // size: file.size, - // }; - // }), - // }); - - // this.ipc.postMessage(message); - // } - // } - // } - }; - - - // ["dragenter", "dragover", "drop"].forEach((event) => { - // window.addEventListener(event, dragEventHandler, false) - // }); - // attach an event listener on the body that prevents file drops from navigating // this is because the browser will try to navigate to the file if it's dropped on the window window.addEventListener("dragover", function (e) { diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index 9ab7cfd00..f4d4acf3f 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -28,7 +28,7 @@ impl Interpreter { /// Convert the interpreter to its baseclass, giving pub fn base(&self) -> &BaseInterpreter { use wasm_bindgen::prelude::JsCast; - &self.js_channel().unchecked_ref() + self.js_channel().unchecked_ref() } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index d949263f5..e47634505 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -54,11 +54,9 @@ mod rehydrate; /// /// # Example /// -/// ```rust, ignore -/// fn main() { -/// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("joe") }); -/// wasm_bindgen_futures::spawn_local(app_fut); -/// } +/// ```ignore, rust +/// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("foo") }); +/// wasm_bindgen_futures::spawn_local(app_fut); /// ``` pub async fn run(virtual_dom: VirtualDom, web_config: Config) { tracing::info!("Starting up"); @@ -66,7 +64,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) { let mut dom = virtual_dom; #[cfg(feature = "eval")] - dom.in_runtime(|| eval::init_eval()); + dom.in_runtime(eval::init_eval); #[cfg(feature = "panic_hook")] if web_config.default_panic_hook { From 45b824346c8a59a3e2658d03ed0af74409c2c01a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 23:49:27 -0800 Subject: [PATCH 21/41] attempt fix on inter --- packages/interpreter/build.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index f3cf0b7de..753e774e1 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -16,6 +16,8 @@ fn main() { if contents.trim() == hash.to_string() { return; } + + panic!("Hashes don't match {} != {}", contents, hash.to_string()); } // Otherwise, generate the bindings and write the new hash to disk From 73e7c3e5602920fb4ac304d11341abdfeab556c3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 00:05:42 -0800 Subject: [PATCH 22/41] Attempt order invariance --- packages/interpreter/build.rs | 17 +++++++++-------- packages/interpreter/src/js/hash.txt | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 753e774e1..619273eed 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -1,6 +1,6 @@ use std::{ fs::read_to_string, - hash::{DefaultHasher, Hash, Hasher}, + hash::{DefaultHasher, Hasher}, process::Command, }; @@ -16,8 +16,6 @@ fn main() { if contents.trim() == hash.to_string() { return; } - - panic!("Hashes don't match {} != {}", contents, hash.to_string()); } // Otherwise, generate the bindings and write the new hash to disk @@ -30,8 +28,8 @@ fn main() { } /// Hashes the contents of a directory -fn hash_dir(dir: &str) -> u64 { - let mut hasher = DefaultHasher::new(); +fn hash_dir(dir: &str) -> u128 { + let mut out = 0_128; for entry in std::fs::read_dir(dir).unwrap() { let entry = entry.unwrap(); @@ -45,11 +43,14 @@ fn hash_dir(dir: &str) -> u64 { continue; } - let contents = std::fs::read(&path).unwrap(); - contents.hash(&mut hasher); + // Hash the contents of the file and then add it to the overall hash + // This makes us order invariant + let mut hasher = DefaultHasher::new(); + hasher.write(&std::fs::read(&path).unwrap()); + out += hasher.finish() as u128; } - hasher.finish() + out } // okay...... so tsc might fail if the user doesn't have it installed diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 4ab3240ae..623ff5f16 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -6653358969285642234 \ No newline at end of file +63611502429846612446 \ No newline at end of file From fa04edfed7f351565302244e167629b0005e7198 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 00:11:21 -0800 Subject: [PATCH 23/41] use hashmap defaulthasher --- packages/interpreter/build.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 619273eed..f28c38ecc 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -1,7 +1,5 @@ use std::{ - fs::read_to_string, - hash::{DefaultHasher, Hasher}, - process::Command, + collections::hash_map::DefaultHasher, fs::read_to_string, hash::Hasher, process::Command, }; fn main() { From 95d7012c8db7441cc0a4a9411e1866811177ba7b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 02:00:24 -0800 Subject: [PATCH 24/41] fix hashing system, bump wasm-bindgen --- Cargo.lock | 44 ++++++++++++++-------------- Cargo.toml | 2 +- examples/file_upload.rs | 4 ++- packages/interpreter/build.rs | 2 +- packages/interpreter/src/js/hash.txt | 2 +- 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d1ebad8e..71af3de8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10089,9 +10089,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -10099,9 +10099,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -10114,9 +10114,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-cli-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806a045c4ec4ef7c3ad86dc27bcb641b84d9eeb3846200f56d7ab0885241d654" +checksum = "ca821da8c1ae6c87c5e94493939a206daa8587caff227c6032e0061a3d80817f" dependencies = [ "anyhow", "base64", @@ -10136,9 +10136,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-externref-xform" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ac5fca1d0992d2328147488169ea166bfe899c88f8ad06cf583f4c492fcf" +checksum = "102582726b35a30d53157fbf8de3d0f0fed4c40c0c7951d69a034e9ef01da725" dependencies = [ "anyhow", "walrus", @@ -10158,9 +10158,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -10168,9 +10168,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -10181,9 +10181,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-multi-value-xform" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1e019acde479e2f090fb7f14a51fa0077ec3a7bb12a56e0e888a82be7b5bd3f" +checksum = "3498e4799f43523d780ceff498f04d882a8dbc9719c28020034822e5952f32a4" dependencies = [ "anyhow", "walrus", @@ -10191,9 +10191,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-bindgen-test" @@ -10222,9 +10222,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-threads-xform" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90a2e577034352f9aa9352730fcf2562c68957f2e9b9ee70ab6379510e49e2fe" +checksum = "2d5add359b7f7d09a55299a9d29be54414264f2b8cf84f8c8fda5be9269b5dd9" dependencies = [ "anyhow", "walrus", @@ -10233,9 +10233,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-wasm-conventions" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6b653f6820409609bda0f176e6949302307af7a7b9479cd4d4b1bdc31eb9cd" +checksum = "8c04e3607b810e76768260db3a5f2e8beb477cb089ef8726da85c8eb9bd3b575" dependencies = [ "anyhow", "walrus", @@ -10243,9 +10243,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-wasm-interpreter" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682940195a701dbf887f20017418b8cac916a37b3f91ededec33226619e973c1" +checksum = "9ea966593c8243a33eb4d643254eb97a69de04e89462f46cf6b4f506aae89b3a" dependencies = [ "anyhow", "log", diff --git a/Cargo.toml b/Cargo.toml index 41137da2f..c07bd33d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ slab = "0.4.2" futures-channel = "0.3.21" futures-util = { version = "0.3", default-features = false } rustc-hash = "1.1.0" -wasm-bindgen = "0.2.88" +wasm-bindgen = "0.2.92" html_parser = "0.7.0" thiserror = "1.0.40" prettyplease = { package = "prettier-please", version = "0.2", features = [ diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 3a433250e..62d30bb79 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; use dioxus::{html::HasFileData, prelude::dioxus_elements::FileEngine}; fn main() { - LaunchBuilder::desktop().launch(app); + launch(app); } struct UploadedFile { @@ -80,6 +80,8 @@ fn app() -> Element { "ondragleave": "this.style.backgroundColor='#FFFFFF';", "ondrop": "this.style.backgroundColor='#FFFFFF';", id: "drop-zone", + prevent_default: "ondragover", + prevent_default: "ondrop", ondrop: handle_file_drop, "Drop files here" } diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index f28c38ecc..7320361d4 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -27,7 +27,7 @@ fn main() { /// Hashes the contents of a directory fn hash_dir(dir: &str) -> u128 { - let mut out = 0_128; + let mut out = 0; for entry in std::fs::read_dir(dir).unwrap() { let entry = entry.unwrap(); diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 623ff5f16..0964a8ca3 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -63611502429846612446 \ No newline at end of file +63611502429846612318 \ No newline at end of file From 713ab98dd3d14d8c33f3539ee2321691c4cbcf2e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 02:08:18 -0800 Subject: [PATCH 25/41] fix els bug --- .../interpreter/src/sledgehammer_bindings.rs | 413 ------------------ packages/interpreter/src/unified_bindings.rs | 10 +- 2 files changed, 5 insertions(+), 418 deletions(-) delete mode 100644 packages/interpreter/src/sledgehammer_bindings.rs diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs deleted file mode 100644 index 1fbdc9823..000000000 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ /dev/null @@ -1,413 +0,0 @@ -#[cfg(feature = "webonly")] -use js_sys::Function; -#[cfg(feature = "webonly")] -use sledgehammer_bindgen::bindgen; -#[cfg(feature = "webonly")] -use web_sys::Node; - -#[cfg(feature = "webonly")] -pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; - -#[cfg(feature = "webonly")] -#[bindgen(module)] -mod js { - const JS_FILE: &str = "./src/common.js"; - const JS: &str = r#" - class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = root; - this.handler = null; - } - - create(event_name, element, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.root.addEventListener(event_name, this.handler); - } else { - this.global[event_name].active++; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - } - } - - removeAllNonBubbling(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } - } - this.LoadChild = function(ptr, len) { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1]; - let ptr_end = ptr + len; - for (; ptr < ptr_end; ptr++) { - let end = this.m.getUint8(ptr); - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - return node; - } - this.listeners = new ListenerMap(); - this.nodes = []; - this.stack = []; - this.templates = {}; - this.save_template = function(nodes, tmpl_id) { - this.templates[tmpl_id] = nodes; - } - this.hydrate = function (ids) { - const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); - for (let i = 0; i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i]; - const hydration = hydrateNode.getAttribute('data-node-hydration'); - const split = hydration.split(','); - const id = ids[parseInt(split[0])]; - this.nodes[id] = hydrateNode; - if (split.length > 1) { - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute('data-dioxus-id', id); - for (let j = 1; j < split.length; j++) { - const listener = split[j]; - const split2 = listener.split(':'); - const event_name = split2[0]; - const bubbles = split2[1] === '1'; - this.listeners.create(event_name, hydrateNode, bubbles); - } - } - } - const treeWalker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_COMMENT, - ); - let currentNode = treeWalker.nextNode(); - while (currentNode) { - const id = currentNode.textContent; - const split = id.split('node-id'); - if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - currentNode = treeWalker.nextNode(); - } - } - this.get_node = function(id) { - return this.nodes[id]; - } - this.initialize = function(root, handler) { - this.listeners.handler = handler; - this.nodes = [root]; - this.stack = [root]; - this.listeners.root = root; - } - this.AppendChildren = function (id, many){ - let root = this.nodes[id]; - let els = this.stack.splice(this.stack.length-many); - for (let k = 0; k < many; k++) { - root.appendChild(els[k]); - } - } - "#; - - fn mount_to_root() { - "{this.AppendChildren(this.listeners.root, this.stack.length-1);}" - } - fn push_root(root: u32) { - "{this.stack.push(this.nodes[$root$]);}" - } - fn append_children(id: u32, many: u16) { - "{this.AppendChildren($id$, $many$);}" - } - fn pop_root() { - "{this.stack.pop();}" - } - fn replace_with(id: u32, n: u16) { - "{const root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" - } - fn insert_after(id: u32, n: u16) { - "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}" - } - fn insert_before(id: u32, n: u16) { - "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}" - } - fn remove(id: u32) { - "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}" - } - fn create_raw_text(text: &str) { - "{this.stack.push(document.createTextNode($text$));}" - } - fn create_text_node(text: &str, id: u32) { - "{let node = document.createTextNode($text$); this.nodes[$id$] = node; this.stack.push(node);}" - } - fn create_placeholder(id: u32) { - "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}" - } - fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { - r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.listeners.create($event_name$, node, $bubbles$);"# - } - fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { - "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}" - } - fn set_text(id: u32, text: &str) { - "{this.nodes[$id$].textContent = $text$;}" - } - fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { - "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}" - } - fn remove_attribute(id: u32, field: &str, ns: &str) { - r#"{ - let node = this.nodes[$id$]; - if (!ns) { - switch (field) { - case "value": - node.value = ""; - break; - case "checked": - node.checked = false; - break; - case "selected": - node.selected = false; - break; - case "dangerous_inner_html": - node.innerHTML = ""; - break; - default: - node.removeAttribute(field); - break; - } - } else if (ns == "style") { - node.style.removeProperty(name); - } else { - node.removeAttributeNS(ns, field); - } - }"# - } - fn assign_id(ptr: u32, len: u8, id: u32) { - "{this.nodes[$id$] = this.LoadChild($ptr$, $len$);}" - } - fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) { - r#"{ - let node = this.LoadChild($ptr$, $len$); - if (node.nodeType == node.TEXT_NODE) { - node.textContent = value; - } else { - let text = document.createTextNode(value); - node.replaceWith(text); - node = text; - } - this.nodes[$id$] = node; - }"# - } - fn replace_placeholder(ptr: u32, len: u8, n: u16) { - "{els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...els);}" - } - fn load_template(tmpl_id: u16, index: u16, id: u32) { - "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}" - } -} - -#[cfg(feature = "webonly")] -#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#" -export function save_template(channel, nodes, tmpl_id) { - channel.save_template(nodes, tmpl_id); -} -export function hydrate(channel, ids) { - channel.hydrate(ids); -} -export function get_node(channel, id) { - return channel.get_node(id); -} -export function initialize(channel, root, handler) { - channel.initialize(root, handler); -} -"#)] -extern "C" { - pub fn save_template(channel: &JSChannel, nodes: Vec, tmpl_id: u16); - - pub fn hydrate(channel: &JSChannel, ids: Vec); - - pub fn get_node(channel: &JSChannel, id: u32) -> Node; - - pub fn initialize(channel: &JSChannel, root: Node, handler: &Function); -} - -#[cfg(feature = "binary-protocol")] -pub mod binary_protocol { - use sledgehammer_bindgen::bindgen; - pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; - - #[bindgen] - mod protocol_js { - const JS_FILE: &str = "./src/interpreter.js"; - const JS_FILE: &str = "./src/common.js"; - - fn push_root(root: u32) { - "{this.stack.push(this.nodes[$root$]);}" - } - fn append_children(id: u32, many: u16) { - "{this.AppendChildren($id$, $many$);}" - } - fn append_children_to_top(many: u16) { - "{ - let root = this.stack[this.stack.length-many-1]; - let els = this.stack.splice(this.stack.length-many); - for (let k = 0; k < many; k++) { - root.appendChild(els[k]); - } - }" - } - fn pop_root() { - "{this.stack.pop();}" - } - fn replace_with(id: u32, n: u16) { - "{let root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" - } - fn insert_after(id: u32, n: u16) { - "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}" - } - fn insert_before(id: u32, n: u16) { - "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}" - } - fn remove(id: u32) { - "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}" - } - fn create_raw_text(text: &str) { - "{this.stack.push(document.createTextNode($text$));}" - } - fn create_text_node(text: &str, id: u32) { - "{let node = document.createTextNode($text$); this.nodes[$id$] = node; this.stack.push(node);}" - } - fn create_element(element: &'static str) { - "{this.stack.push(document.createElement($element$))}" - } - fn create_element_ns(element: &'static str, ns: &'static str) { - "{this.stack.push(document.createElementNS($ns$, $element$))}" - } - fn create_placeholder(id: u32) { - "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}" - } - fn add_placeholder() { - "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node);}" - } - fn new_event_listener(event: &str, id: u32, bubbles: u8) { - r#" - bubbles = bubbles == 1; - let node = this.nodes[id]; - if(node.listening){ - node.listening += 1; - } else { - node.listening = 1; - } - node.setAttribute('data-dioxus-id', `\${id}`); - const event_name = $event$; - - // if this is a mounted listener, we send the event immediately - if (event_name === "mounted") { - window.ipc.postMessage( - this.serializeIpcMessage("user_event", { - name: event_name, - element: id, - data: null, - bubbles, - }) - ); - } else { - this.listeners.create(event_name, node, bubbles, (event) => { - this.handler(event, event_name, bubbles); - }); - }"# - } - fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { - "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}" - } - fn set_text(id: u32, text: &str) { - "{this.nodes[$id$].textContent = $text$;}" - } - fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { - "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}" - } - fn set_top_attribute(field: &str, value: &str, ns: &str) { - "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}" - } - fn remove_attribute(id: u32, field: &str, ns: &str) { - r#"{ - let node = this.nodes[$id$]; - if (!ns) { - switch (field) { - case "value": - node.value = ""; - break; - case "checked": - node.checked = false; - break; - case "selected": - node.selected = false; - break; - case "dangerous_inner_html": - node.innerHTML = ""; - break; - default: - node.removeAttribute(field); - break; - } - } else if (ns == "style") { - node.style.removeProperty(name); - } else { - node.removeAttributeNS(ns, field); - } - }"# - } - fn assign_id(array: &[u8], id: u32) { - "{this.nodes[$id$] = this.LoadChild($array$);}" - } - fn hydrate_text(array: &[u8], value: &str, id: u32) { - r#"{ - let node = this.LoadChild($array$); - if (node.nodeType == node.TEXT_NODE) { - node.textContent = value; - } else { - let text = document.createTextNode(value); - node.replaceWith(text); - node = text; - } - this.nodes[$id$] = node; - }"# - } - fn replace_placeholder(array: &[u8], n: u16) { - "{let els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...els);}" - } - fn load_template(tmpl_id: u16, index: u16, id: u32) { - "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}" - } - fn add_templates(tmpl_id: u16, len: u16) { - "{this.templates[$tmpl_id$] = this.stack.splice(this.stack.length-$len$);}" - } - } -} diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index f4d4acf3f..f83f3bc5c 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -54,7 +54,7 @@ mod js { "{this.stack.pop();}" } fn replace_with(id: u32, n: u16) { - "{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.removeAllNonBubblingListeners(root); } root.replaceWith(...this.els);}" + "{const root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.removeAllNonBubblingListeners(root); } root.replaceWith(...els);}" } fn insert_after(id: u32, n: u16) { "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}" @@ -131,7 +131,7 @@ mod js { }"# } fn replace_placeholder(ptr: u32, len: u8, n: u16) { - "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($ptr$, $len$); node.replaceWith(...this.els);}" + "{let els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($ptr$, $len$); node.replaceWith(...els);}" } fn load_template(tmpl_id: u16, index: u16, id: u32) { "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}" @@ -141,9 +141,9 @@ mod js { fn append_children_to_top(many: u16) { "{ let root = this.stack[this.stack.length-many-1]; - this.els = this.stack.splice(this.stack.length-many); + let els = this.stack.splice(this.stack.length-many); for (let k = 0; k < many; k++) { - root.appendChild(this.els[k]); + root.appendChild(els[k]); } }" } @@ -227,6 +227,6 @@ mod js { #[cfg(feature = "binary-protocol")] fn replace_placeholder_ref(array: &[u8], n: u16) { - "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($array$); node.replaceWith(...this.els);}" + "{let els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($array$); node.replaceWith(...els);}" } } From f7f416ee3aa2f04456218cb6772264bb0f406381 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 12:19:59 -0800 Subject: [PATCH 26/41] Fix hydration --- examples/router.rs | 2 +- packages/interpreter/src/js/core.js | 2 +- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/ts/core.ts | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/router.rs b/examples/router.rs index 7b41e94e1..5752de3e6 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; fn main() { - launch_desktop(|| { + launch(|| { rsx! { style { {include_str!("./assets/router.css")} } Router:: {} diff --git a/packages/interpreter/src/js/core.js b/packages/interpreter/src/js/core.js index 1940d5195..c15a50557 100644 --- a/packages/interpreter/src/js/core.js +++ b/packages/interpreter/src/js/core.js @@ -1 +1 @@ -function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){if(this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},handler)this.handler=handler}createListener(event_name,element,bubbles){if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}}removeListener(element,event_name,bubbles){if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrateRoot(ids){const hydrateNodes=document.querySelectorAll("[data-node-hydration]");for(let i=0;i1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j1)this.nodes[ids[parseInt(split[1])]]=currentNode.nextSibling;currentNode=treeWalker.nextNode()}}setAttributeInner(node,field,value,ns){setAttributeInner(node,field,value,ns)}}export{BaseInterpreter}; +function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){if(this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},handler)this.handler=handler}createListener(event_name,element,bubbles){if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}}removeListener(element,event_name,bubbles){if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrate(ids){const hydrateNodes=document.querySelectorAll("[data-node-hydration]");for(let i=0;i1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j1)this.nodes[ids[parseInt(split[1])]]=currentNode.nextSibling;currentNode=treeWalker.nextNode()}}setAttributeInner(node,field,value,ns){setAttributeInner(node,field,value,ns)}}export{BaseInterpreter}; diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 0964a8ca3..a6441265c 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -63611502429846612318 \ No newline at end of file +66508259673533768445 \ No newline at end of file diff --git a/packages/interpreter/src/ts/core.ts b/packages/interpreter/src/ts/core.ts index 6f78e9ce3..c0c91b640 100644 --- a/packages/interpreter/src/ts/core.ts +++ b/packages/interpreter/src/ts/core.ts @@ -22,7 +22,7 @@ export class BaseInterpreter { nodes: Node[]; stack: Node[]; templates: { - [key: string]: Node[] + [key: number]: Node[] }; // sledgehammer is generating this... @@ -118,11 +118,11 @@ export class BaseInterpreter { return node; } - saveTemplate(nodes: HTMLElement[], tmpl_id: string) { + saveTemplate(nodes: HTMLElement[], tmpl_id: number) { this.templates[tmpl_id] = nodes; } - hydrateRoot(ids: { [key: number]: number }) { + hydrate(ids: { [key: number]: number }) { const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); for (let i = 0; i < hydrateNodes.length; i++) { From 81f38a0bc596f0b7ddee70832ba55a73a86ca688 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 16:42:45 -0800 Subject: [PATCH 27/41] cfg out globalhotkey --- Cargo.lock | 4 +-- examples/file_upload.rs | 26 +++++++++----------- packages/desktop/Cargo.toml | 2 +- packages/desktop/src/app.rs | 7 ++++-- packages/desktop/src/file_upload.rs | 4 +-- packages/desktop/src/ipc.rs | 4 +-- packages/desktop/src/launch.rs | 4 +++ packages/desktop/src/shortcut.rs | 1 + packages/html/src/transit.rs | 2 +- packages/interpreter/src/unified_bindings.rs | 7 +++++- packages/web/src/dom.rs | 2 +- 11 files changed, 37 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71af3de8d..0e16f7aaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4055,9 +4055,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "global-hotkey" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a00fd7c31d82029f99ce2481a8de1ae974758017d6a55ebbe7f22edcd1617" +checksum = "34300b13d16b1593de1b6bd571a376704820a1e6f6fe57be2f106ded8d164030" dependencies = [ "crossbeam-channel", "keyboard-types", diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 62d30bb79..26f01f9af 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -20,6 +20,7 @@ struct UploadedFile { fn app() -> Element { let mut enable_directory_upload = use_signal(|| false); let mut files_uploaded = use_signal(|| Vec::new() as Vec); + let mut hovered = use_signal(|| false); let read_files = move |file_engine: Arc| async move { let files = file_engine.files(); @@ -39,18 +40,12 @@ fn app() -> Element { } }; - let handle_file_drop = move |evt: DragEvent| async move { - if let Some(file_engine) = evt.files() { - read_files(file_engine).await; - } - }; - rsx! { style { {include_str!("./assets/file_upload.css")} } h1 { "File Upload Example" } p { "Drop a .txt, .rs, or .js file here to read it" } - + button { onclick: move |_| files_uploaded.write().clear(), "Clear files" } div { label { r#for: "directory-upload", "Enable directory upload" } @@ -75,14 +70,17 @@ fn app() -> Element { } div { - // cheating with a little bit of JS... - "ondragover": "this.style.backgroundColor='#88FF88';", - "ondragleave": "this.style.backgroundColor='#FFFFFF';", - "ondrop": "this.style.backgroundColor='#FFFFFF';", id: "drop-zone", - prevent_default: "ondragover", - prevent_default: "ondrop", - ondrop: handle_file_drop, + prevent_default: "ondragover ondrop", + background_color: if hovered() { "lightblue" } else { "lightgray" }, + ondragover: move |_| hovered.set(true), + ondragleave: move |_| hovered.set(false), + ondrop: move |evt| async move { + hovered.set(false); + if let Some(file_engine) = evt.files() { + read_files(file_engine).await; + } + }, "Drop files here" } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index dfca27c58..5e05af658 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -52,8 +52,8 @@ async-trait = "0.1.68" tao = { version = "0.24.0", features = ["rwh_05"] } [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +global-hotkey = "0.5.0" rfd = "0.12" -global-hotkey = "0.4.1" muda = "0.11.3" [target.'cfg(target_os = "ios")'.dependencies] diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index d70788bd3..31c644c0a 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -5,7 +5,7 @@ use crate::{ file_upload::{DesktopFileDragEvent, DesktopFileUploadForm, FileDialogRequest}, ipc::{IpcMessage, UserWindowEvent}, query::QueryResult, - shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, + shortcut::ShortcutRegistry, webview::WebviewInstance, }; use dioxus_core::ElementId; @@ -77,6 +77,7 @@ impl App { dioxus_html::set_event_converter(Box::new(crate::events::SerializedHtmlEventConverter)); // Wire up the global hotkey handler + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] app.set_global_hotkey_handler(); // Allow hotreloading to work - but only in debug mode @@ -93,7 +94,8 @@ impl App { .apply_event(window_event, &self.shared.target); } - pub fn handle_global_hotkey(&self, event: GlobalHotKeyEvent) { + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + pub fn handle_global_hotkey(&self, event: global_hotkey::GlobalHotKeyEvent) { self.shared.shortcut_manager.call_handlers(event); } @@ -322,6 +324,7 @@ impl App { view.poll_vdom(); } + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] fn set_global_hotkey_handler(&self) { let receiver = self.shared.proxy.clone(); diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index e31352814..b9e76b668 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -10,7 +10,7 @@ use dioxus_html::{ prelude::{SerializedMouseData, SerializedPointInteraction}, FileEngine, HasDragData, HasFileData, HasFormData, HasMouseData, }; -use muda::accelerator::Modifiers; + use serde::Deserialize; use std::{ cell::{Cell, RefCell}, @@ -223,7 +223,7 @@ impl InteractionElementOffset for DesktopFileDragEvent { } impl ModifiersInteraction for DesktopFileDragEvent { - fn modifiers(&self) -> Modifiers { + fn modifiers(&self) -> dioxus_html::prelude::Modifiers { self.mouse.modifiers() } } diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs index 785f0a4e7..b4e86c64c 100644 --- a/packages/desktop/src/ipc.rs +++ b/packages/desktop/src/ipc.rs @@ -1,11 +1,11 @@ -use global_hotkey::GlobalHotKeyEvent; use serde::{Deserialize, Serialize}; use tao::window::WindowId; #[derive(Debug, Clone)] pub enum UserWindowEvent { /// A global hotkey event - GlobalHotKeyEvent(GlobalHotKeyEvent), + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + GlobalHotKeyEvent(global_hotkey::GlobalHotKeyEvent), /// Poll the virtualdom Poll(WindowId), diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index ed75163fa..ea37b2827 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -32,9 +32,13 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf UserWindowEvent::Poll(id) => app.poll_vdom(id), UserWindowEvent::NewWindow => app.handle_new_window(), UserWindowEvent::CloseWindow(id) => app.handle_close_msg(id), + + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] UserWindowEvent::GlobalHotKeyEvent(evnt) => app.handle_global_hotkey(evnt), + #[cfg(all(feature = "hot-reload", debug_assertions))] UserWindowEvent::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), + UserWindowEvent::Ipc { id, msg } => match msg.method() { IpcMethod::Initialize => app.handle_initialize_msg(id), IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs index b647d1337..34722faea 100644 --- a/packages/desktop/src/shortcut.rs +++ b/packages/desktop/src/shortcut.rs @@ -64,6 +64,7 @@ impl ShortcutRegistry { } } + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] pub(crate) fn call_handlers(&self, id: GlobalHotKeyEvent) { if let Some(ShortcutInner { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id.id) { for (_, callback) in callbacks.iter_mut() { diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index c1b561014..867313750 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -147,7 +147,7 @@ fn deserialize_raw( #[cfg(feature = "serialize")] impl HtmlEvent { pub fn bubbles(&self) -> bool { - event_bubbles(&self.name) + self.bubbles } } diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index f83f3bc5c..e64b412a5 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -75,7 +75,12 @@ mod js { "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}" } fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { - r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.createListener($event_name$, node, $bubbles$);"# + r#" + let node = this.nodes[id]; + if(node.listening){node.listening += 1;}else{node.listening = 1;} + node.setAttribute('data-dioxus-id', `\${id}`); + this.createListener($event_name$, node, $bubbles$); + "# } fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.removeListener(node, $event_name$, $bubbles$);}" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index f975fd738..81da59e76 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -73,7 +73,7 @@ impl WebsysDom { move |event: &web_sys::Event| { let name = event.type_(); let element = walk_event_for_id(event); - let bubbles = dioxus_html::event_bubbles(name.as_str()); + let bubbles = event.bubbles(); let Some((element, target)) = element else { return; From 1f6195b2ec4afc25f708bd5fc92f76df9574136e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 17:05:03 -0800 Subject: [PATCH 28/41] try new hashing strategy --- packages/interpreter/build.rs | 35 ++++++++++------------------ packages/interpreter/src/js/hash.txt | 2 +- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 7320361d4..9c8cb7893 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -1,19 +1,15 @@ -use std::{ - collections::hash_map::DefaultHasher, fs::read_to_string, hash::Hasher, process::Command, -}; +use std::{collections::hash_map::DefaultHasher, hash::Hasher, process::Command}; fn main() { // If any TS changes, re-run the build script println!("cargo:rerun-if-changed=src/ts/*.ts"); // Compute the hash of the ts files - let hash = hash_dir("./src/ts"); + let hash = hash_ts_files(); // If the hash matches the one on disk, we're good and don't need to update bindings - if let Ok(contents) = read_to_string("./src/js/hash.txt") { - if contents.trim() == hash.to_string() { - return; - } + if include_str!("src/js/hash.txt").trim() == hash.to_string() { + return; } // Otherwise, generate the bindings and write the new hash to disk @@ -26,25 +22,18 @@ fn main() { } /// Hashes the contents of a directory -fn hash_dir(dir: &str) -> u128 { +fn hash_ts_files() -> u128 { let mut out = 0; - for entry in std::fs::read_dir(dir).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); + let files = [ + include_str!("src/ts/common.ts"), + include_str!("src/ts/native.ts"), + include_str!("src/ts/core.ts"), + ]; - let Some(ext) = path.extension() else { - continue; - }; - - if ext != "ts" { - continue; - } - - // Hash the contents of the file and then add it to the overall hash - // This makes us order invariant + for file in files.iter() { let mut hasher = DefaultHasher::new(); - hasher.write(&std::fs::read(&path).unwrap()); + hasher.write(file.as_bytes()); out += hasher.finish() as u128; } diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index a6441265c..dbd7ce6e1 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -66508259673533768445 \ No newline at end of file +36839850333612604813 \ No newline at end of file From 60616d0ba77d0895a5f1377d67785036ab7e5ab0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 17:26:52 -0800 Subject: [PATCH 29/41] fix liveview --- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 2 +- packages/interpreter/src/ts/native.ts | 5 ++++- packages/liveview/src/lib.rs | 15 ++++++++++++--- packages/liveview/src/main.js | 7 ++++--- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index dbd7ce6e1..5d5868901 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -36839850333612604813 \ No newline at end of file +31613791829349842071 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index a281625d9..290823ee7 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1 +1 @@ -function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key]+=","+value;else contents[key]=value}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files:{files:{a:[1,2,3]}}}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;liveview;constructor(editsPath){super();this.editsPath=editsPath}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message)}event.preventDefault()}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;kresponse.arrayBuffer()).then((bytes)=>{if(headless)this.run_from_bytes(bytes);else requestAnimationFrame(()=>this.run_from_bytes(bytes));this.waitForRequest(headless)})}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i{if(contents[key])contents[key]+=","+value;else contents[key]=value}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files:{files:{a:[1,2,3]}}}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;liveview;constructor(editsPath){super();this.editsPath=editsPath}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message)}event.preventDefault()}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;kresponse.arrayBuffer()).then((bytes)=>{if(headless)this.run_from_bytes(bytes);else requestAnimationFrame(()=>this.run_from_bytes(bytes));this.waitForRequest(headless)})}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i String { return; } }"#; - let mut interpreter = SLEDGEHAMMER_JS - .replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads) - .replace("export", ""); + let mut interpreter = format!( + r#" + // Bring the sledgehammer code + {SLEDGEHAMMER_JS} + + // And then extend it with our native bindings + {NATIVE_JS} + "# + ) + .replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads) + .replace("export", ""); while let Some(import_start) = interpreter.find("import") { let import_end = interpreter[import_start..] .find(|c| c == ';' || c == '\n') diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 59a32dd0b..9fb1f0d8e 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -9,8 +9,9 @@ function main() { class IPC { constructor(root) { - window.interpreter = new JSChannel(); + window.interpreter = new NativeInterpreter(); window.interpreter.initialize(root); + window.interpreter.ipc = this; const ws = new WebSocket(WS_ADDR); ws.binaryType = "arraybuffer"; @@ -42,7 +43,7 @@ class IPC { let decoder = new TextDecoder("utf-8"); - // Using decode method to get string output + // Using decode method to get string output let str = decoder.decode(messageData); // Ignore pongs if (str != "__pong__") { @@ -64,4 +65,4 @@ class IPC { } } -main(); \ No newline at end of file +main(); From e02e41d6f19fd1246515a826ec6139050a7334e8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 17:43:43 -0800 Subject: [PATCH 30/41] Attempt even dumber hashing mechanism --- packages/interpreter/build.rs | 15 +++++++++------ packages/interpreter/src/js/hash.txt | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 9c8cb7893..db9295c46 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -1,4 +1,4 @@ -use std::{collections::hash_map::DefaultHasher, hash::Hasher, process::Command}; +use std::process::Command; fn main() { // If any TS changes, re-run the build script @@ -31,12 +31,15 @@ fn hash_ts_files() -> u128 { include_str!("src/ts/core.ts"), ]; - for file in files.iter() { - let mut hasher = DefaultHasher::new(); - hasher.write(file.as_bytes()); - out += hasher.finish() as u128; + // Let's make the dumbest hasher by summing the bytes of the files + // The location is multiplied by the byte value to make sure that the order of the bytes matters + let mut idx = 0; + for file in files { + for byte in file.bytes() { + idx += 1; + out += (byte as u128) * (idx as u128); + } } - out } diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 5d5868901..88a0824d3 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -31613791829349842071 \ No newline at end of file +12705686530 \ No newline at end of file From 8df87c64c0a9d8b2fc3bf55c4a9f0ebe61bfc89c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 17:54:20 -0800 Subject: [PATCH 31/41] debug windows build --- packages/interpreter/build.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index db9295c46..26203ad01 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -8,7 +8,9 @@ fn main() { let hash = hash_ts_files(); // If the hash matches the one on disk, we're good and don't need to update bindings - if include_str!("src/js/hash.txt").trim() == hash.to_string() { + let expected = include_str!("src/js/hash.txt").trim(); + if expected == hash.to_string() { + panic!("Hashes match, no need to update bindings. {expected} != {hash}",); return; } From 99748a611c9834091e37d045ea4b9a7246ebebff Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 17:59:05 -0800 Subject: [PATCH 32/41] move debug --- packages/interpreter/build.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 26203ad01..5ca0f26d4 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -10,10 +10,11 @@ fn main() { // If the hash matches the one on disk, we're good and don't need to update bindings let expected = include_str!("src/js/hash.txt").trim(); if expected == hash.to_string() { - panic!("Hashes match, no need to update bindings. {expected} != {hash}",); return; } + panic!("Hashes match, no need to update bindings. {expected} != {hash}",); + // Otherwise, generate the bindings and write the new hash to disk // Generate the bindings for both native and web gen_bindings("common", "common"); From f65ded2cb49771c1af71cad8515c9f3ebd92b0c7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 18:16:09 -0800 Subject: [PATCH 33/41] raw bytes instead of str --- packages/interpreter/build.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 5ca0f26d4..992a6779d 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -28,19 +28,19 @@ fn main() { fn hash_ts_files() -> u128 { let mut out = 0; - let files = [ - include_str!("src/ts/common.ts"), - include_str!("src/ts/native.ts"), - include_str!("src/ts/core.ts"), + let files: &[&[u8]] = &[ + include_bytes!("src/ts/common.ts"), + include_bytes!("src/ts/native.ts"), + include_bytes!("src/ts/core.ts"), ]; // Let's make the dumbest hasher by summing the bytes of the files // The location is multiplied by the byte value to make sure that the order of the bytes matters let mut idx = 0; for file in files { - for byte in file.bytes() { + for byte in file.iter() { idx += 1; - out += (byte as u128) * (idx as u128); + out += (*byte as u128) * (idx as u128); } } out From f073322bb3d8e822a4e620588e75782b2b4f716e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 18:43:07 -0800 Subject: [PATCH 34/41] use md5 instead --- Cargo.lock | 1 + packages/interpreter/Cargo.toml | 3 +++ packages/interpreter/build.rs | 25 ++++++++++--------------- packages/interpreter/src/js/hash.txt | 2 +- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7be5a792..e8b276778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2557,6 +2557,7 @@ dependencies = [ "dioxus-core", "dioxus-html", "js-sys", + "md5", "serde", "sledgehammer_bindgen", "sledgehammer_utils", diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 1cb573911..7ec60ba38 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -24,6 +24,9 @@ serde = { version = "1.0", features = ["derive"], optional = true } dioxus-core = { workspace = true, optional = true } dioxus-html = { workspace = true, optional = true } +[build-dependencies] +md5 = "0.7.0" + [features] default = [] serialize = ["serde"] diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 992a6779d..ae44e33b5 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -8,12 +8,12 @@ fn main() { let hash = hash_ts_files(); // If the hash matches the one on disk, we're good and don't need to update bindings - let expected = include_str!("src/js/hash.txt").trim(); + let expected = include_str!("./src/js/hash.txt").trim(); if expected == hash.to_string() { return; } - panic!("Hashes match, no need to update bindings. {expected} != {hash}",); + // panic!("Hashes match, no need to update bindings. {expected} != {hash}",); // Otherwise, generate the bindings and write the new hash to disk // Generate the bindings for both native and web @@ -25,24 +25,19 @@ fn main() { } /// Hashes the contents of a directory -fn hash_ts_files() -> u128 { - let mut out = 0; +fn hash_ts_files() -> String { + let mut out = "".to_string(); - let files: &[&[u8]] = &[ - include_bytes!("src/ts/common.ts"), - include_bytes!("src/ts/native.ts"), - include_bytes!("src/ts/core.ts"), + let files = &[ + include_str!("./src/ts/common.ts"), + include_str!("./src/ts/native.ts"), + include_str!("./src/ts/core.ts"), ]; - // Let's make the dumbest hasher by summing the bytes of the files - // The location is multiplied by the byte value to make sure that the order of the bytes matters - let mut idx = 0; for file in files { - for byte in file.iter() { - idx += 1; - out += (*byte as u128) * (idx as u128); - } + out = format!("{out}{:?}", md5::compute(file)); } + out } diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 88a0824d3..0eab9f367 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -12705686530 \ No newline at end of file +3bd5a1310ebd728bdedb486881d5ad99151cf07b0006de52240dfd757ea3353e90df122a7ae681f33ff620cf15c47a20 \ No newline at end of file From c75aa48c74ef800eb4407b0bc8510035431b54e7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 18:43:19 -0800 Subject: [PATCH 35/41] add warning --- packages/interpreter/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index ae44e33b5..af67eb3cc 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -13,7 +13,7 @@ fn main() { return; } - // panic!("Hashes match, no need to update bindings. {expected} != {hash}",); + panic!("Hashes match, no need to update bindings. {expected} != {hash}",); // Otherwise, generate the bindings and write the new hash to disk // Generate the bindings for both native and web From a2907d138d80676a5f86dd8a2cd63451a7d27bb3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 18:59:15 -0800 Subject: [PATCH 36/41] dont even hash it! --- packages/interpreter/build.rs | 6 +- packages/interpreter/src/js/hash.txt | 558 ++++++++++++++++++++++++++- 2 files changed, 560 insertions(+), 4 deletions(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index af67eb3cc..bae015f06 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -13,7 +13,7 @@ fn main() { return; } - panic!("Hashes match, no need to update bindings. {expected} != {hash}",); + // panic!("Hashes match, no need to update bindings. {expected} != {hash}",); // Otherwise, generate the bindings and write the new hash to disk // Generate the bindings for both native and web @@ -21,7 +21,7 @@ fn main() { gen_bindings("native", "native"); gen_bindings("core", "core"); - std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); + std::fs::write("src/js/hash.txt", hash).unwrap(); } /// Hashes the contents of a directory @@ -35,7 +35,7 @@ fn hash_ts_files() -> String { ]; for file in files { - out = format!("{out}{:?}", md5::compute(file)); + out = format!("{out}{file}"); } out diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 0eab9f367..c37c153a7 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1,557 @@ -3bd5a1310ebd728bdedb486881d5ad99151cf07b0006de52240dfd757ea3353e90df122a7ae681f33ff620cf15c47a20 \ No newline at end of file +export { setAttributeInner } from "./set_attribute"; +export { retrieveFormValues } from "./form"; +// This file provides an extended variant of the interpreter used for desktop and liveview interaction +// +// This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't +// provide since it doesn't have access to the dom. + +import { BaseInterpreter, NodeId } from "./core"; +import { SerializedEvent, serializeEvent } from "./serialize"; + +// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope +// we want to extend it, and it technically extends base intepreter. To make typescript happy, +// we're going to bind the JSChannel_ object to the JSChannel object, and then extend it +var JSChannel_: typeof BaseInterpreter; + +// @ts-ignore - this is coming from the host +if (RawInterpreter !== undefined && RawInterpreter !== null) { + // @ts-ignore - this is coming from the host + JSChannel_ = RawInterpreter; +}; + +export class NativeInterpreter extends JSChannel_ { + intercept_link_redirects: boolean; + ipc: any; + editsPath: string; + + // eventually we want to remove liveview and build it into the server-side-events of fullstack + // however, for now we need to support it since SSE in fullstack doesn't exist yet + liveview: boolean; + + constructor(editsPath: string) { + super(); + this.editsPath = editsPath; + } + + initialize(root: HTMLElement): void { + this.intercept_link_redirects = true; + this.liveview = false; + + // attach an event listener on the body that prevents file drops from navigating + // this is because the browser will try to navigate to the file if it's dropped on the window + window.addEventListener("dragover", function (e) { + // // check which element is our target + if (e.target instanceof Element && e.target.tagName != "INPUT") { + e.preventDefault(); + } + }, false); + + window.addEventListener("drop", function (e) { + let target = e.target; + + if (!(target instanceof Element)) { + return; + } + + // Dropping a file on the window will navigate to the file, which we don't want + e.preventDefault(); + }, false); + + // attach a listener to the route that listens for clicks and prevents the default file dialog + window.addEventListener("click", (event) => { + const target = event.target; + if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") { + // Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it + let target_id = getTargetId(target); + if (target_id !== null) { + const message = this.serializeIpcMessage("file_dialog", { + event: "change&input", + accept: target.getAttribute("accept"), + directory: target.getAttribute("webkitdirectory") === "true", + multiple: target.hasAttribute("multiple"), + target: target_id, + bubbles: event.bubbles, + }); + this.ipc.postMessage(message); + } + + // Prevent default regardless - we don't want file dialogs and we don't want the browser to navigate + event.preventDefault(); + } + }); + + + // @ts-ignore - wry gives us this + this.ipc = window.ipc; + + // make sure we pass the handler to the base interpreter + const handler: EventListener = (event) => this.handleEvent(event, event.type, true); + super.initialize(root, handler); + } + + serializeIpcMessage(method: string, params = {}) { + return JSON.stringify({ method, params }); + } + + scrollTo(id: NodeId, behavior: ScrollBehavior) { + const node = this.nodes[id]; + if (node instanceof HTMLElement) { + node.scrollIntoView({ behavior }); + } + } + + getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined { + const node = this.nodes[id]; + if (node instanceof HTMLElement) { + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [rect.x, rect.y], + size: [rect.width, rect.height], + }; + } + } + + setFocus(id: NodeId, focus: boolean) { + const node = this.nodes[id]; + + if (node instanceof HTMLElement) { + if (focus) { + node.focus(); + } else { + node.blur(); + } + } + } + + // ignore the fact the base interpreter uses ptr + len but we use array... + // @ts-ignore + loadChild(array: number[]) { + // iterate through each number and get that child + let node = this.stack[this.stack.length - 1]; + + for (let i = 0; i < array.length; i++) { + let end = array[i]; + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + + return node; + } + + appendChildren(id: NodeId, many: number) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + + for (let k = 0; k < many; k++) { + root.appendChild(els[k]); + } + } + + handleEvent(event: Event, name: string, bubbles: boolean) { + const target = event.target!; + const realId = getTargetId(target)!; + const contents = serializeEvent(event, target); + + // Handle the event on the virtualdom and then preventDefault if it also preventsDefault + // Some listeners + let body = { + name: name, + data: contents, + element: realId, + bubbles, + }; + + // Run any prevent defaults the user might've set + // This is to support the prevent_default: "onclick" attribute that dioxus has had for a while, but is not necessary + // now that we expose preventDefault to the virtualdom on desktop + // Liveview will still need to use this + this.preventDefaults(event, target); + + // liveview does not have syncronous event handling, so we need to send the event to the host + if (this.liveview) { + // Okay, so the user might've requested some files to be read + if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { + if (target.getAttribute("type") === "file") { + this.readFiles(target, contents, bubbles, realId, name); + } + } + } else { + + const message = this.serializeIpcMessage("user_event", body); + this.ipc.postMessage(message); + + // // Run the event handler on the virtualdom + // // capture/prevent default of the event if the virtualdom wants to + // const res = handleVirtualdomEventSync(JSON.stringify(body)); + + // if (res.preventDefault) { + // event.preventDefault(); + // } + + // if (res.stopPropagation) { + // event.stopPropagation(); + // } + } + } + + + + // This should: + // - prevent form submissions from navigating + // - prevent anchor tags from navigating + // - prevent buttons from submitting forms + // - let the virtualdom attempt to prevent the event + preventDefaults(event: Event, target: EventTarget) { + let preventDefaultRequests: string | null = null; + + // Some events can be triggered on text nodes, which don't have attributes + if (target instanceof Element) { + preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); + } + + if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { + event.preventDefault(); + } + + if (event.type === "submit") { + event.preventDefault(); + } + + // Attempt to intercept if the event is a click + if (target instanceof Element && event.type === "click") { + this.handleClickNavigate(event, target, preventDefaultRequests); + } + } + + handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) { + // todo call prevent default if it's the right type of event + if (!this.intercept_link_redirects) { + return; + } + + // prevent buttons in forms from submitting the form + if (target.tagName === "BUTTON" && event.type == "submit") { + event.preventDefault(); + } + + // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating + let a_element = target.closest("a"); + if (a_element == null) { + return; + } + + event.preventDefault(); + + let elementShouldPreventDefault = + preventDefaultRequests && preventDefaultRequests.includes(`onclick`); + + let aElementShouldPreventDefault = a_element.getAttribute( + `dioxus-prevent-default` + ); + + let linkShouldPreventDefault = + aElementShouldPreventDefault && + aElementShouldPreventDefault.includes(`onclick`); + + if (!elementShouldPreventDefault && !linkShouldPreventDefault) { + const href = a_element.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + this.ipc.postMessage( + this.serializeIpcMessage("browser_open", { href }) + ); + } + } + } + + waitForRequest(headless: boolean) { + fetch(new Request(this.editsPath)) + .then(response => response.arrayBuffer()) + .then(bytes => { + // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly + if (headless) { + // @ts-ignore + this.run_from_bytes(bytes); + } else { + // @ts-ignore + requestAnimationFrame(() => this.run_from_bytes(bytes)); + } + this.waitForRequest(headless); + }); + } + + + // A liveview only function + // Desktop will intercept the event before it hits this + async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) { + let files = target.files!; + let file_contents: { [name: string]: number[] } = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + + contents.files = { files: file_contents }; + + const message = this.serializeIpcMessage("user_event", { + name: name, + element: realId, + data: contents, + bubbles, + }); + + this.ipc.postMessage(message); + } +} + +type EventSyncResult = { + preventDefault: boolean; + stopPropagation: boolean; + stopImmediatePropagation: boolean; + filesRequested: boolean; +}; + +// This function sends the event to the virtualdom and then waits for the virtualdom to process it +// +// However, it's not really suitable for liveview, because it's synchronous and will block the main thread +// We should definitely consider using a websocket if we want to block... or just not block on liveview +// Liveview is a little bit of a tricky beast +function handleVirtualdomEventSync(contents: string): EventSyncResult { + // Handle the event on the virtualdom and then process whatever its output was + const xhr = new XMLHttpRequest(); + + // Serialize the event and send it to the custom protocol in the Rust side of things + xhr.timeout = 1000; + xhr.open("GET", "/handle/event.please", false); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(contents); + + // Deserialize the response, and then prevent the default/capture the event if the virtualdom wants to + return JSON.parse(xhr.responseText); +} + +function getTargetId(target: EventTarget): NodeId | null { + // Ensure that the target is a node, sometimes it's nota + if (!(target instanceof Node)) { + return null; + } + + let ourTarget = target; + let realId = null; + + while (realId == null) { + if (ourTarget === null) { + return null; + } + + if (ourTarget instanceof Element) { + realId = ourTarget.getAttribute(`data-dioxus-id`); + } + + ourTarget = ourTarget.parentNode; + } + + return parseInt(realId); +} + + +// function applyFileUpload() { +// let inputs = document.querySelectorAll("input"); +// for (let input of inputs) { +// if (!input.getAttribute("data-dioxus-file-listener")) { +// // prevent file inputs from opening the file dialog on click +// const type = input.getAttribute("type"); +// if (type === "file") { +// input.setAttribute("data-dioxus-file-listener", true); +// input.addEventListener("click", (event) => { +// let target = event.target; +// let target_id = find_real_id(target); +// if (target_id !== null) { +// const send = (event_name) => { +// const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); +// window.ipc.postMessage(message); +// }; +// send("change&input"); +// } +// event.preventDefault(); +// }); +// } +// } +// } +// The root interpreter class that holds state about the mapping between DOM and VirtualDom +// This always lives in the JS side of things, and is extended by the native and web interpreters + +import { setAttributeInner } from "./set_attribute"; + +export type NodeId = number; + +export class BaseInterpreter { + // non bubbling events listen at the element the listener was created at + global: { + [key: string]: { active: number, callback: EventListener } + }; + // bubbling events can listen at the root element + local: { + [key: string]: { + [key: string]: EventListener + } + }; + + root: HTMLElement; + handler: EventListener; + nodes: Node[]; + stack: Node[]; + templates: { + [key: number]: Node[] + }; + + // sledgehammer is generating this... + m: any; + + constructor() { } + + initialize(root: HTMLElement, handler: EventListener | null = null) { + this.global = {}; + this.local = {}; + this.root = root; + + this.nodes = [root]; + this.stack = [root]; + this.templates = {}; + + if (handler) { + this.handler = handler; + } + } + + createListener(event_name: string, element: HTMLElement, bubbles: boolean) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = { active: 1, callback: this.handler }; + this.root.addEventListener(event_name, this.handler); + } else { + this.global[event_name].active++; + } + } else { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, this.handler); + } + } + + removeListener(element: HTMLElement, event_name: string, bubbles: boolean) { + if (bubbles) { + this.removeBubblingListener(event_name); + } else { + this.removeNonBubblingListener(element, event_name); + } + } + + removeBubblingListener(event_name: string) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + + removeNonBubblingListener(element: HTMLElement, event_name: string) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (Object.keys(this.local[id]).length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + } + + removeAllNonBubblingListeners(element: HTMLElement) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } + + getNode(id: NodeId): Node { + return this.nodes[id]; + } + + appendChildren(id: NodeId, many: number) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + for (let k = 0; k < many; k++) { + root.appendChild(els[k]); + } + } + + loadChild(ptr: number, len: number): Node { + // iterate through each number and get that child + let node = this.stack[this.stack.length - 1] as Node; + let ptr_end = ptr + len; + + for (; ptr < ptr_end; ptr++) { + let end = this.m.getUint8(ptr); + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + + return node; + } + + saveTemplate(nodes: HTMLElement[], tmpl_id: number) { + this.templates[tmpl_id] = nodes; + } + + hydrate(ids: { [key: number]: number }) { + const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); + + for (let i = 0; i < hydrateNodes.length; i++) { + const hydrateNode = hydrateNodes[i] as HTMLElement; + const hydration = hydrateNode.getAttribute('data-node-hydration'); + const split = hydration!.split(','); + const id = ids[parseInt(split[0])]; + + this.nodes[id] = hydrateNode; + + if (split.length > 1) { + // @ts-ignore + hydrateNode.listening = split.length - 1; + hydrateNode.setAttribute('data-dioxus-id', id.toString()); + for (let j = 1; j < split.length; j++) { + const listener = split[j]; + const split2 = listener.split(':'); + const event_name = split2[0]; + const bubbles = split2[1] === '1'; + this.createListener(event_name, hydrateNode, bubbles); + } + } + } + + const treeWalker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_COMMENT, + ); + + let currentNode = treeWalker.nextNode(); + + while (currentNode) { + const id = currentNode.textContent!; + const split = id.split('node-id'); + + if (split.length > 1) { + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; + } + + currentNode = treeWalker.nextNode(); + } + } + + setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { + setAttributeInner(node, field, value, ns); + } +} + From c99c07db0cc16e80c42be87b787ba6af23e16e24 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 18:59:27 -0800 Subject: [PATCH 37/41] warn --- packages/interpreter/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index bae015f06..6f0edeb31 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -13,7 +13,7 @@ fn main() { return; } - // panic!("Hashes match, no need to update bindings. {expected} != {hash}",); + panic!("Hashes match, no need to update bindings. {expected} != {hash}",); // Otherwise, generate the bindings and write the new hash to disk // Generate the bindings for both native and web From 2995647e991c77b06519fdff7a28d64e8e9314af Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 19:18:41 -0800 Subject: [PATCH 38/41] trim line feeds --- packages/interpreter/build.rs | 29 +- packages/interpreter/src/js/hash.txt | 558 +-------------------------- 2 files changed, 20 insertions(+), 567 deletions(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 6f0edeb31..a95f973f9 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -8,7 +8,7 @@ fn main() { let hash = hash_ts_files(); // If the hash matches the one on disk, we're good and don't need to update bindings - let expected = include_str!("./src/js/hash.txt").trim(); + let expected = include_str!("src/js/hash.txt").trim(); if expected == hash.to_string() { return; } @@ -21,23 +21,32 @@ fn main() { gen_bindings("native", "native"); gen_bindings("core", "core"); - std::fs::write("src/js/hash.txt", hash).unwrap(); + std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); } /// Hashes the contents of a directory -fn hash_ts_files() -> String { - let mut out = "".to_string(); +fn hash_ts_files() -> u128 { + let mut out = 0; - let files = &[ - include_str!("./src/ts/common.ts"), - include_str!("./src/ts/native.ts"), - include_str!("./src/ts/core.ts"), + let files = [ + include_str!("src/ts/common.ts"), + include_str!("src/ts/native.ts"), + include_str!("src/ts/core.ts"), ]; + // Let's make the dumbest hasher by summing the bytes of the files + // The location is multiplied by the byte value to make sure that the order of the bytes matters + let mut idx = 0; for file in files { - out = format!("{out}{file}"); + // windows + git does a weird thing with line endings, so we need to normalize them + for line in file.lines() { + idx += 1; + for byte in line.bytes() { + idx += 1; + out += (byte as u128) * (idx as u128); + } + } } - out } diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index c37c153a7..9fe9e2627 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1,557 +1 @@ -export { setAttributeInner } from "./set_attribute"; -export { retrieveFormValues } from "./form"; -// This file provides an extended variant of the interpreter used for desktop and liveview interaction -// -// This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't -// provide since it doesn't have access to the dom. - -import { BaseInterpreter, NodeId } from "./core"; -import { SerializedEvent, serializeEvent } from "./serialize"; - -// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope -// we want to extend it, and it technically extends base intepreter. To make typescript happy, -// we're going to bind the JSChannel_ object to the JSChannel object, and then extend it -var JSChannel_: typeof BaseInterpreter; - -// @ts-ignore - this is coming from the host -if (RawInterpreter !== undefined && RawInterpreter !== null) { - // @ts-ignore - this is coming from the host - JSChannel_ = RawInterpreter; -}; - -export class NativeInterpreter extends JSChannel_ { - intercept_link_redirects: boolean; - ipc: any; - editsPath: string; - - // eventually we want to remove liveview and build it into the server-side-events of fullstack - // however, for now we need to support it since SSE in fullstack doesn't exist yet - liveview: boolean; - - constructor(editsPath: string) { - super(); - this.editsPath = editsPath; - } - - initialize(root: HTMLElement): void { - this.intercept_link_redirects = true; - this.liveview = false; - - // attach an event listener on the body that prevents file drops from navigating - // this is because the browser will try to navigate to the file if it's dropped on the window - window.addEventListener("dragover", function (e) { - // // check which element is our target - if (e.target instanceof Element && e.target.tagName != "INPUT") { - e.preventDefault(); - } - }, false); - - window.addEventListener("drop", function (e) { - let target = e.target; - - if (!(target instanceof Element)) { - return; - } - - // Dropping a file on the window will navigate to the file, which we don't want - e.preventDefault(); - }, false); - - // attach a listener to the route that listens for clicks and prevents the default file dialog - window.addEventListener("click", (event) => { - const target = event.target; - if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") { - // Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it - let target_id = getTargetId(target); - if (target_id !== null) { - const message = this.serializeIpcMessage("file_dialog", { - event: "change&input", - accept: target.getAttribute("accept"), - directory: target.getAttribute("webkitdirectory") === "true", - multiple: target.hasAttribute("multiple"), - target: target_id, - bubbles: event.bubbles, - }); - this.ipc.postMessage(message); - } - - // Prevent default regardless - we don't want file dialogs and we don't want the browser to navigate - event.preventDefault(); - } - }); - - - // @ts-ignore - wry gives us this - this.ipc = window.ipc; - - // make sure we pass the handler to the base interpreter - const handler: EventListener = (event) => this.handleEvent(event, event.type, true); - super.initialize(root, handler); - } - - serializeIpcMessage(method: string, params = {}) { - return JSON.stringify({ method, params }); - } - - scrollTo(id: NodeId, behavior: ScrollBehavior) { - const node = this.nodes[id]; - if (node instanceof HTMLElement) { - node.scrollIntoView({ behavior }); - } - } - - getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined { - const node = this.nodes[id]; - if (node instanceof HTMLElement) { - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; - } - } - - setFocus(id: NodeId, focus: boolean) { - const node = this.nodes[id]; - - if (node instanceof HTMLElement) { - if (focus) { - node.focus(); - } else { - node.blur(); - } - } - } - - // ignore the fact the base interpreter uses ptr + len but we use array... - // @ts-ignore - loadChild(array: number[]) { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1]; - - for (let i = 0; i < array.length; i++) { - let end = array[i]; - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - - return node; - } - - appendChildren(id: NodeId, many: number) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - - for (let k = 0; k < many; k++) { - root.appendChild(els[k]); - } - } - - handleEvent(event: Event, name: string, bubbles: boolean) { - const target = event.target!; - const realId = getTargetId(target)!; - const contents = serializeEvent(event, target); - - // Handle the event on the virtualdom and then preventDefault if it also preventsDefault - // Some listeners - let body = { - name: name, - data: contents, - element: realId, - bubbles, - }; - - // Run any prevent defaults the user might've set - // This is to support the prevent_default: "onclick" attribute that dioxus has had for a while, but is not necessary - // now that we expose preventDefault to the virtualdom on desktop - // Liveview will still need to use this - this.preventDefaults(event, target); - - // liveview does not have syncronous event handling, so we need to send the event to the host - if (this.liveview) { - // Okay, so the user might've requested some files to be read - if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { - if (target.getAttribute("type") === "file") { - this.readFiles(target, contents, bubbles, realId, name); - } - } - } else { - - const message = this.serializeIpcMessage("user_event", body); - this.ipc.postMessage(message); - - // // Run the event handler on the virtualdom - // // capture/prevent default of the event if the virtualdom wants to - // const res = handleVirtualdomEventSync(JSON.stringify(body)); - - // if (res.preventDefault) { - // event.preventDefault(); - // } - - // if (res.stopPropagation) { - // event.stopPropagation(); - // } - } - } - - - - // This should: - // - prevent form submissions from navigating - // - prevent anchor tags from navigating - // - prevent buttons from submitting forms - // - let the virtualdom attempt to prevent the event - preventDefaults(event: Event, target: EventTarget) { - let preventDefaultRequests: string | null = null; - - // Some events can be triggered on text nodes, which don't have attributes - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); - } - - if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - // Attempt to intercept if the event is a click - if (target instanceof Element && event.type === "click") { - this.handleClickNavigate(event, target, preventDefaultRequests); - } - } - - handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) { - // todo call prevent default if it's the right type of event - if (!this.intercept_link_redirects) { - return; - } - - // prevent buttons in forms from submitting the form - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - - // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating - let a_element = target.closest("a"); - if (a_element == null) { - return; - } - - event.preventDefault(); - - let elementShouldPreventDefault = - preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - - let aElementShouldPreventDefault = a_element.getAttribute( - `dioxus-prevent-default` - ); - - let linkShouldPreventDefault = - aElementShouldPreventDefault && - aElementShouldPreventDefault.includes(`onclick`); - - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - this.ipc.postMessage( - this.serializeIpcMessage("browser_open", { href }) - ); - } - } - } - - waitForRequest(headless: boolean) { - fetch(new Request(this.editsPath)) - .then(response => response.arrayBuffer()) - .then(bytes => { - // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly - if (headless) { - // @ts-ignore - this.run_from_bytes(bytes); - } else { - // @ts-ignore - requestAnimationFrame(() => this.run_from_bytes(bytes)); - } - this.waitForRequest(headless); - }); - } - - - // A liveview only function - // Desktop will intercept the event before it hits this - async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) { - let files = target.files!; - let file_contents: { [name: string]: number[] } = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - - contents.files = { files: file_contents }; - - const message = this.serializeIpcMessage("user_event", { - name: name, - element: realId, - data: contents, - bubbles, - }); - - this.ipc.postMessage(message); - } -} - -type EventSyncResult = { - preventDefault: boolean; - stopPropagation: boolean; - stopImmediatePropagation: boolean; - filesRequested: boolean; -}; - -// This function sends the event to the virtualdom and then waits for the virtualdom to process it -// -// However, it's not really suitable for liveview, because it's synchronous and will block the main thread -// We should definitely consider using a websocket if we want to block... or just not block on liveview -// Liveview is a little bit of a tricky beast -function handleVirtualdomEventSync(contents: string): EventSyncResult { - // Handle the event on the virtualdom and then process whatever its output was - const xhr = new XMLHttpRequest(); - - // Serialize the event and send it to the custom protocol in the Rust side of things - xhr.timeout = 1000; - xhr.open("GET", "/handle/event.please", false); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.send(contents); - - // Deserialize the response, and then prevent the default/capture the event if the virtualdom wants to - return JSON.parse(xhr.responseText); -} - -function getTargetId(target: EventTarget): NodeId | null { - // Ensure that the target is a node, sometimes it's nota - if (!(target instanceof Node)) { - return null; - } - - let ourTarget = target; - let realId = null; - - while (realId == null) { - if (ourTarget === null) { - return null; - } - - if (ourTarget instanceof Element) { - realId = ourTarget.getAttribute(`data-dioxus-id`); - } - - ourTarget = ourTarget.parentNode; - } - - return parseInt(realId); -} - - -// function applyFileUpload() { -// let inputs = document.querySelectorAll("input"); -// for (let input of inputs) { -// if (!input.getAttribute("data-dioxus-file-listener")) { -// // prevent file inputs from opening the file dialog on click -// const type = input.getAttribute("type"); -// if (type === "file") { -// input.setAttribute("data-dioxus-file-listener", true); -// input.addEventListener("click", (event) => { -// let target = event.target; -// let target_id = find_real_id(target); -// if (target_id !== null) { -// const send = (event_name) => { -// const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); -// window.ipc.postMessage(message); -// }; -// send("change&input"); -// } -// event.preventDefault(); -// }); -// } -// } -// } -// The root interpreter class that holds state about the mapping between DOM and VirtualDom -// This always lives in the JS side of things, and is extended by the native and web interpreters - -import { setAttributeInner } from "./set_attribute"; - -export type NodeId = number; - -export class BaseInterpreter { - // non bubbling events listen at the element the listener was created at - global: { - [key: string]: { active: number, callback: EventListener } - }; - // bubbling events can listen at the root element - local: { - [key: string]: { - [key: string]: EventListener - } - }; - - root: HTMLElement; - handler: EventListener; - nodes: Node[]; - stack: Node[]; - templates: { - [key: number]: Node[] - }; - - // sledgehammer is generating this... - m: any; - - constructor() { } - - initialize(root: HTMLElement, handler: EventListener | null = null) { - this.global = {}; - this.local = {}; - this.root = root; - - this.nodes = [root]; - this.stack = [root]; - this.templates = {}; - - if (handler) { - this.handler = handler; - } - } - - createListener(event_name: string, element: HTMLElement, bubbles: boolean) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = { active: 1, callback: this.handler }; - this.root.addEventListener(event_name, this.handler); - } else { - this.global[event_name].active++; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - } - - removeListener(element: HTMLElement, event_name: string, bubbles: boolean) { - if (bubbles) { - this.removeBubblingListener(event_name); - } else { - this.removeNonBubblingListener(element, event_name); - } - } - - removeBubblingListener(event_name: string) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - - removeNonBubblingListener(element: HTMLElement, event_name: string) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (Object.keys(this.local[id]).length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - } - - removeAllNonBubblingListeners(element: HTMLElement) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } - - getNode(id: NodeId): Node { - return this.nodes[id]; - } - - appendChildren(id: NodeId, many: number) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - for (let k = 0; k < many; k++) { - root.appendChild(els[k]); - } - } - - loadChild(ptr: number, len: number): Node { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1] as Node; - let ptr_end = ptr + len; - - for (; ptr < ptr_end; ptr++) { - let end = this.m.getUint8(ptr); - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - - return node; - } - - saveTemplate(nodes: HTMLElement[], tmpl_id: number) { - this.templates[tmpl_id] = nodes; - } - - hydrate(ids: { [key: number]: number }) { - const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); - - for (let i = 0; i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i] as HTMLElement; - const hydration = hydrateNode.getAttribute('data-node-hydration'); - const split = hydration!.split(','); - const id = ids[parseInt(split[0])]; - - this.nodes[id] = hydrateNode; - - if (split.length > 1) { - // @ts-ignore - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute('data-dioxus-id', id.toString()); - for (let j = 1; j < split.length; j++) { - const listener = split[j]; - const split2 = listener.split(':'); - const event_name = split2[0]; - const bubbles = split2[1] === '1'; - this.createListener(event_name, hydrateNode, bubbles); - } - } - } - - const treeWalker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_COMMENT, - ); - - let currentNode = treeWalker.nextNode(); - - while (currentNode) { - const id = currentNode.textContent!; - const split = id.split('node-id'); - - if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - - currentNode = treeWalker.nextNode(); - } - } - - setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { - setAttributeInner(node, field, value, ns); - } -} - +12655652627 \ No newline at end of file From bb5ecd779fbaa70b9c1bae9f044410510a89adb8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 19:50:07 -0800 Subject: [PATCH 39/41] clean up some examples, ensuring stuff works --- examples/image_generator_openai.rs | 109 +++++++++++------------------ examples/todomvc.rs | 2 +- examples/weather_app.rs | 53 +++++--------- examples/xss_safety.rs | 2 +- packages/interpreter/build.rs | 2 - 5 files changed, 59 insertions(+), 109 deletions(-) diff --git a/examples/image_generator_openai.rs b/examples/image_generator_openai.rs index 8cebbb373..9c0492273 100644 --- a/examples/image_generator_openai.rs +++ b/examples/image_generator_openai.rs @@ -7,6 +7,7 @@ fn main() { } fn app() -> Element { + let mut loading = use_signal(|| "".to_string()); let mut api = use_signal(|| "".to_string()); let mut prompt = use_signal(|| "".to_string()); let mut n_image = use_signal(|| 1.to_string()); @@ -14,7 +15,6 @@ fn app() -> Element { created: 0, data: Vec::new(), }); - let mut loading = use_signal(|| "".to_string()); let mut generate_images = use_resource(move || async move { let api_key = api.peek().clone(); @@ -26,79 +26,54 @@ fn app() -> Element { } loading.set("is-loading".to_string()); - let images = request(api_key, prompt, number_of_images).await; - match images { - Ok(imgz) => { - image.set(imgz); - } - Err(e) => { - println!("Error: {:?}", e); - } + + match request(api_key, prompt, number_of_images).await { + Ok(imgz) => image.set(imgz), + Err(e) => println!("Error: {:?}", e), } + loading.set("".to_string()); }); rsx! { - head { - link { - rel: "stylesheet", - href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css", - } - } + head { link { rel: "stylesheet", href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" } } div { class: "container", - div { class: "columns", - div { class: "column", - input { class: "input is-primary mt-4", - value:"{api}", - r#type: "text", - placeholder: "API", - oninput: move |evt| { - api.set(evt.value().clone()); - }, - } - - input { class: "input is-primary mt-4", - placeholder: "MAX 1000 Dgts", - r#type: "text", - value:"{prompt}", - oninput: move |evt| { - prompt.set(evt.value().clone()); - }, - } - - input { class: "input is-primary mt-4", - r#type: "number", - min:"1", - max:"10", - value:"{n_image}", - oninput: move |evt| { - n_image.set(evt.value().clone()); - }, + div { class: "columns", + div { class: "column", + input { class: "input is-primary mt-4", + value: "{api}", + r#type: "text", + placeholder: "API", + oninput: move |evt| api.set(evt.value()), + } + input { class: "input is-primary mt-4", + placeholder: "MAX 1000 Dgts", + r#type: "text", + value:"{prompt}", + oninput: move |evt| prompt.set(evt.value()) + } + input { class: "input is-primary mt-4", + r#type: "number", + min:"1", + max:"10", + value:"{n_image}", + oninput: move |evt| n_image.set(evt.value()), + } } } - } - - button { class: "button is-primary {loading}", - onclick: move |_| { - generate_images.restart(); - }, - "Generate image" - } - br { - } - } - {image.read().data.iter().map(|image| { - rsx!( + button { class: "button is-primary {loading}", + onclick: move |_| generate_images.restart(), + "Generate image" + } + br {} + for image in image.read().data.as_slice() { section { class: "is-flex", - div { class: "container is-fluid", - div { class: "container has-text-centered", - div { class: "is-justify-content-center", - div { class: "level", - div { class: "level-item", - figure { class: "image", - img { - alt: "", - src: "{image.url}", + div { class: "container is-fluid", + div { class: "container has-text-centered", + div { class: "is-justify-content-center", + div { class: "level", + div { class: "level-item", + figure { class: "image", img { alt: "", src: "{image.url}", } } } } } @@ -107,9 +82,7 @@ fn app() -> Element { } } } - ) - }) - } } + } } async fn request(api: String, prompt: String, n_image: String) -> Result { let client = reqwest::Client::new(); diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 2af2daf03..7b2405f40 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -131,7 +131,7 @@ fn TodoHeader(mut todos: Signal>) -> Element { placeholder: "What needs to be done?", value: "{draft}", autofocus: "true", - oninput: move |evt| draft.set(evt.value().clone()), + oninput: move |evt| draft.set(evt.value()), onkeydown, } } diff --git a/examples/weather_app.rs b/examples/weather_app.rs index 9615c8d49..a5774a327 100644 --- a/examples/weather_app.rs +++ b/examples/weather_app.rs @@ -16,18 +16,14 @@ fn app() -> Element { id: 2950159, }); - let current_weather = - use_resource(move || async move { get_weather(&country.read().clone()).await }); + let current_weather = use_resource(move || async move { get_weather(&country()).await }); rsx! { - link { - rel: "stylesheet", - href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" - } + link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" } div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center", div { class: "flex items-center justify-center flex-row", div { class: "flex items-start justify-center flex-row", - SearchBox { country: country } + SearchBox { country } div { class: "flex flex-wrap w-full px-2", div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full bg-white dark:bg-gray-600", div { class: "px-6 py-6 relative", @@ -36,14 +32,9 @@ fn app() -> Element { country: country.read().clone(), weather: weather.clone(), } - Forecast { - weather: weather.clone(), - } - + Forecast { weather: weather.clone() } } else { - p { - "Loading.." - } + p { "Loading.." } } } } @@ -93,6 +84,7 @@ fn Forecast(weather: WeatherResponse) -> Element { let past_tomorrow = (weather.daily.temperature_2m_max.get(2).unwrap() + weather.daily.temperature_2m_max.get(2).unwrap()) / 2.0; + rsx! { div { class: "px-6 pt-4 relative", div { class: "w-full h-px bg-gray-100 mb-4" } @@ -119,10 +111,7 @@ fn Forecast(weather: WeatherResponse) -> Element { fn SearchBox(mut country: Signal) -> Element { let mut input = use_signal(|| "".to_string()); - let locations = use_resource(move || async move { - let current_location = input.read().clone(); - get_locations(¤t_location).await - }); + let locations = use_resource(move || async move { get_locations(&input()).await }); rsx! { div { @@ -150,27 +139,17 @@ fn SearchBox(mut country: Signal) -> Element { } } ul { class: "bg-white border border-gray-100 w-full mt-2 max-h-72 overflow-auto", - { - if let Some(Ok(locs)) = locations.read().as_ref() { - rsx! { - { - locs.iter().cloned().map(move |wl| { - rsx! { - li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900", - onclick: move |_| country.set(wl.clone()), - MapIcon {} - b { - "{wl.name}" - } - " · {wl.country}" - } - } - }) - } + if let Some(Ok(locs)) = locations.read().as_ref() { + for wl in locs.iter().take(5).cloned() { + li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900", + onclick: move |_| country.set(wl.clone()), + MapIcon {} + b { "{wl.name}" } + " · {wl.country}" } - } else { - rsx! { "loading locations..." } } + } else { + "loading locations..." } } } diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs index 8f4dc7245..cb8d414dd 100644 --- a/examples/xss_safety.rs +++ b/examples/xss_safety.rs @@ -5,7 +5,7 @@ use dioxus::prelude::*; fn main() { - launch_desktop(app); + launch(app); } fn app() -> Element { diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index a95f973f9..f5d69db40 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -13,8 +13,6 @@ fn main() { return; } - panic!("Hashes match, no need to update bindings. {expected} != {hash}",); - // Otherwise, generate the bindings and write the new hash to disk // Generate the bindings for both native and web gen_bindings("common", "common"); From c3df8c55e32b6cf6f6f5faf449bde404348ab70d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 19:59:47 -0800 Subject: [PATCH 40/41] fix windows, enable tokio for examples on wasm --- Cargo.toml | 1 + packages/desktop/headless_tests/utils.rs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c07bd33d4..f770e0017 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ form_urlencoded = "1.2.0" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] getrandom = { version = "0.2.12", features = ["js"] } +tokio = { version = "1.16.1", no-default-features = true, features = ["sync", "macros", "io-util", "rt", "time"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.16.1", features = ["full"] } diff --git a/packages/desktop/headless_tests/utils.rs b/packages/desktop/headless_tests/utils.rs index 419f5c8cd..a497d3586 100644 --- a/packages/desktop/headless_tests/utils.rs +++ b/packages/desktop/headless_tests/utils.rs @@ -36,7 +36,10 @@ pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'sta EXPECTED_EVENTS.with_mut(|x| *x += 1); spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(500)).await; + // We need to wait for edits to be applied before we can send the event + // Sometimes (windows...) this takes a while + // we should really be running this check when mounted + tokio::time::sleep(std::time::Duration::from_millis(10000)).await; let js = format!( r#" From feb80ce50932206fee45177373bad627febbd01a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Mar 2024 20:24:37 -0800 Subject: [PATCH 41/41] remove old interpreter --- Cargo.toml | 2 +- packages/interpreter/src/interpreter.js | 751 ------------------------ 2 files changed, 1 insertion(+), 752 deletions(-) delete mode 100644 packages/interpreter/src/interpreter.js diff --git a/Cargo.toml b/Cargo.toml index f770e0017..0f68d3054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,7 +155,7 @@ form_urlencoded = "1.2.0" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] getrandom = { version = "0.2.12", features = ["js"] } -tokio = { version = "1.16.1", no-default-features = true, features = ["sync", "macros", "io-util", "rt", "time"] } +tokio = { version = "1.16.1", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.16.1", features = ["full"] } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js deleted file mode 100644 index 63ee6d630..000000000 --- a/packages/interpreter/src/interpreter.js +++ /dev/null @@ -1,751 +0,0 @@ -// this handler is only provided on the desktop and liveview implementations since this -// method is not used by the web implementation -this.handler = async function (event, name, bubbles) { - let target = event.target; - if (target != null) { - let preventDefaultRequests = null; - // Some events can be triggered on text nodes, which don't have attributes - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); - } - - if (event.type === "click") { - // todo call prevent default if it's the right type of event - if (intercept_link_redirects) { - let a_element = target.closest("a"); - if (a_element != null) { - event.preventDefault(); - - let elementShouldPreventDefault = - preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - let aElementShouldPreventDefault = a_element.getAttribute( - `dioxus-prevent-default` - ); - let linkShouldPreventDefault = - aElementShouldPreventDefault && - aElementShouldPreventDefault.includes(`onclick`); - - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - this.serializeIpcMessage("browser_open", { href }) - ); - } - } - } - } - - // also prevent buttons from submitting - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - } - - const realId = find_real_id(target); - - if ( - preventDefaultRequests && - preventDefaultRequests.includes(`on${event.type}`) - ) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - let contents = await serialize_event(event); - - // TODO: this should be liveview only - if ( - target.tagName === "INPUT" && - (event.type === "change" || event.type === "input") - ) { - const type = target.getAttribute("type"); - if (type === "file") { - async function read_files() { - const files = target.files; - const file_contents = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - let file_engine = { - files: file_contents, - }; - contents.files = file_engine; - - if (realId === null) { - return; - } - const message = window.interpreter.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }); - window.ipc.postMessage(message); - } - read_files(); - return; - } - } - - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - const formData = new FormData(target); - - for (let name of formData.keys()) { - const fieldType = target.elements[name].type; - - switch (fieldType) { - case "select-multiple": - contents.values[name] = formData.getAll(name); - break; - - // add cases for fieldTypes that can hold multiple values here - default: - contents.values[name] = formData.get(name); - break; - } - } - } - - if ( - target.tagName === "SELECT" && - event.type === "input" - ) { - const selectData = target.options; - contents.values["options"] = []; - for (let i = 0; i < selectData.length; i++) { - let option = selectData[i]; - if (option.selected) { - contents.values["options"].push(option.value.toString()); - } - } - } - - if (realId === null) { - return; - } - window.ipc.postMessage( - this.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }) - ); - } -} - -function find_real_id(target) { - let realId = null; - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } - // walk the tree to find the real element - while (realId == null) { - // we've reached the root we don't want to send an event - if (target.parentElement === null) { - return; - } - - target = target.parentElement; - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } - } - return realId; -} - -class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = null; - } - - create(event_name, element, bubbles, handler) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.root.addEventListener(event_name, handler); - } else { - this.global[event_name].active++; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.global[event_name].callback); - } - } - - removeAllNonBubbling(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } -} -this.LoadChild = function (array) { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1]; - - for (let i = 0; i < array.length; i++) { - this.end = array[i]; - for (node = node.firstChild; this.end > 0; this.end--) { - node = node.nextSibling; - } - } - return node; -} -this.listeners = new ListenerMap(); -this.nodes = []; -this.stack = []; -this.templates = {}; -this.end = null; - -this.AppendChildren = function (id, many) { - let root = this.nodes[id]; - let els = this.stack.splice(this.stack.length - many); - for (let k = 0; k < many; k++) { - root.appendChild(els[k]); - } -} - -this.initialize = function (root) { - this.nodes = [root]; - this.stack = [root]; - this.listeners.root = root; -} - -this.getClientRect = function (id) { - const node = this.nodes[id]; - if (!node) { - return; - } - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; -} - -this.scrollTo = function (id, behavior) { - const node = this.nodes[id]; - if (!node) { - return false; - } - node.scrollIntoView({ - behavior: behavior, - }); - return true; -} - -/// Set the focus on the element -this.setFocus = function (id, focus) { - const node = this.nodes[id]; - if (!node) { - return false; - } - if (focus) { - node.focus(); - } else { - node.blur(); - } - return true; -} - -function get_mouse_data(event) { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - offsetX, - offsetY, - pageX, - pageY, - screenX, - screenY, - shiftKey, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - offset_x: offsetX, - offset_y: offsetY, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; -} - -async function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { - charCode, - isComposing, - key, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, - location, - repeat, - which, - code, - } = event; - return { - char_code: charCode, - is_composing: isComposing, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - code, - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - return { - value: value, - values: {}, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - - if (target.type === "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - values: {}, - }; - } - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": { - let files = null; - if (event.dataTransfer && event.dataTransfer.files) { - files = await serializeFileList(event.dataTransfer.files); - } - - return { mouse: get_mouse_data(event), files }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "dblclick": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - return get_mouse_data(event); - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - pointerId, - width, - height, - pressure, - tangentialPressure, - tiltX, - tiltY, - twist, - pointerType, - isPrimary, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -this.serializeIpcMessage = function (method, params = {}) { - return JSON.stringify({ method, params }); -} - -function is_element_node(node) { - return node.nodeType == 1; -} - -function event_bubbles(event) { - switch (event) { - case "copy": - return true; - case "cut": - return true; - case "paste": - return true; - case "compositionend": - return true; - case "compositionstart": - return true; - case "compositionupdate": - return true; - case "keydown": - return true; - case "keypress": - return true; - case "keyup": - return true; - case "focus": - return false; - case "focusout": - return true; - case "focusin": - return true; - case "blur": - return false; - case "change": - return true; - case "input": - return true; - case "invalid": - return true; - case "reset": - return true; - case "submit": - return true; - case "click": - return true; - case "contextmenu": - return true; - case "doubleclick": - return true; - case "dblclick": - return true; - case "drag": - return true; - case "dragend": - return true; - case "dragenter": - return false; - case "dragexit": - return false; - case "dragleave": - return true; - case "dragover": - return true; - case "dragstart": - return true; - case "drop": - return true; - case "mousedown": - return true; - case "mouseenter": - return false; - case "mouseleave": - return false; - case "mousemove": - return true; - case "mouseout": - return true; - case "scroll": - return false; - case "mouseover": - return true; - case "mouseup": - return true; - case "pointerdown": - return true; - case "pointermove": - return true; - case "pointerup": - return true; - case "pointercancel": - return true; - case "gotpointercapture": - return true; - case "lostpointercapture": - return true; - case "pointerenter": - return false; - case "pointerleave": - return false; - case "pointerover": - return true; - case "pointerout": - return true; - case "select": - return true; - case "touchcancel": - return true; - case "touchend": - return true; - case "touchmove": - return true; - case "touchstart": - return true; - case "wheel": - return true; - case "abort": - return false; - case "canplay": - return false; - case "canplaythrough": - return false; - case "durationchange": - return false; - case "emptied": - return false; - case "encrypted": - return true; - case "ended": - return false; - case "error": - return false; - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "load": - return false; - case "pause": - return false; - case "play": - return false; - case "playing": - return false; - case "progress": - return false; - case "ratechange": - return false; - case "seeked": - return false; - case "seeking": - return false; - case "stalled": - return false; - case "suspend": - return false; - case "timeupdate": - return false; - case "volumechange": - return false; - case "waiting": - return false; - case "animationstart": - return true; - case "animationend": - return true; - case "animationiteration": - return true; - case "transitionend": - return true; - case "toggle": - return true; - case "mounted": - return false; - } - - return true; -}