Merge pull request #67 from DioxusLabs/jk/router2dotoh

Fix/Feat: Memoize VComponents from their ptrs. Add new router!
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2022-01-05 16:35:02 -05:00
commit 8994467189
33 files changed
+614 -320

No files matched your search

+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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:
+2 -1
View File
@@ -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 {
+25 -10
View File
@@ -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::<Token![:]>()?;
let name_str = name.to_string();
let content = if name_str.starts_with("on") {
ContentField::OnHandlerRaw(input.parse()?)
} else {
ContentField::ManExpr(input.parse::<Expr>()?)
};
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::<LitStr>().unwrap();
proc_macro_error::emit_error!(item, "This attribute is misisng a trailing comma")
}
let content = ContentField::ManExpr(input.parse()?);
Ok(Self { name, content })
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ impl Parse for Element {
content.parse::<Token![:]>()?;
if content.peek(LitStr) {
if content.peek(LitStr) && content.peek2(Token![,]) {
let value = content.parse::<LitStr>()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
+38 -29
View File
@@ -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<dyn AnyProps + 'bump> = vcomponent.props.borrow_mut().take().unwrap();
let props: Box<dyn AnyProps + 'static> = 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<dyn AnyProps + 'bump> = vcomponent.props.borrow_mut().take().unwrap();
let props: Box<dyn AnyProps + 'static> = 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)
}
}
+66 -49
View File
@@ -60,10 +60,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
}
}
pub fn new<F>(_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::<usize>() - 1) / mem::size_of::<usize>()
}
#[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<i32>,
}
fn app(cx: Scope<AppProps>) -> 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::<Vec<_>>();
LazyNodes::new_some(|f| {
log::debug!("main closure");
f.fragment_from_iter(it)
})
})
.collect::<Vec<_>>();
};
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);
}
}
+28 -10
View File
@@ -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<Option<ScopeId>>,
pub can_memoize: bool,
pub user_fc: *const (),
@@ -395,12 +396,16 @@ impl<P> AnyProps for VComponentProps<P> {
/// 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<VNode<'a>> {
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<VNode>))
}
}
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>;
}
+15 -3
View File
@@ -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<T: 'static>(&self, value: T) {
pub fn provide_context<T: 'static>(&self, value: T) -> Rc<T> {
let value = Rc::new(value);
self.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(value))
.insert(TypeId::of::<T>(), value.clone())
.map(|f| f.downcast::<T>().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<VNode<'src>> {
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 {
+4 -1
View File
@@ -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)
}
+29 -1
View File
@@ -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);
}
+34 -35
View File
@@ -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();
+1 -31
View File
@@ -279,37 +279,7 @@ impl DesktopController {
cfg: &DesktopConfig,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
) {
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();
View File
Whitespace-only changes.
+5 -4
View File
@@ -12,8 +12,8 @@ pub struct UseStateOwned<T: 'static> {
}
impl<T> UseStateOwned<T> {
pub fn get(&self) -> Ref<T> {
Ref::map(self.wip.borrow(), |x| x.as_ref().unwrap())
pub fn get(&self) -> Ref<Option<T>> {
self.wip.borrow()
}
pub fn set(&self, new_val: T) {
@@ -21,8 +21,9 @@ impl<T> UseStateOwned<T> {
(self.update_callback)();
}
pub fn modify(&self) -> RefMut<T> {
RefMut::map(self.wip.borrow_mut(), |x| x.as_mut().unwrap())
pub fn modify(&self) -> RefMut<Option<T>> {
(self.update_callback)();
self.wip.borrow_mut()
}
}
+5
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
{
"rust-analyzer.cargo.allFeatures": true
}
+4 -2
View File
@@ -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 = []
+21 -32
View File
@@ -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.
+32 -23
View File
@@ -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::<usize>("id")?;
cx.render(rsx! {
div {
}
})
}
@@ -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::<RouterService<R>>()?;
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
let service = cx.consume_context::<RouterService>().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
}
})
+65
View File
@@ -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::<RouterService>())
.as_ref()?;
cx.use_hook(|_| {
// create a bigger, better, longer route if one above us exists
let total_route = match cx.consume_context::<RouteContext>() {
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!()
}
}
+28
View File
@@ -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 }
))
}
+30
View File
@@ -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<T>(&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<T>(&self, name: &str) -> Option<&T> {
todo!()
}
}
pub fn use_route(cx: &ScopeState) -> UseRoute {
todo!()
}
+22 -25
View File
@@ -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<T> Routable for T where
T: 'static + Send + Clone + PartialEq + Serialize + DeserializeOwned + Default
{
}
-7
View File
@@ -1,7 +0,0 @@
pub fn parse_current_route() -> String {
todo!()
}
pub(crate) fn get_base_route() -> String {
todo!()
}
+11
View File
@@ -0,0 +1,11 @@
use url::Url;
pub trait RouterProvider {
fn get_current_route(&self) -> String;
fn subscribe_to_route_changes(&self, callback: Box<dyn Fn(String)>);
}
enum RouteChange {
LinkTo(String),
Back(String),
}
+7
View File
@@ -0,0 +1,7 @@
pub struct RouteContext {
// "/name/:id"
pub declared_route: String,
// "app/name/:id"
pub total_route: String,
}
+102 -11
View File
@@ -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<R: Routable> {
pub(crate) regen_route: Rc<dyn Fn()>,
pub(crate) pending_routes: RefCell<Vec<R>>,
use dioxus_core::ScopeId;
pub struct RouterService {
pub(crate) regen_route: Rc<dyn Fn(ScopeId)>,
history: Rc<RefCell<BrowserHistory>>,
registerd_routes: RefCell<RouteSlot>,
slots: Rc<RefCell<Vec<(ScopeId, String)>>>,
root_found: Rc<Cell<bool>>,
cur_root: RefCell<String>,
listener: HistoryListener,
}
impl<R: Routable> RouterService<R> {
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<RouteSlot>,
},
}
impl RouterService {
pub fn new(regen_route: Rc<dyn Fn(ScopeId)>, root_scope: ScopeId) -> Self {
let history = BrowserHistory::default();
let location = history.location();
let path = location.path();
let slots: Rc<RefCell<Vec<(ScopeId, String)>>> = 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,
}
}
}
-33
View File
@@ -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<R> = 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::<RouterService<R>>().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<R: Routable> {
svc: Rc<RouterService<R>>,
history: Vec<R>,
}
+15 -1
View File
@@ -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::<HtmlElement>() {
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),
};
});
+4
View File
@@ -157,6 +157,7 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, 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<T: 'static + Send>(root: Component<T>, 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;