examples: webview and async

This commit is contained in:
Jonathan Kelley 2021-07-08 23:25:27 -04:00
parent 99d94b69ab
commit eb82051000
13 changed files with 355 additions and 57 deletions

View File

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

74
examples/async.rs Normal file
View File

@ -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}
// }
// ))
// };

View File

@ -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!" }
}
})
}
};

View File

@ -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
notes/SUSPENSE.md Normal file
View File

@ -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
```

View File

@ -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
- []

View File

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

View File

@ -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 {

View File

@ -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 {}

View File

@ -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) {

View File

@ -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'");
}

View File

@ -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

View File

@ -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 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<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,