Merge remote-tracking branch 'upstream/master' into optimize-templates-v3
This commit is contained in:
commit
b79ad4f50e
68 files changed
+1030
-1281
No files matched your search
@@ -41,7 +41,7 @@ fn app(cx: Scope) -> Element {
|
||||
|
||||
Then, we'll want to create a new VirtualDom using this app as the root component.
|
||||
|
||||
```rust, ingore
|
||||
```rust, ignore
|
||||
let mut dom = VirtualDom::new(app);
|
||||
```
|
||||
|
||||
@@ -65,9 +65,9 @@ dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)));
|
||||
|
||||
If an event occurs from outside the virtualdom while waiting for work, then we can cancel the wait using a `select!` block and inject the event.
|
||||
|
||||
```rust
|
||||
```rust, ignore
|
||||
loop {
|
||||
tokio::select! {
|
||||
select! {
|
||||
evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles),
|
||||
_ = dom.wait_for_work() => {}
|
||||
}
|
||||
|
||||
+59
-34
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode,
|
||||
ScopeId,
|
||||
};
|
||||
use crate::{nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, DynamicNode, ScopeId};
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
|
||||
/// An Element's unique identifier.
|
||||
@@ -37,21 +34,20 @@ impl ElementRef {
|
||||
|
||||
impl VirtualDom {
|
||||
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
|
||||
let entry = self.elements.vacant_entry();
|
||||
let id = entry.key();
|
||||
entry.insert(ElementRef {
|
||||
template: template as *const _ as *mut _,
|
||||
path: ElementPath::Deep(path),
|
||||
});
|
||||
ElementId(id)
|
||||
self.next(template, ElementPath::Deep(path))
|
||||
}
|
||||
|
||||
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
|
||||
self.next(template, ElementPath::Root(path))
|
||||
}
|
||||
|
||||
fn next(&mut self, template: &VNode, path: ElementPath) -> ElementId {
|
||||
let entry = self.elements.vacant_entry();
|
||||
let id = entry.key();
|
||||
|
||||
entry.insert(ElementRef {
|
||||
template: template as *const _ as *mut _,
|
||||
path: ElementPath::Root(path),
|
||||
path,
|
||||
});
|
||||
ElementId(id)
|
||||
}
|
||||
@@ -64,9 +60,9 @@ impl VirtualDom {
|
||||
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
|
||||
if el.0 == 0 {
|
||||
panic!(
|
||||
"Invalid element set to 0 - {:#?}",
|
||||
"Cannot reclaim the root element - {:#?}",
|
||||
std::backtrace::Backtrace::force_capture()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
self.elements.try_remove(el.0)
|
||||
@@ -79,17 +75,15 @@ impl VirtualDom {
|
||||
|
||||
// Drop a scope and all its children
|
||||
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
|
||||
let scope = self.scopes.get(id.0).unwrap();
|
||||
|
||||
if let Some(root) = scope.as_ref().try_root_node() {
|
||||
let root = unsafe { root.extend_lifetime_ref() };
|
||||
if let RenderReturn::Sync(Ok(node)) = root {
|
||||
if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
|
||||
if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } {
|
||||
self.drop_scope_inner(node)
|
||||
}
|
||||
}
|
||||
|
||||
let scope = self.scopes.get_mut(id.0).unwrap();
|
||||
scope.props.take();
|
||||
self.scopes[id.0].props.take();
|
||||
|
||||
let scope = &mut self.scopes[id.0];
|
||||
|
||||
// Drop all the hooks once the children are dropped
|
||||
// this means we'll drop hooks bottom-up
|
||||
@@ -99,24 +93,55 @@ impl VirtualDom {
|
||||
}
|
||||
|
||||
fn drop_scope_inner(&mut self, node: &VNode) {
|
||||
for attr in node.dynamic_attrs {
|
||||
if let AttributeValue::Listener(l) = &attr.value {
|
||||
l.borrow_mut().take();
|
||||
node.clear_listeners();
|
||||
node.dynamic_nodes.iter().for_each(|node| match node {
|
||||
DynamicNode::Component(c) => self.drop_scope(c.scope.get().unwrap()),
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
nodes.iter().for_each(|node| self.drop_scope_inner(node))
|
||||
}
|
||||
}
|
||||
DynamicNode::Placeholder(t) => {
|
||||
self.try_reclaim(t.get());
|
||||
}
|
||||
DynamicNode::Text(t) => {
|
||||
self.try_reclaim(t.id.get());
|
||||
}
|
||||
});
|
||||
|
||||
for (idx, _) in node.template.roots.iter().enumerate() {
|
||||
match node.dynamic_root(idx) {
|
||||
Some(DynamicNode::Component(c)) => self.drop_scope(c.scope.get().unwrap()),
|
||||
Some(DynamicNode::Fragment(nodes)) => {
|
||||
for node in *nodes {
|
||||
self.drop_scope_inner(node);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
for root in node.root_ids {
|
||||
let id = root.get();
|
||||
if id.0 != 0 {
|
||||
self.try_reclaim(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Descend through the tree, removing any borrowed props and listeners
|
||||
pub(crate) fn ensure_drop_safety(&self, scope: ScopeId) {
|
||||
let node = unsafe { self.scopes[scope.0].previous_frame().try_load_node() };
|
||||
|
||||
// And now we want to make sure the previous frame has dropped anything that borrows self
|
||||
if let Some(RenderReturn::Sync(Ok(node))) = node {
|
||||
self.ensure_drop_safety_inner(node);
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_drop_safety_inner(&self, node: &VNode) {
|
||||
node.clear_listeners();
|
||||
|
||||
node.dynamic_nodes.iter().for_each(|child| match child {
|
||||
// Only descend if the props are borrowed
|
||||
DynamicNode::Component(c) if !c.static_props => {
|
||||
self.ensure_drop_safety(c.scope.get().unwrap());
|
||||
c.props.set(None);
|
||||
}
|
||||
|
||||
DynamicNode::Fragment(f) => f
|
||||
.iter()
|
||||
.for_each(|node| self.ensure_drop_safety_inner(node)),
|
||||
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementPath {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::innerlude::{VComponent, VText};
|
||||
use crate::mutations::Mutation;
|
||||
@@ -42,7 +43,7 @@ impl<'b> VirtualDom {
|
||||
for (root_idx, root) in template.template.roots.iter().enumerate() {
|
||||
// We might need to generate an ID for the root node
|
||||
on_stack += match root {
|
||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
||||
TemplateNode::DynamicText { id } | TemplateNode::Dynamic { id } => {
|
||||
match &template.dynamic_nodes[*id] {
|
||||
// a dynamic text node doesn't replace a template node, instead we create it on the fly
|
||||
DynamicNode::Text(VText { id: slot, value }) => {
|
||||
@@ -72,7 +73,7 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text { .. } => {
|
||||
let this_id = self.next_root(template, root_idx);
|
||||
|
||||
template.root_ids[root_idx].set(this_id);
|
||||
@@ -213,7 +214,7 @@ impl<'b> VirtualDom {
|
||||
if template.template.roots.iter().all(|root| {
|
||||
matches!(
|
||||
root,
|
||||
TemplateNode::Dynamic(_) | TemplateNode::DynamicText(_)
|
||||
TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. }
|
||||
)
|
||||
}) {
|
||||
return;
|
||||
@@ -302,7 +303,8 @@ impl<'b> VirtualDom {
|
||||
|
||||
let unbounded_props = unsafe { std::mem::transmute(props) };
|
||||
|
||||
let scope = self.new_scope(unbounded_props).id;
|
||||
let scope = self.new_scope(unbounded_props, component.name);
|
||||
let scope = scope.id;
|
||||
component.scope.set(Some(scope));
|
||||
|
||||
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
||||
@@ -336,8 +338,8 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
// If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
|
||||
let boundary = match self.scopes[scope.0].has_context::<SuspenseContext>() {
|
||||
Some(boundary) => unsafe { &*(boundary as *const SuspenseContext) },
|
||||
let boundary = match self.scopes[scope.0].has_context::<Rc<SuspenseContext>>() {
|
||||
Some(boundary) => boundary,
|
||||
_ => return created,
|
||||
};
|
||||
|
||||
|
||||
+30
-32
@@ -25,40 +25,35 @@ impl<'b> VirtualDom {
|
||||
.previous_frame()
|
||||
.try_load_node()
|
||||
.expect("Call rebuild before diffing");
|
||||
|
||||
let new = scope_state
|
||||
.current_frame()
|
||||
.try_load_node()
|
||||
.expect("Call rebuild before diffing");
|
||||
self.diff_maybe_node(old, new);
|
||||
|
||||
use RenderReturn::{Async, Sync};
|
||||
|
||||
match (old, new) {
|
||||
(Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
|
||||
|
||||
// Err cases
|
||||
(Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e),
|
||||
(Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r),
|
||||
(Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ }
|
||||
|
||||
// Async
|
||||
(Sync(Ok(_l)), Async(_)) => todo!(),
|
||||
(Sync(Err(_e)), Async(_)) => todo!(),
|
||||
(Async(_), Sync(Ok(_r))) => todo!(),
|
||||
(Async(_), Sync(Err(_e))) => { /* nothing */ }
|
||||
(Async(_), Async(_)) => { /* nothing */ }
|
||||
};
|
||||
}
|
||||
self.scope_stack.pop();
|
||||
}
|
||||
|
||||
fn diff_maybe_node(&mut self, old: &'b RenderReturn<'b>, new: &'b RenderReturn<'b>) {
|
||||
use RenderReturn::{Async, Sync};
|
||||
match (old, new) {
|
||||
(Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
|
||||
|
||||
// Err cases
|
||||
(Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e),
|
||||
(Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r),
|
||||
(Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ }
|
||||
|
||||
// Async
|
||||
(Sync(Ok(_l)), Async(_)) => todo!(),
|
||||
(Sync(Err(_e)), Async(_)) => todo!(),
|
||||
(Async(_), Sync(Ok(_r))) => todo!(),
|
||||
(Async(_), Sync(Err(_e))) => { /* nothing */ }
|
||||
(Async(_), Async(_)) => { /* nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {
|
||||
todo!("Not yet handling error rollover")
|
||||
}
|
||||
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {
|
||||
todo!("Not yet handling error rollover")
|
||||
}
|
||||
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {}
|
||||
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {}
|
||||
|
||||
fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
|
||||
if !std::ptr::eq(left_template.template.name, right_template.template.name)
|
||||
@@ -144,16 +139,18 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell<ElementId>) {
|
||||
// Remove the old nodes, except for one
|
||||
self.remove_nodes(&l[1..]);
|
||||
|
||||
// Now create the new one
|
||||
let first = self.replace_inner(&l[0]);
|
||||
|
||||
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
|
||||
let placeholder = self.next_element(&l[0], &[]);
|
||||
r.set(placeholder);
|
||||
self.mutations
|
||||
.push(Mutation::CreatePlaceholder { id: placeholder });
|
||||
|
||||
// Remove the old nodes, except for onea
|
||||
let first = self.replace_inner(&l[0]);
|
||||
self.remove_nodes(&l[1..]);
|
||||
|
||||
self.mutations
|
||||
.push(Mutation::ReplaceWith { id: first, m: 1 });
|
||||
|
||||
@@ -746,7 +743,8 @@ impl<'b> VirtualDom {
|
||||
/// Remove these nodes from the dom
|
||||
/// Wont generate mutations for the inner nodes
|
||||
fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) {
|
||||
nodes.iter().for_each(|node| self.remove_node(node));
|
||||
// note that we iterate in reverse to unlink lists of nodes in their rough index order
|
||||
nodes.iter().rev().for_each(|node| self.remove_node(node));
|
||||
}
|
||||
|
||||
fn remove_node(&mut self, node: &'b VNode<'b>) {
|
||||
@@ -911,7 +909,7 @@ fn matching_components<'a>(
|
||||
.zip(right.template.roots.iter())
|
||||
.map(|(l, r)| {
|
||||
let (l, r) = match (l, r) {
|
||||
(TemplateNode::Dynamic(l), TemplateNode::Dynamic(r)) => (l, r),
|
||||
(TemplateNode::Dynamic { id: l }, TemplateNode::Dynamic { id: r }) => (l, r),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ use crate::innerlude::*;
|
||||
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
|
||||
let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
Ok(VNode {
|
||||
key: children.key.clone(),
|
||||
parent: children.parent.clone(),
|
||||
key: children.key,
|
||||
parent: children.parent,
|
||||
template: children.template,
|
||||
root_ids: children.root_ids,
|
||||
dynamic_nodes: children.dynamic_nodes,
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{arena::ElementId, ScopeId, Template};
|
||||
/// Templates, however, apply to all subtrees, not just target subtree.
|
||||
///
|
||||
/// Mutations are the only link between the RealDOM and the VirtualDOM.
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
|
||||
#[derive(Debug, Default)]
|
||||
#[must_use = "not handling edits can lead to visual inconsistencies in UI"]
|
||||
pub struct Mutations<'a> {
|
||||
|
||||
@@ -78,12 +78,20 @@ impl<'a> VNode<'a> {
|
||||
/// Returns [`None`] if the root is actually a static node (Element/Text)
|
||||
pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> {
|
||||
match &self.template.roots[idx] {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => None,
|
||||
TemplateNode::Dynamic(id) | TemplateNode::DynamicText(id) => {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => None,
|
||||
TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => {
|
||||
Some(&self.dynamic_nodes[*id])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_listeners(&self) {
|
||||
for attr in self.dynamic_attrs {
|
||||
if let AttributeValue::Listener(l) = &attr.value {
|
||||
l.borrow_mut().take();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A static layout of a UI tree that describes a set of dynamic and static nodes.
|
||||
@@ -124,7 +132,7 @@ pub struct Template<'a> {
|
||||
///
|
||||
/// This can be created at compile time, saving the VirtualDom time when diffing the tree
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize), serde(tag = "type"))]
|
||||
pub enum TemplateNode<'a> {
|
||||
/// An statically known element in the dom.
|
||||
///
|
||||
@@ -151,15 +159,24 @@ pub enum TemplateNode<'a> {
|
||||
},
|
||||
|
||||
/// This template node is just a piece of static text
|
||||
Text(&'a str),
|
||||
Text {
|
||||
/// The actual text
|
||||
text: &'a str,
|
||||
},
|
||||
|
||||
/// This template node is unknown, and needs to be created at runtime.
|
||||
Dynamic(usize),
|
||||
Dynamic {
|
||||
/// The index of the dynamic node in the VNode's dynamic_nodes list
|
||||
id: usize,
|
||||
},
|
||||
|
||||
/// This template node is known to be some text, but needs to be created at runtime
|
||||
///
|
||||
/// This is separate from the pure Dynamic variant for various optimizations
|
||||
DynamicText(usize),
|
||||
DynamicText {
|
||||
/// The index of the dynamic node in the VNode's dynamic_nodes list
|
||||
id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// A node created at runtime
|
||||
@@ -244,7 +261,11 @@ pub struct VText<'a> {
|
||||
|
||||
/// An attribute of the TemplateNode, created at compile time
|
||||
#[derive(Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(tag = "type")
|
||||
)]
|
||||
pub enum TemplateAttribute<'a> {
|
||||
/// This attribute is entirely known at compile time, enabling
|
||||
Static {
|
||||
@@ -265,7 +286,10 @@ pub enum TemplateAttribute<'a> {
|
||||
/// The attribute in this position is actually determined dynamically at runtime
|
||||
///
|
||||
/// This is the index into the dynamic_attributes field on the container VNode
|
||||
Dynamic(usize),
|
||||
Dynamic {
|
||||
/// The index
|
||||
id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
|
||||
@@ -416,6 +440,8 @@ impl<'a> IntoDynNode<'a> for VNode<'a> {
|
||||
DynamicNode::Fragment(_cx.bump().alloc([self]))
|
||||
}
|
||||
}
|
||||
|
||||
// An element that's an error is currently lost into the ether
|
||||
impl<'a> IntoDynNode<'a> for Element<'a> {
|
||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||
match self {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use futures_util::FutureExt;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
innerlude::{Mutation, Mutations, SuspenseContext},
|
||||
@@ -31,12 +34,10 @@ impl VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn acquire_suspense_boundary<'a>(&self, id: ScopeId) -> &'a SuspenseContext {
|
||||
let ct = self.scopes[id.0]
|
||||
.consume_context::<SuspenseContext>()
|
||||
.unwrap();
|
||||
|
||||
unsafe { &*(ct as *const SuspenseContext) }
|
||||
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
|
||||
self.scopes[id.0]
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
scheduler::RcWake,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
virtual_dom::VirtualDom,
|
||||
AttributeValue, DynamicNode, VNode,
|
||||
};
|
||||
use bumpalo::Bump;
|
||||
use futures_util::FutureExt;
|
||||
@@ -19,7 +18,11 @@ use std::{
|
||||
};
|
||||
|
||||
impl VirtualDom {
|
||||
pub(super) fn new_scope(&mut self, props: Box<dyn AnyProps<'static>>) -> &ScopeState {
|
||||
pub(super) fn new_scope(
|
||||
&mut self,
|
||||
props: Box<dyn AnyProps<'static>>,
|
||||
name: &'static str,
|
||||
) -> &ScopeState {
|
||||
let parent = self.acquire_current_scope_raw();
|
||||
let entry = self.scopes.vacant_entry();
|
||||
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
||||
@@ -30,6 +33,7 @@ impl VirtualDom {
|
||||
id,
|
||||
height,
|
||||
props: Some(props),
|
||||
name,
|
||||
placeholder: Default::default(),
|
||||
node_arena_1: BumpFrame::new(0),
|
||||
node_arena_2: BumpFrame::new(0),
|
||||
@@ -50,42 +54,6 @@ impl VirtualDom {
|
||||
.and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _))
|
||||
}
|
||||
|
||||
fn ensure_drop_safety(&self, scope: ScopeId) {
|
||||
let scope = &self.scopes[scope.0];
|
||||
let node = unsafe { scope.previous_frame().try_load_node() };
|
||||
|
||||
// And now we want to make sure the previous frame has dropped anything that borrows self
|
||||
if let Some(RenderReturn::Sync(Ok(node))) = node {
|
||||
self.ensure_drop_safety_inner(node);
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_drop_safety_inner(&self, node: &VNode) {
|
||||
for attr in node.dynamic_attrs {
|
||||
if let AttributeValue::Listener(l) = &attr.value {
|
||||
l.borrow_mut().take();
|
||||
}
|
||||
}
|
||||
|
||||
for child in node.dynamic_nodes {
|
||||
match child {
|
||||
DynamicNode::Component(c) => {
|
||||
// Only descend if the props are borrowed
|
||||
if !c.static_props {
|
||||
self.ensure_drop_safety(c.scope.get().unwrap());
|
||||
c.props.set(None);
|
||||
}
|
||||
}
|
||||
DynamicNode::Fragment(f) => {
|
||||
for node in *f {
|
||||
self.ensure_drop_safety_inner(node);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
||||
// Cycle to the next frame and then reset it
|
||||
// This breaks any latent references, invalidating every pointer referencing into it.
|
||||
|
||||
+22
-22
@@ -69,6 +69,7 @@ pub struct ScopeId(pub usize);
|
||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||
pub struct ScopeState {
|
||||
pub(crate) render_cnt: Cell<usize>,
|
||||
pub(crate) name: &'static str,
|
||||
|
||||
pub(crate) node_arena_1: BumpFrame,
|
||||
pub(crate) node_arena_2: BumpFrame,
|
||||
@@ -116,6 +117,11 @@ impl<'src> ScopeState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of this component
|
||||
pub fn name(&self) -> &str {
|
||||
self.name
|
||||
}
|
||||
|
||||
/// Get the current render since the inception of this component
|
||||
///
|
||||
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
|
||||
@@ -251,17 +257,18 @@ impl<'src> ScopeState {
|
||||
}
|
||||
|
||||
/// Return any context of type T if it exists on this scope
|
||||
pub fn has_context<T: 'static>(&self) -> Option<&T> {
|
||||
let contextex = self.shared_contexts.borrow();
|
||||
let val = contextex.get(&TypeId::of::<T>())?;
|
||||
let as_concrete = val.downcast_ref::<T>()? as *const T;
|
||||
Some(unsafe { &*as_concrete })
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
self.shared_contexts
|
||||
.borrow()
|
||||
.get(&TypeId::of::<T>())?
|
||||
.downcast_ref::<T>()
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||
///
|
||||
/// To release the borrow, use `cloned` if the context is clone.
|
||||
pub fn consume_context<T: 'static>(&self) -> Option<&T> {
|
||||
/// Clones the state if it exists.
|
||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
if let Some(this_ctx) = self.has_context() {
|
||||
return Some(this_ctx);
|
||||
}
|
||||
@@ -271,8 +278,7 @@ impl<'src> ScopeState {
|
||||
// safety: all parent pointers are valid thanks to the bump arena
|
||||
let parent = unsafe { &*parent_ptr };
|
||||
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
||||
let as_concrete = shared.downcast_ref::<T>()? as *const T;
|
||||
return Some(unsafe { &*as_concrete });
|
||||
return shared.downcast_ref::<T>().cloned();
|
||||
}
|
||||
search_parent = parent.parent;
|
||||
}
|
||||
@@ -284,7 +290,7 @@ impl<'src> ScopeState {
|
||||
///
|
||||
/// This is a "fundamental" operation and should only be called during initialization of a hook.
|
||||
///
|
||||
/// For a hook that provides the same functionality, use `use_provide_context` and `use_consume_context` instead.
|
||||
/// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
|
||||
///
|
||||
/// If a state is provided that already exists, the new value will not be inserted. Instead, this method will
|
||||
/// return the existing value. This behavior is chosen so shared values do not need to be `Clone`. This particular
|
||||
@@ -305,20 +311,14 @@ impl<'src> ScopeState {
|
||||
/// render!(div { "hello {state.0}" })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_context<T: 'static>(&self, value: T) -> &T {
|
||||
let mut contexts = self.shared_contexts.borrow_mut();
|
||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
let value2 = value.clone();
|
||||
|
||||
let any = match contexts.get(&TypeId::of::<T>()) {
|
||||
Some(item) => item.downcast_ref::<T>().unwrap() as *const T,
|
||||
None => {
|
||||
let boxed = Box::new(value);
|
||||
let boxed_ptr = boxed.as_ref() as *const T;
|
||||
contexts.insert(TypeId::of::<T>(), boxed);
|
||||
boxed_ptr
|
||||
}
|
||||
};
|
||||
self.shared_contexts
|
||||
.borrow_mut()
|
||||
.insert(TypeId::of::<T>(), Box::new(value));
|
||||
|
||||
unsafe { &*any }
|
||||
value2
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
|
||||
@@ -240,20 +240,19 @@ impl VirtualDom {
|
||||
mutations: Mutations::default(),
|
||||
};
|
||||
|
||||
let root = dom.new_scope(Box::new(VProps::new(
|
||||
root,
|
||||
|_, _| unreachable!(),
|
||||
root_props,
|
||||
)));
|
||||
let root = dom.new_scope(
|
||||
Box::new(VProps::new(root, |_, _| unreachable!(), root_props)),
|
||||
"app",
|
||||
);
|
||||
|
||||
// The root component is always a suspense boundary for any async children
|
||||
// This could be unexpected, so we might rethink this behavior later
|
||||
//
|
||||
// We *could* just panic if the suspense boundary is not found
|
||||
root.provide_context(SuspenseContext::new(ScopeId(0)));
|
||||
root.provide_context(Rc::new(SuspenseContext::new(ScopeId(0))));
|
||||
|
||||
// Unlike react, we provide a default error boundary that just renders the error as a string
|
||||
root.provide_context(ErrorBoundary::new(ScopeId(0)));
|
||||
root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0))));
|
||||
|
||||
// the root element is always given element ID 0 since it's the container for the entire tree
|
||||
dom.elements.insert(ElementRef::null());
|
||||
@@ -297,7 +296,7 @@ impl VirtualDom {
|
||||
/// currently suspended.
|
||||
pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
|
||||
!self.scopes[id.0]
|
||||
.consume_context::<SuspenseContext>()
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap()
|
||||
.waiting_on
|
||||
.borrow()
|
||||
@@ -528,7 +527,7 @@ impl VirtualDom {
|
||||
// first, unload any complete suspense trees
|
||||
for finished_fiber in self.finished_fibers.drain(..) {
|
||||
let scope = &mut self.scopes[finished_fiber.0];
|
||||
let context = scope.has_context::<SuspenseContext>().unwrap();
|
||||
let context = scope.has_context::<Rc<SuspenseContext>>().unwrap();
|
||||
|
||||
self.mutations
|
||||
.templates
|
||||
@@ -566,7 +565,7 @@ impl VirtualDom {
|
||||
// No placeholder necessary since this is a diff
|
||||
if !self.collected_leaves.is_empty() {
|
||||
let mut boundary = self.scopes[dirty.id.0]
|
||||
.consume_context::<SuspenseContext>()
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap();
|
||||
|
||||
let boundary_mut = boundary.borrow_mut();
|
||||
|
||||
@@ -17,12 +17,12 @@ fn app(cx: Scope) -> Element {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_goes() {
|
||||
fn bubbles_error() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
let _edits = dom.rebuild().santize();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
|
||||
dom.render_immediate();
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
@@ -320,9 +320,9 @@ fn remove_list() {
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
Remove { id: ElementId(3) },
|
||||
Remove { id: ElementId(5) },
|
||||
Remove { id: ElementId(4) },
|
||||
Remove { id: ElementId(5) }
|
||||
Remove { id: ElementId(3) },
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -345,10 +345,10 @@ fn no_common_keys() {
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
Remove { id: ElementId(2) },
|
||||
Remove { id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
Remove { id: ElementId(2) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||
ReplaceWith { id: ElementId(1), m: 3 }
|
||||
]
|
||||
|
||||
@@ -357,11 +357,11 @@ fn remove_many() {
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
Remove { id: ElementId(1,) },
|
||||
Remove { id: ElementId(5,) },
|
||||
Remove { id: ElementId(7,) },
|
||||
Remove { id: ElementId(9,) },
|
||||
CreatePlaceholder { id: ElementId(9,) },
|
||||
Remove { id: ElementId(7,) },
|
||||
Remove { id: ElementId(5,) },
|
||||
Remove { id: ElementId(1,) },
|
||||
CreatePlaceholder { id: ElementId(3,) },
|
||||
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
@@ -372,8 +372,8 @@ fn remove_many() {
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
HydrateText { path: &[0,], value: "hello 0", id: ElementId(10,) },
|
||||
ReplaceWith { id: ElementId(9,), m: 1 },
|
||||
HydrateText { path: &[0,], value: "hello 0", id: ElementId(1,) },
|
||||
ReplaceWith { id: ElementId(3,), m: 1 },
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -8,16 +8,16 @@ fn miri_rollover() {
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
for x in 0..3 {
|
||||
for _ in 0..3 {
|
||||
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
|
||||
dom.process_events();
|
||||
dom.render_immediate();
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut idx = use_state(cx, || 0);
|
||||
let onhover = |h| println!("go!");
|
||||
let onhover = |_| println!("go!");
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
@@ -31,7 +31,7 @@ fn app(cx: Scope) -> Element {
|
||||
button { onclick: move |_| idx -= 1, "-" }
|
||||
ul {
|
||||
(0..**idx).map(|i| rsx! {
|
||||
Child { i: i, onhover: onhover }
|
||||
child_example { i: i, onhover: onhover }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ fn app(cx: Scope) -> Element {
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||
fn child_example<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||
cx.render(rsx! {
|
||||
li {
|
||||
onmouseover: move |e| onhover.call(e),
|
||||
|
||||
@@ -97,8 +97,8 @@ fn memo_works_properly() {
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
dom.rebuild();
|
||||
todo!()
|
||||
_ = dom.rebuild();
|
||||
// todo!()
|
||||
// dom.hard_diff(ScopeId(0));
|
||||
// dom.hard_diff(ScopeId(0));
|
||||
// dom.hard_diff(ScopeId(0));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Tests related to safety of the library.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::SuspenseContext;
|
||||
|
||||
@@ -18,5 +20,5 @@ fn root_node_isnt_null() {
|
||||
|
||||
// There should be a default suspense context
|
||||
// todo: there should also be a default error boundary
|
||||
assert!(scope.has_context::<SuspenseContext>().is_some());
|
||||
assert!(scope.has_context::<Rc<SuspenseContext>>().is_some());
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use dioxus::core::ElementId;
|
||||
use dioxus::core::{Mutation::*, SuspenseContext};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::SuspenseContext;
|
||||
use std::future::IntoFuture;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_works() {
|
||||
@@ -51,10 +51,12 @@ fn app(cx: Scope) -> Element {
|
||||
}
|
||||
|
||||
fn suspense_boundary(cx: Scope) -> Element {
|
||||
cx.use_hook(|| cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id()))));
|
||||
cx.use_hook(|| {
|
||||
cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id())));
|
||||
});
|
||||
|
||||
// Ensure the right types are found
|
||||
cx.has_context::<SuspenseContext>().unwrap();
|
||||
cx.has_context::<Rc<SuspenseContext>>().unwrap();
|
||||
|
||||
cx.render(rsx!(async_child {}))
|
||||
}
|
||||
@@ -72,7 +74,6 @@ async fn async_text(cx: Scope<'_>) -> Element {
|
||||
|
||||
let age = use_future!(cx, || async {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
println!("long future completed");
|
||||
1234
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ webbrowser = "0.8.0"
|
||||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
|
||||
interprocess = { version = "1.1.1" }
|
||||
interprocess = { version = "1.1.1", optional = true}
|
||||
futures-util = "0.3.25"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
@@ -44,11 +44,12 @@ objc_id = "0.1.1"
|
||||
core-foundation = "0.9.3"
|
||||
|
||||
[features]
|
||||
default = ["tokio_runtime"]
|
||||
default = ["tokio_runtime", "hot-reload"]
|
||||
tokio_runtime = ["tokio"]
|
||||
fullscreen = ["wry/fullscreen"]
|
||||
transparent = ["wry/transparent"]
|
||||
tray = ["wry/tray"]
|
||||
hot-reload = ["interprocess"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||
use crate::events::{decode_event, EventMessage};
|
||||
use dioxus_core::*;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use futures_channel::mpsc::{unbounded, UnboundedSender};
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(target_os = "ios")]
|
||||
use objc::runtime::Object;
|
||||
@@ -22,6 +22,9 @@ pub(super) struct DesktopController {
|
||||
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
|
||||
pub(super) quit_app_on_close: bool,
|
||||
pub(super) is_ready: Arc<AtomicBool>,
|
||||
pub(super) proxy: EventLoopProxy<UserWindowEvent>,
|
||||
pub(super) event_tx: UnboundedSender<serde_json::Value>,
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(super) views: Vec<*mut Object>,
|
||||
}
|
||||
@@ -33,9 +36,10 @@ impl DesktopController {
|
||||
root: Component<P>,
|
||||
props: P,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
mut event_rx: UnboundedReceiver<serde_json::Value>,
|
||||
) -> Self {
|
||||
let edit_queue = Arc::new(Mutex::new(Vec::new()));
|
||||
let (event_tx, mut event_rx) = unbounded();
|
||||
let proxy2 = proxy.clone();
|
||||
|
||||
let pending_edits = edit_queue.clone();
|
||||
let desktop_context_proxy = proxy.clone();
|
||||
@@ -54,8 +58,7 @@ impl DesktopController {
|
||||
{
|
||||
let edits = dom.rebuild();
|
||||
let mut queue = edit_queue.lock().unwrap();
|
||||
queue.push(serde_json::to_string(&edits.templates).unwrap());
|
||||
queue.push(serde_json::to_string(&edits.edits).unwrap());
|
||||
queue.push(serde_json::to_string(&edits).unwrap());
|
||||
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
|
||||
}
|
||||
|
||||
@@ -77,12 +80,8 @@ impl DesktopController {
|
||||
.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
|
||||
.await;
|
||||
|
||||
{
|
||||
let mut queue = edit_queue.lock().unwrap();
|
||||
queue.push(serde_json::to_string(&muts.templates).unwrap());
|
||||
queue.push(serde_json::to_string(&muts.edits).unwrap());
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
edit_queue.lock().unwrap().push(serde_json::to_string(&muts).unwrap());
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -92,6 +91,8 @@ impl DesktopController {
|
||||
webviews: HashMap::new(),
|
||||
is_ready: Arc::new(AtomicBool::new(false)),
|
||||
quit_app_on_close: true,
|
||||
proxy: proxy2,
|
||||
event_tx,
|
||||
#[cfg(target_os = "ios")]
|
||||
views: vec![],
|
||||
}
|
||||
@@ -116,12 +117,14 @@ impl DesktopController {
|
||||
|
||||
let (_id, view) = self.webviews.iter_mut().next().unwrap();
|
||||
|
||||
println!("processing pending edits {:?}", new_queue.len());
|
||||
|
||||
for edit in new_queue.drain(..) {
|
||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_template(&self, _serialized_template: String) {
|
||||
todo!("hot reloading currently WIP")
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,11 @@ pub enum UserWindowEvent {
|
||||
DragWindow,
|
||||
FocusWindow,
|
||||
|
||||
/// Set a new Dioxus template for hot-reloading
|
||||
///
|
||||
/// Is a no-op in release builds. Must fit the right format for templates
|
||||
SetTemplate(String),
|
||||
|
||||
Visible(bool),
|
||||
Minimize(bool),
|
||||
Maximize(bool),
|
||||
@@ -195,99 +200,101 @@ pub enum UserWindowEvent {
|
||||
PopView,
|
||||
}
|
||||
|
||||
pub(super) fn handler(
|
||||
user_event: UserWindowEvent,
|
||||
desktop: &mut DesktopController,
|
||||
control_flow: &mut ControlFlow,
|
||||
) {
|
||||
// currently dioxus-desktop supports a single window only,
|
||||
// so we can grab the only webview from the map;
|
||||
// on wayland it is possible that a user event is emitted
|
||||
// before the webview is initialized. ignore the event.
|
||||
let webview = if let Some(webview) = desktop.webviews.values().next() {
|
||||
webview
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let window = webview.window();
|
||||
impl DesktopController {
|
||||
pub(super) fn handle_event(
|
||||
&mut self,
|
||||
user_event: UserWindowEvent,
|
||||
control_flow: &mut ControlFlow,
|
||||
) {
|
||||
// currently dioxus-desktop supports a single window only,
|
||||
// so we can grab the only webview from the map;
|
||||
// on wayland it is possible that a user event is emitted
|
||||
// before the webview is initialized. ignore the event.
|
||||
let webview = if let Some(webview) = self.webviews.values().next() {
|
||||
webview
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
println!("user_event: {:?}", user_event);
|
||||
let window = webview.window();
|
||||
|
||||
match user_event {
|
||||
Initialize | EditsReady => desktop.try_load_ready_webviews(),
|
||||
CloseWindow => *control_flow = ControlFlow::Exit,
|
||||
DragWindow => {
|
||||
// if the drag_window has any errors, we don't do anything
|
||||
window.fullscreen().is_none().then(|| window.drag_window());
|
||||
}
|
||||
Visible(state) => window.set_visible(state),
|
||||
Minimize(state) => window.set_minimized(state),
|
||||
Maximize(state) => window.set_maximized(state),
|
||||
MaximizeToggle => window.set_maximized(!window.is_maximized()),
|
||||
Fullscreen(state) => {
|
||||
if let Some(handle) = window.current_monitor() {
|
||||
window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle))));
|
||||
match user_event {
|
||||
Initialize | EditsReady => self.try_load_ready_webviews(),
|
||||
SetTemplate(template) => self.set_template(template),
|
||||
CloseWindow => *control_flow = ControlFlow::Exit,
|
||||
DragWindow => {
|
||||
// if the drag_window has any errors, we don't do anything
|
||||
window.fullscreen().is_none().then(|| window.drag_window());
|
||||
}
|
||||
}
|
||||
FocusWindow => window.set_focus(),
|
||||
Resizable(state) => window.set_resizable(state),
|
||||
AlwaysOnTop(state) => window.set_always_on_top(state),
|
||||
|
||||
CursorVisible(state) => window.set_cursor_visible(state),
|
||||
CursorGrab(state) => {
|
||||
let _ = window.set_cursor_grab(state);
|
||||
}
|
||||
|
||||
SetTitle(content) => window.set_title(&content),
|
||||
SetDecorations(state) => window.set_decorations(state),
|
||||
|
||||
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
|
||||
|
||||
Print => {
|
||||
if let Err(e) = webview.print() {
|
||||
// we can't panic this error.
|
||||
log::warn!("Open print modal failed: {e}");
|
||||
Visible(state) => window.set_visible(state),
|
||||
Minimize(state) => window.set_minimized(state),
|
||||
Maximize(state) => window.set_maximized(state),
|
||||
MaximizeToggle => window.set_maximized(!window.is_maximized()),
|
||||
Fullscreen(state) => {
|
||||
if let Some(handle) = window.current_monitor() {
|
||||
window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle))));
|
||||
}
|
||||
}
|
||||
}
|
||||
DevTool => {
|
||||
#[cfg(debug_assertions)]
|
||||
webview.open_devtools();
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
FocusWindow => window.set_focus(),
|
||||
Resizable(state) => window.set_resizable(state),
|
||||
AlwaysOnTop(state) => window.set_always_on_top(state),
|
||||
|
||||
Eval(code) => {
|
||||
if let Err(e) = webview.evaluate_script(code.as_str()) {
|
||||
// we can't panic this error.
|
||||
log::warn!("Eval script error: {e}");
|
||||
CursorVisible(state) => window.set_cursor_visible(state),
|
||||
CursorGrab(state) => {
|
||||
let _ = window.set_cursor_grab(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PushView(view) => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
let ui_view = window.ui_view() as *mut Object;
|
||||
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
|
||||
let _: () = msg_send![view, setFrame: ui_view_frame];
|
||||
let _: () = msg_send![view, setAutoresizingMask: 31];
|
||||
SetTitle(content) => window.set_title(&content),
|
||||
SetDecorations(state) => window.set_decorations(state),
|
||||
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
desktop.views.push(ui_view);
|
||||
},
|
||||
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
|
||||
|
||||
Print => {
|
||||
if let Err(e) = webview.print() {
|
||||
// we can't panic this error.
|
||||
log::warn!("Open print modal failed: {e}");
|
||||
}
|
||||
}
|
||||
DevTool => {
|
||||
#[cfg(debug_assertions)]
|
||||
webview.open_devtools();
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
|
||||
Eval(code) => {
|
||||
if let Err(e) = webview.evaluate_script(code.as_str()) {
|
||||
// we can't panic this error.
|
||||
log::warn!("Eval script error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PushView(view) => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
let ui_view = window.ui_view() as *mut Object;
|
||||
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
|
||||
let _: () = msg_send![view, setFrame: ui_view_frame];
|
||||
let _: () = msg_send![view, setAutoresizingMask: 31];
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PopView => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
if let Some(view) = desktop.views.pop() {
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
}
|
||||
},
|
||||
self.views.push(ui_view);
|
||||
},
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PopView => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
if let Some(view) = self.views.pop() {
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Convert a serialized event to an event trigger
|
||||
|
||||
use dioxus_core::ElementId;
|
||||
use dioxus_html::events::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::from_value;
|
||||
@@ -43,7 +42,6 @@ macro_rules! match_data {
|
||||
) => {
|
||||
match $name {
|
||||
$( $($mname)|* => {
|
||||
println!("casting to type {:?}", std::any::TypeId::of::<$tip>());
|
||||
let val: $tip = from_value::<$tip>($m).ok()?;
|
||||
Rc::new(val) as Rc<dyn Any>
|
||||
})*
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use dioxus_core::VirtualDom;
|
||||
|
||||
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::time::Duration;
|
||||
@@ -10,18 +13,21 @@ fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalS
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn init(dom: &VirtualDom) {
|
||||
pub(crate) fn init(_dom: &VirtualDom) {
|
||||
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
|
||||
Arc::new(Mutex::new(None));
|
||||
|
||||
let latest_in_connection_handle = latest_in_connection.clone();
|
||||
|
||||
// connect to processes for incoming data
|
||||
std::thread::spawn(move || {
|
||||
// if let Ok(listener) = LocalSocketListener::bind("@dioxusin") {
|
||||
// for conn in listener.incoming().filter_map(handle_error) {
|
||||
// *latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
|
||||
// }
|
||||
// }
|
||||
let temp_file = std::env::temp_dir().join("@dioxusin");
|
||||
|
||||
if let Ok(listener) = LocalSocketListener::bind(temp_file) {
|
||||
for conn in listener.incoming().filter_map(handle_error) {
|
||||
*latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
|
||||
+125
-112
@@ -8,13 +8,17 @@ mod controller;
|
||||
mod desktop_context;
|
||||
mod escape;
|
||||
mod events;
|
||||
#[cfg(any(feature = "hot-reload", debug_assertions))]
|
||||
mod hot_reload;
|
||||
mod protocol;
|
||||
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
mod hot_reload;
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use desktop_context::UserWindowEvent;
|
||||
pub use desktop_context::{use_eval, use_window, DesktopContext};
|
||||
use futures_channel::mpsc::unbounded;
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
|
||||
@@ -100,15 +104,68 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
||||
/// ```
|
||||
pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cfg: Config) {
|
||||
let event_loop = EventLoop::with_user_event();
|
||||
let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy());
|
||||
|
||||
let (event_tx, event_rx) = unbounded();
|
||||
let mut desktop =
|
||||
DesktopController::new_on_tokio(root, props, event_loop.create_proxy(), event_rx);
|
||||
let proxy = event_loop.create_proxy();
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
// We assume that if the icon is None, then the user just didnt set it
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop),
|
||||
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::UserEvent(user_event) => desktop.handle_event(user_event, control_flow),
|
||||
Event::MainEventsCleared => {}
|
||||
Event::Resumed => {}
|
||||
Event::Suspended => {}
|
||||
Event::LoopDestroyed => {}
|
||||
Event::RedrawRequested(_id) => {}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl DesktopController {
|
||||
fn start(
|
||||
&mut self,
|
||||
cfg: &mut Config,
|
||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
let webview = build_webview(
|
||||
cfg,
|
||||
event_loop,
|
||||
self.is_ready.clone(),
|
||||
self.proxy.clone(),
|
||||
self.event_tx.clone(),
|
||||
);
|
||||
|
||||
self.webviews.insert(webview.window().id(), webview);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_webview(
|
||||
cfg: &mut Config,
|
||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||
is_ready: Arc<AtomicBool>,
|
||||
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
|
||||
event_tx: UnboundedSender<serde_json::Value>,
|
||||
) -> wry::webview::WebView {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let resource_dir = cfg.resource_dir.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
|
||||
// We assume that if the icon is None in cfg, then the user just didnt set it
|
||||
if cfg.window.window.window_icon.is_none() {
|
||||
cfg.window = cfg.window.with_window_icon(Some(
|
||||
window.set_window_icon(Some(
|
||||
tao::window::Icon::from_rgba(
|
||||
include_bytes!("./assets/default_icon.bin").to_vec(),
|
||||
460,
|
||||
@@ -118,85 +175,62 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
|
||||
));
|
||||
}
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
// println!("window event: {:?}", window_event);
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => {
|
||||
let builder = cfg.window.clone();
|
||||
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let window_id = window.id();
|
||||
|
||||
let (is_ready, _) = (desktop.is_ready.clone(), ());
|
||||
// let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone());
|
||||
|
||||
let proxy = proxy.clone();
|
||||
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let resource_dir = cfg.resource_dir.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
let event_tx = event_tx.clone();
|
||||
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_transparent(cfg.window.window.transparent)
|
||||
.with_url("dioxus://index.html/")
|
||||
.unwrap()
|
||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||
parse_ipc_message(&payload)
|
||||
.map(|message| match message.method() {
|
||||
"user_event" => {
|
||||
_ = event_tx.unbounded_send(message.params());
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_transparent(cfg.window.window.transparent)
|
||||
.with_url("dioxus://index.html/")
|
||||
.unwrap()
|
||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||
parse_ipc_message(&payload)
|
||||
.map(|message| match message.method() {
|
||||
"user_event" => {
|
||||
_ = event_tx.unbounded_send(message.params());
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
"browser_open" => {
|
||||
let data = message.params();
|
||||
log::trace!("Open browser: {:?}", data);
|
||||
if let Some(temp) = data.as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let url = temp.get("href").unwrap().as_str().unwrap();
|
||||
if let Err(e) = webbrowser::open(url) {
|
||||
log::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
println!("initializing...");
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
"browser_open" => {
|
||||
let data = message.params();
|
||||
log::trace!("Open browser: {:?}", data);
|
||||
if let Some(temp) = data.as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let url = temp.get("href").unwrap().as_str().unwrap();
|
||||
if let Err(e) = webbrowser::open(url) {
|
||||
log::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("invalid IPC message received");
|
||||
});
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
resource_dir.clone(),
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
)
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("invalid IPC message received");
|
||||
});
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
resource_dir.clone(),
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
)
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
}
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
}
|
||||
|
||||
if cfg.disable_context_menu {
|
||||
// in release mode, we don't want to show the dev tool or reload menus
|
||||
webview = webview.with_initialization_script(
|
||||
r#"
|
||||
if cfg.disable_context_menu {
|
||||
// in release mode, we don't want to show the dev tool or reload menus
|
||||
webview = webview.with_initialization_script(
|
||||
r#"
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
@@ -207,32 +241,11 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
|
||||
});
|
||||
}
|
||||
"#,
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
|
||||
desktop.webviews.insert(window_id, webview.build().unwrap());
|
||||
}
|
||||
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::UserEvent(user_event) => {
|
||||
desktop_context::handler(user_event, &mut desktop, control_flow)
|
||||
}
|
||||
Event::MainEventsCleared => {}
|
||||
Event::Resumed => {}
|
||||
Event::Suspended => {}
|
||||
Event::LoopDestroyed => {}
|
||||
Event::RedrawRequested(_id) => {}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
webview.build().unwrap()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::AtomRoot;
|
||||
use dioxus_core::ScopeState;
|
||||
use std::rc::Rc;
|
||||
|
||||
// Returns the atom root, initiaizing it at the root of the app if it does not exist.
|
||||
pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use dioxus_core::{prelude::EventHandler, ScopeState};
|
||||
use dioxus_core::ScopeState;
|
||||
use std::future::Future;
|
||||
|
||||
#[macro_export]
|
||||
@@ -19,8 +17,14 @@ macro_rules! use_callback {
|
||||
move || $($rest)*
|
||||
)
|
||||
};
|
||||
($cx:ident, $($rest:tt)*) => {
|
||||
use_callback(
|
||||
$cx,
|
||||
move || $($rest)*
|
||||
)
|
||||
};
|
||||
}
|
||||
pub fn use_callback<'a, T, R, F>(cx: &'a ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + 'a
|
||||
pub fn use_callback<T, R, F>(cx: &ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + '_
|
||||
where
|
||||
R: FnMut(T) -> F + 'static,
|
||||
F: Future<Output = ()> + 'static,
|
||||
@@ -30,8 +34,8 @@ where
|
||||
move |evt| cx.spawn(hook(evt))
|
||||
}
|
||||
|
||||
fn it_works(cx: &ScopeState) {
|
||||
let p = use_callback(cx, || {
|
||||
fn _it_works(cx: &ScopeState) {
|
||||
let _p = use_callback(cx, || {
|
||||
|()| async {
|
||||
//
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use dioxus_core::ScopeState;
|
||||
|
||||
/// Consume some context in the tree
|
||||
pub fn use_context<T: 'static>(cx: &ScopeState) -> Option<&T> {
|
||||
match *cx.use_hook(|| cx.consume_context::<T>().map(|t| t as *const T)) {
|
||||
Some(res) => Some(unsafe { &*res }),
|
||||
None => None,
|
||||
}
|
||||
/// Consume some context in the tree, providing a sharable handle to the value
|
||||
pub fn use_context<T: 'static + Clone>(cx: &ScopeState) -> Option<&T> {
|
||||
cx.use_hook(|| cx.consume_context::<T>()).as_ref()
|
||||
}
|
||||
|
||||
/// Provide some context via the tree and return a reference to it
|
||||
///
|
||||
/// Once the context has been provided, it is immutable. Mutations should be done via interior mutability.
|
||||
pub fn use_context_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) -> &T {
|
||||
let ptr = *cx.use_hook(|| cx.provide_context(f()) as *const T);
|
||||
unsafe { &*ptr }
|
||||
pub fn use_context_provider<T: 'static + Clone>(cx: &ScopeState, f: impl FnOnce() -> T) -> &T {
|
||||
cx.use_hook(|| {
|
||||
let val = f();
|
||||
cx.provide_context(val.clone());
|
||||
val
|
||||
})
|
||||
}
|
||||
@@ -66,9 +66,7 @@ where
|
||||
// if there's a waker, we dont re-render the component. Instead we just progress that future
|
||||
match waker.borrow().as_ref() {
|
||||
Some(waker) => waker.wake_by_ref(),
|
||||
None => {
|
||||
// schedule_update()
|
||||
}
|
||||
None => schedule_update(),
|
||||
}
|
||||
})));
|
||||
}
|
||||
@@ -122,7 +120,7 @@ impl<T> UseFuture<T> {
|
||||
}
|
||||
|
||||
// Manually set the value in the future slot without starting the future over
|
||||
pub fn set(&self, new_value: T) {
|
||||
pub fn set(&self, _new_value: T) {
|
||||
// self.slot.set(Some(new_value));
|
||||
// self.needs_regen.set(true);
|
||||
// (self.update)();
|
||||
|
||||
@@ -33,9 +33,9 @@ fn render_template_node(node: &TemplateNode, out: &mut String) -> std::fmt::Resu
|
||||
}
|
||||
write!(out, "</{tag}>")?;
|
||||
}
|
||||
TemplateNode::Text(t) => write!(out, "{t}")?,
|
||||
TemplateNode::Dynamic(_) => write!(out, "<pre hidden />")?,
|
||||
TemplateNode::DynamicText(t) => write!(out, "<!-- --> {t} <!-- -->")?,
|
||||
TemplateNode::Text { text: t } => write!(out, "{t}")?,
|
||||
TemplateNode::Dynamic { id: _ } => write!(out, "<pre hidden />")?,
|
||||
TemplateNode::DynamicText { id: t } => write!(out, "<!-- --> {t} <!-- -->")?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,11 +12,14 @@ extern "C" {
|
||||
pub fn new(arg: Element) -> Interpreter;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn SaveTemplate(this: &Interpreter, nodes: Vec<Node>, name: &str);
|
||||
pub fn SaveTemplate(this: &Interpreter, template: JsValue);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn MountToRoot(this: &Interpreter);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn AppendChildren(this: &Interpreter, m: u32);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
|
||||
|
||||
@@ -58,8 +61,8 @@ extern "C" {
|
||||
this: &Interpreter,
|
||||
name: &str,
|
||||
id: u32,
|
||||
handler: &Function,
|
||||
bubbles: bool,
|
||||
handler: &Function,
|
||||
);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
|
||||
@@ -76,9 +76,6 @@ export class Interpreter {
|
||||
pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
SaveTemplate(nodes, name) {
|
||||
this.templates[name] = nodes;
|
||||
}
|
||||
MountToRoot() {
|
||||
this.AppendChildren(this.stack.length - 1);
|
||||
}
|
||||
@@ -140,7 +137,7 @@ export class Interpreter {
|
||||
this.stack.push(el);
|
||||
this.nodes[root] = el;
|
||||
}
|
||||
NewEventListener(event_name, root, handler, bubbles) {
|
||||
NewEventListener(event_name, root, bubbles, handler) {
|
||||
const element = this.nodes[root];
|
||||
element.setAttribute("data-dioxus-id", `${root}`);
|
||||
this.listeners.create(event_name, element, handler, bubbles);
|
||||
@@ -213,10 +210,56 @@ export class Interpreter {
|
||||
}
|
||||
}
|
||||
handleEdits(edits) {
|
||||
for (let edit of edits) {
|
||||
for (let template of edits.templates) {
|
||||
this.SaveTemplate(template);
|
||||
}
|
||||
|
||||
for (let edit of edits.edits) {
|
||||
this.handleEdit(edit);
|
||||
}
|
||||
}
|
||||
|
||||
SaveTemplate(template) {
|
||||
let roots = [];
|
||||
for (let root of template.roots) {
|
||||
roots.push(this.MakeTemplateNode(root));
|
||||
}
|
||||
this.templates[template.name] = roots;
|
||||
}
|
||||
|
||||
MakeTemplateNode(node) {
|
||||
console.log("making template node", node);
|
||||
switch (node.type) {
|
||||
case "Text":
|
||||
return document.createTextNode(node.text);
|
||||
case "Dynamic":
|
||||
let dyn = document.createElement("pre");
|
||||
dyn.hidden = true;
|
||||
return dyn;
|
||||
case "DynamicText":
|
||||
return document.createTextNode("placeholder");
|
||||
case "Element":
|
||||
let el;
|
||||
|
||||
if (node.namespace != null) {
|
||||
el = document.createElementNS(node.namespace, node.tag);
|
||||
} else {
|
||||
el = document.createElement(node.tag);
|
||||
}
|
||||
|
||||
for (let attr of node.attrs) {
|
||||
if (attr.type == "Static") {
|
||||
this.SetAttributeInner(el, attr.name, attr.value, attr.namespace);
|
||||
}
|
||||
}
|
||||
|
||||
for (let child of node.children) {
|
||||
el.appendChild(this.MakeTemplateNode(child));
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
}
|
||||
AssignId(path, id) {
|
||||
this.nodes[id] = this.LoadChild(path);
|
||||
}
|
||||
@@ -232,7 +275,16 @@ export class Interpreter {
|
||||
}
|
||||
HydrateText(path, value, id) {
|
||||
let node = this.LoadChild(path);
|
||||
node.textContent = value;
|
||||
|
||||
if (node.nodeType == Node.TEXT_NODE) {
|
||||
node.textContent = value;
|
||||
} else {
|
||||
// replace with a textnode
|
||||
let text = document.createTextNode(value);
|
||||
node.replaceWith(text);
|
||||
node = text;
|
||||
}
|
||||
|
||||
this.nodes[id] = node;
|
||||
}
|
||||
ReplacePlaceholder(path, m) {
|
||||
@@ -296,17 +348,12 @@ export class Interpreter {
|
||||
this.RemoveAttribute(edit.id, edit.name, edit.ns);
|
||||
break;
|
||||
case "RemoveEventListener":
|
||||
this.RemoveEventListener(edit.id, edit.event_name);
|
||||
this.RemoveEventListener(edit.id, edit.name);
|
||||
break;
|
||||
case "NewEventListener":
|
||||
// console.log("creating listener! ", edit);
|
||||
|
||||
// this handler is only provided on desktop implementations since this
|
||||
// method is not used by the web implementation
|
||||
let handler = (event) => {
|
||||
|
||||
console.log("event", event);
|
||||
|
||||
let target = event.target;
|
||||
if (target != null) {
|
||||
let realId = target.getAttribute(`data-dioxus-id`);
|
||||
@@ -387,17 +434,14 @@ export class Interpreter {
|
||||
}
|
||||
window.ipc.postMessage(
|
||||
serializeIpcMessage("user_event", {
|
||||
event: edit.event_name,
|
||||
event: edit.name,
|
||||
mounted_dom_id: parseInt(realId),
|
||||
contents: contents,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
console.log("adding event listener", edit);
|
||||
this.NewEventListener(edit.event_name, edit.id, handler, event_bubbles(edit.event_name));
|
||||
|
||||
this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ impl<S: State> RealDom<S> {
|
||||
},
|
||||
OwnedAttributeValue::Text(value.to_string()),
|
||||
)),
|
||||
dioxus_core::TemplateAttribute::Dynamic(_) => None,
|
||||
dioxus_core::TemplateAttribute::Dynamic { .. } => None,
|
||||
})
|
||||
.collect(),
|
||||
listeners: FxHashSet::default(),
|
||||
@@ -144,17 +144,17 @@ impl<S: State> RealDom<S> {
|
||||
}
|
||||
node_id
|
||||
}
|
||||
TemplateNode::Text(txt) => {
|
||||
TemplateNode::Text { text } => {
|
||||
let node_id = self.create_node(Node::new(NodeType::Text {
|
||||
text: txt.to_string(),
|
||||
text: text.to_string(),
|
||||
}));
|
||||
node_id
|
||||
}
|
||||
TemplateNode::Dynamic(_) => {
|
||||
TemplateNode::Dynamic { .. } => {
|
||||
let node_id = self.create_node(Node::new(NodeType::Placeholder));
|
||||
node_id
|
||||
}
|
||||
TemplateNode::DynamicText(_) => {
|
||||
TemplateNode::DynamicText { .. } => {
|
||||
let node_id = self.create_node(Node::new(NodeType::Text {
|
||||
text: String::new(),
|
||||
}));
|
||||
@@ -220,7 +220,12 @@ impl<S: State> RealDom<S> {
|
||||
let node = self.tree.get_mut(node_id).unwrap();
|
||||
if let NodeType::Text { text } = &mut node.node_data.node_type {
|
||||
*text = value.to_string();
|
||||
} else {
|
||||
node.node_data.node_type = NodeType::Text {
|
||||
text: value.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
mark_dirty(node_id, NodeMask::new().with_text(), &mut nodes_updated);
|
||||
}
|
||||
LoadTemplate { name, index, id } => {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{use_route, RouterCore};
|
||||
use crate::{use_route, RouterContext};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// Props for the [`Link`](struct.Link.html) component.
|
||||
@@ -77,7 +75,7 @@ pub struct LinkProps<'a> {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
let svc = cx.use_hook(|| cx.consume_context::<Arc<RouterCore>>());
|
||||
let svc = use_context::<RouterContext>(cx);
|
||||
|
||||
let LinkProps {
|
||||
to,
|
||||
@@ -107,7 +105,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
}
|
||||
};
|
||||
|
||||
let route = use_route(&cx);
|
||||
let route = use_route(cx);
|
||||
let url = route.url();
|
||||
let path = url.path();
|
||||
let active = path == cx.props.to;
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct RedirectProps<'a> {
|
||||
///
|
||||
/// It will replace the current route rather than pushing the current one to the stack.
|
||||
pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element {
|
||||
let router = use_router(&cx);
|
||||
let router = use_router(cx);
|
||||
|
||||
let immediate_redirect = cx.use_hook(|| {
|
||||
if let Some(from) = cx.props.from {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::{RouteContext, RouterContext};
|
||||
use dioxus::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{RouteContext, RouterCore};
|
||||
|
||||
/// Props for the [`Route`](struct.Route.html) component.
|
||||
#[derive(Props)]
|
||||
@@ -27,15 +25,13 @@ pub struct RouteProps<'a> {
|
||||
/// )
|
||||
/// ```
|
||||
pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
|
||||
let router_root = cx
|
||||
.use_hook(|| cx.consume_context::<Arc<RouterCore>>())
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let router_root = use_context::<RouterContext>(cx).unwrap();
|
||||
let root_context = use_context::<RouteContext>(cx);
|
||||
|
||||
cx.use_hook(|| {
|
||||
// create a bigger, better, longer route if one above us exists
|
||||
let total_route = match cx.consume_context::<RouteContext>() {
|
||||
Some(ctx) => ctx.total_route,
|
||||
let total_route = match root_context {
|
||||
Some(ctx) => ctx.total_route.clone(),
|
||||
None => cx.props.to.to_string(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::{cfg::RouterCfg, RouterCore};
|
||||
use crate::{cfg::RouterCfg, RouterContext, RouterService};
|
||||
use dioxus::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The props for the [`Router`](fn.Router.html) component.
|
||||
#[derive(Props)]
|
||||
@@ -21,7 +20,7 @@ pub struct RouterProps<'a> {
|
||||
///
|
||||
/// This lets you easily implement redirects
|
||||
#[props(default)]
|
||||
pub onchange: EventHandler<'a, Arc<RouterCore>>,
|
||||
pub onchange: EventHandler<'a, RouterContext>,
|
||||
|
||||
/// Set the active class of all Link components contained in this router.
|
||||
///
|
||||
@@ -40,15 +39,15 @@ pub struct RouterProps<'a> {
|
||||
/// Will fallback to HashRouter is BrowserRouter is not available, or through configuration.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
|
||||
let svc = cx.use_hook(|| {
|
||||
cx.provide_context(RouterCore::new(
|
||||
&cx,
|
||||
let svc = use_context_provider(cx, || {
|
||||
RouterService::new(
|
||||
cx,
|
||||
RouterCfg {
|
||||
base_url: cx.props.base_url.map(|s| s.to_string()),
|
||||
active_class: cx.props.active_class.map(|s| s.to_string()),
|
||||
initial_url: cx.props.initial_url.clone(),
|
||||
},
|
||||
))
|
||||
)
|
||||
});
|
||||
|
||||
// next time we run the rout_found will be filled
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{ParsedRoute, RouteContext, RouterCore, RouterService};
|
||||
use crate::{ParsedRoute, RouteContext, RouterContext};
|
||||
use dioxus::core::{ScopeId, ScopeState};
|
||||
use std::{borrow::Cow, str::FromStr, sync::Arc};
|
||||
use url::Url;
|
||||
@@ -7,11 +7,11 @@ use url::Url;
|
||||
/// context of a [`Router`]. If this function is called outside of a `Router`
|
||||
/// component it will panic.
|
||||
pub fn use_route(cx: &ScopeState) -> &UseRoute {
|
||||
let handle = cx.use_hook(|| {
|
||||
let router = cx
|
||||
.consume_context::<RouterService>()
|
||||
.expect("Cannot call use_route outside the scope of a Router component");
|
||||
let router = cx
|
||||
.consume_context::<RouterContext>()
|
||||
.expect("Cannot call use_route outside the scope of a Router component");
|
||||
|
||||
let handle = cx.use_hook(|| {
|
||||
let route_context = cx.consume_context::<RouteContext>();
|
||||
|
||||
router.subscribe_onchange(cx.scope_id());
|
||||
@@ -115,7 +115,7 @@ impl UseRoute {
|
||||
// and reveal our cached version of UseRoute to the component.
|
||||
struct UseRouteListener {
|
||||
state: UseRoute,
|
||||
router: Arc<RouterCore>,
|
||||
router: RouterContext,
|
||||
scope: ScopeId,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use crate::RouterService;
|
||||
use dioxus::core::ScopeState;
|
||||
use crate::RouterContext;
|
||||
use dioxus::{core::ScopeState, prelude::use_context};
|
||||
|
||||
/// This hook provides access to the `RouterService` for the app.
|
||||
pub fn use_router(cx: &ScopeState) -> &RouterService {
|
||||
cx.use_hook(|| {
|
||||
cx.consume_context::<RouterService>()
|
||||
.expect("Cannot call use_route outside the scope of a Router component")
|
||||
})
|
||||
pub fn use_router(cx: &ScopeState) -> &RouterContext {
|
||||
use_context::<RouterContext>(cx)
|
||||
.expect("Cannot call use_route outside the scope of a Router component")
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
use crate::cfg::RouterCfg;
|
||||
use dioxus::core::{ScopeId, ScopeState, VirtualDom};
|
||||
use std::any::Any;
|
||||
use std::sync::Weak;
|
||||
use std::rc::Weak;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -14,6 +14,9 @@ use std::{
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
/// A clonable handle to the router
|
||||
pub type RouterContext = Rc<RouterService>;
|
||||
|
||||
/// An abstraction over the platform's history API.
|
||||
///
|
||||
/// The history is denoted using web-like semantics, with forward slashes delmitiing
|
||||
@@ -41,7 +44,7 @@ use url::Url;
|
||||
/// - On the web, this is a [`BrowserHistory`](https://docs.rs/gloo/0.3.0/gloo/history/struct.BrowserHistory.html).
|
||||
/// - On desktop, mobile, and SSR, this is just a Vec of Strings. Currently on
|
||||
/// desktop, there is no way to tap into forward/back for the app unless explicitly set.
|
||||
pub struct RouterCore {
|
||||
pub struct RouterService {
|
||||
pub(crate) route_found: Cell<Option<ScopeId>>,
|
||||
|
||||
pub(crate) stack: RefCell<Vec<Arc<ParsedRoute>>>,
|
||||
@@ -61,9 +64,6 @@ pub struct RouterCore {
|
||||
pub(crate) cfg: RouterCfg,
|
||||
}
|
||||
|
||||
/// A shared type for the RouterCore.
|
||||
pub type RouterService = Arc<RouterCore>;
|
||||
|
||||
/// A route is a combination of window title, saved state, and a URL.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedRoute {
|
||||
@@ -77,8 +77,8 @@ pub struct ParsedRoute {
|
||||
pub serialized_state: Option<String>,
|
||||
}
|
||||
|
||||
impl RouterCore {
|
||||
pub(crate) fn new(cx: &ScopeState, cfg: RouterCfg) -> Arc<Self> {
|
||||
impl RouterService {
|
||||
pub(crate) fn new(cx: &ScopeState, cfg: RouterCfg) -> RouterContext {
|
||||
#[cfg(feature = "web")]
|
||||
let history = Box::new(web::new());
|
||||
|
||||
@@ -99,7 +99,7 @@ impl RouterCore {
|
||||
None => Arc::new(history.init_location()),
|
||||
};
|
||||
|
||||
let svc = Arc::new(Self {
|
||||
let svc = Rc::new(Self {
|
||||
cfg,
|
||||
regen_any_route: cx.schedule_update_any(),
|
||||
router_id: cx.scope_id(),
|
||||
@@ -111,7 +111,7 @@ impl RouterCore {
|
||||
history,
|
||||
});
|
||||
|
||||
svc.history.attach_listeners(Arc::downgrade(&svc));
|
||||
svc.history.attach_listeners(Rc::downgrade(&svc));
|
||||
|
||||
svc
|
||||
}
|
||||
@@ -247,12 +247,9 @@ impl RouterCore {
|
||||
/// that owns the router.
|
||||
///
|
||||
/// This might change in the future.
|
||||
pub fn get_router_from_vdom(
|
||||
dom: &VirtualDom,
|
||||
target_scope: Option<ScopeId>,
|
||||
) -> Option<Arc<RouterCore>> {
|
||||
dom.get_scope(target_scope.unwrap_or(ScopeId(0)))
|
||||
.and_then(|scope| scope.consume_context::<Arc<RouterCore>>())
|
||||
pub fn get_router_from_vdom(dom: &VirtualDom, target_scope: ScopeId) -> Option<RouterContext> {
|
||||
dom.get_scope(target_scope)
|
||||
.and_then(|scope| scope.consume_context::<RouterContext>())
|
||||
}
|
||||
|
||||
fn clean_route(route: String) -> String {
|
||||
@@ -319,7 +316,7 @@ pub(crate) trait RouterProvider {
|
||||
fn replace(&self, route: &ParsedRoute);
|
||||
fn native_location(&self) -> Box<dyn Any>;
|
||||
fn init_location(&self) -> ParsedRoute;
|
||||
fn attach_listeners(&self, svc: Weak<RouterCore>);
|
||||
fn attach_listeners(&self, svc: Weak<RouterService>);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "web"))]
|
||||
@@ -350,7 +347,7 @@ mod hash {
|
||||
|
||||
fn replace(&self, _route: &ParsedRoute) {}
|
||||
|
||||
fn attach_listeners(&self, _svc: Weak<RouterCore>) {}
|
||||
fn attach_listeners(&self, _svc: Weak<RouterService>) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,7 +415,7 @@ mod web {
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_listeners(&self, svc: std::sync::Weak<crate::RouterCore>) {
|
||||
fn attach_listeners(&self, svc: std::sync::Weak<crate::RouterService>) {
|
||||
self._listener.set(Some(EventListener::new(
|
||||
&web_sys::window().unwrap(),
|
||||
"popstate",
|
||||
|
||||
@@ -60,7 +60,7 @@ impl Component {
|
||||
match self
|
||||
.fields
|
||||
.iter()
|
||||
.find(|f| f.name.to_string() == "key")
|
||||
.find(|f| f.name == "key")
|
||||
.map(|f| &f.content)
|
||||
{
|
||||
Some(ContentField::Formatted(fmt)) => Some(fmt),
|
||||
|
||||
@@ -21,10 +21,7 @@ pub struct IfmtInput {
|
||||
|
||||
impl IfmtInput {
|
||||
pub fn is_static(&self) -> bool {
|
||||
match self.segments.as_slice() {
|
||||
&[Segment::Literal(_)] => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self.segments.as_slice(), &[Segment::Literal(_)])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-9
@@ -180,10 +180,10 @@ impl<'a> DynamicContext<'a> {
|
||||
// [0, 2]
|
||||
// [0, 2, 1]
|
||||
|
||||
let static_attrs = el.attributes.iter().filter_map(|attr| match &attr.attr {
|
||||
let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
|
||||
ElementAttr::AttrText { name, value } if value.is_static() => {
|
||||
let value = value.source.as_ref().unwrap();
|
||||
Some(quote! {
|
||||
quote! {
|
||||
::dioxus::core::TemplateAttribute::Static {
|
||||
name: dioxus_elements::#el_name::#name.0,
|
||||
namespace: dioxus_elements::#el_name::#name.1,
|
||||
@@ -192,12 +192,12 @@ impl<'a> DynamicContext<'a> {
|
||||
// todo: we don't diff these so we never apply the volatile flag
|
||||
// volatile: dioxus_elements::#el_name::#name.2,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ElementAttr::CustomAttrText { name, value } if value.is_static() => {
|
||||
let value = value.source.as_ref().unwrap();
|
||||
Some(quote! {
|
||||
quote! {
|
||||
::dioxus::core::TemplateAttribute::Static {
|
||||
name: dioxus_elements::#el_name::#name.0,
|
||||
namespace: dioxus_elements::#el_name::#name.1,
|
||||
@@ -206,7 +206,7 @@ impl<'a> DynamicContext<'a> {
|
||||
// todo: we don't diff these so we never apply the volatile flag
|
||||
// volatile: dioxus_elements::#el_name::#name.2,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ElementAttr::AttrExpression { .. }
|
||||
@@ -217,7 +217,7 @@ impl<'a> DynamicContext<'a> {
|
||||
let ct = self.dynamic_attributes.len();
|
||||
self.dynamic_attributes.push(attr);
|
||||
self.attr_paths.push(self.current_path.clone());
|
||||
Some(quote! { ::dioxus::core::TemplateAttribute::Dynamic(#ct) })
|
||||
quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -245,7 +245,7 @@ impl<'a> DynamicContext<'a> {
|
||||
|
||||
BodyNode::Text(text) if text.is_static() => {
|
||||
let text = text.source.as_ref().unwrap();
|
||||
quote! { ::dioxus::core::TemplateNode::Text(#text) }
|
||||
quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
|
||||
}
|
||||
|
||||
BodyNode::RawExpr(_)
|
||||
@@ -258,8 +258,10 @@ impl<'a> DynamicContext<'a> {
|
||||
self.node_paths.push(self.current_path.clone());
|
||||
|
||||
match root {
|
||||
BodyNode::Text(_) => quote! { ::dioxus::core::TemplateNode::DynamicText(#ct) },
|
||||
_ => quote! { ::dioxus::core::TemplateNode::Dynamic(#ct) },
|
||||
BodyNode::Text(_) => {
|
||||
quote! { ::dioxus::core::TemplateNode::DynamicText { id: #ct } }
|
||||
}
|
||||
_ => quote! { ::dioxus::core::TemplateNode::Dynamic { id: #ct } },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ impl ToTokens for BodyNode {
|
||||
pat, expr, body, ..
|
||||
} = exp;
|
||||
|
||||
let renderer = TemplateRenderer { roots: &body };
|
||||
let renderer = TemplateRenderer { roots: body };
|
||||
|
||||
tokens.append_all(quote! {
|
||||
__cx.make_node(
|
||||
|
||||
@@ -65,12 +65,12 @@ impl StringCache {
|
||||
TemplateAttribute::Static { name, value, .. } => {
|
||||
write!(chain, " {}=\"{}\"", name, value)?;
|
||||
}
|
||||
TemplateAttribute::Dynamic(index) => {
|
||||
TemplateAttribute::Dynamic { id: index } => {
|
||||
chain.segments.push(Segment::Attr(*index))
|
||||
}
|
||||
}
|
||||
}
|
||||
if children.len() == 0 && tag_is_self_closing(tag) {
|
||||
if children.is_empty() && tag_is_self_closing(tag) {
|
||||
write!(chain, "/>")?;
|
||||
} else {
|
||||
write!(chain, ">")?;
|
||||
@@ -81,8 +81,8 @@ impl StringCache {
|
||||
}
|
||||
cur_path.pop();
|
||||
}
|
||||
TemplateNode::Text(text) => write!(chain, "{}", text)?,
|
||||
TemplateNode::Dynamic(idx) | TemplateNode::DynamicText(idx) => {
|
||||
TemplateNode::Text { text } => write!(chain, "{}", text)?,
|
||||
TemplateNode::Dynamic { id: idx } | TemplateNode::DynamicText { id: idx } => {
|
||||
chain.segments.push(Segment::Node(*idx))
|
||||
}
|
||||
}
|
||||
@@ -92,9 +92,21 @@ impl StringCache {
|
||||
}
|
||||
|
||||
fn tag_is_self_closing(tag: &str) -> bool {
|
||||
match tag {
|
||||
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" | "meta"
|
||||
| "param" | "source" | "track" | "wbr" => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(
|
||||
tag,
|
||||
"area"
|
||||
| "base"
|
||||
| "br"
|
||||
| "col"
|
||||
| "embed"
|
||||
| "hr"
|
||||
| "img"
|
||||
| "input"
|
||||
| "link"
|
||||
| "meta"
|
||||
| "param"
|
||||
| "source"
|
||||
| "track"
|
||||
| "wbr"
|
||||
)
|
||||
}
|
||||
@@ -1,38 +1 @@
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SsrConfig {
|
||||
/// currently not supported - control if we indent the HTML output
|
||||
indent: bool,
|
||||
|
||||
/// Control if elements are written onto a new line
|
||||
newline: bool,
|
||||
|
||||
/// Choose to write ElementIDs into elements so the page can be re-hydrated later on
|
||||
pre_render: bool,
|
||||
|
||||
// Currently not implemented
|
||||
// Don't proceed onto new components. Instead, put the name of the component.
|
||||
// TODO: components don't have names :(
|
||||
skip_components: bool,
|
||||
}
|
||||
|
||||
impl SsrConfig {
|
||||
pub fn indent(mut self, a: bool) -> Self {
|
||||
self.indent = a;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn newline(mut self, a: bool) -> Self {
|
||||
self.newline = a;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pre_render(mut self, a: bool) -> Self {
|
||||
self.pre_render = a;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn skip_components(mut self, a: bool) -> Self {
|
||||
self.skip_components = a;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use dioxus_core::{LazyNodes, ScopeId, VirtualDom};
|
||||
|
||||
use crate::config::SsrConfig;
|
||||
|
||||
pub fn pre_render(dom: &VirtualDom) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn pre_render_to(dom: &VirtualDom, write: impl Write) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn render_vdom(dom: &VirtualDom) -> String {
|
||||
todo!()
|
||||
// format!("{:}", TextRenderer::from_vdom(dom, SsrConfig::default()))
|
||||
}
|
||||
|
||||
pub fn pre_render_vdom(dom: &VirtualDom) -> String {
|
||||
todo!()
|
||||
// format!(
|
||||
// "{:}",
|
||||
// TextRenderer::from_vdom(dom, SsrConfig::default().pre_render(true))
|
||||
// )
|
||||
}
|
||||
|
||||
pub fn render_vdom_cfg(dom: &VirtualDom, cfg: SsrConfig) -> String {
|
||||
todo!()
|
||||
// format!(
|
||||
// "{:}",
|
||||
// TextRenderer::from_vdom(dom, cfg(SsrConfig::default()))
|
||||
// )
|
||||
}
|
||||
|
||||
pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
|
||||
todo!()
|
||||
// Some(format!(
|
||||
// "{:}",
|
||||
// TextRenderer {
|
||||
// cfg: SsrConfig::default(),
|
||||
// root: vdom.get_scope(scope).unwrap().root_node(),
|
||||
// vdom: Some(vdom),
|
||||
// }
|
||||
// ))
|
||||
}
|
||||
|
||||
pub fn render_lazy<'a, 'b>(f: LazyNodes<'a, 'b>) -> String {
|
||||
todo!()
|
||||
}
|
||||
+49
-3
@@ -2,8 +2,54 @@
|
||||
|
||||
mod cache;
|
||||
pub mod config;
|
||||
pub mod helpers;
|
||||
pub mod renderer;
|
||||
pub mod template;
|
||||
pub use helpers::*;
|
||||
pub use template::SsrRender;
|
||||
use dioxus_core::{Element, LazyNodes, Scope, VirtualDom};
|
||||
use std::cell::Cell;
|
||||
|
||||
pub use crate::renderer::Renderer;
|
||||
|
||||
/// A convenience function to render an `rsx!` call to a string
|
||||
///
|
||||
/// For advanced rendering, create a new `SsrRender`.
|
||||
pub fn render_lazy(f: LazyNodes<'_, '_>) -> String {
|
||||
// We need to somehow get the lazy call into the virtualdom even with the lifetime
|
||||
// Since the lazy lifetime is valid for this function, we can just transmute it to static temporarily
|
||||
// This is okay since we're returning an owned value
|
||||
struct RootProps<'a, 'b> {
|
||||
caller: Cell<Option<LazyNodes<'a, 'b>>>,
|
||||
}
|
||||
|
||||
fn lazy_app<'a>(cx: Scope<'a, RootProps<'static, 'static>>) -> Element<'a> {
|
||||
let lazy = cx.props.caller.take().unwrap();
|
||||
let lazy: LazyNodes = unsafe { std::mem::transmute(lazy) };
|
||||
Ok(lazy.call(cx))
|
||||
}
|
||||
|
||||
let props: RootProps = unsafe {
|
||||
std::mem::transmute(RootProps {
|
||||
caller: Cell::new(Some(f)),
|
||||
})
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new_with_props(lazy_app, props);
|
||||
_ = dom.rebuild();
|
||||
|
||||
Renderer::new().render(&dom)
|
||||
}
|
||||
|
||||
/// A convenience function to render an existing VirtualDom to a string
|
||||
///
|
||||
/// We generally recommend creating a new `Renderer` to take advantage of template caching.
|
||||
pub fn render(dom: &VirtualDom) -> String {
|
||||
Renderer::new().render(dom)
|
||||
}
|
||||
|
||||
/// A convenience function to pre-render an existing VirtualDom to a string
|
||||
///
|
||||
/// We generally recommend creating a new `Renderer` to take advantage of template caching.
|
||||
pub fn pre_render(dom: &VirtualDom) -> String {
|
||||
let mut renderer = Renderer::new();
|
||||
renderer.pre_render = true;
|
||||
renderer.render(dom)
|
||||
}
|
||||
+163
-128
@@ -1,146 +1,181 @@
|
||||
// use dioxus_core::VirtualDom;
|
||||
use super::cache::Segment;
|
||||
use crate::cache::StringCache;
|
||||
use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::rc::Rc;
|
||||
|
||||
// use crate::config::SsrConfig;
|
||||
/// A virtualdom renderer that caches the templates it has seen for faster rendering
|
||||
#[derive(Default)]
|
||||
pub struct Renderer {
|
||||
/// should we do our best to prettify the output?
|
||||
pub pretty: bool,
|
||||
|
||||
// pub struct SsrRenderer {
|
||||
// vdom: VirtualDom,
|
||||
// cfg: SsrConfig,
|
||||
// }
|
||||
/// Control if elements are written onto a new line
|
||||
pub newline: bool,
|
||||
|
||||
// impl Default for SsrRenderer {
|
||||
// fn default() -> Self {
|
||||
// Self::new(SsrConfig::default())
|
||||
// }
|
||||
// }
|
||||
/// Should we sanitize text nodes? (escape HTML)
|
||||
pub sanitize: bool,
|
||||
|
||||
// impl SsrRenderer {
|
||||
// pub fn new(cfg: SsrConfig) -> Self {
|
||||
// Self {
|
||||
// vdom: VirtualDom::new(app),
|
||||
// cfg,
|
||||
// }
|
||||
// }
|
||||
/// Choose to write ElementIDs into elements so the page can be re-hydrated later on
|
||||
pub pre_render: bool,
|
||||
|
||||
// pub fn render_lazy<'a>(&'a mut self, f: LazyNodes<'a, '_>) -> String {
|
||||
// let scope = self.vdom.base_scope();
|
||||
// let root = f.call(scope);
|
||||
// format!(
|
||||
// "{:}",
|
||||
// TextRenderer {
|
||||
// cfg: self.cfg.clone(),
|
||||
// root: &root,
|
||||
// vdom: Some(&self.vdom),
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// Currently not implemented
|
||||
// Don't proceed onto new components. Instead, put the name of the component.
|
||||
pub skip_components: bool,
|
||||
|
||||
// fn html_render(
|
||||
// &self,
|
||||
// node: &VNode,
|
||||
// f: &mut impl Write,
|
||||
// il: u16,
|
||||
// last_node_was_text: &mut bool,
|
||||
// ) -> std::fmt::Result {
|
||||
// // match &node {
|
||||
// // VNode::Text(text) => {
|
||||
// // if *last_node_was_text {
|
||||
// // write!(f, "<!--spacer-->")?;
|
||||
// // }
|
||||
/// A cache of templates that have been rendered
|
||||
template_cache: HashMap<&'static str, Rc<StringCache>>,
|
||||
}
|
||||
|
||||
// // if self.cfg.indent {
|
||||
// // for _ in 0..il {
|
||||
// // write!(f, " ")?;
|
||||
// // }
|
||||
// // }
|
||||
impl Renderer {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// // *last_node_was_text = true;
|
||||
pub fn render(&mut self, dom: &VirtualDom) -> String {
|
||||
let mut buf = String::new();
|
||||
self.render_to(&mut buf, dom).unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
// // write!(f, "{}", text.text)?
|
||||
// // }
|
||||
// // VNode::Element(el) => {
|
||||
// // *last_node_was_text = false;
|
||||
pub fn render_to(&mut self, buf: &mut impl Write, dom: &VirtualDom) -> std::fmt::Result {
|
||||
self.render_scope(buf, dom, ScopeId(0))
|
||||
}
|
||||
|
||||
// // if self.cfg.indent {
|
||||
// // for _ in 0..il {
|
||||
// // write!(f, " ")?;
|
||||
// // }
|
||||
// // }
|
||||
pub fn render_scope(
|
||||
&mut self,
|
||||
buf: &mut impl Write,
|
||||
dom: &VirtualDom,
|
||||
scope: ScopeId,
|
||||
) -> 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(Ok(node)) = dom.get_scope(scope).unwrap().root_node() {
|
||||
self.render_template(buf, dom, node)?
|
||||
};
|
||||
|
||||
// // write!(f, "<{}", el.tag)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // let inner_html = render_attributes(el.attributes.iter(), f)?;
|
||||
fn render_template(
|
||||
&mut self,
|
||||
buf: &mut impl Write,
|
||||
dom: &VirtualDom,
|
||||
template: &VNode,
|
||||
) -> std::fmt::Result {
|
||||
let entry = self
|
||||
.template_cache
|
||||
.entry(template.template.name)
|
||||
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
|
||||
.clone();
|
||||
|
||||
// // match self.cfg.newline {
|
||||
// // true => writeln!(f, ">")?,
|
||||
// // false => write!(f, ">")?,
|
||||
// // }
|
||||
for segment in entry.segments.iter() {
|
||||
match segment {
|
||||
Segment::Attr(idx) => {
|
||||
let attr = &template.dynamic_attrs[*idx];
|
||||
match attr.value {
|
||||
AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?,
|
||||
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
|
||||
DynamicNode::Component(node) => {
|
||||
if self.skip_components {
|
||||
write!(buf, "<{}><{}/>", node.name, node.name)?;
|
||||
} else {
|
||||
let id = node.scope.get().unwrap();
|
||||
let scope = dom.get_scope(id).unwrap();
|
||||
let node = scope.root_node();
|
||||
match node {
|
||||
RenderReturn::Sync(Ok(node)) => {
|
||||
self.render_template(buf, dom, node)?
|
||||
}
|
||||
_ => todo!(
|
||||
"generally, scopes should be sync, only if being traversed"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
DynamicNode::Text(text) => {
|
||||
// in SSR, we are concerned that we can't hunt down the right text node since they might get merged
|
||||
if self.pre_render {
|
||||
write!(buf, "<!--#-->")?;
|
||||
}
|
||||
|
||||
// // if let Some(inner_html) = inner_html {
|
||||
// // write!(f, "{}", inner_html)?;
|
||||
// // } else {
|
||||
// // let mut last_node_was_text = false;
|
||||
// // for child in el.children {
|
||||
// // self.html_render(child, f, il + 1, &mut last_node_was_text)?;
|
||||
// // }
|
||||
// // }
|
||||
// todo: escape the text
|
||||
write!(buf, "{}", text.value)?;
|
||||
|
||||
// // if self.cfg.newline {
|
||||
// // writeln!(f)?;
|
||||
// // }
|
||||
// // if self.cfg.indent {
|
||||
// // for _ in 0..il {
|
||||
// // write!(f, " ")?;
|
||||
// // }
|
||||
// // }
|
||||
if self.pre_render {
|
||||
write!(buf, "<!--#-->")?;
|
||||
}
|
||||
}
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
for child in *nodes {
|
||||
self.render_template(buf, dom, child)?;
|
||||
}
|
||||
}
|
||||
|
||||
// // write!(f, "</{}>", el.tag)?;
|
||||
// // if self.cfg.newline {
|
||||
// // writeln!(f)?;
|
||||
// // }
|
||||
// // }
|
||||
// // VNode::Fragment(frag) => match frag.children.len() {
|
||||
// // 0 => {
|
||||
// // *last_node_was_text = false;
|
||||
// // if self.cfg.indent {
|
||||
// // for _ in 0..il {
|
||||
// // write!(f, " ")?;
|
||||
// // }
|
||||
// // }
|
||||
// // write!(f, "<!--placeholder-->")?;
|
||||
// // }
|
||||
// // _ => {
|
||||
// // for child in frag.children {
|
||||
// // self.html_render(child, f, il + 1, last_node_was_text)?;
|
||||
// // }
|
||||
// // }
|
||||
// // },
|
||||
// // VNode::Component(vcomp) => {
|
||||
// // let idx = vcomp.scope.get().unwrap();
|
||||
DynamicNode::Placeholder(_el) => {
|
||||
if self.pre_render {
|
||||
write!(buf, "<pre><pre/>")?;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// // if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
|
||||
// // let new_node = vdom.get_scope(idx).unwrap().root_node();
|
||||
// // self.html_render(new_node, f, il + 1, last_node_was_text)?;
|
||||
// // } else {
|
||||
// // }
|
||||
// // }
|
||||
// // VNode::Template(t) => {
|
||||
// // if let Some(vdom) = self.vdom {
|
||||
// // todo!()
|
||||
// // } else {
|
||||
// // panic!("Cannot render template without vdom");
|
||||
// // }
|
||||
// // }
|
||||
// // VNode::Placeholder(_) => {
|
||||
// // todo!()
|
||||
// // }
|
||||
// // }
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
Segment::PreRendered(contents) => write!(buf, "{}", contents)?,
|
||||
}
|
||||
}
|
||||
|
||||
// impl<'a: 'c, 'c> Display for SsrRenderer<'a, '_, 'c> {
|
||||
// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// let mut last_node_was_text = false;
|
||||
// self.html_render(self.root, f, 0, &mut last_node_was_text)
|
||||
// }
|
||||
// }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string_works() {
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let dynamic = 123;
|
||||
let dyn2 = "</diiiiiiiiv>"; // todo: escape this
|
||||
|
||||
render! {
|
||||
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
|
||||
"Hello world 1 -->" "{dynamic}" "<-- Hello world 2"
|
||||
div { "nest 1" }
|
||||
div {}
|
||||
div { "nest 2" }
|
||||
"{dyn2}"
|
||||
(0..5).map(|i| rsx! { div { "finalize {i}" } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
_ = dom.rebuild();
|
||||
|
||||
let mut renderer = Renderer::new();
|
||||
|
||||
let out = renderer.render(&dom);
|
||||
|
||||
use Segment::*;
|
||||
assert_eq!(
|
||||
renderer.template_cache.iter().next().unwrap().1.segments,
|
||||
vec![
|
||||
PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
|
||||
Attr(0,),
|
||||
PreRendered(">Hello world 1 -->".into(),),
|
||||
Node(0,),
|
||||
PreRendered("<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),),
|
||||
Node(1,),
|
||||
Node(2,),
|
||||
PreRendered("</div>".into(),),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
out,
|
||||
"<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --><!--#-->123<!--/#--><-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div><!--#--></diiiiiiiiv><!--/#--><div><!--#-->finalize 0<!--/#--></div><div><!--#-->finalize 1<!--/#--></div><div><!--#-->finalize 2<!--/#--></div><div><!--#-->finalize 3<!--/#--></div><div><!--#-->finalize 4<!--/#--></div></div>"
|
||||
);
|
||||
}
|
||||
@@ -1,140 +1 @@
|
||||
use super::cache::Segment;
|
||||
use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn, VText};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::cache::StringCache;
|
||||
|
||||
/// A virtualdom renderer that caches the templates it has seen for faster rendering
|
||||
#[derive(Default)]
|
||||
pub struct SsrRender {
|
||||
template_cache: HashMap<&'static str, Rc<StringCache>>,
|
||||
}
|
||||
|
||||
impl SsrRender {
|
||||
pub fn render_vdom(&mut self, dom: &VirtualDom) -> String {
|
||||
let scope = dom.base_scope();
|
||||
let root = scope.root_node();
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
match root {
|
||||
RenderReturn::Sync(Ok(node)) => self.render_template(&mut out, dom, node).unwrap(),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn render_template(
|
||||
&mut self,
|
||||
buf: &mut String,
|
||||
dom: &VirtualDom,
|
||||
template: &VNode,
|
||||
) -> std::fmt::Result {
|
||||
let entry = self
|
||||
.template_cache
|
||||
.entry(template.template.name)
|
||||
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
|
||||
.clone();
|
||||
|
||||
for segment in entry.segments.iter() {
|
||||
match segment {
|
||||
Segment::Attr(idx) => {
|
||||
let attr = &template.dynamic_attrs[*idx];
|
||||
match attr.value {
|
||||
AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?,
|
||||
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
|
||||
DynamicNode::Component(node) => {
|
||||
let id = node.scope.get().unwrap();
|
||||
let scope = dom.get_scope(id).unwrap();
|
||||
let node = scope.root_node();
|
||||
match node {
|
||||
RenderReturn::Sync(Ok(node)) => self.render_template(buf, dom, node)?,
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
DynamicNode::Text(text) => {
|
||||
// in SSR, we are concerned that we can't hunt down the right text node since they might get merged
|
||||
// if !*inner {
|
||||
// write!(buf, "<!--#-->")?;
|
||||
// }
|
||||
|
||||
// todo: escape the text
|
||||
write!(buf, "{}", text.value)?;
|
||||
|
||||
// if !*inner {
|
||||
// write!(buf, "<!--/#-->")?;
|
||||
// }
|
||||
}
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
for child in *nodes {
|
||||
self.render_template(buf, dom, child)?;
|
||||
}
|
||||
}
|
||||
|
||||
DynamicNode::Placeholder(_el) => {
|
||||
// todo write a placeholder if in pre-render mode
|
||||
// write!(buf, "<!--placeholder-->")?;
|
||||
}
|
||||
},
|
||||
|
||||
Segment::PreRendered(contents) => buf.push_str(contents),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string_works() {
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let dynamic = 123;
|
||||
let dyn2 = "</diiiiiiiiv>"; // todo: escape this
|
||||
|
||||
render! {
|
||||
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
|
||||
"Hello world 1 -->" "{dynamic}" "<-- Hello world 2"
|
||||
div { "nest 1" }
|
||||
div {}
|
||||
div { "nest 2" }
|
||||
"{dyn2}"
|
||||
(0..5).map(|i| rsx! { div { "finalize {i}" } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
|
||||
use Segment::*;
|
||||
|
||||
// assert_eq!(
|
||||
// StringCache::from_template(&dom.base_scope().root_node())
|
||||
// .unwrap()
|
||||
// .segments,
|
||||
// vec![
|
||||
// PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
|
||||
// Attr(0,),
|
||||
// PreRendered(">Hello world 1 -->".into(),),
|
||||
// Node(0,),
|
||||
// PreRendered("<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),),
|
||||
// Node(1,),
|
||||
// Node(2,),
|
||||
// PreRendered("</div>".into(),),
|
||||
// ]
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// SsrRender::default().render_vdom(&dom),
|
||||
// "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --><!--#-->123<!--/#--><-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div><!--#--></diiiiiiiiv><!--/#--><div><!--#-->finalize 0<!--/#--></div><div><!--#-->finalize 1<!--/#--></div><div><!--#-->finalize 2<!--/#--></div><div><!--#-->finalize 3<!--/#--></div><div><!--#-->finalize 4<!--/#--></div></div>"
|
||||
// );
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_ssr::{render_lazy, render_vdom, render_vdom_cfg, SsrConfig, SsrRenderer, TextRenderer};
|
||||
|
||||
static SIMPLE_APP: Component = |cx| {
|
||||
cx.render(rsx!(div {
|
||||
"hello world!"
|
||||
}))
|
||||
};
|
||||
|
||||
static SLIGHTLY_MORE_COMPLEX: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
div { title: "About W3Schools",
|
||||
(0..20).map(|f| rsx!{
|
||||
div {
|
||||
title: "About W3Schools",
|
||||
style: "color:blue;text-align:center",
|
||||
class: "About W3Schools",
|
||||
p {
|
||||
title: "About W3Schools",
|
||||
"Hello world!: {f}"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
static NESTED_APP: Component = |cx| {
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
SIMPLE_APP {}
|
||||
}
|
||||
))
|
||||
};
|
||||
static FRAGMENT_APP: Component = |cx| {
|
||||
cx.render(rsx!(
|
||||
div { "f1" }
|
||||
div { "f2" }
|
||||
div { "f3" }
|
||||
div { "f4" }
|
||||
))
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn to_string_works() {
|
||||
let mut dom = VirtualDom::new(SIMPLE_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hydration() {
|
||||
let mut dom = VirtualDom::new(NESTED_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom_cfg(&dom, |c| c.pre_render(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested() {
|
||||
let mut dom = VirtualDom::new(NESTED_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_app() {
|
||||
let mut dom = VirtualDom::new(FRAGMENT_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_to_file() {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
let mut file = File::create("index.html").unwrap();
|
||||
|
||||
let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX);
|
||||
dom.rebuild();
|
||||
|
||||
file.write_fmt(format_args!(
|
||||
"{}",
|
||||
TextRenderer::from_vdom(&dom, SsrConfig::default())
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles() {
|
||||
static STLYE_APP: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
div { color: "blue", font_size: "46px" }
|
||||
})
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new(STLYE_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lazy() {
|
||||
let p1 = SsrRenderer::new(|c| c).render_lazy(rsx! {
|
||||
div { "ello" }
|
||||
});
|
||||
|
||||
let p2 = render_lazy(rsx! {
|
||||
div {
|
||||
"ello"
|
||||
}
|
||||
});
|
||||
assert_eq!(p1, p2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_lazy() {
|
||||
let s = render_lazy(rsx! {
|
||||
div {
|
||||
div {
|
||||
div {
|
||||
h1 { "ello world" }
|
||||
h1 { "ello world" }
|
||||
h1 { "ello world" }
|
||||
h1 { "ello world" }
|
||||
h1 { "ello world" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dbg!(s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_html() {
|
||||
let s = render_lazy(rsx! {
|
||||
div {
|
||||
dangerous_inner_html: "<div> ack </div>"
|
||||
}
|
||||
});
|
||||
|
||||
dbg!(s);
|
||||
}
|
||||
@@ -7,66 +7,43 @@ fn simple() {
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
_ = dom.rebuild();
|
||||
|
||||
assert_eq!(dioxus_ssr::render(&dom), "<div>hello!</div>");
|
||||
|
||||
assert_eq!(
|
||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
||||
dioxus_ssr::render_lazy(rsx!( div {"hello!"} )),
|
||||
"<div>hello!</div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
render! {
|
||||
assert_eq!(
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
ul {
|
||||
(0..5).map(|i| rsx! {
|
||||
li { "item {i}" }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
|
||||
assert_eq!(
|
||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
||||
}),
|
||||
"<ul><li>item 0</li><li>item 1</li><li>item 2</li><li>item 3</li><li>item 4</li></ul>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let dynamic = 123;
|
||||
|
||||
render! {
|
||||
div { "Hello world 1 -->" "{dynamic}" "<-- Hello world 2" }
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
|
||||
let dynamic = 123;
|
||||
assert_eq!(
|
||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
div { "Hello world 1 -->" "{dynamic}" "<-- Hello world 2" }
|
||||
}),
|
||||
"<div>Hello world 1 -->123<-- Hello world 2</div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn components() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
render! {
|
||||
div {
|
||||
(0..5).map(|name| rsx! {
|
||||
my_component { name: name }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn my_component(cx: Scope, name: i32) -> Element {
|
||||
render! {
|
||||
@@ -74,11 +51,26 @@ fn components() {
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
|
||||
assert_eq!(
|
||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
div {
|
||||
(0..5).map(|name| rsx! {
|
||||
my_component { name: name }
|
||||
})
|
||||
}
|
||||
}),
|
||||
"<div><div>component 0</div><div>component 1</div><div>component 2</div><div>component 3</div><div>component 4</div></div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragments() {
|
||||
assert_eq!(
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
div {
|
||||
(0..5).map(|_| rsx! (()))
|
||||
}
|
||||
}),
|
||||
"<div></div>"
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ futures-util = "0.3.19"
|
||||
smallstr = "0.2.0"
|
||||
futures-channel = "0.3.21"
|
||||
serde_json = { version = "1.0" }
|
||||
serde-wasm-bindgen = "0.4.5"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.56"
|
||||
|
||||
@@ -11,7 +11,7 @@ use dioxus_core::{Mutation, Template, TemplateAttribute, TemplateNode};
|
||||
use dioxus_html::{event_bubbles, CompositionData, FormData};
|
||||
use dioxus_interpreter_js::{save_template, Channel};
|
||||
use futures_channel::mpsc;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{any::Any, rc::Rc};
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use web_sys::{Document, Element, Event, HtmlElement};
|
||||
@@ -106,15 +106,15 @@ impl WebsysDom {
|
||||
}
|
||||
}
|
||||
for child in *children {
|
||||
el.append_child(&self.create_template_node(child));
|
||||
let _ = el.append_child(&self.create_template_node(child));
|
||||
}
|
||||
el.dyn_into().unwrap()
|
||||
}
|
||||
Text(t) => self.document.create_text_node(t).dyn_into().unwrap(),
|
||||
DynamicText(_) => self.document.create_text_node("p").dyn_into().unwrap(),
|
||||
Dynamic(_) => {
|
||||
Text { text } => self.document.create_text_node(text).dyn_into().unwrap(),
|
||||
DynamicText { .. } => self.document.create_text_node("p").dyn_into().unwrap(),
|
||||
Dynamic { .. } => {
|
||||
let el = self.document.create_element("pre").unwrap();
|
||||
el.toggle_attribute("hidden");
|
||||
let _ = el.toggle_attribute("hidden");
|
||||
el.dyn_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
|
||||
let mut res = {
|
||||
let work = dom.wait_for_work().fuse();
|
||||
pin_mut!(work);
|
||||
|
||||
futures_util::select! {
|
||||
_ = work => None,
|
||||
new_template = hotreload_rx.next() => {
|
||||
|
||||
Reference in new issue
Block a user