From eb82051000fe8772b97f879195fc412092228023 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 8 Jul 2021 23:25:27 -0400 Subject: [PATCH] examples: webview and async --- .vscode/spellright.dict | 1 + examples/async.rs | 74 +++++++++++++++++++++++++++++ examples/readme.rs | 16 ++++--- examples/slideshow.rs | 54 +++++++++++++++++---- notes/SUSPENSE.md | 34 ++++++++++++++ notes/TODO.md | 27 +++++++---- packages/core/Cargo.toml | 2 + packages/core/src/events.rs | 4 +- packages/core/src/virtual_dom.rs | 21 ++++++++- packages/webview/src/dom.rs | 17 ++++++- packages/webview/src/escape.rs | 81 ++++++++++++++++++++++++++++++++ packages/webview/src/index.html | 7 ++- packages/webview/src/lib.rs | 74 ++++++++++++++++++++--------- 13 files changed, 355 insertions(+), 57 deletions(-) create mode 100644 examples/async.rs create mode 100644 notes/SUSPENSE.md create mode 100644 packages/webview/src/escape.rs diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 436b431e2..8f1809d0f 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -61,3 +61,4 @@ wasm attr derefed Tokio +asynchronicity diff --git a/examples/async.rs b/examples/async.rs new file mode 100644 index 000000000..8a1d631e0 --- /dev/null +++ b/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::().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 + '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::().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} +// } +// )) +// }; diff --git a/examples/readme.rs b/examples/readme.rs index 0a546f35f..c78d74962 100644 --- a/examples/readme.rs +++ b/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!" } + } }) -} +}; diff --git a/examples/slideshow.rs b/examples/slideshow.rs index c99e58255..4a0f853b2 100644 --- a/examples/slideshow.rs +++ b/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 {} } }) }; diff --git a/notes/SUSPENSE.md b/notes/SUSPENSE.md new file mode 100644 index 000000000..d991d8959 --- /dev/null +++ b/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 +``` diff --git a/notes/TODO.md b/notes/TODO.md index daef30710..ce61996ec 100644 --- a/notes/TODO.md +++ b/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 +- [] diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index c4ba2bd2e..e1383bd1e 100644 --- a/packages/core/Cargo.toml +++ b/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 = [] diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index d630f56c2..9549a90e3 100644 --- a/packages/core/src/events.rs +++ b/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 { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 55a2ce3c9..42c7023c8 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -401,7 +401,7 @@ pub struct Scope { // NEEDS TO BE PRIVATE pub(crate) listeners: RefCell, *mut dyn FnMut(VirtualEvent))>>, - pub(crate) suspended_tasks: Vec>>>, + pub(crate) suspended_tasks: Vec<*mut Pin>>>>, // pub(crate) listeners: RefCell>, // pub(crate) listeners: RefCell>, // pub(crate) listeners: RefCell>, @@ -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 + '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 {} diff --git a/packages/webview/src/dom.rs b/packages/webview/src/dom.rs index 05153bd76..f8bec436d 100644 --- a/packages/webview/src/dom.rs +++ b/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>, 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) { diff --git a/packages/webview/src/escape.rs b/packages/webview/src/escape.rs new file mode 100644 index 000000000..654ecbeeb --- /dev/null +++ b/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'"); +} diff --git a/packages/webview/src/index.html b/packages/webview/src/index.html index d6d96ad80..f70077581 100644 --- a/packages/webview/src/index.html +++ b/packages/webview/src/index.html @@ -5,9 +5,6 @@ - - -
@@ -16,7 +13,9 @@ class Interpreter { constructor(root) { this.stack = [root]; - this.listeners = {}; + this.listeners = { + + }; this.lastNodeWasText = false; this.nodes = { 0: root diff --git a/packages/webview/src/lib.rs b/packages/webview/src/lib.rs index 5760d6eb5..f17aa4ab9 100644 --- a/packages/webview/src/lib.rs +++ b/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 WebviewRenderer { 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 vir = VirtualDom::new_with_props(root, props); - 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"), - } - }; + // 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 WebviewRenderer { use serde::{Deserialize, Serialize}; use serde_json::Value; +use crate::dom::WebviewRegistry; + #[derive(Debug, Serialize, Deserialize)] struct MessageParameters { message: String,