diff --git a/Cargo.toml b/Cargo.toml index 3f92e2c62..e58d54825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ dioxus-web = { path = "./packages/web", version = "^0.0.3", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.3", optional = true } dioxus-ssr = { path = "./packages/ssr", version = "0.1.1", optional = true } -# dioxus-router = { path = "./packages/router", optional = true } +dioxus-router = { path = "./packages/router", optional = true } dioxus-mobile = { path = "./packages/mobile", version = "^0.0.2", optional = true } # dioxus-liveview = { path = "./packages/liveview", optional = true } @@ -33,7 +33,7 @@ html = ["dioxus-html"] ssr = ["dioxus-ssr"] web = ["dioxus-web"] desktop = ["dioxus-desktop"] -# router = ["dioxus-router"] +router = ["dioxus-router"] # "dioxus-router/web" # "dioxus-router/desktop" diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 11855d487..142ba5413 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -27,7 +27,7 @@ Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust Once installed, make sure to install wasm32-unknown-unknown as a target if you're planning on deploying your app to the web. ``` -rustup target add wasm32-unknown-uknown +rustup target add wasm32-unknown-unknown ``` ### Dioxus-CLI for dev server, bundling, etc. diff --git a/docs/reference/src/web/index.md b/docs/reference/src/web/index.md index dd1afb3e0..062cba177 100644 --- a/docs/reference/src/web/index.md +++ b/docs/reference/src/web/index.md @@ -19,7 +19,7 @@ $ cargo install trunk Make sure the `wasm32-unknown-unknown` target is installed: ```shell -$ rustup target add wasm32-unknown-uknown +$ rustup target add wasm32-unknown-unknown ``` Create a new project: diff --git a/examples/hello_world.rs b/examples/hello_world.rs index ab7bfffa3..5b732f140 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,7 +1,8 @@ use dioxus::prelude::*; +use dioxus_desktop::tao::menu::MenuBar; fn main() { - dioxus::desktop::launch(app); + dioxus::desktop::launch_cfg(app, |c| c.with_window(|w| w.with_menu(MenuBar::default()))); } fn app(cx: Scope) -> Element { diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index fe8475131..211539bbd 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -19,7 +19,7 @@ use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, - token, Expr, Ident, Result, Token, + token, Expr, Ident, LitStr, Result, Token, }; pub struct Component { @@ -135,8 +135,6 @@ impl ToTokens for Component { None => quote! { None }, }; - // #children - tokens.append_all(quote! { __cx.component( #name, @@ -155,7 +153,7 @@ pub struct ComponentField { enum ContentField { ManExpr(Expr), - + Formatted(LitStr), OnHandlerRaw(Expr), } @@ -163,6 +161,9 @@ impl ToTokens for ContentField { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { ContentField::ManExpr(e) => e.to_tokens(tokens), + ContentField::Formatted(s) => tokens.append_all(quote! { + __cx.raw_text(format_args_f!(#s)).0 + }), ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { __cx.bump().alloc(#e) }), @@ -175,13 +176,27 @@ impl Parse for ComponentField { let name = Ident::parse_any(input)?; input.parse::()?; - let name_str = name.to_string(); - let content = if name_str.starts_with("on") { - ContentField::OnHandlerRaw(input.parse()?) - } else { - ContentField::ManExpr(input.parse::()?) - }; + if name.to_string().starts_with("on") { + let content = ContentField::OnHandlerRaw(input.parse()?); + return Ok(Self { name, content }); + } + if name.to_string() == "key" { + let content = ContentField::ManExpr(input.parse()?); + return Ok(Self { name, content }); + } + + if input.peek(LitStr) && input.peek2(Token![,]) { + let content = ContentField::Formatted(input.parse()?); + return Ok(Self { name, content }); + } + + if input.peek(LitStr) && input.peek2(LitStr) { + let item = input.parse::().unwrap(); + proc_macro_error::emit_error!(item, "This attribute is misisng a trailing comma") + } + + let content = ContentField::ManExpr(input.parse()?); Ok(Self { name, content }) } } diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 71e0e90ac..a85b46694 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -46,7 +46,7 @@ impl Parse for Element { content.parse::()?; - if content.peek(LitStr) { + if content.peek(LitStr) && content.peek2(Token![,]) { let value = content.parse::()?; attributes.push(ElementAttrNamed { el_name: el_name.clone(), diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 4f8af6601..499d9543e 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -263,7 +263,7 @@ impl<'bump> DiffState<'bump> { }; if deadline_expired() { - log::debug!("Deadline expired before we could finish!"); + log::trace!("Deadline expired before we could finish!"); return false; } } @@ -390,10 +390,7 @@ impl<'bump> DiffState<'bump> { self.stack.add_child_count(1); if let Some(cur_scope_id) = self.stack.current_scope() { - let scope = self.scopes.get_scope(cur_scope_id).unwrap(); - for listener in *listeners { - self.attach_listener_to_scope(listener, scope); listener.mounted_node.set(Some(real_id)); self.mutations.new_event_listener(listener, cur_scope_id); } @@ -426,16 +423,25 @@ impl<'bump> DiffState<'bump> { fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { let parent_idx = self.stack.current_scope().unwrap(); - // Insert a new scope into our component list - let props: Box = vcomponent.props.borrow_mut().take().unwrap(); - let props: Box = unsafe { std::mem::transmute(props) }; - let new_idx = self.scopes.new_with_key( - vcomponent.user_fc, - props, - Some(parent_idx), - self.stack.element_stack.last().copied().unwrap(), - 0, - ); + // the component might already exist - if it does, we need to reuse it + // this makes figure out when to drop the component more complicated + let new_idx = if let Some(idx) = vcomponent.scope.get() { + assert!(self.scopes.get_scope(idx).is_some()); + idx + } else { + // Insert a new scope into our component list + let props: Box = vcomponent.props.borrow_mut().take().unwrap(); + let props: Box = unsafe { std::mem::transmute(props) }; + let new_idx = self.scopes.new_with_key( + vcomponent.user_fc, + props, + Some(parent_idx), + self.stack.element_stack.last().copied().unwrap(), + 0, + ); + + new_idx + }; // Actually initialize the caller's slot with the right address vcomponent.scope.set(Some(new_idx)); @@ -446,9 +452,6 @@ impl<'bump> DiffState<'bump> { } false => { // track this component internally so we know the right drop order - let cur_scope = self.scopes.get_scope(parent_idx).unwrap(); - let extended = unsafe { std::mem::transmute(vcomponent) }; - cur_scope.items.borrow_mut().borrowed_props.push(extended); } } @@ -580,8 +583,6 @@ impl<'bump> DiffState<'bump> { // // TODO: take a more efficient path than this if let Some(cur_scope_id) = self.stack.current_scope() { - let scope = self.scopes.get_scope(cur_scope_id).unwrap(); - if old.listeners.len() == new.listeners.len() { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { if old_l.event != new_l.event { @@ -590,7 +591,6 @@ impl<'bump> DiffState<'bump> { self.mutations.new_event_listener(new_l, cur_scope_id); } new_l.mounted_node.set(old_l.mounted_node.get()); - self.attach_listener_to_scope(new_l, scope); } } else { for listener in old.listeners { @@ -600,7 +600,6 @@ impl<'bump> DiffState<'bump> { for listener in new.listeners { listener.mounted_node.set(Some(root)); self.mutations.new_event_listener(listener, cur_scope_id); - self.attach_listener_to_scope(listener, scope); } } } @@ -687,6 +686,12 @@ impl<'bump> DiffState<'bump> { new: &'bump VComponent<'bump>, ) { let scope_addr = old.scope.get().unwrap(); + log::trace!("diff_component_nodes: {:?}", scope_addr); + + if std::ptr::eq(old, new) { + log::trace!("skipping component diff - component is the sames"); + return; + } // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { @@ -728,11 +733,14 @@ impl<'bump> DiffState<'bump> { // this should auto drop the previous props self.scopes.run_scope(scope_addr); + self.mutations.dirty_scopes.insert(scope_addr); + self.diff_node( self.scopes.wip_head(scope_addr), self.scopes.fin_head(scope_addr), ); } else { + log::trace!("memoized"); // memoization has taken place drop(new_props); }; @@ -1237,7 +1245,10 @@ impl<'bump> DiffState<'bump> { let scope_id = c.scope.get().unwrap(); - self.scopes.try_remove(scope_id).unwrap(); + // we can only remove components if they are actively being diffed + if self.stack.scope_stack.contains(&c.originator) { + self.scopes.try_remove(scope_id).unwrap(); + } } } } @@ -1288,15 +1299,13 @@ impl<'bump> DiffState<'bump> { let scope_id = c.scope.get().unwrap(); let root = self.scopes.root_node(scope_id); self.remove_nodes(Some(root), gen_muts); - self.scopes.try_remove(scope_id).unwrap(); + + // we can only remove this node if the originator is actively + if self.stack.scope_stack.contains(&c.originator) { + self.scopes.try_remove(scope_id).unwrap(); + } } } } } - - /// Adds a listener closure to a scope during diff. - fn attach_listener_to_scope(&mut self, listener: &'bump Listener<'bump>, scope: &ScopeState) { - let long_listener = unsafe { std::mem::transmute(listener) }; - scope.items.borrow_mut().listeners.push(long_listener) - } } diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index bd551da50..1340c916c 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -60,10 +60,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> { } } - pub fn new(_val: F) -> Self - where - F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, - { + pub fn new(_val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self { // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch let mut slot = Some(_val); @@ -240,57 +237,77 @@ fn round_to_words(len: usize) -> usize { (len + mem::size_of::() - 1) / mem::size_of::() } -#[test] -fn it_works() { - let bump = bumpalo::Bump::new(); +#[cfg(test)] +mod tests { + use super::*; + use crate::innerlude::{Element, Scope, VirtualDom}; - let factory = NodeFactory { bump: &bump }; - - let caller = LazyNodes::new_some(|f| { - // - f.text(format_args!("hello world!")) - }); - let g = caller.call(factory); - - dbg!(g); -} - -#[test] -fn it_drops() { - use std::rc::Rc; - let bump = bumpalo::Bump::new(); - - let factory = NodeFactory { bump: &bump }; - - struct DropInner { - id: i32, - } - impl Drop for DropInner { - fn drop(&mut self) { - log::debug!("dropping inner"); + #[test] + fn it_works() { + fn app(cx: Scope<()>) -> Element { + cx.render(LazyNodes::new_some(|f| { + f.text(format_args!("hello world!")) + })) } + + let mut dom = VirtualDom::new(app); + dom.rebuild(); + + let g = dom.base_scope().root_node(); + dbg!(g); } - let val = Rc::new(10); - let caller = { - let it = (0..10) - .map(|i| { - let val = val.clone(); + #[test] + fn it_drops() { + use std::rc::Rc; - LazyNodes::new_some(move |f| { - log::debug!("hell closure"); - let inner = DropInner { id: i }; - f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) + struct AppProps { + inner: Rc, + } + + fn app(cx: Scope) -> Element { + struct DropInner { + id: i32, + } + impl Drop for DropInner { + fn drop(&mut self) { + log::debug!("dropping inner"); + } + } + + let caller = { + let it = (0..10) + .map(|i| { + let val = cx.props.inner.clone(); + + LazyNodes::new_some(move |f| { + log::debug!("hell closure"); + let inner = DropInner { id: i }; + f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) + }) + }) + .collect::>(); + + LazyNodes::new_some(|f| { + log::debug!("main closure"); + f.fragment_from_iter(it) }) - }) - .collect::>(); + }; - LazyNodes::new_some(|f| { - log::debug!("main closure"); - f.fragment_from_iter(it) - }) - }; + cx.render(caller) + } - let _ = caller.call(factory); - assert_eq!(Rc::strong_count(&val), 1); + let inner = Rc::new(0); + let mut dom = VirtualDom::new_with_props( + app, + AppProps { + inner: inner.clone(), + }, + ); + dom.rebuild(); + + drop(dom); + + assert_eq!(Rc::strong_count(&inner), 1); + } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 0acb08829..ed4a6b0df 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -350,6 +350,7 @@ impl Clone for EventHandler<'_> { /// Only supports the functional syntax pub struct VComponent<'src> { pub key: Option<&'src str>, + pub originator: ScopeId, pub scope: Cell>, pub can_memoize: bool, pub user_fc: *const (), @@ -395,12 +396,16 @@ impl

AnyProps for VComponentProps

{ /// This struct adds metadata to the final VNode about listeners, attributes, and children #[derive(Copy, Clone)] pub struct NodeFactory<'a> { + pub(crate) scope: &'a ScopeState, pub(crate) bump: &'a Bump, } impl<'a> NodeFactory<'a> { - pub fn new(bump: &'a Bump) -> NodeFactory<'a> { - NodeFactory { bump } + pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> { + NodeFactory { + scope, + bump: &scope.wip_frame().bump, + } } #[inline] @@ -473,6 +478,12 @@ impl<'a> NodeFactory<'a> { ) -> VNode<'a> { let key = key.map(|f| self.raw_text(f).0); + let mut items = self.scope.items.borrow_mut(); + for listener in listeners { + let long_listener = unsafe { std::mem::transmute(listener) }; + items.listeners.push(long_listener); + } + VNode::Element(self.bump.alloc(VElement { tag: tag_name, key, @@ -511,11 +522,12 @@ impl<'a> NodeFactory<'a> { where P: Properties + 'a, { - VNode::Component(self.bump.alloc(VComponent { + let vcomp = self.bump.alloc(VComponent { key: key.map(|f| self.raw_text(f).0), scope: Default::default(), can_memoize: P::IS_STATIC, user_fc: component as *const (), + originator: self.scope.scope_id(), props: RefCell::new(Some(Box::new(VComponentProps { // local_props: RefCell::new(Some(props)), // heap_props: RefCell::new(None), @@ -528,7 +540,15 @@ impl<'a> NodeFactory<'a> { // the transformation from this specific lifetime to the for<'a> lifetime render_fn: unsafe { std::mem::transmute(component) }, }))), - })) + }); + + if !P::IS_STATIC { + let vcomp = &*vcomp; + let vcomp = unsafe { std::mem::transmute(vcomp) }; + self.scope.items.borrow_mut().borrowed_props.push(vcomp); + } + + VNode::Component(vcomp) } pub fn listener(self, event: &'static str, callback: EventHandler<'a>) -> Listener<'a> { @@ -730,17 +750,15 @@ impl IntoVNode<'_> for Arguments<'_> { impl<'a> IntoVNode<'a> for &Option> { fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { - let r = self.as_ref().map(|f| f.decouple()); - cx.fragment_from_iter(r) + self.as_ref() + .map(|f| f.into_vnode(cx)) + .unwrap_or_else(|| cx.fragment_from_iter(None as Option)) } } impl<'a> IntoVNode<'a> for &VNode<'a> { fn into_vnode(self, _cx: NodeFactory<'a>) -> VNode<'a> { + // borrowed nodes are strange self.decouple() } } - -trait IntoAcceptedVnode<'a> { - fn into_accepted_vnode(self, cx: NodeFactory<'a>) -> VNode<'a>; -} diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 8f35a3ec0..66d6cdff5 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -143,6 +143,7 @@ impl ScopeArena { // Removes a scope and its descendents from the arena pub fn try_remove(&self, id: ScopeId) -> Option<()> { + log::trace!("removing scope {:?}", id); self.ensure_drop_safety(id); // Safety: @@ -188,6 +189,8 @@ impl ScopeArena { /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will /// be dropped. pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { + log::trace!("Ensuring drop safety for scope {:?}", scope_id); + if let Some(scope) = self.get_scope(scope_id) { let mut items = scope.items.borrow_mut(); @@ -217,6 +220,7 @@ impl ScopeArena { // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners + log::trace!("Running scope {:?}", id); self.ensure_drop_safety(id); // todo: we *know* that this is aliased by the contents of the scope itself @@ -278,14 +282,19 @@ impl ScopeArena { let nodes = self.nodes.borrow(); let mut cur_el = Some(element); + log::trace!("calling listener {:?}, {:?}", event, element); let state = Rc::new(BubbleState::new()); while let Some(id) = cur_el.take() { if let Some(el) = nodes.get(id.0) { + log::trace!("Found valid receiver element"); + let real_el = unsafe { &**el }; if let VNode::Element(real_el) = real_el { for listener in real_el.listeners.borrow().iter() { if listener.event == event.name { + log::trace!("Found valid receiver event"); + if state.canceled.get() { // stop bubbling if canceled break; @@ -651,12 +660,14 @@ impl ScopeState { /// rsx!(cx, div { "hello {state.0}" }) /// } /// ``` - pub fn provide_context(&self, value: T) { + pub fn provide_context(&self, value: T) -> Rc { + let value = Rc::new(value); self.shared_contexts .borrow_mut() - .insert(TypeId::of::(), Rc::new(value)) + .insert(TypeId::of::(), value.clone()) .map(|f| f.downcast::().ok()) .flatten(); + value } /// Try to retrieve a SharedState with type T from the any parent Scope. @@ -713,6 +724,7 @@ impl ScopeState { ///``` pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option> { Some(rsx.call(NodeFactory { + scope: self, bump: &self.wip_frame().bump, })) } @@ -900,7 +912,7 @@ impl TaskQueue { } else { // todo: it should be okay to remote a fut while the queue is being polled // However, it's not currently possible to do that. - log::debug!("Unable to remove task from task queue. This is probably a bug."); + log::trace!("Unable to remove task from task queue. This is probably a bug."); } } pub(crate) fn has_tasks(&self) -> bool { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 3cbb3fe5d..29e8355cc 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -531,6 +531,9 @@ impl VirtualDom { diff_state.stack.element_stack.push(ElementId(0)); diff_state.stack.scope_stack.push(scope_id); diff_state.work(|| false); + self.dirty_scopes.clear(); + assert!(self.dirty_scopes.is_empty()); + diff_state.mutations } @@ -599,7 +602,7 @@ impl VirtualDom { pub fn render_vnodes<'a>(&'a self, lazy_nodes: LazyNodes<'a, '_>) -> &'a VNode<'a> { let scope = self.scopes.get_scope(ScopeId(0)).unwrap(); let frame = scope.wip_frame(); - let factory = NodeFactory { bump: &frame.bump }; + let factory = NodeFactory::new(scope); let node = lazy_nodes.call(factory); frame.bump.alloc(node) } diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 2d381fddb..bb4963b71 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -97,7 +97,7 @@ fn memo_works_properly() { cx.render(rsx!( div { "Hello, world! {name}" } - child(na: "asdfg".to_string()) + child(na: "asdfg".to_string() ) )) } @@ -285,3 +285,31 @@ fn basic() { dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); dom.work_with_deadline(|| false); } + +#[test] +fn leak_thru_children() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + Child { + name: "asd".to_string(), + } + }); + cx.render(rsx! { + div {} + }) + } + + #[inline_props] + fn Child(cx: Scope, name: String) -> Element { + rsx!(cx, div { "child {name}" }) + } + + let mut dom = new_dom(app, ()); + let _ = dom.rebuild(); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); +} diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 2ef907f78..7860c67f0 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -1,4 +1,3 @@ - function serialize_event(event) { switch (event.type) { case "copy": @@ -10,8 +9,8 @@ function serialize_event(event) { case "compositionstart": case "compositionupdate": return { - data: event.data - } + data: event.data, + }; case "keydown": case "keypress": @@ -29,7 +28,7 @@ function serialize_event(event) { repeat: event.repeat, which: event.which, // locale: event.locale, - } + }; case "focus": case "blur": @@ -45,7 +44,7 @@ function serialize_event(event) { } return { - value: value + value: value, }; case "input": @@ -60,7 +59,7 @@ function serialize_event(event) { } return { - value: value + value: value, }; } @@ -95,7 +94,7 @@ function serialize_event(event) { screen_x: event.screenX, screen_y: event.screenY, shift_key: event.shiftKey, - } + }; case "pointerdown": case "pointermove": @@ -130,7 +129,7 @@ function serialize_event(event) { twist: event.twist, pointer_type: event.pointerType, is_primary: event.isPrimary, - } + }; case "select": return {}; @@ -148,7 +147,7 @@ function serialize_event(event) { // changed_touches: event.changedTouches, // target_touches: event.targetTouches, // touches: event.touches, - } + }; case "scroll": return {}; @@ -208,17 +207,14 @@ function serialize_event(event) { default: return {}; } - - } - class Interpreter { constructor(root) { this.root = root; this.stack = [root]; this.listeners = { - "onclick": {} + onclick: {}, }; this.lastNodeWasText = false; this.nodes = [root]; @@ -304,35 +300,45 @@ class Interpreter { this.nodes[edit.root] = el; } - RemoveEventListener(edit) { } + RemoveEventListener(edit) {} NewEventListener(edit) { const event_name = edit.event_name; const mounted_node_id = edit.root; const scope = edit.scope; - const element = this.nodes[edit.root] - element.setAttribute(`dioxus-event-${event_name}`, `${scope}.${mounted_node_id}`); + const element = this.nodes[edit.root]; + element.setAttribute( + `dioxus-event-${event_name}`, + `${scope}.${mounted_node_id}` + ); if (this.listeners[event_name] === undefined) { this.listeners[event_name] = true; this.root.addEventListener(event_name, (event) => { - console.log("handling event", event); - const target = event.target; const real_id = target.getAttribute(`dioxus-id`); + + const should_prevent_default = target.getAttribute( + `dioxus-prevent-default` + ); + + let contents = serialize_event(event); + + if (should_prevent_default === `on${event.type}`) { + event.preventDefault(); + } + if (real_id == null) { - alert("no id"); return; } - rpc.call('user_event', { + rpc.call("user_event", { event: event_name, mounted_dom_id: parseInt(real_id), - contents: serialize_event(event), + contents: contents, }); - }); } } @@ -346,11 +352,11 @@ class Interpreter { const name = edit.field; const value = edit.value; const ns = edit.ns; - const node = this.nodes[edit.root] + const node = this.nodes[edit.root]; if (ns == "style") { node.style[name] = value; - } else if ((ns != null) || (ns != undefined)) { + } else if (ns != null || ns != undefined) { node.setAttributeNS(ns, name, value); } else { switch (name) { @@ -358,17 +364,15 @@ class Interpreter { node.value = value; break; case "checked": - // console.log("setting checked"); - node.checked = (value === "true"); + node.checked = value === "true"; break; case "selected": - node.selected = (value === "true"); + node.selected = value === "true"; break; case "dangerous_inner_html": node.innerHTML = value; break; default: - // console.log("setting attr directly ", name, value); node.setAttribute(name, value); } } @@ -390,11 +394,6 @@ class Interpreter { } handleEdits(edits) { - - // console.log("handling raw edits", rawedits); - // let edits = JSON.parse(rawedits); - // console.log("handling edits", edits); - this.stack.push(this.root); for (let x = 0; x < edits.length; x++) { @@ -410,7 +409,7 @@ function main() { window.interpreter = new Interpreter(root); console.log(window.interpreter); - rpc.call('initialize'); + rpc.call("initialize"); } -main() +main(); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index bcc8d1b82..1d5ebba43 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -279,37 +279,7 @@ impl DesktopController { cfg: &DesktopConfig, event_loop: &EventLoopWindowTarget, ) { - let builder = cfg.window.clone().with_menu({ - // create main menubar menu - let mut menu_bar_menu = MenuBar::new(); - - // create `first_menu` - let mut first_menu = MenuBar::new(); - - first_menu.add_native_item(MenuItem::About("App".to_string())); - first_menu.add_native_item(MenuItem::Services); - first_menu.add_native_item(MenuItem::Separator); - first_menu.add_native_item(MenuItem::Hide); - first_menu.add_native_item(MenuItem::HideOthers); - first_menu.add_native_item(MenuItem::ShowAll); - - first_menu.add_native_item(MenuItem::Quit); - first_menu.add_native_item(MenuItem::CloseWindow); - - // create second menu - let mut second_menu = MenuBar::new(); - - // second_menu.add_submenu("Sub menu", true, my_sub_menu); - second_menu.add_native_item(MenuItem::Copy); - second_menu.add_native_item(MenuItem::Paste); - second_menu.add_native_item(MenuItem::SelectAll); - - menu_bar_menu.add_submenu("First menu", true, first_menu); - menu_bar_menu.add_submenu("Second menu", true, second_menu); - - menu_bar_menu - }); - + let builder = cfg.window.clone(); let window = builder.build(event_loop).unwrap(); let window_id = window.id(); diff --git a/packages/desktop/src/serialize.js b/packages/desktop/src/serialize.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/hooks/src/usestate/owned.rs b/packages/hooks/src/usestate/owned.rs index c4cfef112..a1a6bdb73 100644 --- a/packages/hooks/src/usestate/owned.rs +++ b/packages/hooks/src/usestate/owned.rs @@ -12,8 +12,8 @@ pub struct UseStateOwned { } impl UseStateOwned { - pub fn get(&self) -> Ref { - Ref::map(self.wip.borrow(), |x| x.as_ref().unwrap()) + pub fn get(&self) -> Ref> { + self.wip.borrow() } pub fn set(&self, new_val: T) { @@ -21,8 +21,9 @@ impl UseStateOwned { (self.update_callback)(); } - pub fn modify(&self) -> RefMut { - RefMut::map(self.wip.borrow_mut(), |x| x.as_mut().unwrap()) + pub fn modify(&self) -> RefMut> { + (self.update_callback)(); + self.wip.borrow_mut() } } diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 02b0046ad..1a2f36e57 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1100,6 +1100,11 @@ impl label { cx.attr("for", val, None, false) } } +impl a { + pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr("dioxus-prevent-default", val, None, false) + } +} builder_constructors! { // SVG components diff --git a/packages/router/.vscode/settings.json b/packages/router/.vscode/settings.json new file mode 100644 index 000000000..e0975d874 --- /dev/null +++ b/packages/router/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.allFeatures": true +} diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 158a233fa..a182822ab 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -19,6 +19,7 @@ serde = "1" url = "2.2.2" serde_urlencoded = "0.7" +# for wasm web-sys = { version = "0.3", features = [ "Attr", "Document", @@ -33,11 +34,12 @@ web-sys = { version = "0.3", features = [ wasm-bindgen = { version = "0.2", optional = true } js-sys = { version = "0.3", optional = true } gloo = { version = "0.5", optional = true } +log = "0.4.14" [features] -default = ["derive"] -web = ["web-sys"] +default = ["derive", "web"] +web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"] desktop = [] mobile = [] derive = [] diff --git a/packages/router/README.md b/packages/router/README.md index 4cfa98c9d..c32441888 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -1,40 +1,29 @@ -# Router hook for Dioxus apps +# Routing for Dioxus App -Dioxus-router provides a use_router hook that returns a different value depending on the route. -The router is generic over any value, however it makes sense to return a different set of VNodes -and feed them into the App's return VNodes. - -Using the router should feel similar to tide's routing framework where an "address" book is assembled at the head of the app. - -Here's an example of how to use the router hook: +DioxusRouter adds React-Router style routing to your Dioxus apps. Works in browser, SSR, and natively. ```rust -#[derive(Clone, PartialEq, Serialize, Deserialize, Routable)] -enum AppRoute { - Home, - Posts, - NotFound -} - -static App: Component = |cx| { - let route = use_router(cx, AppRoute::parse); - - match route { - AppRoute::Home => rsx!(cx, Home {}) - AppRoute::Posts => rsx!(cx, Posts {}) - AppRoute::Notfound => rsx!(cx, Notfound {}) - } -}; -``` - -Adding links into your app: - -```rust -static Leaf: Component = |cx| { - rsx!(cx, div { - Link { to: AppRoute::Home } +fn app() { + cx.render(rsx! { + Routes { + Route { to: "/", Component {} }, + Route { to: "/blog", Blog {} }, + Route { to: "/blog/:id", BlogPost {} }, + } }) } ``` +Then, in your route, you can choose to parse the Route any way you want through `use_route`. +```rust +let id: usize = use_route(&cx).path("id")?; + +let state: CustomState = use_route(&cx).parse()?; +``` + +Adding links into your app: +```rust +Link { to: "id/{id}" } +``` + Currently, the router is only supported in a web environment, but we plan to add 1st-party support via the context API when new renderers are available. diff --git a/packages/router/examples/simple.rs b/packages/router/examples/simple.rs index 134237dbb..4aebafb84 100644 --- a/packages/router/examples/simple.rs +++ b/packages/router/examples/simple.rs @@ -11,31 +11,40 @@ fn main() { } static APP: Component = |cx| { - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] - enum Route { - Home, - About, - NotFound, - } - impl Default for Route { - fn default() -> Self { - Route::Home - } - } - - let route = use_router(&cx, |c| {}); - cx.render(rsx! { - div { - {match route { - Route::Home => rsx!(h1 { "Home" }), - Route::About => rsx!(h1 { "About" }), - Route::NotFound => rsx!(h1 { "NotFound" }), - }} - nav { - Link { to: Route::Home, href: "/" } - Link { to: Route::About, href: "/about" } + Router { + onchange: move |route| log::info!("route changed to {}", route), + Route { to: "/", Home {} } + Route { to: "blog" + Route { to: "/", BlogList {} } + Route { to: ":id", BlogPost {} } } } }) }; + +fn Home(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Home" } + } + }) +} + +fn BlogList(cx: Scope) -> Element { + cx.render(rsx! { + div { + + } + }) +} + +fn BlogPost(cx: Scope) -> Element { + let id = use_route(&cx).segment::("id")?; + + cx.render(rsx! { + div { + + } + }) +} diff --git a/packages/router/src/link.rs b/packages/router/src/components/link.rs similarity index 71% rename from packages/router/src/link.rs rename to packages/router/src/components/link.rs index 4ad7a45d0..a08eb288b 100644 --- a/packages/router/src/link.rs +++ b/packages/router/src/components/link.rs @@ -1,4 +1,4 @@ -use crate::{Routable, RouterService}; +use crate::RouterService; use dioxus::Attribute; use dioxus_core as dioxus; use dioxus_core::prelude::*; @@ -6,8 +6,8 @@ use dioxus_core_macro::{format_args_f, rsx, Props}; use dioxus_html as dioxus_elements; #[derive(Props)] -pub struct LinkProps<'a, R: Routable> { - to: R, +pub struct LinkProps<'a> { + to: &'a str, /// The url that gets pushed to the history stack /// @@ -28,20 +28,26 @@ pub struct LinkProps<'a, R: Routable> { #[props(default, setter(strip_option))] class: Option<&'a str>, + #[props(default, setter(strip_option))] + id: Option<&'a str>, + children: Element<'a>, #[props(default)] attributes: Option<&'a [Attribute<'a>]>, } -#[allow(non_snake_case)] -pub fn Link<'a, R: Routable>(cx: Scope<'a, LinkProps<'a, R>>) -> Element { - let service = cx.consume_context::>()?; +pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { + let service = cx.consume_context::().unwrap(); cx.render(rsx! { a { - href: "#", + href: "{cx.props.to}", class: format_args!("{}", cx.props.class.unwrap_or("")), + id: format_args!("{}", cx.props.id.unwrap_or("")), + + prevent_default: "onclick", onclick: move |_| service.push_route(cx.props.to.clone()), + &cx.props.children } }) diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs new file mode 100644 index 000000000..09dabb28b --- /dev/null +++ b/packages/router/src/components/route.rs @@ -0,0 +1,65 @@ +use dioxus_core::Element; + +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_core_macro::Props; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +use crate::{RouteContext, RouterService}; + +#[derive(Props)] +pub struct RouteProps<'a> { + to: &'a str, + + children: Element<'a>, + + #[props(default)] + fallback: bool, +} + +pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { + // now we want to submit + let router_root = cx + .use_hook(|_| cx.consume_context::()) + .as_ref()?; + + cx.use_hook(|_| { + // create a bigger, better, longer route if one above us exists + let total_route = match cx.consume_context::() { + Some(ctx) => format!("{}", ctx.total_route.clone()), + None => format!("{}", cx.props.to.clone()), + }; + + // provide our route context + let route_context = cx.provide_context(RouteContext { + declared_route: cx.props.to.to_string(), + total_route, + }); + + // submit our rout + router_root.register_total_route( + route_context.total_route.clone(), + cx.scope_id(), + cx.props.fallback, + ); + + Some(RouteInner {}) + }); + + log::debug!("Checking route {}", cx.props.to); + + if router_root.should_render(cx.scope_id()) { + cx.render(rsx!(&cx.props.children)) + } else { + None + } +} + +struct RouteInner {} + +impl Drop for RouteInner { + fn drop(&mut self) { + // todo!() + } +} diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs new file mode 100644 index 000000000..90c101fa8 --- /dev/null +++ b/packages/router/src/components/router.rs @@ -0,0 +1,28 @@ +use dioxus_core::Element; + +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +use crate::RouterService; + +#[derive(Props)] +pub struct RouterProps<'a> { + children: Element<'a>, + + #[props(default, setter(strip_option))] + onchange: Option<&'a Fn(&'a str)>, +} + +#[allow(non_snake_case)] +pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { + cx.use_hook(|_| { + let update = cx.schedule_update_any(); + cx.provide_context(RouterService::new(update, cx.scope_id())) + }); + + cx.render(rsx!( + div { &cx.props.children } + )) +} diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs new file mode 100644 index 000000000..66b0b4f11 --- /dev/null +++ b/packages/router/src/hooks/use_route.rs @@ -0,0 +1,30 @@ +use dioxus_core::ScopeState; + +pub struct UseRoute<'a> { + cur_route: String, + cx: &'a ScopeState, +} + +impl<'a> UseRoute<'a> { + /// Parse the query part of the URL + pub fn param(&self, param: &str) -> Option<&T> { + todo!() + } + + pub fn nth_segment(&self, n: usize) -> Option<&str> { + todo!() + } + + pub fn last_segment(&self) -> Option<&str> { + todo!() + } + + /// Parse the segments of the URL, using named parameters (defined in your router) + pub fn segment(&self, name: &str) -> Option<&T> { + todo!() + } +} + +pub fn use_route(cx: &ScopeState) -> UseRoute { + todo!() +} diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index da7feb05e..23d1c4a07 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -7,17 +7,6 @@ //! //! ```rust //! fn app(cx: Scope) -> Element { -//! let route = use_router(&cx, |svc, path| { -//! match path { -//! "/about" => Route::About, -//! _ => Route::Home, -//! } -//! }); -//! -//! match route { -//! Route::Home => rsx!(cx, h1 { "Home" }), -//! Route::About => rsx!(cx, h1 { "About" }), -//! } //! } //! //! @@ -35,22 +24,30 @@ //! //! -mod link; +mod hooks { + mod use_route; + pub use use_route::*; +} +pub use hooks::*; + +mod components { + #![allow(non_snake_case)] + + mod router; + pub use router::*; + + mod route; + pub use route::*; + + mod link; + pub use link::*; +} +pub use components::*; + mod platform; +mod routecontext; mod service; -mod userouter; mod utils; -pub use link::*; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +pub use routecontext::*; pub use service::*; -pub use userouter::*; - -pub trait Routable: - 'static + Send + Clone + PartialEq + Serialize + DeserializeOwned + Default -{ -} -impl Routable for T where - T: 'static + Send + Clone + PartialEq + Serialize + DeserializeOwned + Default -{ -} diff --git a/packages/router/src/platform.rs b/packages/router/src/platform.rs deleted file mode 100644 index 55cdceb83..000000000 --- a/packages/router/src/platform.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub fn parse_current_route() -> String { - todo!() -} - -pub(crate) fn get_base_route() -> String { - todo!() -} diff --git a/packages/router/src/platform/mod.rs b/packages/router/src/platform/mod.rs new file mode 100644 index 000000000..bbb0e7c18 --- /dev/null +++ b/packages/router/src/platform/mod.rs @@ -0,0 +1,11 @@ +use url::Url; + +pub trait RouterProvider { + fn get_current_route(&self) -> String; + fn subscribe_to_route_changes(&self, callback: Box); +} + +enum RouteChange { + LinkTo(String), + Back(String), +} diff --git a/packages/router/src/routecontext.rs b/packages/router/src/routecontext.rs new file mode 100644 index 000000000..fdd4f9d90 --- /dev/null +++ b/packages/router/src/routecontext.rs @@ -0,0 +1,7 @@ +pub struct RouteContext { + // "/name/:id" + pub declared_route: String, + + // "app/name/:id" + pub total_route: String, +} diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index e70aaf410..29a892748 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,18 +1,109 @@ -use crate::Routable; -use std::{cell::RefCell, rc::Rc}; +use gloo::history::{BrowserHistory, History, HistoryListener}; +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + rc::Rc, +}; -pub struct RouterService { - pub(crate) regen_route: Rc, - pub(crate) pending_routes: RefCell>, +use dioxus_core::ScopeId; + +pub struct RouterService { + pub(crate) regen_route: Rc, + history: Rc>, + registerd_routes: RefCell, + slots: Rc>>, + root_found: Rc>, + cur_root: RefCell, + listener: HistoryListener, } -impl RouterService { - pub fn current_path(&self) -> &str { - todo!() +enum RouteSlot { + Routes { + // the partial route + partial: String, + + // the total route + total: String, + + // Connections to other routs + rest: Vec, + }, +} + +impl RouterService { + pub fn new(regen_route: Rc, root_scope: ScopeId) -> Self { + let history = BrowserHistory::default(); + let location = history.location(); + let path = location.path(); + + let slots: Rc>> = Default::default(); + + let _slots = slots.clone(); + + let root_found = Rc::new(Cell::new(false)); + let regen = regen_route.clone(); + let _root_found = root_found.clone(); + let listener = history.listen(move || { + _root_found.set(false); + // checking if the route is valid is cheap, so we do it + for (slot, _) in _slots.borrow_mut().iter().rev() { + log::debug!("regenerating slot {:?}", slot); + regen(*slot); + } + }); + + Self { + registerd_routes: RefCell::new(RouteSlot::Routes { + partial: String::from("/"), + total: String::from("/"), + rest: Vec::new(), + }), + root_found, + history: Rc::new(RefCell::new(history)), + regen_route, + slots, + cur_root: RefCell::new(path.to_string()), + listener, + } } - pub fn push_route(&self, route: R) { - self.pending_routes.borrow_mut().push(route); - (self.regen_route)(); + + pub fn push_route(&self, route: &str) { + self.history.borrow_mut().push(route); + } + + pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { + self.slots.borrow_mut().push((scope, route)); + } + + pub fn should_render(&self, scope: ScopeId) -> bool { + if self.root_found.get() { + return false; + } + + let location = self.history.borrow().location(); + let path = location.path(); + + let roots = self.slots.borrow(); + + let root = roots.iter().find(|(id, route)| id == &scope); + + // fallback logic + match root { + Some((_id, route)) => { + if route == path { + self.root_found.set(true); + true + } else { + if route == "" { + self.root_found.set(true); + true + } else { + false + } + } + } + None => false, + } } } diff --git a/packages/router/src/userouter.rs b/packages/router/src/userouter.rs deleted file mode 100644 index 4736b634e..000000000 --- a/packages/router/src/userouter.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use crate::{Routable, RouterCfg, RouterService}; -use dioxus_core::ScopeState; - -/// Initialize the app's router service and provide access to `Link` components -pub fn use_router<'a, R: Routable>(cx: &'a ScopeState, _cfg: impl FnOnce(&mut RouterCfg)) -> &'a R { - let router = cx.use_hook(|_| { - let svc: RouterService = RouterService { - regen_route: cx.schedule_update(), - pending_routes: RefCell::new(Vec::new()), - }; - let first_path = R::default(); - cx.provide_context(svc); - UseRouterInner { - svc: cx.consume_context::>().unwrap(), - history: vec![first_path], - } - }); - - let mut pending_routes = router.svc.pending_routes.borrow_mut(); - - for route in pending_routes.drain(..) { - router.history.push(route); - } - - router.history.last().unwrap() -} - -struct UseRouterInner { - svc: Rc>, - history: Vec, -} diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 655b6c8c5..b93be0f63 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -274,7 +274,21 @@ impl WebsysDom { // "Result" cannot be received from JS // Instead, we just build and immediately execute a closure that returns result match decode_trigger(event) { - Ok(synthetic_event) => trigger.as_ref()(SchedulerMsg::Event(synthetic_event)), + Ok(synthetic_event) => { + let target = event.target().unwrap(); + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::debug!("Preventing default"); + event.prevent_default(); + } + } + } + + trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) + } Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), }; }); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index ac07f7e71..61caa99b9 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -157,6 +157,7 @@ pub async fn run_with_props(root: Component, root_props: T let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback); + log::debug!("rebuilding app"); let mut mutations = dom.rebuild(); // hydrating is simply running the dom for a single render. If the page is already written, then the corresponding @@ -168,10 +169,13 @@ pub async fn run_with_props(root: Component, root_props: T let work_loop = ric_raf::RafLoop::new(); loop { + log::debug!("waiting for work"); // if virtualdom has nothing, wait for it to have something before requesting idle time // if there is work then this future resolves immediately. dom.wait_for_work().await; + log::debug!("working.."); + // wait for the mainthread to schedule us in let mut deadline = work_loop.wait_for_idle_time().await;