瀏覽代碼

Merge branch 'upstream' into server-fn

Evan Almloff 2 年之前
父節點
當前提交
579da12ab6
共有 48 個文件被更改,包括 1189 次插入212 次删除
  1. 1 0
      Cargo.toml
  2. 1 2
      docs/guide/examples/event_prevent_default.rs
  3. 2 2
      docs/guide/src/en/interactivity/event_handlers.md
  4. 17 0
      examples/PWA-example/Cargo.toml
  5. 42 0
      examples/PWA-example/Dioxus.toml
  6. 21 0
      examples/PWA-example/LICENSE
  7. 44 0
      examples/PWA-example/README.md
  8. 30 0
      examples/PWA-example/index.html
  9. 二進制
      examples/PWA-example/public/favicon.ico
  10. 二進制
      examples/PWA-example/public/logo_192.png
  11. 二進制
      examples/PWA-example/public/logo_512.png
  12. 34 0
      examples/PWA-example/public/manifest.json
  13. 198 0
      examples/PWA-example/public/sw.js
  14. 20 0
      examples/PWA-example/src/main.rs
  15. 二進制
      examples/assets/logo.png
  16. 3 2
      examples/custom_assets.rs
  17. 37 0
      examples/file_upload.rs
  18. 16 3
      examples/generic_component.rs
  19. 1 1
      examples/svg.rs
  20. 6 7
      packages/autofmt/src/element.rs
  21. 58 36
      packages/core/src/virtual_dom.rs
  22. 4 2
      packages/desktop/Cargo.toml
  23. 二進制
      packages/desktop/src/assets/default_icon.bin
  24. 二進制
      packages/desktop/src/assets/default_icon.png
  25. 77 0
      packages/desktop/src/file_upload.rs
  26. 31 2
      packages/desktop/src/lib.rs
  27. 27 1
      packages/desktop/src/protocol.rs
  28. 11 0
      packages/desktop/src/webview.rs
  29. 3 0
      packages/html/Cargo.toml
  30. 52 3
      packages/html/src/events/form.rs
  31. 2 0
      packages/html/src/lib.rs
  32. 3 0
      packages/html/src/native_bind/mod.rs
  33. 43 0
      packages/html/src/native_bind/native_file_engine.rs
  34. 2 3
      packages/interpreter/Cargo.toml
  35. 112 100
      packages/interpreter/src/interpreter.js
  36. 1 0
      packages/interpreter/src/sledgehammer_bindings.rs
  37. 2 1
      packages/liveview/Cargo.toml
  38. 47 2
      packages/liveview/src/lib.rs
  39. 0 1
      packages/native-core/Cargo.toml
  40. 2 1
      packages/rink/src/lib.rs
  41. 3 3
      packages/router/src/components/route.rs
  42. 3 0
      packages/rsx-rosetta/Cargo.toml
  43. 68 0
      packages/rsx-rosetta/tests/simple.rs
  44. 4 0
      packages/ssr/src/renderer.rs
  45. 4 0
      packages/web/Cargo.toml
  46. 52 39
      packages/web/src/dom.rs
  47. 103 0
      packages/web/src/file_engine.rs
  48. 2 1
      packages/web/src/lib.rs

+ 1 - 0
Cargo.toml

@@ -30,6 +30,7 @@ members = [
     "packages/fullstack/examples/salvo-hello-world",
     "packages/fullstack/examples/warp-hello-world",
     "docs/guide",
+    "examples/PWA-example",
 ]
 
 # This is a "virtual package"

+ 1 - 2
docs/guide/examples/event_prevent_default.rs

@@ -10,8 +10,7 @@ fn App(cx: Scope) -> Element {
     // ANCHOR: prevent_default
 cx.render(rsx! {
     input {
-        prevent_default: "oninput",
-        prevent_default: "onclick",
+        prevent_default: "oninput onclick",
     }
 })
     // ANCHOR_END: prevent_default

+ 2 - 2
docs/guide/src/en/interactivity/event_handlers.md

@@ -31,7 +31,7 @@ Some events will trigger first on the element the event originated at upward. Fo
 
 > For more information about event propigation see [the mdn docs on event bubling](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling)
 
-If you want to prevent this behavior, you can call `stop_propogation()` on the event:
+If you want to prevent this behavior, you can call `stop_propagation()` on the event:
 
 ```rust
 {{#include ../../../examples/event_nested.rs:rsx}}
@@ -41,7 +41,7 @@ If you want to prevent this behavior, you can call `stop_propogation()` on the e
 
 Some events have a default behavior. For keyboard events, this might be entering the typed character. For mouse events, this might be selecting some text.
 
-In some instances, might want to avoid this default behavior. For this, you can add the `prevent_default` attribute with the name of the handler whose default behavior you want to stop. This attribute is special: you can attach it multiple times for multiple attributes:
+In some instances, might want to avoid this default behavior. For this, you can add the `prevent_default` attribute with the name of the handler whose default behavior you want to stop. This attribute can be used for multiple handlers using their name separated by spaces:
 
 ```rust
 {{#include ../../../examples/event_prevent_default.rs:prevent_default}}

+ 17 - 0
examples/PWA-example/Cargo.toml

@@ -0,0 +1,17 @@
+[package]
+name = "dioxus-pwa-example"
+version = "0.1.0"
+authors = ["Antonio Curavalea <one.kyonblack@gmail.com>"]
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+dioxus = { path = "../../packages/dioxus", version = "^0.3.0"}
+dioxus-web = { path = "../../packages/web", version = "^0.3.0"}
+
+log = "0.4.6"
+
+# WebAssembly Debug
+wasm-logger = "0.2.0"
+console_error_panic_hook = "0.1.7"

+ 42 - 0
examples/PWA-example/Dioxus.toml

@@ -0,0 +1,42 @@
+[application]
+
+# App (Project) Name
+name = "dioxus-pwa-example"
+
+# Dioxus App Default Platform
+# desktop, web, mobile, ssr
+default_platform = "web"
+
+# `build` & `serve` dist path
+out_dir = "dist"
+
+# resource (public) file folder
+asset_dir = "public"
+
+[web.app]
+
+# HTML title tag content
+title = "dioxus | ⛺"
+
+[web.watcher]
+
+# when watcher trigger, regenerate the `index.html`
+reload_html = true
+
+# which files or dirs will be watcher monitoring
+watch_path = ["src", "public"]
+
+# include `assets` in web platform
+[web.resource]
+
+# CSS style file
+style = []
+
+# Javascript code file
+script = []
+
+[web.resource.dev]
+
+# Javascript code file
+# serve: [dev-server] only
+script = []

+ 21 - 0
examples/PWA-example/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Dioxus
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 44 - 0
examples/PWA-example/README.md

@@ -0,0 +1,44 @@
+# Dioxus PWA example
+
+This is a basic example of a progressive web app (PWA) using Dioxus and Dioxus CLI.
+Currently PWA functionality requires the use of a service worker and manifest file, so this isn't 100% Rust yet.
+
+It is also very much usable as a template for your projects, if you're aiming to create a PWA.
+
+## Try the example
+
+Make sure you have Dioxus CLI installed (if you're unsure, run `cargo install dioxus-cli`).
+
+You can run `dioxus serve` in this directory to start the web server locally, or run
+`dioxus build --release` to build the project so you can deploy it on a separate web-server.
+
+## Project Structure
+```
+├── Cargo.toml
+├── Dioxus.toml
+├── index.html // Custom HTML is needed for this, to load the SW and manifest.
+├── LICENSE
+├── public
+│   ├── favicon.ico
+│   ├── logo_192.png
+│   ├── logo_512.png
+│   ├── manifest.json // The manifest file - edit this as you need to.
+│   └── sw.js // The service worker - you must edit this for actual projects.
+├── README.md
+└── src
+    └── main.rs
+```
+
+## Resources
+
+If you're just getting started with PWAs, here are some useful resources:
+
+* [PWABuilder docs](https://docs.pwabuilder.com/#/)
+* [MDN article on PWAs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
+
+For service worker scripting (in JavaScript):
+
+* [Service worker guide from PWABuilder](https://docs.pwabuilder.com/#/home/sw-intro)
+* [Service worker examples, also from PWABuilder](https://github.com/pwa-builder/pwabuilder-serviceworkers)
+
+If you want to stay as close to 100% Rust as possible, you can try using [wasi-worker](https://github.com/dunnock/wasi-worker) to replace the JS service worker file. The JSON manifest will still be required though.

+ 30 - 0
examples/PWA-example/index.html

@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>{app_title}</title>
+  <script>
+    if ('serviceWorker' in navigator) {
+      navigator.serviceWorker.register(
+        '/sw.js'
+      );
+    }
+  </script>
+  <link rel="manifest" href="manifest.json">
+  <meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <meta charset="UTF-8" />
+  {style_include}
+</head>
+<body>
+  <div id="main"></div>
+  <script type="module">
+    import init from "/{base_path}/assets/dioxus/{app_name}.js";
+    init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
+      if (wasm.__wbindgen_start == undefined) {
+        wasm.main();
+      }
+    });
+  </script>
+  {script_include}
+</body>
+</html>

二進制
examples/PWA-example/public/favicon.ico


二進制
examples/PWA-example/public/logo_192.png


二進制
examples/PWA-example/public/logo_512.png


+ 34 - 0
examples/PWA-example/public/manifest.json

@@ -0,0 +1,34 @@
+{
+  "name": "Dioxus",
+  "icons": [
+    {
+      "src": "logo_192.png",
+      "type": "image/png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "logo_512.png",
+      "type": "image/png",
+      "sizes": "512x512",
+      "purpose": "any"
+    },
+    {
+      "src": "logo_512.png",
+      "type": "image/png",
+      "sizes": "any",
+      "purpose": "any"
+    }
+  ],
+  "start_url": "/",
+  "id": "/",
+  "display": "standalone",
+  "display_override": ["window-control-overlay", "standalone"],
+  "scope": "/",
+  "theme_color": "#000000",
+  "background_color": "#ffffff",
+  "short_name": "Dioxus",
+  "description": "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust.",
+  "dir": "ltr",
+  "lang": "en",
+  "orientation": "portrait"
+}

+ 198 - 0
examples/PWA-example/public/sw.js

@@ -0,0 +1,198 @@
+"use strict";
+
+//console.log('WORKER: executing.');
+
+/* A version number is useful when updating the worker logic,
+   allowing you to remove outdated cache entries during the update.
+*/
+var version = 'v1.0.0::';
+
+/* These resources will be downloaded and cached by the service worker
+   during the installation process. If any resource fails to be downloaded,
+   then the service worker won't be installed either.
+*/
+var offlineFundamentals = [
+  // add here the files you want to cache
+  'favicon.ico'
+];
+
+/* The install event fires when the service worker is first installed.
+   You can use this event to prepare the service worker to be able to serve
+   files while visitors are offline.
+*/
+self.addEventListener("install", function (event) {
+  //console.log('WORKER: install event in progress.');
+  /* Using event.waitUntil(p) blocks the installation process on the provided
+     promise. If the promise is rejected, the service worker won't be installed.
+  */
+  event.waitUntil(
+    /* The caches built-in is a promise-based API that helps you cache responses,
+       as well as finding and deleting them.
+    */
+    caches
+      /* You can open a cache by name, and this method returns a promise. We use
+         a versioned cache name here so that we can remove old cache entries in
+         one fell swoop later, when phasing out an older service worker.
+      */
+      .open(version + 'fundamentals')
+      .then(function (cache) {
+        /* After the cache is opened, we can fill it with the offline fundamentals.
+           The method below will add all resources in `offlineFundamentals` to the
+           cache, after making requests for them.
+        */
+        return cache.addAll(offlineFundamentals);
+      })
+      .then(function () {
+        //console.log('WORKER: install completed');
+      })
+  );
+});
+
+/* The fetch event fires whenever a page controlled by this service worker requests
+   a resource. This isn't limited to `fetch` or even XMLHttpRequest. Instead, it
+   comprehends even the request for the HTML page on first load, as well as JS and
+   CSS resources, fonts, any images, etc.
+*/
+self.addEventListener("fetch", function (event) {
+  //console.log('WORKER: fetch event in progress.');
+
+  /* We should only cache GET requests, and deal with the rest of method in the
+     client-side, by handling failed POST,PUT,PATCH,etc. requests.
+  */
+  if (event.request.method !== 'GET') {
+    /* If we don't block the event as shown below, then the request will go to
+       the network as usual.
+    */
+    //console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
+    return;
+  }
+  /* Similar to event.waitUntil in that it blocks the fetch event on a promise.
+     Fulfillment result will be used as the response, and rejection will end in a
+     HTTP response indicating failure.
+  */
+  event.respondWith(
+    caches
+      /* This method returns a promise that resolves to a cache entry matching
+         the request. Once the promise is settled, we can then provide a response
+         to the fetch request.
+      */
+      .match(event.request)
+      .then(function (cached) {
+        /* Even if the response is in our cache, we go to the network as well.
+           This pattern is known for producing "eventually fresh" responses,
+           where we return cached responses immediately, and meanwhile pull
+           a network response and store that in the cache.
+
+           Read more:
+           https://ponyfoo.com/articles/progressive-networking-serviceworker
+        */
+        var networked = fetch(event.request)
+          // We handle the network request with success and failure scenarios.
+          .then(fetchedFromNetwork, unableToResolve)
+          // We should catch errors on the fetchedFromNetwork handler as well.
+          .catch(unableToResolve);
+
+        /* We return the cached response immediately if there is one, and fall
+           back to waiting on the network as usual.
+        */
+        //console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
+        return cached || networked;
+
+        function fetchedFromNetwork(response) {
+          /* We copy the response before replying to the network request.
+             This is the response that will be stored on the ServiceWorker cache.
+          */
+          var cacheCopy = response.clone();
+
+          //console.log('WORKER: fetch response from network.', event.request.url);
+
+          caches
+            // We open a cache to store the response for this request.
+            .open(version + 'pages')
+            .then(function add(cache) {
+              /* We store the response for this request. It'll later become
+                 available to caches.match(event.request) calls, when looking
+                 for cached responses.
+              */
+              cache.put(event.request, cacheCopy);
+            })
+            .then(function () {
+              //console.log('WORKER: fetch response stored in cache.', event.request.url);
+            });
+
+          // Return the response so that the promise is settled in fulfillment.
+          return response;
+        }
+
+        /* When this method is called, it means we were unable to produce a response
+           from either the cache or the network. This is our opportunity to produce
+           a meaningful response even when all else fails. It's the last chance, so
+           you probably want to display a "Service Unavailable" view or a generic
+           error response.
+        */
+        function unableToResolve() {
+          /* There's a couple of things we can do here.
+             - Test the Accept header and then return one of the `offlineFundamentals`
+               e.g: `return caches.match('/some/cached/image.png')`
+             - You should also consider the origin. It's easier to decide what
+               "unavailable" means for requests against your origins than for requests
+               against a third party, such as an ad provider.
+             - Generate a Response programmaticaly, as shown below, and return that.
+          */
+
+          //console.log('WORKER: fetch request failed in both cache and network.');
+
+          /* Here we're creating a response programmatically. The first parameter is the
+             response body, and the second one defines the options for the response.
+          */
+          return new Response('<h1>Service Unavailable</h1>', {
+            status: 503,
+            statusText: 'Service Unavailable',
+            headers: new Headers({
+              'Content-Type': 'text/html'
+            })
+          });
+        }
+      })
+  );
+});
+
+/* The activate event fires after a service worker has been successfully installed.
+   It is most useful when phasing out an older version of a service worker, as at
+   this point you know that the new worker was installed correctly. In this example,
+   we delete old caches that don't match the version in the worker we just finished
+   installing.
+*/
+self.addEventListener("activate", function (event) {
+  /* Just like with the install event, event.waitUntil blocks activate on a promise.
+     Activation will fail unless the promise is fulfilled.
+  */
+  //console.log('WORKER: activate event in progress.');
+
+  event.waitUntil(
+    caches
+      /* This method returns a promise which will resolve to an array of available
+         cache keys.
+      */
+      .keys()
+      .then(function (keys) {
+        // We return a promise that settles when all outdated caches are deleted.
+        return Promise.all(
+          keys
+            .filter(function (key) {
+              // Filter by keys that don't start with the latest version prefix.
+              return !key.startsWith(version);
+            })
+            .map(function (key) {
+              /* Return a promise that's fulfilled
+                 when each outdated cache is deleted.
+              */
+              return caches.delete(key);
+            })
+        );
+      })
+      .then(function () {
+        //console.log('WORKER: activate completed.');
+      })
+  );
+});

+ 20 - 0
examples/PWA-example/src/main.rs

@@ -0,0 +1,20 @@
+use dioxus::prelude::*;
+
+fn main() {
+    // init debug tool for WebAssembly
+    wasm_logger::init(wasm_logger::Config::default());
+    console_error_panic_hook::set_once();
+
+    dioxus_web::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! (
+        div {
+            style: "text-align: center;",
+            h1 { "🌗 Dioxus 🚀" }
+            h3 { "Frontend that scales." }
+            p { "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust." }
+        }
+    ))
+}

二進制
examples/assets/logo.png


+ 3 - 2
examples/custom_assets.rs

@@ -7,9 +7,10 @@ fn main() {
 fn app(cx: Scope) -> Element {
     cx.render(rsx! {
         div {
-            "This should show an image:"
+            p {
+                "This should show an image:"
+            }
             img { src: "examples/assets/logo.png" }
-            img { src: "/Users/jonkelley/Desktop/blitz.png" }
         }
     })
 }

+ 37 - 0
examples/file_upload.rs

@@ -0,0 +1,37 @@
+#![allow(non_snake_case)]
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus_desktop::launch(App);
+}
+
+fn App(cx: Scope) -> Element {
+    let files_uploaded: &UseRef<Vec<String>> = use_ref(cx, Vec::new);
+
+    cx.render(rsx! {
+        input {
+            r#type: "file",
+            accept: ".txt, .rs",
+            multiple: true,
+            onchange: |evt| {
+                to_owned![files_uploaded];
+                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);
+                            }
+                        }
+                    }
+                }
+            },
+        }
+
+        ul {
+            for file in files_uploaded.read().iter() {
+                li { "{file}" }
+            }
+        }
+    })
+}

+ 16 - 3
examples/generic_component.rs

@@ -1,3 +1,5 @@
+use std::fmt::Display;
+
 use dioxus::prelude::*;
 
 fn main() {
@@ -5,9 +7,20 @@ fn main() {
 }
 
 fn app(cx: Scope) -> Element {
-    cx.render(rsx! { generic_child::<i32>{} })
+    cx.render(rsx! { generic_child {
+        data: 0i32
+    } })
+}
+
+#[derive(PartialEq, Props)]
+struct GenericChildProps<T: Display + PartialEq> {
+    data: T,
 }
 
-fn generic_child<T>(cx: Scope) -> Element {
-    cx.render(rsx! { div {} })
+fn generic_child<T: Display + PartialEq>(cx: Scope<GenericChildProps<T>>) -> Element {
+    let data = &cx.props.data;
+
+    cx.render(rsx! { div {
+        "{data}"
+    } })
 }

+ 1 - 1
examples/svg.rs

@@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element {
                     onclick: move |_| {
                         use rand::Rng;
                         let mut rng = rand::thread_rng();
-                        val.set(rng.gen_range(1..6));
+                        val.set(rng.gen_range(1..=6));
                     }
                 }
             }

+ 6 - 7
packages/autofmt/src/element.rs

@@ -276,13 +276,12 @@ impl Writer<'_> {
         let start = location.start();
         let line_start = start.line - 1;
 
-        let this_line = self.src[line_start];
-
-        let beginning = if this_line.len() > start.column {
-            this_line[..start.column].trim()
-        } else {
-            ""
-        };
+        let beginning = self
+            .src
+            .get(line_start)
+            .filter(|this_line| this_line.len() > start.column)
+            .map(|this_line| this_line[..start.column].trim())
+            .unwrap_or_default();
 
         beginning.is_empty()
     }

+ 58 - 36
packages/core/src/virtual_dom.rs

@@ -384,51 +384,73 @@ impl VirtualDom {
             data,
         };
 
-        // Loop through each dynamic attribute in this template before moving up to the template's parent.
-        while let Some(el_ref) = parent_path {
-            // safety: we maintain references of all vnodes in the element slab
-            let template = unsafe { el_ref.template.unwrap().as_ref() };
-            let node_template = template.template.get();
-            let target_path = el_ref.path;
-
-            for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
-                let this_path = node_template.attr_paths[idx];
-
-                // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
-                if attr.name.trim_start_matches("on") == name
-                    && target_path.is_decendant(&this_path)
-                {
-                    listeners.push(&attr.value);
-
-                    // Break if the event doesn't bubble anyways
-                    if !bubbles {
-                        break;
+        // If the event bubbles, we traverse through the tree until we find the target element.
+        if bubbles {
+            // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
+            while let Some(el_ref) = parent_path {
+                // safety: we maintain references of all vnodes in the element slab
+                let template = unsafe { el_ref.template.unwrap().as_ref() };
+                let node_template = template.template.get();
+                let target_path = el_ref.path;
+
+                for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
+                    let this_path = node_template.attr_paths[idx];
+
+                    // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
+                    if attr.name.trim_start_matches("on") == name
+                        && target_path.is_decendant(&this_path)
+                    {
+                        listeners.push(&attr.value);
+
+                        // Break if this is the exact target element.
+                        // This means we won't call two listeners with the same name on the same element. This should be
+                        // documented, or be rejected from the rsx! macro outright
+                        if target_path == this_path {
+                            break;
+                        }
                     }
+                }
 
-                    // Break if this is the exact target element.
-                    // This means we won't call two listeners with the same name on the same element. This should be
-                    // documented, or be rejected from the rsx! macro outright
-                    if target_path == this_path {
-                        break;
+                // Now that we've accumulated all the parent attributes for the target element, call them in reverse order
+                // We check the bubble state between each call to see if the event has been stopped from bubbling
+                for listener in listeners.drain(..).rev() {
+                    if let AttributeValue::Listener(listener) = listener {
+                        if let Some(cb) = listener.borrow_mut().as_deref_mut() {
+                            cb(uievent.clone());
+                        }
+
+                        if !uievent.propagates.get() {
+                            return;
+                        }
                     }
                 }
-            }
 
-            // Now that we've accumulated all the parent attributes for the target element, call them in reverse order
-            // We check the bubble state between each call to see if the event has been stopped from bubbling
-            for listener in listeners.drain(..).rev() {
-                if let AttributeValue::Listener(listener) = listener {
-                    if let Some(cb) = listener.borrow_mut().as_deref_mut() {
-                        cb(uievent.clone());
-                    }
+                parent_path = template.parent.and_then(|id| self.elements.get(id.0));
+            }
+        } else {
+            // Otherwise, we just call the listener on the target element
+            if let Some(el_ref) = parent_path {
+                // safety: we maintain references of all vnodes in the element slab
+                let template = unsafe { el_ref.template.unwrap().as_ref() };
+                let node_template = template.template.get();
+                let target_path = el_ref.path;
+
+                for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
+                    let this_path = node_template.attr_paths[idx];
+
+                    // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
+                    // Only call the listener if this is the exact target element.
+                    if attr.name.trim_start_matches("on") == name && target_path == this_path {
+                        if let AttributeValue::Listener(listener) = &attr.value {
+                            if let Some(cb) = listener.borrow_mut().as_deref_mut() {
+                                cb(uievent.clone());
+                            }
 
-                    if !uievent.propagates.get() {
-                        return;
+                            break;
+                        }
                     }
                 }
             }
-
-            parent_path = template.parent.and_then(|id| self.elements.get(id.0));
         }
     }
 

+ 4 - 2
packages/desktop/Cargo.toml

@@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react"]
 
 [dependencies]
 dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
-dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
+dioxus-html = { path = "../html", features = ["serialize", "native-bind"], version = "^0.3.0" }
 dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
 dioxus-hot-reload = { path = "../hot-reload", optional = true }
 
@@ -23,12 +23,13 @@ thiserror = "1.0.30"
 log = "0.4.14"
 wry = { version = "0.27.2" }
 futures-channel = "0.3.21"
-tokio = { version = "1.16.1", features = [
+tokio = { version = "1.27", features = [
     "sync",
     "rt-multi-thread",
     "rt",
     "time",
     "macros",
+    "fs",
 ], optional = true, default-features = false }
 webbrowser = "0.8.0"
 infer = "0.11.0"
@@ -36,6 +37,7 @@ dunce = "1.0.2"
 slab = "0.4"
 
 futures-util = "0.3.25"
+rfd = "0.11.3"
 
 [target.'cfg(target_os = "ios")'.dependencies]
 objc = "0.2.7"

二進制
packages/desktop/src/assets/default_icon.bin


二進制
packages/desktop/src/assets/default_icon.png


+ 77 - 0
packages/desktop/src/file_upload.rs

@@ -0,0 +1,77 @@
+use std::{path::PathBuf, str::FromStr};
+
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+pub(crate) struct FileDiologRequest {
+    #[serde(default)]
+    accept: Option<String>,
+    multiple: bool,
+    pub event: String,
+    pub target: usize,
+    pub bubbles: bool,
+}
+
+pub(crate) fn get_file_event(request: &FileDiologRequest) -> Vec<PathBuf> {
+    let mut dialog = rfd::FileDialog::new();
+
+    let filters: Vec<_> = request
+        .accept
+        .as_deref()
+        .unwrap_or_default()
+        .split(',')
+        .filter_map(|s| Filters::from_str(s).ok())
+        .collect();
+
+    let file_extensions: Vec<_> = filters
+        .iter()
+        .flat_map(|f| f.as_extensions().into_iter())
+        .collect();
+
+    dialog = dialog.add_filter("name", file_extensions.as_slice());
+
+    let files: Vec<_> = if request.multiple {
+        dialog.pick_files().into_iter().flatten().collect()
+    } else {
+        dialog.pick_file().into_iter().collect()
+    };
+
+    files
+}
+
+enum Filters {
+    Extension(String),
+    Mime(String),
+    Audio,
+    Video,
+    Image,
+}
+
+impl Filters {
+    fn as_extensions(&self) -> Vec<&str> {
+        match self {
+            Filters::Extension(extension) => vec![extension.as_str()],
+            Filters::Mime(_) => vec![],
+            Filters::Audio => vec!["mp3", "wav", "ogg"],
+            Filters::Video => vec!["mp4", "webm"],
+            Filters::Image => vec!["png", "jpg", "jpeg", "gif", "webp"],
+        }
+    }
+}
+
+impl FromStr for Filters {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(extension) = s.strip_prefix('.') {
+            Ok(Filters::Extension(extension.to_string()))
+        } else {
+            match s {
+                "audio/*" => Ok(Filters::Audio),
+                "video/*" => Ok(Filters::Video),
+                "image/*" => Ok(Filters::Image),
+                _ => Ok(Filters::Mime(s.to_string())),
+            }
+        }
+    }
+}

+ 31 - 2
packages/desktop/src/lib.rs

@@ -8,6 +8,7 @@ mod desktop_context;
 mod escape;
 mod eval;
 mod events;
+mod file_upload;
 mod protocol;
 mod shortcut;
 mod waker;
@@ -19,14 +20,14 @@ pub use desktop_context::{
 };
 use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
 use dioxus_core::*;
-use dioxus_html::HtmlEvent;
+use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
 pub use eval::{use_eval, EvalResult};
 use futures_util::{pin_mut, FutureExt};
 use shortcut::ShortcutRegistry;
 pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
-use std::collections::HashMap;
 use std::rc::Rc;
 use std::task::Waker;
+use std::{collections::HashMap, sync::Arc};
 pub use tao::dpi::{LogicalSize, PhysicalSize};
 use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
 pub use tao::window::WindowBuilder;
@@ -264,6 +265,34 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
                     }
                 }
 
+                EventData::Ipc(msg) if msg.method() == "file_diolog" => {
+                    if let Ok(file_diolog) =
+                        serde_json::from_value::<file_upload::FileDiologRequest>(msg.params())
+                    {
+                        let id = ElementId(file_diolog.target);
+                        let event_name = &file_diolog.event;
+                        let event_bubbles = file_diolog.bubbles;
+                        let files = file_upload::get_file_event(&file_diolog);
+                        let data = Rc::new(FormData {
+                            value: Default::default(),
+                            values: Default::default(),
+                            files: Some(Arc::new(NativeFileEngine::new(files))),
+                        });
+
+                        let view = webviews.get_mut(&event.1).unwrap();
+
+                        if event_name == "change&input" {
+                            view.dom
+                                .handle_event("input", data.clone(), id, event_bubbles);
+                            view.dom.handle_event("change", data, id, event_bubbles);
+                        } else {
+                            view.dom.handle_event(event_name, data, id, event_bubbles);
+                        }
+
+                        send_edits(view.dom.render_immediate(), &view.webview);
+                    }
+                }
+
                 _ => {}
             },
             Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),

+ 27 - 1
packages/desktop/src/protocol.rs

@@ -9,10 +9,36 @@ use wry::{
 };
 
 fn module_loader(root_name: &str) -> String {
+    let js = INTERPRETER_JS.replace(
+        "/*POST_HANDLE_EDITS*/",
+        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 = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), 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();
+          });
+        }
+      }
+    }"#,
+    );
     format!(
         r#"
 <script>
-    {INTERPRETER_JS}
+    {js}
 
     let rootname = "{root_name}";
     let root = window.document.getElementById(rootname);

+ 11 - 0
packages/desktop/src/webview.rs

@@ -57,6 +57,17 @@ pub fn build(
         })
         .with_web_context(&mut web_context);
 
+    #[cfg(windows)]
+    {
+        // Windows has a platform specific settings to disable the browser shortcut keys
+        use wry::webview::WebViewBuilderExtWindows;
+        webview = webview.with_browser_accelerator_keys(false);
+    }
+
+    // These are commented out because wry is currently broken in wry
+    // let mut web_context = WebContext::new(cfg.data_dir.clone());
+    // .with_web_context(&mut web_context);
+
     for (name, handler) in cfg.protocols.drain(..) {
         webview = webview.with_custom_protocol(name, handler)
     }

+ 3 - 0
packages/html/Cargo.toml

@@ -21,6 +21,8 @@ enumset = "1.0.11"
 keyboard-types = "0.6.2"
 async-trait = "0.1.58"
 serde-value = "0.7.0"
+tokio = { version = "1.27", features = ["fs", "io-util"], optional = true }
+rfd = { version = "0.11.3", optional = true }
 
 [dependencies.web-sys]
 optional = true
@@ -48,4 +50,5 @@ serde_json = "1"
 default = ["serialize"]
 serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"]
 wasm-bind = ["web-sys", "wasm-bindgen"]
+native-bind = ["tokio", "rfd"]
 hot-reload-context = ["dioxus-rsx"]

+ 52 - 3
packages/html/src/events/form.rs

@@ -10,12 +10,61 @@ pub type FormEvent = Event<FormData>;
 pub struct FormData {
     pub value: String,
 
-    pub values: HashMap<String, String>,
-
-    #[cfg_attr(feature = "serialize", serde(skip))]
+    pub values: HashMap<String, Vec<String>>,
+
+    #[cfg_attr(
+        feature = "serialize",
+        serde(
+            default,
+            skip_serializing,
+            deserialize_with = "deserialize_file_engine"
+        )
+    )]
     pub files: Option<std::sync::Arc<dyn FileEngine>>,
 }
 
+#[cfg(feature = "serialize")]
+#[derive(serde::Serialize, serde::Deserialize)]
+struct SerializedFileEngine {
+    files: HashMap<String, Vec<u8>>,
+}
+
+#[cfg(feature = "serialize")]
+#[async_trait::async_trait(?Send)]
+impl FileEngine for SerializedFileEngine {
+    fn files(&self) -> Vec<String> {
+        self.files.keys().cloned().collect()
+    }
+
+    async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
+        self.files.get(file).cloned()
+    }
+
+    async fn read_file_to_string(&self, file: &str) -> Option<String> {
+        self.read_file(file)
+            .await
+            .map(|bytes| String::from_utf8_lossy(&bytes).to_string())
+    }
+}
+
+#[cfg(feature = "serialize")]
+fn deserialize_file_engine<'de, D>(
+    deserializer: D,
+) -> Result<Option<std::sync::Arc<dyn FileEngine>>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    use serde::Deserialize;
+
+    let Ok(file_engine) =
+        SerializedFileEngine::deserialize(deserializer) else{
+            return Ok(None);
+        };
+
+    let file_engine = std::sync::Arc::new(file_engine);
+    Ok(Some(file_engine))
+}
+
 impl PartialEq for FormData {
     fn eq(&self, other: &Self) -> bool {
         self.value == other.value && self.values == other.values

+ 2 - 0
packages/html/src/lib.rs

@@ -20,6 +20,8 @@ pub mod events;
 pub mod geometry;
 mod global_attributes;
 pub mod input_data;
+#[cfg(feature = "native-bind")]
+pub mod native_bind;
 mod render_template;
 #[cfg(feature = "wasm-bind")]
 mod web_sys_bind;

+ 3 - 0
packages/html/src/native_bind/mod.rs

@@ -0,0 +1,3 @@
+mod native_file_engine;
+
+pub use native_file_engine::*;

+ 43 - 0
packages/html/src/native_bind/native_file_engine.rs

@@ -0,0 +1,43 @@
+use std::path::PathBuf;
+
+use crate::FileEngine;
+use tokio::fs::File;
+use tokio::io::AsyncReadExt;
+
+pub struct NativeFileEngine {
+    files: Vec<PathBuf>,
+}
+
+impl NativeFileEngine {
+    pub fn new(files: Vec<PathBuf>) -> Self {
+        Self { files }
+    }
+}
+
+#[async_trait::async_trait(?Send)]
+impl FileEngine for NativeFileEngine {
+    fn files(&self) -> Vec<String> {
+        self.files
+            .iter()
+            .filter_map(|f| Some(f.to_str()?.to_string()))
+            .collect()
+    }
+
+    async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
+        let mut file = File::open(file).await.ok()?;
+
+        let mut contents = Vec::new();
+        file.read_to_end(&mut contents).await.ok()?;
+
+        Some(contents)
+    }
+
+    async fn read_file_to_string(&self, file: &str) -> Option<String> {
+        let mut file = File::open(file).await.ok()?;
+
+        let mut contents = String::new();
+        file.read_to_string(&mut contents).await.ok()?;
+
+        Some(contents)
+    }
+}

+ 2 - 3
packages/interpreter/Cargo.toml

@@ -10,15 +10,14 @@ homepage = "https://dioxuslabs.com"
 documentation = "https://docs.rs/dioxus"
 keywords = ["dom", "ui", "gui", "react", "wasm"]
 
-
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
 wasm-bindgen = { version = "0.2.79", optional = true }
 js-sys = { version = "0.3.56", optional = true }
 web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
-sledgehammer_bindgen = { version = "0.1.3", optional = true }
-sledgehammer_utils = { version = "0.1.0", optional = true }
+sledgehammer_bindgen = { version = "0.2.1", optional = true }
+sledgehammer_utils = { version = "0.1.1", optional = true }
 
 [features]
 default = []

+ 112 - 100
packages/interpreter/src/interpreter.js

@@ -17,8 +17,7 @@ class ListenerMap {
       } else {
         this.global[event_name].active++;
       }
-    }
-    else {
+    } else {
       const id = element.getAttribute("data-dioxus-id");
       if (!this.local[id]) {
         this.local[id] = {};
@@ -32,11 +31,13 @@ class ListenerMap {
     if (bubbles) {
       this.global[event_name].active--;
       if (this.global[event_name].active === 0) {
-        this.root.removeEventListener(event_name, this.global[event_name].callback);
+        this.root.removeEventListener(
+          event_name,
+          this.global[event_name].callback
+        );
         delete this.global[event_name];
       }
-    }
-    else {
+    } else {
       const id = element.getAttribute("data-dioxus-id");
       delete this.local[id][event_name];
       if (this.local[id].length === 0) {
@@ -143,8 +144,7 @@ class Interpreter {
   SetAttribute(id, field, value, ns) {
     if (value === null) {
       this.RemoveAttribute(id, field, ns);
-    }
-    else {
+    } else {
       const node = this.nodes[id];
       this.SetAttributeInner(node, field, value, ns);
     }
@@ -167,10 +167,10 @@ class Interpreter {
           }
           break;
         case "checked":
-          node.checked = value === "true";
+          node.checked = value === "true" || value === true;
           break;
         case "selected":
-          node.selected = value === "true";
+          node.selected = value === "true" || value === true;
           break;
         case "dangerous_inner_html":
           node.innerHTML = value;
@@ -212,6 +212,8 @@ class Interpreter {
     for (let edit of edits.edits) {
       this.handleEdit(edit);
     }
+
+    /*POST_HANDLE_EDITS*/
   }
 
   SaveTemplate(template) {
@@ -342,105 +344,115 @@ class Interpreter {
         this.RemoveEventListener(edit.id, edit.name);
         break;
       case "NewEventListener":
-
         let bubbles = event_bubbles(edit.name);
 
-        // this handler is only provided on desktop implementations since this
-        // method is not used by the web implementation
-        let handler = (event) => {
-          let target = event.target;
-          if (target != null) {
-            let realId = target.getAttribute(`data-dioxus-id`);
-            let shouldPreventDefault = target.getAttribute(
-              `dioxus-prevent-default`
-            );
-
-            if (event.type === "click") {
-              // todo call prevent default if it's the right type of event
-              let a_element = target.closest("a");
-              if (a_element != null) {
-                event.preventDefault();
-                if (shouldPreventDefault !== `onclick` && a_element.getAttribute(`dioxus-prevent-default`) !== `onclick`) {
-                  const href = a_element.getAttribute("href");
-                  if (href !== "" && href !== null && href !== undefined) {
-                    window.ipc.postMessage(
-                      serializeIpcMessage("browser_open", { href })
-                    );
-                  }
-                }
-              }
-
-              // also prevent buttons from submitting
-              if (target.tagName === "BUTTON" && event.type == "submit") {
-                event.preventDefault();
-              }
-            }
-            // 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;
-              realId = target.getAttribute(`data-dioxus-id`);
-            }
-
-            shouldPreventDefault = target.getAttribute(
-              `dioxus-prevent-default`
-            );
+        this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
+          handler(event, edit.name, bubbles);
+        });
+        break;
+    }
+  }
+}
 
-            let contents = serialize_event(event);
-
-            if (shouldPreventDefault === `on${event.type}`) {
-              event.preventDefault();
-            }
-
-            if (event.type === "submit") {
-              event.preventDefault();
-            }
-
-            if (
-              target.tagName === "FORM" &&
-              (event.type === "submit" || event.type === "input")
-            ) {
-              for (let x = 0; x < target.elements.length; x++) {
-                let element = target.elements[x];
-                let name = element.getAttribute("name");
-                if (name != null) {
-                  if (element.getAttribute("type") === "checkbox") {
-                    // @ts-ignore
-                    contents.values[name] = element.checked ? "true" : "false";
-                  } else if (element.getAttribute("type") === "radio") {
-                    if (element.checked) {
-                      contents.values[name] = element.value;
-                    }
-                  } else {
-                    // @ts-ignore
-                    contents.values[name] =
-                      element.value ?? element.textContent;
-                  }
-                }
-              }
-            }
-
-            if (realId === null) {
-              return;
-            }
+// this handler is only provided on the desktop and liveview implementations since this
+// method is not used by the web implementation
+function handler(event, name, bubbles) {
+  let target = event.target;
+  if (target != null) {
+    let preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
+
+    if (event.type === "click") {
+      // todo call prevent default if it's the right type of event
+      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(
-              serializeIpcMessage("user_event", {
-                name: edit.name,
-                element: parseInt(realId),
-                data: contents,
-                bubbles,
-              })
+              serializeIpcMessage("browser_open", { href })
             );
           }
-        };
-        this.NewEventListener(edit.name, edit.id, bubbles, handler);
-        break;
+        }
+      }
+
+      // 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 = serialize_event(event);
+
+    /*POST_EVENT_SERIALIZATION*/
+
+    if (
+      target.tagName === "FORM" &&
+      (event.type === "submit" || event.type === "input")
+    ) {
+      if (
+        target.tagName === "FORM" &&
+        (event.type === "submit" || event.type === "input")
+      ) {
+        const formData = new FormData(target);
+
+        for (let name of formData.keys()) {
+          let value = formData.getAll(name);
+          contents.values[name] = value;
+        }
+      }
+    }
+
+    if (realId === null) {
+      return;
+    }
+    window.ipc.postMessage(
+      serializeIpcMessage("user_event", {
+        name: name,
+        element: parseInt(realId),
+        data: contents,
+        bubbles,
+      })
+    );
+  }
+}
+
+function find_real_id(target) {
+  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;
+    }
+
+    target = target.parentElement;
+    realId = target.getAttribute(`data-dioxus-id`);
   }
+  return realId;
 }
 
 function get_mouse_data(event) {

+ 1 - 0
packages/interpreter/src/sledgehammer_bindings.rs

@@ -108,6 +108,7 @@ mod js {
     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) {

+ 2 - 1
packages/liveview/Cargo.toml

@@ -18,7 +18,7 @@ futures-util = { version = "0.3.25", default-features = false, features = [
     "sink",
 ] }
 futures-channel = { version = "0.3.25", features = ["sink"] }
-tokio = { version = "1.22.0", features = ["time"] }
+tokio = { version = "1.22.0", features = ["time", "macros"] }
 tokio-stream = { version = "0.1.11", features = ["net"] }
 tokio-util = { version = "0.7.4", features = ["rt"] }
 serde = { version = "1.0.151", features = ["derive"] }
@@ -36,6 +36,7 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] }
 
 # salvo
 salvo = { version = "0.37.7", optional = true, features = ["ws"] }
+once_cell = "1.17.1"
 
 # actix is ... complicated?
 # actix-files = { version = "0.6.2", optional = true }

+ 47 - 2
packages/liveview/src/lib.rs

@@ -34,7 +34,51 @@ pub enum LiveViewError {
     SendingFailed,
 }
 
-use dioxus_interpreter_js::INTERPRETER_JS;
+use once_cell::sync::Lazy;
+
+static INTERPRETER_JS: Lazy<String> = Lazy::new(|| {
+    let interpreter = dioxus_interpreter_js::INTERPRETER_JS;
+    let serialize_file_uploads = r#"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 = serializeIpcMessage("user_event", {
+            name: name,
+            element: parseInt(realId),
+            data: contents,
+            bubbles,
+          });
+          window.ipc.postMessage(message);
+        }
+        read_files();
+        return;
+      }
+    }"#;
+
+    interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads)
+});
+
 static MAIN_JS: &str = include_str!("./main.js");
 
 /// This script that gets injected into your app connects this page to the websocket endpoint
@@ -42,11 +86,12 @@ static MAIN_JS: &str = include_str!("./main.js");
 /// Once the endpoint is connected, it will send the initial state of the app, and then start
 /// processing user events and returning edits to the liveview instance
 pub fn interpreter_glue(url: &str) -> String {
+    let js = &*INTERPRETER_JS;
     format!(
         r#"
 <script>
     var WS_ADDR = "{url}";
-    {INTERPRETER_JS}
+    {js}
     {MAIN_JS}
     main();
 </script>

+ 0 - 1
packages/native-core/Cargo.toml

@@ -29,7 +29,6 @@ lightningcss = "1.0.0-alpha.39"
 
 rayon = "1.6.1"
 shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false }
-shipyard_hierarchy = "0.6.0"
 
 [dev-dependencies]
 rand = "0.8.5"

+ 2 - 1
packages/rink/src/lib.rs

@@ -98,7 +98,8 @@ pub fn render<R: Driver>(
     let event_tx_clone = event_tx.clone();
     if !cfg.headless {
         std::thread::spawn(move || {
-            let tick_rate = Duration::from_millis(1000);
+            // Timeout after 10ms when waiting for events
+            let tick_rate = Duration::from_millis(10);
             loop {
                 if crossterm::event::poll(tick_rate).unwrap() {
                     let evt = crossterm::event::read().unwrap();

+ 3 - 3
packages/router/src/components/route.rs

@@ -45,13 +45,13 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
         router_root.register_total_route(route_context.total_route, cx.scope_id());
     });
 
-    log::debug!("Checking Route: {:?}", cx.props.to);
+    log::trace!("Checking Route: {:?}", cx.props.to);
 
     if router_root.should_render(cx.scope_id()) {
-        log::debug!("Route should render: {:?}", cx.scope_id());
+        log::trace!("Route should render: {:?}", cx.scope_id());
         cx.render(rsx!(&cx.props.children))
     } else {
-        log::debug!("Route should *not* render: {:?}", cx.scope_id());
+        log::trace!("Route should *not* render: {:?}", cx.scope_id());
         cx.render(rsx!(()))
     }
 }

+ 3 - 0
packages/rsx-rosetta/Cargo.toml

@@ -25,3 +25,6 @@ convert_case = "0.5.0"
 # default = ["html"]
 
 # eventually more output options
+
+[dev-dependencies]
+pretty_assertions = "1.2.1"

+ 68 - 0
packages/rsx-rosetta/tests/simple.rs

@@ -0,0 +1,68 @@
+use html_parser::Dom;
+
+#[test]
+fn simple_elements() {
+    let html = r#"
+    <div>
+        <div class="asd">hello world!</div>
+        <div id="asd">hello world!</div>
+        <div id="asd">hello world!</div>
+        <div for="asd">hello world!</div>
+        <div async="asd">hello world!</div>
+        <div LargeThing="asd">hello world!</div>
+        <ai-is-awesome>hello world!</ai-is-awesome>
+    </div>
+    "#
+    .trim();
+
+    let dom = Dom::parse(html).unwrap();
+
+    let body = rsx_rosetta::rsx_from_html(&dom);
+
+    let out = dioxus_autofmt::write_block_out(body).unwrap();
+
+    let expected = r#"
+    div {
+        div { class: "asd", "hello world!" }
+        div { id: "asd", "hello world!" }
+        div { id: "asd", "hello world!" }
+        div { r#for: "asd", "hello world!" }
+        div { r#async: "asd", "hello world!" }
+        div { large_thing: "asd", "hello world!" }
+        ai_is_awesome { "hello world!" }
+    }"#;
+    pretty_assertions::assert_eq!(&out, &expected);
+}
+
+#[test]
+fn deeply_nested() {
+    let html = r#"
+    <div>
+        <div class="asd">
+            <div class="asd">
+                <div class="asd">
+                    <div class="asd">
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    "#
+    .trim();
+
+    let dom = Dom::parse(html).unwrap();
+
+    let body = rsx_rosetta::rsx_from_html(&dom);
+
+    let out = dioxus_autofmt::write_block_out(body).unwrap();
+
+    let expected = r#"
+    div {
+        div { class: "asd",
+            div { class: "asd",
+                div { class: "asd", div { class: "asd" } }
+            }
+        }
+    }"#;
+    pretty_assertions::assert_eq!(&out, &expected);
+}

+ 4 - 0
packages/ssr/src/renderer.rs

@@ -89,6 +89,10 @@ impl Renderer {
                                 write!(buf, " {}=\"{}\"", attr.name, value)?
                             }
                             AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
+                            AttributeValue::Int(value) => write!(buf, " {}={}", attr.name, value)?,
+                            AttributeValue::Float(value) => {
+                                write!(buf, " {}={}", attr.name, value)?
+                            }
                             _ => {}
                         };
                     }

+ 4 - 0
packages/web/Cargo.toml

@@ -32,6 +32,7 @@ futures-channel = "0.3.21"
 serde_json = { version = "1.0" }
 serde = { version = "1.0" }
 serde-wasm-bindgen = "0.4.5"
+async-trait = "0.1.58"
 
 [dependencies.web-sys]
 version = "0.3.56"
@@ -76,6 +77,9 @@ features = [
     "Location",
     "MessageEvent",
     "console",
+    "FileList",
+    "File",
+    "FileReader"
 ]
 
 [features]

+ 52 - 39
packages/web/src/dom.rs

@@ -10,15 +10,16 @@
 use dioxus_core::{
     BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
 };
-use dioxus_html::{event_bubbles, CompositionData, FormData};
+use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData};
 use dioxus_interpreter_js::{save_template, Channel};
 use futures_channel::mpsc;
+use js_sys::Array;
 use rustc_hash::FxHashMap;
-use std::{any::Any, rc::Rc};
-use wasm_bindgen::{closure::Closure, JsCast};
-use web_sys::{Document, Element, Event, HtmlElement};
+use std::{any::Any, rc::Rc, sync::Arc};
+use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast};
+use web_sys::{console, Document, Element, Event, HtmlElement};
 
-use crate::Config;
+use crate::{file_engine::WebFileEngine, Config};
 
 pub struct WebsysDom {
     document: Document,
@@ -54,13 +55,17 @@ impl WebsysDom {
                 let element = walk_event_for_id(event);
                 let bubbles = dioxus_html::event_bubbles(name.as_str());
                 if let Some((element, target)) = element {
-                    if target
+                    if let Some(prevent_requests) = target
                         .get_attribute("dioxus-prevent-default")
                         .as_deref()
-                        .map(|f| f.trim_start_matches("on"))
-                        == Some(&name)
+                        .map(|f| f.split_whitespace())
                     {
-                        event.prevent_default();
+                        if prevent_requests
+                            .map(|f| f.trim_start_matches("on"))
+                            .any(|f| f == name)
+                        {
+                            event.prevent_default();
+                        }
                     }
 
                     let data = virtual_event_from_websys_event(event.clone(), target);
@@ -206,6 +211,7 @@ impl WebsysDom {
                 },
                 SetText { value, id } => i.set_text(id.0 as u32, value),
                 NewEventListener { name, id, .. } => {
+                    console::log_1(&format!("new event listener: {}", name).into());
                     i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
                 }
                 RemoveEventListener { name, id } => {
@@ -224,6 +230,7 @@ impl WebsysDom {
 // We need tests that simulate clicks/etc and make sure every event type works.
 pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc<dyn Any> {
     use dioxus_html::events::*;
+    console::log_1(&event.clone().into());
 
     match event.type_().as_str() {
         "copy" | "cut" | "paste" => Rc::new(ClipboardData {}),
@@ -325,47 +332,53 @@ fn read_input_to_data(target: Element) -> Rc<FormData> {
 
     // try to fill in form values
     if let Some(form) = target.dyn_ref::<web_sys::HtmlFormElement>() {
-        let elements = form.elements();
-        for x in 0..elements.length() {
-            let element = elements.item(x).unwrap();
-            if let Some(name) = element.get_attribute("name") {
-                let value: Option<String> = element
-                    .dyn_ref()
-                    .map(|input: &web_sys::HtmlInputElement| {
-                        match input.type_().as_str() {
-                            "checkbox" => {
-                                match input.checked() {
-                                    true => Some("true".to_string()),
-                                    false => Some("false".to_string()),
-                                }
-                            },
-                            "radio" => {
-                                match input.checked() {
-                                    true => Some(input.value()),
-                                    false => None,
-                                }
-                            }
-                            _ => Some(input.value())
-                        }
-                    })
-                    .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value())))
-                    .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value())))
-                    .or_else(|| Some(element.dyn_ref::<web_sys::HtmlElement>().unwrap().text_content()))
-                    .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
-                if let Some(value) = value {
-                    values.insert(name, value);
+        let form_data = get_form_data(form);
+        for value in form_data.entries().into_iter().flatten() {
+            if let Ok(array) = value.dyn_into::<Array>() {
+                if let Some(name) = array.get(0).as_string() {
+                    if let Ok(item_values) = array.get(1).dyn_into::<Array>() {
+                        let item_values =
+                            item_values.iter().filter_map(|v| v.as_string()).collect();
+
+                        values.insert(name, item_values);
+                    }
                 }
             }
         }
     }
 
+    let files = target
+        .dyn_ref()
+        .and_then(|input: &web_sys::HtmlInputElement| {
+            input.files().and_then(|files| {
+                WebFileEngine::new(files).map(|f| Arc::new(f) as Arc<dyn FileEngine>)
+            })
+        });
+
     Rc::new(FormData {
         value,
         values,
-        files: None,
+        files,
     })
 }
 
+// web-sys does not expose the keys api for form data, so we need to manually bind to it
+#[wasm_bindgen(inline_js = r#"
+    export function get_form_data(form) {
+        let values = new Map();
+        const formData = new FormData(form);
+
+        for (let name of formData.keys()) {
+            values.set(name, formData.getAll(name));
+        }
+
+        return values;
+    }
+"#)]
+extern "C" {
+    fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map;
+}
+
 fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> {
     let mut target = event
         .target()

+ 103 - 0
packages/web/src/file_engine.rs

@@ -0,0 +1,103 @@
+use dioxus_html::FileEngine;
+use futures_channel::oneshot;
+use js_sys::Uint8Array;
+use wasm_bindgen::{prelude::Closure, JsCast};
+use web_sys::{File, FileList, FileReader};
+
+pub(crate) struct WebFileEngine {
+    file_reader: FileReader,
+    file_list: FileList,
+}
+
+impl WebFileEngine {
+    pub fn new(file_list: FileList) -> Option<Self> {
+        Some(Self {
+            file_list,
+            file_reader: FileReader::new().ok()?,
+        })
+    }
+
+    fn len(&self) -> usize {
+        self.file_list.length() as usize
+    }
+
+    fn get(&self, index: usize) -> Option<File> {
+        self.file_list.item(index as u32)
+    }
+
+    fn find(&self, name: &str) -> Option<File> {
+        (0..self.len())
+            .filter_map(|i| self.get(i))
+            .find(|f| f.name() == name)
+    }
+}
+
+#[async_trait::async_trait(?Send)]
+impl FileEngine for WebFileEngine {
+    fn files(&self) -> Vec<String> {
+        (0..self.len())
+            .filter_map(|i| self.get(i).map(|f| f.name()))
+            .collect()
+    }
+
+    // read a file to bytes
+    async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
+        let file = self.find(file)?;
+
+        let file_reader = self.file_reader.clone();
+        let (rx, tx) = oneshot::channel();
+        let on_load: Closure<dyn FnMut()> = Closure::new({
+            let mut rx = Some(rx);
+            move || {
+                let result = file_reader.result();
+                let _ = rx
+                    .take()
+                    .expect("multiple files read without refreshing the channel")
+                    .send(result);
+            }
+        });
+
+        self.file_reader
+            .set_onload(Some(on_load.as_ref().unchecked_ref()));
+        on_load.forget();
+        self.file_reader.read_as_array_buffer(&file).ok()?;
+
+        if let Ok(Ok(js_val)) = tx.await {
+            let as_u8_arr = Uint8Array::new(&js_val);
+            let as_u8_vec = as_u8_arr.to_vec();
+
+            Some(as_u8_vec)
+        } else {
+            None
+        }
+    }
+
+    // read a file to string
+    async fn read_file_to_string(&self, file: &str) -> Option<String> {
+        let file = self.find(file)?;
+
+        let file_reader = self.file_reader.clone();
+        let (rx, tx) = oneshot::channel();
+        let on_load: Closure<dyn FnMut()> = Closure::new({
+            let mut rx = Some(rx);
+            move || {
+                let result = file_reader.result();
+                let _ = rx
+                    .take()
+                    .expect("multiple files read without refreshing the channel")
+                    .send(result);
+            }
+        });
+
+        self.file_reader
+            .set_onload(Some(on_load.as_ref().unchecked_ref()));
+        on_load.forget();
+        self.file_reader.read_as_text(&file).ok()?;
+
+        if let Ok(Ok(js_val)) = tx.await {
+            js_val.as_string()
+        } else {
+            None
+        }
+    }
+}

+ 2 - 1
packages/web/src/lib.rs

@@ -61,6 +61,7 @@ use futures_util::{pin_mut, FutureExt, StreamExt};
 mod cache;
 mod cfg;
 mod dom;
+mod file_engine;
 mod hot_reload;
 #[cfg(feature = "hydrate")]
 mod rehydrate;
@@ -225,7 +226,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
     websys_dom.mount();
 
     loop {
-        log::debug!("waiting for work");
+        log::trace!("waiting for work");
 
         // if virtualdom has nothing, wait for it to have something before requesting idle time
         // if there is work then this future resolves immediately.