Bladeren bron

examples: webview and async

Jonathan Kelley 4 jaren geleden
bovenliggende
commit
eb82051

+ 1 - 0
.vscode/spellright.dict

@@ -61,3 +61,4 @@ wasm
 attr
 derefed
 Tokio
+asynchronicity

+ 74 - 0
examples/async.rs

@@ -0,0 +1,74 @@
+//! Example: README.md showcase
+//!
+//! The example from the README.md
+
+use std::pin::Pin;
+
+use dioxus::prelude::*;
+use futures::Future;
+fn main() {
+    dioxus::web::launch(App)
+}
+
+#[derive(serde::Deserialize)]
+struct DogApi {
+    message: String,
+}
+
+const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random";
+
+static App: FC<()> = |cx| {
+    // let mut count = use_state(cx, || 0);
+    let mut fut = cx.use_hook(
+        move || {
+            Box::pin(async {
+                //
+                loop {
+                    // repeatadly get new doggos
+                    match surf::get(ENDPOINT).recv_json::<DogApi>().await {
+                        Ok(_) => (),
+                        Err(_) => (),
+                        // Ok(res) => rsx!(in cx, img { src: "{res.message}" }),
+                        // Err(_) => rsx!(in cx, div { "No doggos for you :(" }),
+                    }
+                    // wait one seconds
+                }
+            }) as Pin<Box<dyn Future<Output = ()> + 'static>>
+        },
+        |h| h,
+        |_| {},
+    );
+
+    cx.submit_task(fut);
+
+    todo!()
+    // cx.render(rsx! {
+    //     div {
+    //         h1 { "Hifive counter: {count}" }
+    //         button { onclick: move |_| count += 1, "Up high!" }
+    //         button { onclick: move |_| count -= 1, "Down low!" }
+    //     }
+    // })
+};
+
+// #[derive(serde::Deserialize)]
+// struct DogApi {
+//     message: String,
+// }
+// const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random";
+
+// pub static App: FC<()> = |cx| {
+//     let doggo = use_future_effect(&cx, move || async move {
+//         match surf::get(ENDPOINT).recv_json::<DogApi>().await {
+//             Ok(res) => rsx!(in cx, img { src: "{res.message}" }),
+//             Err(_) => rsx!(in cx, div { "No doggos for you :(" }),
+//         }
+//     });
+
+//     cx.render(rsx!(
+//         div {
+//             h1 {"Waiting for a doggo..."}
+//             {doggo}
+//         }
+//     ))
+// };

+ 9 - 7
examples/readme.rs

@@ -4,15 +4,17 @@
 
 use dioxus::prelude::*;
 fn main() {
-    dioxus::web::launch(Example)
+    dioxus::web::launch(App)
 }
 
-fn Example(cx: Context<()>) -> VNode {
-    let name = use_state(cx, || "..?");
+static App: FC<()> = |cx| {
+    let mut count = use_state(cx, || 0);
 
     cx.render(rsx! {
-        h1 { "Hello, {name}" }
-        button { "?", onclick: move |_| name.set("world!")}
-        button { "?", onclick: move |_| name.set("Dioxus 🎉")}
+        div {
+            h1 { "Hifive counter: {count}" }
+            button { onclick: move |_| count += 1, "Up high!" }
+            button { onclick: move |_| count -= 1, "Down low!" }
+        }
     })
-}
+};

+ 44 - 10
examples/slideshow.rs

@@ -16,9 +16,9 @@ fn main() {
 }
 
 static App: FC<()> = |cx| {
-    let slide_id = use_state(cx, || 0);
+    let slides = use_state(cx, SlideController::new);
 
-    let slide = match *slide_id {
+    let slide = match slides.slide_id {
         0 => cx.render(rsx!(Title {})),
         1 => cx.render(rsx!(Slide1 {})),
         2 => cx.render(rsx!(Slide2 {})),
@@ -28,12 +28,15 @@ static App: FC<()> = |cx| {
 
     cx.render(rsx! {
         div {
+            style: {
+                background_color: "red"
+            }
             div {
                 div { h1 {"my awesome slideshow"} }
                 div {
-                    button {"<-", onclick: move |_| if *slide_id != 0 { *slide_id.get_mut() -= 1}}
-                    h3 { "{slide_id}" }
-                    button {"->" onclick: move |_| if *slide_id != 4 { *slide_id.get_mut() += 1 }}
+                    button {"<-", onclick: move |_| slides.get_mut().go_forward()}
+                    h3 { "{slides.slide_id}" }
+                    button {"->" onclick: move |_| slides.get_mut().go_backward()}
                  }
             }
             {slide}
@@ -41,38 +44,69 @@ static App: FC<()> = |cx| {
     })
 };
 
+#[derive(Clone)]
+struct SlideController {
+    slide_id: isize,
+}
+impl SlideController {
+    fn new() -> Self {
+        Self { slide_id: 0 }
+    }
+    fn can_go_forward(&self) -> bool {
+        false
+    }
+    fn can_go_backward(&self) -> bool {
+        true
+    }
+    fn go_forward(&mut self) {
+        if self.can_go_forward() {
+            self.slide_id += 1;
+        }
+    }
+    fn go_backward(&mut self) {
+        if self.can_go_backward() {
+            self.slide_id -= 1;
+        }
+    }
+}
+
 const Title: FC<()> = |cx| {
     cx.render(rsx! {
         div {
-
+            h1 { "Title" }
+            p {}
         }
     })
 };
 const Slide1: FC<()> = |cx| {
     cx.render(rsx! {
         div {
-
+            h1 { "Slide1" }
+            p {}
         }
     })
 };
 const Slide2: FC<()> = |cx| {
     cx.render(rsx! {
         div {
-
+            h1 { "Slide2" }
+            p {}
         }
     })
 };
 const Slide3: FC<()> = |cx| {
     cx.render(rsx! {
         div {
-
+            h1 { "Slide3" }
+            p {}
         }
     })
 };
 const End: FC<()> = |cx| {
     cx.render(rsx! {
         div {
-
+            h1 { "End" }
+            p {}
         }
     })
 };

+ 34 - 0
notes/SUSPENSE.md

@@ -0,0 +1,34 @@
+# Suspense, Async, and More
+
+This doc goes into the design of asynchronicity in Dioxus.
+
+
+## for UI elements
+
+`suspend`-ing a future submits an &mut future to Dioxus. the future must return VNodes. the future is still waiting before the component renders, the `.await` is dropped and tried again. users will want to attach their future to a hook so the future doesn't really get dropped.
+
+
+## for tasks
+
+for more general tasks, we need some way of submitting a future or task into some sort of task system. 
+
+
+`use_task()` submits a future to Dioxus. the future is polled infinitely until it finishes. The caller of `use_task` may drop, pause, restart, or insert a new the task
+
+```rust
+
+let task = use_hook(|| {
+    // create the future
+}, || {
+    update the future if it needs to be updated
+}, || {
+
+});
+cx.poll_future()
+// let recoil_event_loop = cx.use_task(move |_| async move {
+//     loop {
+//         let msg = receiver.await?;
+//     }
+// });
+// where suspend wraps use_task
+```

+ 18 - 9
notes/TODO.md

@@ -1,14 +1,14 @@
-- [] Move the builder API onto NodeFactory
-- [] Transition away from names and towards compile-time safe tags
-- [] Fix diffing of fragments
-- [] Properly integrate memoization to prevent safety issues with children
-- [] Understand and fix the issue with callbacks (outdated generations)
-- [] Fix examples for core, web, ssr, and general
+- [x] Move the builder API onto NodeFactory
+- [x] Transition away from names and towards compile-time safe tags
+- [x] Fix diffing of fragments
+- [x] Properly integrate memoization to prevent safety issues with children
+- [x] Understand and fix the issue with callbacks (outdated generations)
+- [x] Fix examples for core, web, ssr, and general
 - [] Finish up documentation
 - [] Polish the Recoil (Dirac?) API
-- [] Find better names for things
+- [x] Find better names for things
 - [] get the html macro up to parity with the rsx! macro
-- [] put warnings in for iterating w/o keys
+- [x] put warnings in for iterating w/o keys
 - [] finish up the crate for generating code blocks
 - [] make progress on the docusarus-style crate for generating the websites
 - [] build the docsite
@@ -17,4 +17,13 @@
 - [] Implement controlled inputs for select and textarea
 - [] ...somehow... noderefs....
 
-use_state(cx, )
+- [x] soup of the use_state hook
+- [ ] wire up inline css
+- [ ] compile-time correct inline CSS
+- [ ] signals
+- [ ] cooperative scheduler
+- [ ] suspense
+- [ ] task system
+- [] wire up events for webview
+- [] JIT for class-based style engines
+- [] 

+ 2 - 0
packages/core/Cargo.toml

@@ -41,6 +41,8 @@ slotmap = "1.0.3"
 futures = "0.3.15"
 
 
+async-std = { version="1.9.0", features=["attributes"] }
+
 [features]
 default = ["serialize"]
 # default = []

+ 2 - 2
packages/core/src/events.rs

@@ -149,9 +149,9 @@ pub mod on {
 
                 $(
                     $(#[$method_attr])*
-                    pub fn $name<'a>(
+                    pub fn $name<'a, F: FnMut($wrapper) + 'a>(
                         c: NodeFactory<'a>,
-                        mut callback: impl FnMut($wrapper) + 'a,
+                        mut callback: F,
                     ) -> Listener<'a> {
                         let bump = &c.bump();
                         Listener {

+ 20 - 1
packages/core/src/virtual_dom.rs

@@ -401,7 +401,7 @@ pub struct Scope {
     // NEEDS TO BE PRIVATE
     pub(crate) listeners: RefCell<Vec<(*mut Cell<RealDomNode>, *mut dyn FnMut(VirtualEvent))>>,
 
-    pub(crate) suspended_tasks: Vec<Box<dyn Future<Output = VNode<'static>>>>,
+    pub(crate) suspended_tasks: Vec<*mut Pin<Box<dyn Future<Output = VNode<'static>>>>>,
     // pub(crate) listeners: RefCell<nohash_hasher::IntMap<u32, *const dyn FnMut(VirtualEvent)>>,
     // pub(crate) listeners: RefCell<Vec<*const dyn FnMut(VirtualEvent)>>,
     // pub(crate) listeners: RefCell<Vec<*const dyn FnMut(VirtualEvent)>>,
@@ -871,8 +871,27 @@ Any function prefixed with "use" should not be called conditionally.
             }
         }
     }
+
+    /// `submit_task` will submit the future to be polled.
+    /// Dioxus will step its internal event loop if the future returns if the future completes while waiting.
+    ///
+    /// Tasks can't return anything, but they can be controlled with the returned handle
+    ///
+    /// Tasks will only run until the component renders again. If you want your task to last longer than one frame, you'll need
+    /// to store it somehow.
+    ///
+    pub fn submit_task(
+        &self,
+        task: &'src mut Pin<Box<dyn Future<Output = ()> + 'static>>,
+    ) -> TaskHandle {
+        // the pointer to the task is stable - we guarantee stability of all &'src references
+        let task_ptr = task as *mut _;
+
+        TaskHandle {}
+    }
 }
 
+pub struct TaskHandle {}
 #[derive(Clone)]
 pub struct SuspendedContext {}
 

+ 16 - 1
packages/webview/src/dom.rs

@@ -9,17 +9,32 @@ use dioxus_core::{
 };
 use DomEdit::*;
 
+pub struct WebviewRegistry {}
+
+impl WebviewRegistry {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
 pub struct WebviewDom<'bump> {
     pub edits: Vec<DomEdit<'bump>>,
     pub node_counter: u64,
+    pub registry: WebviewRegistry,
 }
 impl WebviewDom<'_> {
-    pub fn new() -> Self {
+    pub fn new(registry: WebviewRegistry) -> Self {
         Self {
             edits: Vec::new(),
             node_counter: 0,
+            registry,
         }
     }
+
+    // Finish using the dom (for its edit list) and give back the node and event registry
+    pub fn consume(self) -> WebviewRegistry {
+        self.registry
+    }
 }
 impl<'bump> RealDom<'bump> for WebviewDom<'bump> {
     fn push_root(&mut self, root: dioxus_core::virtual_dom::RealDomNode) {

+ 81 - 0
packages/webview/src/escape.rs

@@ -0,0 +1,81 @@
+use std::fmt::{self, Write};
+
+/// Escape a string to pass it into JavaScript.
+///
+/// # Example
+///
+/// ```rust,no_run
+/// # use web_view::WebView;
+/// # use std::mem;
+/// #
+/// # let mut view: WebView<()> = unsafe { mem::uninitialized() };
+/// #
+/// let string = "Hello, world!";
+///
+/// // Calls the function callback with "Hello, world!" as its parameter.
+///
+/// view.eval(&format!("callback({});", web_view::escape(string)));
+/// ```
+pub fn escape(string: &str) -> Escaper {
+    Escaper(string)
+}
+
+// "All code points may appear literally in a string literal except for the
+// closing quote code points, U+005C (REVERSE SOLIDUS), U+000D (CARRIAGE
+// RETURN), U+2028 (LINE SEPARATOR), U+2029 (PARAGRAPH SEPARATOR), and U+000A
+// (LINE FEED)." - ES6 Specification
+
+pub struct Escaper<'a>(&'a str);
+
+const SPECIAL: &[char] = &[
+    '\n',       // U+000A (LINE FEED)
+    '\r',       // U+000D (CARRIAGE RETURN)
+    '\'',       // U+0027 (APOSTROPHE)
+    '\\',       // U+005C (REVERSE SOLIDUS)
+    '\u{2028}', // U+2028 (LINE SEPARATOR)
+    '\u{2029}', // U+2029 (PARAGRAPH SEPARATOR)
+];
+
+impl<'a> fmt::Display for Escaper<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let &Escaper(mut string) = self;
+
+        f.write_char('\'')?;
+
+        while !string.is_empty() {
+            if let Some(i) = string.find(SPECIAL) {
+                if i > 0 {
+                    f.write_str(&string[..i])?;
+                }
+
+                let mut chars = string[i..].chars();
+
+                f.write_str(match chars.next().unwrap() {
+                    '\n' => "\\n",
+                    '\r' => "\\r",
+                    '\'' => "\\'",
+                    '\\' => "\\\\",
+                    '\u{2028}' => "\\u2028",
+                    '\u{2029}' => "\\u2029",
+                    _ => unreachable!(),
+                })?;
+
+                string = chars.as_str();
+            } else {
+                f.write_str(string)?;
+                break;
+            }
+        }
+
+        f.write_char('\'')?;
+
+        Ok(())
+    }
+}
+
+#[test]
+fn test() {
+    let plain = "ABC \n\r' abc \\  \u{2028}   \u{2029}123";
+    let escaped = escape(plain).to_string();
+    assert!(escaped == "'ABC \\n\\r\\' abc \\\\  \\u2028   \\u2029123'");
+}

+ 3 - 4
packages/webview/src/index.html

@@ -5,9 +5,6 @@
     <!-- <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" /> -->
 </head>
 
-
-
-
 <body>
     <div></div>
 </body>
@@ -16,7 +13,9 @@
     class Interpreter {
         constructor(root) {
             this.stack = [root];
-            this.listeners = {};
+            this.listeners = {
+
+            };
             this.lastNodeWasText = false;
             this.nodes = {
                 0: root

+ 52 - 24
packages/webview/src/lib.rs

@@ -1,4 +1,5 @@
 use std::borrow::BorrowMut;
+use std::ops::{Deref, DerefMut};
 use std::sync::mpsc::channel;
 use std::sync::{Arc, RwLock};
 
@@ -10,6 +11,7 @@ use wry::{
 };
 
 mod dom;
+mod escape;
 
 static HTML_CONTENT: &'static str = include_str!("./index.html");
 
@@ -59,33 +61,57 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
 
         let window = user_builder(WindowBuilder::new()).build(&event_loop)?;
 
-        let mut vdom = VirtualDom::new_with_props(root, props);
-        let mut real_dom = dom::WebviewDom::new();
-        vdom.rebuild(&mut real_dom)?;
-
-        let edits = Arc::new(RwLock::new(Some(serde_json::to_value(real_dom.edits)?)));
-
-        // let ref_edits = Arc::new(serde_json::to_string(&real_dom.edits)?);
-
-        let handler = move |window: &Window, mut req: RpcRequest| {
-            //
-            let d = edits.clone();
-            match req.method.as_str() {
-                "initiate" => {
-                    let mut ed = d.write().unwrap();
-                    let edits = match ed.as_mut() {
-                        Some(ed) => Some(ed.take()),
-                        None => None,
-                    };
-                    Some(RpcResponse::new_result(req.id.take(), edits))
-                }
-                _ => todo!("this message failed"),
-            }
-        };
+        let vir = VirtualDom::new_with_props(root, props);
+
+        // todo: combine these or something
+        let vdom = Arc::new(RwLock::new(vir));
+        let registry = Arc::new(RwLock::new(Some(WebviewRegistry::new())));
 
         let webview = WebViewBuilder::new(window)?
             .with_url(&format!("data:text/html,{}", HTML_CONTENT))?
-            .with_rpc_handler(handler)
+            .with_rpc_handler(move |window: &Window, mut req: RpcRequest| {
+                match req.method.as_str() {
+                    "initiate" => {
+                        let mut lock = vdom.write().unwrap();
+                        let mut reg_lock = registry.write().unwrap();
+
+                        // Create the thin wrapper around the registry to collect the edits into
+                        let mut real = dom::WebviewDom::new(reg_lock.take().unwrap());
+
+                        // Serialize the edit stream
+                        let edits = {
+                            lock.rebuild(&mut real).unwrap();
+                            serde_json::to_value(&real.edits).unwrap()
+                        };
+
+                        // Give back the registry into its slot
+                        *reg_lock = Some(real.consume());
+
+                        // Return the edits into the webview runtime
+                        Some(RpcResponse::new_result(req.id.take(), Some(edits)))
+                    }
+                    "user_event" => {
+                        let mut lock = vdom.write().unwrap();
+                        let mut reg_lock = registry.write().unwrap();
+
+                        // Create the thin wrapper around the registry to collect the edits into
+                        let mut real = dom::WebviewDom::new(reg_lock.take().unwrap());
+
+                        // Serialize the edit stream
+                        let edits = {
+                            lock.rebuild(&mut real).unwrap();
+                            serde_json::to_value(&real.edits).unwrap()
+                        };
+
+                        // Give back the registry into its slot
+                        *reg_lock = Some(real.consume());
+
+                        // Return the edits into the webview runtime
+                        Some(RpcResponse::new_result(req.id.take(), Some(edits)))
+                    }
+                    _ => todo!("this message failed"),
+                }
+            })
             .build()?;
 
         event_loop.run(move |event, _, control_flow| {
@@ -173,6 +199,8 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 
+use crate::dom::WebviewRegistry;
+
 #[derive(Debug, Serialize, Deserialize)]
 struct MessageParameters {
     message: String,