diff --git a/examples/button.rs b/examples/button.rs new file mode 100644 index 000000000..9d32819e8 --- /dev/null +++ b/examples/button.rs @@ -0,0 +1,11 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + button { "hello, desktop!" } + }) +} diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index c3ea4dc2c..7014a2771 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, panic::AssertUnwindSafe}; use crate::{ innerlude::Scoped, @@ -62,12 +62,19 @@ where } fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> { - let scope: &mut Scoped

= cx.bump().alloc(Scoped { - props: &self.props, - scope: cx, - }); + let res = std::panic::catch_unwind(AssertUnwindSafe(move || { + // Call the render function directly + let scope: &mut Scoped

= cx.bump().alloc(Scoped { + props: &self.props, + scope: cx, + }); - // Call the render function directly - (self.render_fn)(scope).into_return(cx) + (self.render_fn)(scope).into_return(cx) + })); + + match res { + Ok(e) => e, + Err(_) => RenderReturn::default(), + } } } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 9909e2fb4..36323730a 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -81,12 +81,12 @@ impl VirtualDom { self.ensure_drop_safety(id); if let Some(root) = self.scopes[id.0].as_ref().try_root_node() { - if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { + if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { self.drop_scope_inner(node) } } if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } { - if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { + if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { self.drop_scope_inner(node) } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index b0bca26d5..f54684410 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -433,9 +433,9 @@ impl<'b> VirtualDom { use DynamicNode::*; match node { Text(text) => self.create_dynamic_text(template, text, idx), - Fragment(frag) => self.create_fragment(frag), - Placeholder(frag) => self.create_placeholder(frag, template, idx), + Placeholder(place) => self.create_placeholder(place, template, idx), Component(component) => self.create_component_node(template, component, idx), + Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(), } } @@ -487,40 +487,38 @@ impl<'b> VirtualDom { 0 } - pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize { - nodes.iter().map(|child| self.create(child)).sum() - } - pub(super) fn create_component_node( &mut self, template: &'b VNode<'b>, component: &'b VComponent<'b>, idx: usize, ) -> usize { - let scope = match component.props.take() { - Some(props) => { - let unbounded_props: Box = unsafe { std::mem::transmute(props) }; - let scope = self.new_scope(unbounded_props, component.name); - scope.id - } + use RenderReturn::*; - // Component is coming back, it probably still exists, right? - None => component.scope.get().unwrap(), - }; + // Load up a ScopeId for this vcomponent + let scope = self.load_scope_from_vcomponent(component); component.scope.set(Some(scope)); - let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; - - use RenderReturn::*; - - match return_nodes { - Sync(Some(t)) => self.mount_component(scope, template, t, idx), - Sync(None) => todo!("Propogate error upwards"), - Async(_) => self.mount_component_placeholder(template, idx, scope), + match unsafe { self.run_scope(scope).extend_lifetime_ref() } { + Ready(t) => self.mount_component(scope, template, t, idx), + Aborted(t) => self.mount_aborted(template, t), + Pending(_) => self.mount_async(template, idx, scope), } } + /// Load a scope from a vcomponent. If the props don't exist, that means the component is currently "live" + fn load_scope_from_vcomponent(&mut self, component: &VComponent) -> ScopeId { + component + .props + .take() + .map(|props| { + let unbounded_props: Box = unsafe { std::mem::transmute(props) }; + self.new_scope(unbounded_props, component.name).id + }) + .unwrap_or_else(|| component.scope.get().unwrap()) + } + fn mount_component( &mut self, scope: ScopeId, @@ -575,15 +573,17 @@ impl<'b> VirtualDom { 0 } + fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize { + let id = self.next_element(parent, &[]); + self.mutations.push(Mutation::CreatePlaceholder { id }); + placeholder.id.set(Some(id)); + 1 + } + /// Take the rendered nodes from a component and handle them if they were async /// /// IE simply assign an ID to the placeholder - fn mount_component_placeholder( - &mut self, - template: &VNode, - idx: usize, - scope: ScopeId, - ) -> usize { + fn mount_async(&mut self, template: &VNode, idx: usize, scope: ScopeId) -> usize { let new_id = self.next_element(template, template.template.get().node_paths[idx]); // Set the placeholder of the scope diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index fa11a6e2f..efd534fe7 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -30,29 +30,44 @@ impl<'b> VirtualDom { .try_load_node() .expect("Call rebuild before diffing"); - use RenderReturn::{Async, Sync}; + use RenderReturn::{Aborted, Pending, Ready}; match (old, new) { - (Sync(Some(l)), Sync(Some(r))) => self.diff_node(l, r), + // Normal pathway + (Ready(l), Ready(r)) => self.diff_node(l, r), - // Err cases - (Sync(Some(l)), Sync(None)) => self.diff_ok_to_err(l), - (Sync(None), Sync(Some(r))) => self.diff_err_to_ok(r), - (Sync(None), Sync(None)) => { /* nothing */ } + // Unwind the mutations if need be + (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p), - // Async - (Sync(Some(_l)), Async(_)) => todo!(), - (Sync(None), Async(_)) => todo!(), - (Async(_), Sync(Some(_r))) => todo!(), - (Async(_), Sync(None)) => { /* nothing */ } - (Async(_), Async(_)) => { /* nothing */ } + // Just move over the placeholder + (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), + + // Becomes async, do nothing while we ait + (Ready(_nodes), Pending(_fut)) => self.diff_ok_to_async(_nodes, scope), + + // Placeholder becomes something + // We should also clear the error now + (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), + + (Aborted(_), Pending(_)) => todo!("async should not resolve here"), + (Pending(_), Ready(_)) => todo!("async should not resolve here"), + (Pending(_), Aborted(_)) => todo!("async should not resolve here"), + (Pending(_), Pending(_)) => { + // All suspense should resolve before we diff it again + panic!("Should not roll from suspense to suspense."); + } }; } self.scope_stack.pop(); } - fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>) {} - fn diff_err_to_ok(&mut self, _l: &'b VNode<'b>) {} + fn diff_ok_to_async(&mut self, _new: &'b VNode<'b>, _scope: ScopeId) { + // + } + + fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _p: &'b VPlaceholder) { + todo!() + } fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { // If hot reloading is enabled, we need to make sure we're using the latest template @@ -129,7 +144,7 @@ impl<'b> VirtualDom { (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx), - (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, right), + (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), }; @@ -208,8 +223,12 @@ impl<'b> VirtualDom { ) { let m = self.create_component_node(right_template, right, idx); + let pre_edits = self.mutations.edits.len(); + self.remove_component_node(left, true); + assert!(self.mutations.edits.len() > pre_edits); + // We want to optimize the replace case to use one less mutation if possible // Since mutations are done in reverse, the last node removed will be the first in the stack // Instead of *just* removing it, we can use the replace mutation @@ -691,7 +710,8 @@ impl<'b> VirtualDom { Component(comp) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(node)) => self.push_all_real_nodes(node), + RenderReturn::Ready(node) => self.push_all_real_nodes(node), + RenderReturn::Aborted(_node) => todo!(), _ => todo!(), } } @@ -719,7 +739,11 @@ impl<'b> VirtualDom { } /// Simply replace a placeholder with a list of nodes - fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) { + fn replace_placeholder( + &mut self, + l: &'b VPlaceholder, + r: impl IntoIterator>, + ) { let m = self.create_children(r); let id = l.id.get().unwrap(); self.mutations.push(Mutation::ReplaceWith { id, m }); @@ -816,8 +840,11 @@ impl<'b> VirtualDom { .map(|path| path.len()); // if the path is 1 the attribute is in the root, so we don't need to clean it up // if the path is 0, the attribute is a not attached at all, so we don't need to clean it up - if let Some(..=1) = path_len { - continue; + + if let Some(len) = path_len { + if (..=1).contains(&len) { + continue; + } } let next_id = attr.mounted_element.get(); @@ -877,14 +904,20 @@ impl<'b> VirtualDom { } fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) { - let scope = comp.scope.take().unwrap(); + let scope = comp + .scope + .take() + .expect("VComponents to always have a scope"); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => { - println!("Removing component node sync {:?}", gen_muts); - self.remove_node(t, gen_muts) + RenderReturn::Ready(t) => self.remove_node(t, gen_muts), + RenderReturn::Aborted(t) => { + if let Some(id) = t.id.get() { + self.try_reclaim(id); + } + return; } - _ => todo!("cannot handle nonstandard nodes"), + _ => todo!(), }; let props = self.scopes[scope.0].props.take(); @@ -910,7 +943,7 @@ impl<'b> VirtualDom { Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => self.find_first_element(t), + RenderReturn::Ready(t) => self.find_first_element(t), _ => todo!("cannot handle nonstandard nodes"), } } @@ -926,7 +959,7 @@ impl<'b> VirtualDom { Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => self.find_last_element(t), + RenderReturn::Ready(t) => self.find_last_element(t), _ => todo!("cannot handle nonstandard nodes"), } } diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index fd8810f1a..071fbe48b 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,19 +1,203 @@ -use std::cell::RefCell; - -use crate::ScopeId; +use crate::{ScopeId, ScopeState}; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + fmt::Debug, +}; /// A boundary that will capture any errors from child components -#[allow(dead_code)] pub struct ErrorBoundary { - error: RefCell>, - id: ScopeId, + error: RefCell>, + _id: ScopeId, +} + +/// An instance of an error captured by a descendant component. +pub struct CapturedError { + /// The error captured by the error boundary + pub error: Box, + + /// The scope that threw the error + pub scope: ScopeId, +} + +impl CapturedError { + /// Downcast the error type into a concrete error type + pub fn downcast(&self) -> Option<&T> { + if TypeId::of::() == self.error.type_id() { + let raw = self.error.as_ref() as *const _ as *const T; + Some(unsafe { &*raw }) + } else { + None + } + } } impl ErrorBoundary { pub fn new(id: ScopeId) -> Self { Self { error: RefCell::new(None), - id, + _id: id, + } + } + + /// Push an error into this Error Boundary + pub fn insert_error(&self, scope: ScopeId, error: Box) { + self.error.replace(Some(CapturedError { error, scope })); + } +} + +/// A trait to allow results to be thrown upwards to the nearest Error Boundary +/// +/// The canonical way of using this trait is to throw results from hooks, aborting rendering +/// through question mark synax. The throw method returns an option that evalutes to None +/// if there is an error, injecting the error to the nearest error boundary. +/// +/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. +/// +/// The call stack is saved for this component and provided to the error boundary +/// +/// ```rust, ignore +/// +/// #[inline_props] +/// fn app(cx: Scope, count: String) -> Element { +/// let id: i32 = count.parse().throw(cx)?; +/// +/// cx.render(rsx! { +/// div { "Count {}" } +/// }) +/// } +/// ``` +pub trait Throw: Sized { + /// The value that will be returned in if the given value is `Ok`. + type Out; + + /// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary. + /// + /// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. + /// + /// The call stack is saved for this component and provided to the error boundary + /// + /// + /// Note that you can also manually throw errors using the throw method on `ScopeState` directly, + /// which is what this trait shells out to. + /// + /// + /// ```rust, ignore + /// + /// #[inline_props] + /// fn app(cx: Scope, count: String) -> Element { + /// let id: i32 = count.parse().throw(cx)?; + /// + /// cx.render(rsx! { + /// div { "Count {}" } + /// }) + /// } + /// ``` + fn throw(self, cx: &ScopeState) -> Option; + + /// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary. + /// + /// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. + /// + /// The call stack is saved for this component and provided to the error boundary + /// + /// + /// Note that you can also manually throw errors using the throw method on `ScopeState` directly, + /// which is what this trait shells out to. + /// + /// + /// ```rust, ignore + /// + /// #[inline_props] + /// fn app(cx: Scope, count: String) -> Element { + /// let id: i32 = count.parse().throw(cx)?; + /// + /// cx.render(rsx! { + /// div { "Count {}" } + /// }) + /// } + /// ``` + fn throw_with( + self, + cx: &ScopeState, + e: impl FnOnce() -> D, + ) -> Option; +} + +/// We call clone on any errors that can be owned out of a reference +impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result { + type Out = &'a T; + + fn throw(self, cx: &ScopeState) -> Option { + match self { + Ok(t) => Some(t), + Err(e) => { + cx.throw(e.to_owned()); + None + } + } + } + + fn throw_with( + self, + cx: &ScopeState, + err: impl FnOnce() -> D, + ) -> Option { + match self { + Ok(t) => Some(t), + Err(_e) => { + cx.throw(err()); + None + } } } } + +/// Or just throw errors we know about +impl Throw for Result { + type Out = T; + + fn throw(self, cx: &ScopeState) -> Option { + match self { + Ok(t) => Some(t), + Err(e) => { + cx.throw(e); + None + } + } + } + + fn throw_with( + self, + cx: &ScopeState, + error: impl FnOnce() -> D, + ) -> Option { + self.ok().or_else(|| { + cx.throw(error()); + None + }) + } +} + +/// Or just throw errors we know about +impl Throw for Option { + type Out = T; + + fn throw(self, cx: &ScopeState) -> Option { + self.or_else(|| { + cx.throw("None error."); + None + }) + } + + fn throw_with( + self, + cx: &ScopeState, + error: impl FnOnce() -> D, + ) -> Option { + self.or_else(|| { + cx.throw(error()); + None + }) + } +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dcf7d7862..f13c20b43 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -70,10 +70,10 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, Attribute, AttributeValue, Component, DynamicNode, Element, ElementId, Event, - Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, - ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, TemplateAttribute, - TemplateNode, VComponent, VNode, VText, VirtualDom, + fc_to_builder, Attribute, AttributeValue, CapturedError, Component, DynamicNode, Element, + ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, + RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, + TemplateAttribute, TemplateNode, VComponent, VNode, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -83,7 +83,7 @@ pub mod prelude { pub use crate::innerlude::{ fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, - VNode, VirtualDom, + Throw, VNode, VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 24f2e083d..dee2f32b3 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -21,10 +21,22 @@ pub type TemplateId = &'static str; /// you might need to handle the case where there's no node immediately ready. pub enum RenderReturn<'a> { /// A currently-available element - Sync(Element<'a>), + Ready(VNode<'a>), + + /// The component aborted rendering early. It might've thrown an error. + /// + /// In its place we've produced a placeholder to locate its spot in the dom when + /// it recovers. + Aborted(VPlaceholder), /// An ongoing future that will resolve to a [`Element`] - Async(BumpBox<'a, dyn Future> + 'a>), + Pending(BumpBox<'a, dyn Future> + 'a>), +} + +impl<'a> Default for RenderReturn<'a> { + fn default() -> Self { + RenderReturn::Aborted(VPlaceholder::default()) + } } /// A reference to a template along with any context needed to hydrate it @@ -574,7 +586,10 @@ pub trait ComponentReturn<'a, A = ()> { impl<'a> ComponentReturn<'a> for Element<'a> { fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> { - RenderReturn::Sync(self) + match self { + Some(node) => RenderReturn::Ready(node), + None => RenderReturn::default(), + } } } @@ -586,7 +601,7 @@ where { fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> { let f: &mut dyn Future> = cx.bump().alloc(self); - RenderReturn::Async(unsafe { BumpBox::from_raw(f) }) + RenderReturn::Pending(unsafe { BumpBox::from_raw(f) }) } } diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index a6442184d..7aed417b1 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -8,7 +8,7 @@ mod waker; pub use suspense::*; pub use task::*; -pub use waker::RcWake; +pub use waker::ArcWake; /// The type of message that can be sent to the scheduler. /// @@ -25,16 +25,16 @@ pub(crate) enum SchedulerMsg { SuspenseNotified(SuspenseId), } -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc, sync::Arc}; pub(crate) struct Scheduler { pub sender: futures_channel::mpsc::UnboundedSender, /// Tasks created with cx.spawn - pub tasks: RefCell>>, + pub tasks: RefCell>>, /// Async components - pub leaves: RefCell>>, + pub leaves: RefCell>>, } impl Scheduler { diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 8d7d7337e..c7cd650bb 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -1,11 +1,11 @@ -use super::{waker::RcWake, SchedulerMsg}; +use super::{waker::ArcWake, SchedulerMsg}; use crate::ElementId; use crate::{innerlude::Mutations, Element, ScopeId}; use std::future::Future; +use std::sync::Arc; use std::{ cell::{Cell, RefCell}, collections::HashSet, - rc::Rc, }; /// An ID representing an ongoing suspended component @@ -42,8 +42,8 @@ pub(crate) struct SuspenseLeaf { pub(crate) task: *mut dyn Future>, } -impl RcWake for SuspenseLeaf { - fn wake_by_ref(arc_self: &Rc) { +impl ArcWake for SuspenseLeaf { + fn wake_by_ref(arc_self: &Arc) { arc_self.notified.set(true); _ = arc_self .tx diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index fb4378104..8c1d7ca76 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -1,8 +1,9 @@ -use super::{waker::RcWake, Scheduler, SchedulerMsg}; +use super::{waker::ArcWake, Scheduler, SchedulerMsg}; use crate::ScopeId; use std::cell::RefCell; use std::future::Future; -use std::{pin::Pin, rc::Rc}; +use std::pin::Pin; +use std::sync::Arc; /// A task's unique identifier. /// @@ -35,7 +36,7 @@ impl Scheduler { let entry = tasks.vacant_entry(); let task_id = TaskId(entry.key()); - entry.insert(Rc::new(LocalTask { + entry.insert(Arc::new(LocalTask { id: task_id, tx: self.sender.clone(), task: RefCell::new(Box::pin(task)), @@ -57,8 +58,8 @@ impl Scheduler { } } -impl RcWake for LocalTask { - fn wake_by_ref(arc_self: &Rc) { +impl ArcWake for LocalTask { + fn wake_by_ref(arc_self: &Arc) { _ = arc_self .tx .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)); diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 8dff17b01..205bd8ea0 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -10,7 +10,7 @@ use crate::{ ScopeId, TaskId, VNode, VirtualDom, }; -use super::{waker::RcWake, SuspenseId}; +use super::{waker::ArcWake, SuspenseId}; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -67,18 +67,21 @@ impl VirtualDom { // we should attach them to that component and then render its children // continue rendering the tree until we hit yet another suspended component if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { - // safety: we're not going to modify the suspense context but we don't want to make a clone of it let fiber = self.acquire_suspense_boundary(leaf.scope_id); let scope = &mut self.scopes[scope_id.0]; let arena = scope.current_frame(); - let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes)); + let ret = arena.bump.alloc(match new_nodes { + Some(new) => RenderReturn::Ready(new), + None => RenderReturn::default(), + }); + arena.node.set(ret); fiber.waiting_on.borrow_mut().remove(&id); - if let RenderReturn::Sync(Some(template)) = ret { + if let RenderReturn::Ready(template) = ret { let mutations_ref = &mut fiber.mutations.borrow_mut(); let mutations = &mut **mutations_ref; let template: &VNode = unsafe { std::mem::transmute(template) }; diff --git a/packages/core/src/scheduler/waker.rs b/packages/core/src/scheduler/waker.rs index 762020167..fc75fe8fd 100644 --- a/packages/core/src/scheduler/waker.rs +++ b/packages/core/src/scheduler/waker.rs @@ -1,36 +1,37 @@ +use std::mem; +use std::sync::Arc; use std::task::{RawWaker, RawWakerVTable, Waker}; -use std::{mem, rc::Rc}; -pub trait RcWake: Sized { +pub trait ArcWake: Sized { /// Create a waker from this self-wakening object - fn waker(self: &Rc) -> Waker { - unsafe fn rc_vtable() -> &'static RawWakerVTable { + fn waker(self: &Arc) -> Waker { + unsafe fn rc_vtable() -> &'static RawWakerVTable { &RawWakerVTable::new( |data| { - let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); + let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); let _rc_clone: mem::ManuallyDrop<_> = arc.clone(); RawWaker::new(data, rc_vtable::()) }, - |data| Rc::from_raw(data.cast::()).wake(), + |data| Arc::from_raw(data.cast::()).wake(), |data| { - let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); - RcWake::wake_by_ref(&arc); + let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); + ArcWake::wake_by_ref(&arc); }, - |data| drop(Rc::::from_raw(data.cast::())), + |data| drop(Arc::::from_raw(data.cast::())), ) } unsafe { Waker::from_raw(RawWaker::new( - Rc::into_raw(self.clone()).cast(), + Arc::into_raw(self.clone()).cast(), rc_vtable::(), )) } } - fn wake_by_ref(arc_self: &Rc); + fn wake_by_ref(arc_self: &Arc); - fn wake(self: Rc) { + fn wake(self: Arc) { Self::wake_by_ref(&self) } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 9c0f96db3..fbe865021 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -4,7 +4,7 @@ use crate::{ innerlude::DirtyScope, innerlude::{SuspenseId, SuspenseLeaf}, nodes::RenderReturn, - scheduler::RcWake, + scheduler::ArcWake, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; @@ -13,7 +13,7 @@ use futures_util::FutureExt; use std::{ mem, pin::Pin, - rc::Rc, + sync::Arc, task::{Context, Poll}, }; @@ -79,17 +79,18 @@ impl VirtualDom { // safety: due to how we traverse the tree, we know that the scope is not currently aliased let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref(); let props: &dyn AnyProps = mem::transmute(props); + props.render(scope).extend_lifetime() }; // immediately resolve futures that can be resolved - if let RenderReturn::Async(task) = &mut new_nodes { + if let RenderReturn::Pending(task) = &mut new_nodes { let mut leaves = self.scheduler.leaves.borrow_mut(); let entry = leaves.vacant_entry(); let suspense_id = SuspenseId(entry.key()); - let leaf = Rc::new(SuspenseLeaf { + let leaf = Arc::new(SuspenseLeaf { scope_id, task: task.as_mut(), id: suspense_id, @@ -108,7 +109,11 @@ impl VirtualDom { match pinned.poll_unpin(&mut cx) { // If nodes are produced, then set it and we can break Poll::Ready(nodes) => { - new_nodes = RenderReturn::Sync(nodes); + new_nodes = match nodes { + Some(nodes) => RenderReturn::Ready(nodes), + None => RenderReturn::default(), + }; + break; } @@ -150,6 +155,6 @@ impl VirtualDom { }); // rebind the lifetime now that its stored internally - unsafe { mem::transmute(allocated) } + unsafe { allocated.extend_lifetime_ref() } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 6ceaa5eae..f6c81db28 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -4,7 +4,7 @@ use crate::{ arena::ElementId, bump_frame::BumpFrame, innerlude::{DynamicNode, EventHandler, VComponent, VText}, - innerlude::{Scheduler, SchedulerMsg}, + innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, lazynodes::LazyNodes, nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn}, Attribute, AttributeValue, Element, Event, Properties, TaskId, @@ -14,7 +14,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, - fmt::Arguments, + fmt::{Arguments, Debug}, future::Future, rc::Rc, sync::Arc, @@ -509,6 +509,19 @@ impl<'src> ScopeState { AttributeValue::Listener(RefCell::new(Some(boxed))) } + /// Inject an error into the nearest error boundary and quit rendering + /// + /// The error doesn't need to implement Error or any specific traits since the boundary + /// itself will downcast the error into a trait object. + pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { + if let Some(cx) = self.consume_context::>() { + cx.insert_error(self.scope_id(), Box::new(error)); + } + + // Always return none during a throw + None + } + /// Store a value between renders. The foundational hook for all other hooks. /// /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index c772b8317..47cbe7245 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -466,7 +466,7 @@ impl VirtualDom { self.register_template_first_byte_index(template); // iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine for (_, scope) in &self.scopes { - if let Some(RenderReturn::Sync(Some(sync))) = scope.try_root_node() { + if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() { if sync.template.get().name.rsplit_once(':').unwrap().0 == template.name.rsplit_once(':').unwrap().0 { @@ -503,7 +503,7 @@ impl VirtualDom { pub fn rebuild(&mut self) -> Mutations { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root - RenderReturn::Sync(Some(node)) => { + RenderReturn::Ready(node) => { let m = self.create_scope(ScopeId(0), node); self.mutations.edits.push(Mutation::AppendChildren { id: ElementId(0), @@ -511,8 +511,8 @@ impl VirtualDom { }); } // If an error occurs, we should try to render the default error component and context where the error occured - RenderReturn::Sync(None) => panic!("Cannot catch errors during rebuild"), - RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"), + RenderReturn::Aborted(_placeholder) => panic!("Cannot catch errors during rebuild"), + RenderReturn::Pending(_) => unreachable!("Root scope cannot be an async component"), } self.finalize() diff --git a/packages/core/tests/error_boundary.rs b/packages/core/tests/error_boundary.rs new file mode 100644 index 000000000..7dbfca343 --- /dev/null +++ b/packages/core/tests/error_boundary.rs @@ -0,0 +1,58 @@ +use dioxus::prelude::*; +use futures_util::Future; + +#[test] +fn catches_panic() { + let mut dom = VirtualDom::new(app); + + let a = dom.rebuild(); + + dbg!(a); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Title" } + + NoneChild {} + } + }) +} + +fn NoneChild(cx: Scope) -> Element { + None +} + +fn PanicChild(cx: Scope) -> Element { + panic!("Rendering panicked for whatever reason"); + + cx.render(rsx! { + h1 { "It works!" } + }) +} + +fn ThrowChild(cx: Scope) -> Element { + cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?; + + let g: i32 = "123123".parse().throw(cx)?; + + cx.render(rsx! { + div {} + }) +} + +fn custom_allocator(cx: Scope) -> Element { + let g = String::new(); + + let p = g.as_str(); + + let g2 = cx.use_hook(|| 123); + // cx.spawn(async move { + + // // + // // println!("Thig is {p}"); + // }); + + None +} diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index d077966eb..3ca0303c7 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -55,6 +55,9 @@ pub struct LinkProps<'a> { /// Pass children into the `` element pub children: Element<'a>, + + /// The onclick event handler. + pub onclick: Option>, } /// A component that renders a link to a route. @@ -119,7 +122,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { title: format_args!("{}", title.unwrap_or("")), prevent_default: "{prevent_default}", target: format_args!("{}", if * new_tab { "_blank" } else { "" }), - onclick: move |_| { + onclick: move |evt| { log::trace!("Clicked link to {}", to); if !outerlink { @@ -138,6 +141,10 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { ); } } + + if let Some(onclick) = cx.props.onclick.as_ref() { + onclick.call(evt); + } }, children } diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs index a1858c318..3bbe50b7e 100644 --- a/packages/router/src/components/redirect.rs +++ b/packages/router/src/components/redirect.rs @@ -47,5 +47,5 @@ pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element { router.replace_route(cx.props.to, None, None); } - None + cx.render(rsx!(())) } diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 37dbe4d26..cf665d2e1 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -52,6 +52,6 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { cx.render(rsx!(&cx.props.children)) } else { log::debug!("Route should *not* render: {:?}", cx.scope_id()); - None + cx.render(rsx!(())) } } diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 2e6b2cd4e..e0b08e22f 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -107,6 +107,23 @@ impl UseRoute { { self.segment(name).map(|value| value.parse::()) } + + /// Get the named parameter from the path, as defined in your router. The + /// value will be parsed into the type specified by `T` by calling + /// `value.parse::()`. This method returns `None` if the named + /// parameter does not exist in the current path. + pub fn parse_segment_or_404(&self, name: &str) -> Option + where + T: FromStr, + { + match self.parse_segment(name) { + Some(Ok(val)) => Some(val), + _ => { + // todo: throw a 404 + None + } + } + } } // The entire purpose of this struct is to unubscribe this component when it is unmounted. diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index 5aaac6ad3..7a931859f 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -30,3 +30,12 @@ mod service; pub use routecontext::*; pub use service::*; + +/// An error specific to the Router +#[derive(Debug)] +pub enum Error { + /// The route was not found while trying to navigate to it. + /// + /// This will force the router to redirect to the 404 page. + NotFound, +} diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 531e108ed..05dd7b80b 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -51,7 +51,7 @@ impl Renderer { ) -> std::fmt::Result { // We should never ever run into async or errored nodes in SSR // Error boundaries and suspense boundaries will convert these to sync - if let RenderReturn::Sync(Some(node)) = dom.get_scope(scope).unwrap().root_node() { + if let RenderReturn::Ready(node) = dom.get_scope(scope).unwrap().root_node() { self.render_template(buf, dom, node)? }; @@ -89,7 +89,7 @@ impl Renderer { let scope = dom.get_scope(id).unwrap(); let node = scope.root_node(); match node { - RenderReturn::Sync(Some(node)) => { + RenderReturn::Ready(node) => { self.render_template(buf, dom, node)? } _ => todo!( diff --git a/packages/tui/examples/tui_colorpicker.rs b/packages/tui/examples/tui_colorpicker.rs index ef5add4ec..f3598af4f 100644 --- a/packages/tui/examples/tui_colorpicker.rs +++ b/packages/tui/examples/tui_colorpicker.rs @@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element { width: "100%", background_color: "hsl({hue}, 70%, {brightness}%)", onmousemove: move |evt| { - if let RenderReturn::Sync(Some(node)) = cx.root_node() { + if let RenderReturn::Ready(node) = cx.root_node() { if let Some(id) = node.root_ids.get(0){ let node = tui_query.get(id); let Size{width, height} = node.size().unwrap(); diff --git a/packages/tui/src/widgets/mod.rs b/packages/tui/src/widgets/mod.rs index 011e95127..cbf3d6d50 100644 --- a/packages/tui/src/widgets/mod.rs +++ b/packages/tui/src/widgets/mod.rs @@ -10,7 +10,7 @@ use dioxus_core::{ElementId, RenderReturn, Scope}; pub use input::*; pub(crate) fn get_root_id(cx: Scope) -> Option { - if let RenderReturn::Sync(Some(sync)) = cx.root_node() { + if let RenderReturn::Ready(sync) = cx.root_node() { sync.root_ids.get(0) } else { None diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index fdae04a9a..33d2c1b81 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -121,12 +121,10 @@ impl WebsysDom { } = attr { match namespace { - Some(ns) if *ns == "style" => el - .dyn_ref::() - .unwrap() - .style() - .set_property(name, value) - .unwrap(), + Some(ns) if *ns == "style" => { + el.dyn_ref::() + .map(|f| f.style().set_property(name, value)); + } Some(ns) => el.set_attribute_ns(Some(ns), name, value).unwrap(), None => el.set_attribute(name, value).unwrap(), } diff --git a/packages/web/src/hot_reload.rs b/packages/web/src/hot_reload.rs index 2c94d8cf3..cf73dc219 100644 --- a/packages/web/src/hot_reload.rs +++ b/packages/web/src/hot_reload.rs @@ -18,15 +18,15 @@ pub(crate) fn init() -> UnboundedReceiver> { #[cfg(debug_assertions)] pub(crate) fn init() -> UnboundedReceiver> { - use core::panic; use std::convert::TryInto; + use serde::Deserialize; + let window = web_sys::window().unwrap(); - let protocol = if window.location().protocol().unwrap() == "https:" { - "wss:" - } else { - "ws:" + let protocol = match window.location().protocol().unwrap() { + prot if prot == "https:" => "wss:", + _ => "ws:", }; let url = format!( @@ -43,10 +43,11 @@ pub(crate) fn init() -> UnboundedReceiver> { if let Ok(text) = e.data().dyn_into::() { let text: Result = text.try_into(); if let Ok(string) = text { - match serde_json::from_str(Box::leak(string.into_boxed_str())) { - Ok(template) => _ = tx.unbounded_send(template), - Err(e) => panic!("Failed to parse template: {}", e), - } + let val = serde_json::from_str::(&string).unwrap(); + // leak the value + let val: &'static serde_json::Value = Box::leak(Box::new(val)); + let template: Template<'_> = Template::deserialize(val).unwrap(); + tx.unbounded_send(template).unwrap(); } } }) as Box);