suspense works in desktop

This commit is contained in:
Jonathan Kelley committed 2022-11-16 11:48:47 -08:00
1 parent 66319cf6e2
commit 642b21f125
12 files changed
+180 -109

No files matched your search

+31 -28
View File
@@ -6,7 +6,11 @@ use dioxus::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
fn main() { fn main() {
dioxus_desktop::launch(app); dioxus_desktop::launch(|cx| {
cx.render(rsx! {
app_root {}
})
});
} }
#[derive(Debug, Clone, PartialEq, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, serde::Deserialize)]
@@ -14,8 +18,8 @@ struct ListBreeds {
message: HashMap<String, Vec<String>>, message: HashMap<String, Vec<String>>,
} }
fn app(cx: Scope) -> Element { async fn app_root(cx: Scope<'_>) -> Element {
let breed = use_state(&cx, || None); let breed = use_state(&cx, || "deerhound".to_string());
let breeds = use_future(&cx, (), |_| async move { let breeds = use_future(&cx, (), |_| async move {
reqwest::get("https://dog.ceo/api/breeds/list/all") reqwest::get("https://dog.ceo/api/breeds/list/all")
@@ -25,32 +29,26 @@ fn app(cx: Scope) -> Element {
.await .await
}); });
match breeds.value() { match breeds.await {
Some(Ok(breeds)) => cx.render(rsx! { Ok(breeds) => cx.render(rsx! {
div { div { height: "500px",
h1 { "Select a dog breed!" } h1 { "Select a dog breed!" }
div { display: "flex", div { display: "flex",
ul { flex: "50%", ul { flex: "50%",
breeds.message.keys().map(|cur_breed| rsx!( breeds.message.keys().take(5).map(|cur_breed| rsx!(
li { li {
button { button {
onclick: move |_| breed.set(Some(cur_breed.clone())), onclick: move |_| breed.set(cur_breed.clone()),
"{cur_breed}" "{cur_breed}"
} }
} }
)) ))
} }
div { flex: "50%", div { flex: "50%", Breed { breed: breed.to_string() } }
match breed.get() {
Some(breed) => rsx!( Breed { breed: breed.clone() } ),
None => rsx!("No Breed selected"),
}
}
} }
} }
}), }),
Some(Err(_e)) => cx.render(rsx! { div { "Error fetching breeds" } }), Err(_e) => cx.render(rsx! { div { "Error fetching breeds" } }),
None => cx.render(rsx! { div { "Loading dogs..." } }),
} }
} }
@@ -60,7 +58,9 @@ struct DogApi {
} }
#[inline_props] #[inline_props]
fn Breed(cx: Scope, breed: String) -> Element { async fn Breed(cx: Scope, breed: String) -> Element {
println!("Rendering Breed: {}", breed);
let fut = use_future(&cx, (breed,), |(breed,)| async move { let fut = use_future(&cx, (breed,), |(breed,)| async move {
reqwest::get(format!("https://dog.ceo/api/breed/{}/images/random", breed)) reqwest::get(format!("https://dog.ceo/api/breed/{}/images/random", breed))
.await .await
@@ -69,21 +69,24 @@ fn Breed(cx: Scope, breed: String) -> Element {
.await .await
}); });
cx.render(match fut.value() { let resp = fut.await;
Some(Ok(resp)) => rsx! {
button { println!("achieved results!");
onclick: move |_| fut.restart(),
"Click to fetch another doggo" match resp {
} Ok(resp) => cx.render(rsx! {
div { div {
button {
onclick: move |_| fut.restart(),
"Click to fetch another doggo"
}
img { img {
src: "{resp.message}",
max_width: "500px", max_width: "500px",
max_height: "500px", max_height: "500px",
src: "{resp.message}",
} }
} }
}, }),
Some(Err(_)) => rsx! { div { "loading dogs failed" } }, Err(e) => cx.render(rsx! { div { "loading dogs failed" } }),
None => rsx! { div { "loading dogs..." } }, }
})
} }
+16
View File
@@ -0,0 +1,16 @@
use dioxus::prelude::*;
use dioxus_desktop::{Config, LogicalSize, WindowBuilder};
fn main() {
dioxus_desktop::launch(|cx| cx.render(rsx! { async_app {} }));
}
async fn async_app(cx: Scope<'_>) -> Element {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
cx.render(rsx! {
div {
"hi!"
}
})
}
+5 -1
View File
@@ -9,6 +9,7 @@ use syn::{
pub struct InlinePropsBody { pub struct InlinePropsBody {
pub attrs: Vec<Attribute>, pub attrs: Vec<Attribute>,
pub vis: syn::Visibility, pub vis: syn::Visibility,
pub maybe_async: Option<Token![async]>,
pub fn_token: Token![fn], pub fn_token: Token![fn],
pub ident: Ident, pub ident: Ident,
pub cx_token: Box<Pat>, pub cx_token: Box<Pat>,
@@ -25,6 +26,7 @@ pub struct InlinePropsBody {
impl Parse for InlinePropsBody { impl Parse for InlinePropsBody {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?; let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
let maybe_async: Option<Token![async]> = input.parse().ok();
let vis: Visibility = input.parse()?; let vis: Visibility = input.parse()?;
let fn_token = input.parse()?; let fn_token = input.parse()?;
@@ -57,6 +59,7 @@ impl Parse for InlinePropsBody {
Ok(Self { Ok(Self {
vis, vis,
maybe_async,
fn_token, fn_token,
ident, ident,
generics, generics,
@@ -84,6 +87,7 @@ impl ToTokens for InlinePropsBody {
block, block,
cx_token, cx_token,
attrs, attrs,
maybe_async,
.. ..
} = self; } = self;
@@ -151,7 +155,7 @@ impl ToTokens for InlinePropsBody {
} }
#(#attrs)* #(#attrs)*
#vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output #maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output
#where_clause #where_clause
{ {
let #struct_name { #(#field_names),* } = &cx.props; let #struct_name { #(#field_names),* } = &cx.props;
+70 -45
View File
@@ -1,11 +1,13 @@
use std::ops::Bound;
use crate::factory::RenderReturn; use crate::factory::RenderReturn;
use crate::innerlude::{Mutations, SuspenseContext}; use crate::innerlude::{Mutations, SuspenseContext, SuspenseId};
use crate::mutations::Mutation; use crate::mutations::Mutation;
use crate::mutations::Mutation::*; use crate::mutations::Mutation::*;
use crate::nodes::VNode; use crate::nodes::VNode;
use crate::nodes::{DynamicNode, TemplateNode}; use crate::nodes::{DynamicNode, TemplateNode};
use crate::virtual_dom::VirtualDom; use crate::virtual_dom::VirtualDom;
use crate::{AttributeValue, ElementId, ScopeId, TemplateAttribute}; use crate::{AttributeValue, ScopeId, TemplateAttribute};
impl VirtualDom { impl VirtualDom {
pub(crate) fn create_scope<'a>( pub(crate) fn create_scope<'a>(
@@ -50,8 +52,6 @@ impl VirtualDom {
let cur_scope = self.scope_stack.last().copied().unwrap(); let cur_scope = self.scope_stack.last().copied().unwrap();
println!("creating template: {:#?}", template);
let mut on_stack = 0; let mut on_stack = 0;
for (root_idx, root) in template.template.roots.iter().enumerate() { for (root_idx, root) in template.template.roots.iter().enumerate() {
on_stack += match root { on_stack += match root {
@@ -64,9 +64,7 @@ impl VirtualDom {
} }
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
let dynamic_node = &template.dynamic_nodes[*id]; match &template.dynamic_nodes[*id] {
match dynamic_node {
DynamicNode::Fragment { .. } | DynamicNode::Component { .. } => self DynamicNode::Fragment { .. } | DynamicNode::Component { .. } => self
.create_dynamic_node( .create_dynamic_node(
mutations, mutations,
@@ -79,19 +77,16 @@ impl VirtualDom {
} => { } => {
let id = self.next_element(template); let id = self.next_element(template);
slot.set(id); slot.set(id);
mutations.push(CreateTextNode { mutations.push(CreateTextNode { value, id });
value: value.clone(),
id,
});
1 1
} }
DynamicNode::Placeholder(id) => { DynamicNode::Placeholder(slot) => {
let id = self.next_element(template); let id = self.next_element(template);
slot.set(id);
mutations.push(CreatePlaceholder { id }); mutations.push(CreatePlaceholder { id });
1 1
} }
} }
// self.create_dynamic_node(mutations, template, &template.dynamic_nodes[*id], *id)
} }
}; };
@@ -141,31 +136,34 @@ impl VirtualDom {
} }
} }
// todo:
//
// we walk the roots front to back when creating nodes, bur want to fill in the dynamic nodes
// back to front. This is because the indices would shift around because the paths become invalid
//
// We could easily implement this without the vec by walking the indicies forward
let mut queued_changes = vec![];
// We're on top of a node that has a dynamic child for a descendant // We're on top of a node that has a dynamic child for a descendant
// Skip any node that's a root // Skip any node that's a root
while let Some((idx, path)) = let mut start = None;
dynamic_nodes.next_if(|(_, p)| p.len() > 1 && p[0] == root_idx as u8) let mut end = None;
{
let node = &template.dynamic_nodes[idx]; while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) {
let m = self.create_dynamic_node(mutations, template, node, idx); if p.len() == 1 {
if m > 0 { continue;
queued_changes.push(ReplacePlaceholder {
m,
path: &path[1..],
});
} }
if start.is_none() {
start = Some(idx);
}
end = Some(idx);
} }
for change in queued_changes.into_iter().rev() { if let (Some(start), Some(end)) = (start, end) {
mutations.push(change); for idx in start..=end {
let node = &template.dynamic_nodes[idx];
let m = self.create_dynamic_node(mutations, template, node, idx);
if m > 0 {
mutations.push(ReplacePlaceholder {
m,
path: &template.template.node_paths[idx][1..],
});
}
}
} }
} }
@@ -240,7 +238,7 @@ impl VirtualDom {
idx: usize, idx: usize,
) -> usize { ) -> usize {
match &node { match &node {
DynamicNode::Text { id, value, inner } => { DynamicNode::Text { id, value, .. } => {
let new_id = self.next_element(template); let new_id = self.next_element(template);
id.set(new_id); id.set(new_id);
mutations.push(HydrateText { mutations.push(HydrateText {
@@ -266,14 +264,32 @@ impl VirtualDom {
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
match return_nodes { match return_nodes {
RenderReturn::Sync(None) | RenderReturn::Async(_) => { RenderReturn::Sync(None) => {
todo!()
}
RenderReturn::Async(_) => {
let new_id = self.next_element(template); let new_id = self.next_element(template);
placeholder.set(Some(new_id)); placeholder.set(Some(new_id));
self.scopes[scope.0].placeholder.set(Some(new_id)); self.scopes[scope.0].placeholder.set(Some(new_id));
mutations.push(AssignId { mutations.push(AssignId {
id: new_id, id: new_id,
path: &template.template.node_paths[idx][1..], path: &template.template.node_paths[idx][1..],
}); });
let boudary = self.scopes[scope.0]
.consume_context::<SuspenseContext>()
.unwrap();
if boudary.placeholder.get().is_none() {
boudary.placeholder.set(Some(new_id));
}
boudary
.waiting_on
.borrow_mut()
.extend(self.collected_leaves.drain(..));
0 0
} }
@@ -288,16 +304,6 @@ impl VirtualDom {
if let Some(boundary) = if let Some(boundary) =
self.scopes[scope.0].has_context::<SuspenseContext>() self.scopes[scope.0].has_context::<SuspenseContext>()
{ {
let mut boundary_mut = boundary.borrow_mut();
let split_off = mutations.split_off(mutations_to_this_point);
let split_off = unsafe { std::mem::transmute(split_off) };
boundary_mut.mutations.edits = split_off;
boundary_mut
.waiting_on
.extend(self.collected_leaves.drain(..));
// Since this is a boundary, use it as a placeholder // Since this is a boundary, use it as a placeholder
let new_id = self.next_element(template); let new_id = self.next_element(template);
placeholder.set(Some(new_id)); placeholder.set(Some(new_id));
@@ -306,6 +312,25 @@ impl VirtualDom {
id: new_id, id: new_id,
path: &template.template.node_paths[idx][1..], path: &template.template.node_paths[idx][1..],
}); });
// Now connect everything to the boundary
let boundary_mut = boundary;
let split_off = mutations.split_off(mutations_to_this_point);
let split_off: Vec<Mutation> =
unsafe { std::mem::transmute(split_off) };
if boundary_mut.placeholder.get().is_none() {
boundary_mut.placeholder.set(Some(new_id));
}
// In the generated edits, we want to pick off from where we left off.
boundary_mut.mutations.borrow_mut().edits.extend(split_off);
boundary_mut
.waiting_on
.borrow_mut()
.extend(self.collected_leaves.drain(..));
created = 0; created = 0;
} }
} }
+10 -7
View File
@@ -1,4 +1,5 @@
use super::{waker::RcWake, SchedulerMsg}; use super::{waker::RcWake, SchedulerMsg};
use crate::ElementId;
use crate::{innerlude::Mutations, Element, ScopeId}; use crate::{innerlude::Mutations, Element, ScopeId};
use std::future::Future; use std::future::Future;
use std::{ use std::{
@@ -10,22 +11,24 @@ use std::{
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct SuspenseId(pub usize); pub struct SuspenseId(pub usize);
pub type SuspenseContext = Rc<RefCell<SuspenseBoundary>>; pub type SuspenseContext = Rc<SuspenseBoundary>;
/// Essentially a fiber in React /// Essentially a fiber in React
pub struct SuspenseBoundary { pub struct SuspenseBoundary {
pub id: ScopeId, pub id: ScopeId,
pub waiting_on: HashSet<SuspenseId>, pub waiting_on: RefCell<HashSet<SuspenseId>>,
pub mutations: Mutations<'static>, pub mutations: RefCell<Mutations<'static>>,
pub placeholder: Cell<Option<ElementId>>,
} }
impl SuspenseBoundary { impl SuspenseBoundary {
pub fn new(id: ScopeId) -> Rc<RefCell<Self>> { pub fn new(id: ScopeId) -> Rc<Self> {
Rc::new(RefCell::new(Self { Rc::new(Self {
id, id,
waiting_on: Default::default(), waiting_on: Default::default(),
mutations: Mutations::new(0), mutations: RefCell::new(Mutations::new(0)),
})) placeholder: Cell::new(None),
})
} }
} }
+21 -6
View File
@@ -53,14 +53,12 @@ impl VirtualDom {
// we should attach them to that component and then render its children // we should attach them to that component and then render its children
// continue rendering the tree until we hit yet another suspended component // continue rendering the tree until we hit yet another suspended component
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
let boundary = &self.scopes[leaf.scope_id.0] let fiber = &self.scopes[leaf.scope_id.0]
.consume_context::<SuspenseContext>() .consume_context::<SuspenseContext>()
.unwrap(); .unwrap();
println!("ready pool"); println!("ready pool");
let mut fiber = boundary.borrow_mut();
println!( println!(
"Existing mutations {:?}, scope {:?}", "Existing mutations {:?}, scope {:?}",
fiber.mutations, fiber.id fiber.mutations, fiber.id
@@ -72,16 +70,33 @@ impl VirtualDom {
let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes)); let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
arena.node.set(ret); arena.node.set(ret);
fiber.waiting_on.borrow_mut().remove(&id);
if let RenderReturn::Sync(Some(template)) = ret { if let RenderReturn::Sync(Some(template)) = ret {
let mutations = &mut fiber.mutations; let mutations_ref = &mut fiber.mutations.borrow_mut();
let mutations = &mut **mutations_ref;
let template: &VNode = unsafe { std::mem::transmute(template) }; let template: &VNode = unsafe { std::mem::transmute(template) };
let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
let place_holder_id = scope.placeholder.get().unwrap();
self.scope_stack.push(scope_id); self.scope_stack.push(scope_id);
self.create(mutations, template); let created = self.create(mutations, template);
self.scope_stack.pop(); self.scope_stack.pop();
mutations.push(Mutation::ReplaceWith {
id: place_holder_id,
m: created,
});
println!("{:#?}", mutations); // for leaf in self.collected_leaves.drain(..) {
// fiber.waiting_on.borrow_mut().insert(leaf);
// }
if fiber.waiting_on.borrow().is_empty() {
println!("fiber is finished!");
self.finished_fibers.push(fiber.id);
} else {
println!("fiber is not finished {:?}", fiber.waiting_on);
}
} else { } else {
println!("nodes arent right"); println!("nodes arent right");
} }
+10 -3
View File
@@ -268,8 +268,8 @@ impl VirtualDom {
!self.scopes[id.0] !self.scopes[id.0]
.consume_context::<SuspenseContext>() .consume_context::<SuspenseContext>()
.unwrap() .unwrap()
.borrow()
.waiting_on .waiting_on
.borrow()
.is_empty() .is_empty()
} }
@@ -469,8 +469,15 @@ impl VirtualDom {
for finished_fiber in self.finished_fibers.drain(..) { for finished_fiber in self.finished_fibers.drain(..) {
let scope = &mut self.scopes[finished_fiber.0]; let scope = &mut self.scopes[finished_fiber.0];
let context = scope.has_context::<SuspenseContext>().unwrap(); let context = scope.has_context::<SuspenseContext>().unwrap();
let mut context = context.borrow_mut(); println!("unloading suspense tree {:?}", context.mutations);
mutations.extend(context.mutations.drain(..));
mutations.extend(context.mutations.borrow_mut().template_mutations.drain(..));
mutations.extend(context.mutations.borrow_mut().drain(..));
mutations.push(Mutation::ReplaceWith {
id: context.placeholder.get().unwrap(),
m: 1,
})
} }
// Next, diff any dirty scopes // Next, diff any dirty scopes
+6 -9
View File
@@ -50,7 +50,7 @@ impl DesktopController {
let edits = dom.rebuild(); let edits = dom.rebuild();
println!("got muts: {:#?}", edits); // println!("got muts: {:#?}", edits);
{ {
let mut queue = edit_queue.lock().unwrap(); let mut queue = edit_queue.lock().unwrap();
@@ -68,20 +68,15 @@ impl DesktopController {
let muts = dom let muts = dom
.render_with_deadline(tokio::time::sleep( .render_with_deadline(tokio::time::sleep(
tokio::time::Duration::from_millis(100), tokio::time::Duration::from_millis(16),
)) ))
.await; .await;
{ {
let mut queue = edit_queue.lock().unwrap(); let mut queue = edit_queue.lock().unwrap();
for edit in muts.template_mutations { queue.push(serde_json::to_string(&muts.template_mutations).unwrap());
queue.push(serde_json::to_string(&edit).unwrap()); queue.push(serde_json::to_string(&muts.edits).unwrap());
}
for edit in muts.edits {
queue.push(serde_json::to_string(&edit).unwrap());
}
drop(queue); drop(queue);
} }
@@ -118,6 +113,8 @@ impl DesktopController {
let (_id, view) = self.webviews.iter_mut().next().unwrap(); let (_id, view) = self.webviews.iter_mut().next().unwrap();
println!("sending edits {:#?}", new_queue);
for edit in new_queue.drain(..) { for edit in new_queue.drain(..) {
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
.unwrap(); .unwrap();
-6
View File
@@ -181,12 +181,6 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
index_file.clone(), index_file.clone(),
) )
}) })
// passing edits via the custom protocol is faster than using eval, maybe?
.with_custom_protocol(String::from("edits"), move |r| {
//
// Ok(Response::body())
todo!()
})
.with_file_drop_handler(move |window, evet| { .with_file_drop_handler(move |window, evet| {
file_handler file_handler
.as_ref() .as_ref()
+2
View File
@@ -18,6 +18,8 @@ pub(super) fn desktop_handler(
custom_head: Option<String>, custom_head: Option<String>,
custom_index: Option<String>, custom_index: Option<String>,
) -> Result<Response<Vec<u8>>> { ) -> Result<Response<Vec<u8>>> {
println!("loading request: {:?}", request.uri());
// Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case". // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case".
// For now, we only serve two pieces of content which get included as bytes into the final binary. // For now, we only serve two pieces of content which get included as bytes into the final binary.
let path = request.uri().to_string().replace("dioxus://", ""); let path = request.uri().to_string().replace("dioxus://", "");
+5 -3
View File
@@ -134,8 +134,11 @@ impl<T> UseFuture<T> {
/// ///
/// If the future has never completed, the returned value will be `None`. /// If the future has never completed, the returned value will be `None`.
pub fn value(&self) -> Option<&T> { pub fn value(&self) -> Option<&T> {
// self.value.as_ref() self.values
todo!() .borrow_mut()
.last()
.cloned()
.map(|x| unsafe { &*x })
} }
/// Get the ID of the future in Dioxus' internal scheduler /// Get the ID of the future in Dioxus' internal scheduler
@@ -181,7 +184,6 @@ impl<'a, T> Future for UseFutureAwait<'a, T> {
self: std::pin::Pin<&mut Self>, self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> { ) -> std::task::Poll<Self::Output> {
println!("polling future");
match self.hook.values.borrow_mut().last().cloned() { match self.hook.values.borrow_mut().last().cloned() {
Some(value) => std::task::Poll::Ready(unsafe { &*value }), Some(value) => std::task::Poll::Ready(unsafe { &*value }),
None => { None => {
+4 -1
View File
@@ -167,7 +167,10 @@ export class Interpreter {
const name = field; const name = field;
const node = this.nodes[root]; const node = this.nodes[root];
if (ns === "style") { if (ns === "style") {
// @ts-ignore // ????? why do we need to do this
if (node.style === undefined) {
node.style = {};
}
node.style[name] = value; node.style[name] = value;
} else if (ns != null || ns != undefined) { } else if (ns != null || ns != undefined) {
node.setAttributeNS(ns, name, value); node.setAttributeNS(ns, name, value);