Merge branch 'upstream' into simplify-native-core

This commit is contained in:
Evan Almloff committed 2023-01-28 18:49:19 -06:00
commit d53bfb6c56
57 files changed
+1553 -368

No files matched your search

+4 -4
View File
@@ -140,7 +140,7 @@ impl Writer<'_> {
let mut written = generics.to_token_stream().to_string();
written.retain(|c| !c.is_whitespace());
write!(self.out, "{}", written)?;
write!(self.out, "{written}")?;
}
write!(self.out, " {{")?;
@@ -165,7 +165,7 @@ impl Writer<'_> {
match &field.content {
ContentField::ManExpr(exp) => {
let out = prettyplease::unparse_expr(exp);
write!(self.out, "{}: {}", name, out)?;
write!(self.out, "{name}: {out}")?;
}
ContentField::Formatted(s) => {
write!(
@@ -179,11 +179,11 @@ impl Writer<'_> {
let out = prettyplease::unparse_expr(exp);
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
write!(self.out, "{}: {}", name, first)?;
write!(self.out, "{name}: {first}")?;
for line in lines {
self.out.new_line()?;
self.out.indented_tab()?;
write!(self.out, "{}", line)?;
write!(self.out, "{line}")?;
}
}
}
+4 -4
View File
@@ -224,7 +224,7 @@ impl Writer<'_> {
}
ElementAttr::AttrExpression { name, value } => {
let out = prettyplease::unparse_expr(value);
write!(self.out, "{}: {}", name, out)?;
write!(self.out, "{name}: {out}")?;
}
ElementAttr::CustomAttrText { name, value } => {
@@ -250,13 +250,13 @@ impl Writer<'_> {
// a one-liner for whatever reason
// Does not need a new line
if lines.peek().is_none() {
write!(self.out, "{}: {}", name, first)?;
write!(self.out, "{name}: {first}")?;
} else {
writeln!(self.out, "{}: {}", name, first)?;
writeln!(self.out, "{name}: {first}")?;
while let Some(line) = lines.next() {
self.out.indented_tab()?;
write!(self.out, "{}", line)?;
write!(self.out, "{line}")?;
if lines.peek().is_none() {
write!(self.out, "")?;
} else {
+3 -3
View File
@@ -20,7 +20,7 @@ impl Writer<'_> {
let start = byte_offset(self.raw_src, start);
let end = byte_offset(self.raw_src, end);
let row = self.raw_src[start..end].trim();
write!(self.out, "{}", row)?;
write!(self.out, "{row}")?;
return Ok(());
}
@@ -56,11 +56,11 @@ impl Writer<'_> {
write!(self.out, " ")?;
}
write!(self.out, "{}", line)?;
write!(self.out, "{line}")?;
} else {
let offset = offset as usize;
let right = &line[offset..];
write!(self.out, "{}", right)?;
write!(self.out, "{right}")?;
}
}
+1 -1
View File
@@ -102,7 +102,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
&& matches!(body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_));
if formatted.len() <= 80 && !formatted.contains('\n') && !body_is_solo_expr {
formatted = format!(" {} ", formatted);
formatted = format!(" {formatted} ");
}
end_span = span.end();
+1 -1
View File
@@ -95,7 +95,7 @@ impl ToTokens for InlinePropsBody {
quote! { #vis #f }
});
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
let struct_name = Ident::new(&format!("{ident}Props"), Span::call_site());
let field_names = inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => todo!(),
+14 -15
View File
@@ -323,7 +323,7 @@ mod field_info {
let tokenized_code = TokenStream::from_str(&code.value())?;
self.default = Some(
syn::parse(tokenized_code.into())
.map_err(|e| Error::new_spanned(code, format!("{}", e)))?,
.map_err(|e| Error::new_spanned(code, format!("{e}")))?,
);
} else {
return Err(Error::new_spanned(assign.right, "Expected string"));
@@ -332,7 +332,7 @@ mod field_info {
}
_ => Err(Error::new_spanned(
&assign,
format!("Unknown parameter {:?}", name),
format!("Unknown parameter {name:?}"),
)),
}
}
@@ -503,11 +503,11 @@ mod struct_info {
builder_attr,
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
conversion_helper_trait_name: syn::Ident::new(
&format!("{}_Optional", builder_name),
&format!("{builder_name}_Optional"),
proc_macro2::Span::call_site(),
),
core: syn::Ident::new(
&format!("{}_core", builder_name),
&format!("{builder_name}_core"),
proc_macro2::Span::call_site(),
),
})
@@ -594,7 +594,6 @@ Finally, call `.build()` to create the instance of `{name}`.
None => {
let doc = format!(
"Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info.",
name = name
);
quote!(#[doc = #doc])
}
@@ -709,9 +708,9 @@ Finally, call `.build()` to create the instance of `{name}`.
});
let reconstructing = self.included_fields().map(|f| f.name);
let &FieldInfo {
name: ref field_name,
ty: ref field_type,
let FieldInfo {
name: field_name,
ty: field_type,
..
} = field;
let mut ty_generics: Vec<syn::GenericArgument> = self
@@ -810,7 +809,7 @@ Finally, call `.build()` to create the instance of `{name}`.
),
proc_macro2::Span::call_site(),
);
let repeated_fields_error_message = format!("Repeated field {}", field_name);
let repeated_fields_error_message = format!("Repeated field {field_name}");
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
@@ -929,7 +928,7 @@ Finally, call `.build()` to create the instance of `{name}`.
),
proc_macro2::Span::call_site(),
);
let early_build_error_message = format!("Missing required field {}", field_name);
let early_build_error_message = format!("Missing required field {field_name}");
Ok(quote! {
#[doc(hidden)]
@@ -1037,7 +1036,7 @@ Finally, call `.build()` to create the instance of `{name}`.
// I’d prefer “a” or “an” to “its”, but determining which is grammatically
// correct is roughly impossible.
let doc =
format!("Finalise the builder and create its [`{}`] instance", name);
format!("Finalise the builder and create its [`{name}`] instance");
quote!(#[doc = #doc])
}
}
@@ -1132,7 +1131,7 @@ Finally, call `.build()` to create the instance of `{name}`.
}
_ => Err(Error::new_spanned(
&assign,
format!("Unknown parameter {:?}", name),
format!("Unknown parameter {name:?}"),
)),
}
}
@@ -1146,7 +1145,7 @@ Finally, call `.build()` to create the instance of `{name}`.
}
_ => Err(Error::new_spanned(
&path,
format!("Unknown parameter {:?}", name),
format!("Unknown parameter {name:?}"),
)),
}
}
@@ -1161,7 +1160,7 @@ Finally, call `.build()` to create the instance of `{name}`.
let call_func = quote!(#call_func);
Error::new_spanned(
&call.func,
format!("Illegal builder setting group {}", call_func),
format!("Illegal builder setting group {call_func}"),
)
})?;
match subsetting_name.as_str() {
@@ -1173,7 +1172,7 @@ Finally, call `.build()` to create the instance of `{name}`.
}
_ => Err(Error::new_spanned(
&call.func,
format!("Illegal builder setting group name {}", subsetting_name),
format!("Illegal builder setting group name {subsetting_name}"),
)),
}
}
+3 -3
View File
@@ -8,11 +8,11 @@ fn formatting_compiles() {
// escape sequences work
assert_eq!(
format_args_f!("{x:?} {{}}}}").to_string(),
format!("{:?} {{}}}}", x)
format!("{x:?} {{}}}}")
);
assert_eq!(
format_args_f!("{{{{}} {x:?}").to_string(),
format!("{{{{}} {:?}", x)
format!("{{{{}} {x:?}")
);
// paths in formating works
@@ -27,6 +27,6 @@ fn formatting_compiles() {
// allows duplicate format args
assert_eq!(
format_args_f!("{x:?} {x:?}").to_string(),
format!("{:?} {:?}", x, x)
format!("{x:?} {x:?}")
);
}
+6 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-core"
version = "0.3.0"
version = "0.3.1"
authors = ["Jonathan Kelley"]
edition = "2018"
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
@@ -23,7 +23,9 @@ rustc-hash = "1.1.0"
# Used in diffing
longest-increasing-subsequence = "0.1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"]}
futures-util = { version = "0.3", default-features = false, features = [
"alloc",
] }
slab = "0.4"
@@ -37,6 +39,8 @@ log = "0.4.17"
# Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
bumpslab = { version = "0.2.0" }
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
dioxus = { path = "../dioxus" }
+5 -5
View File
@@ -92,21 +92,21 @@ impl VirtualDom {
// Note: This will not remove any ids from the arena
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
self.dirty_scopes.remove(&DirtyScope {
height: self.scopes[id.0].height,
height: self.scopes[id].height,
id,
});
self.ensure_drop_safety(id);
if recursive {
if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
if let Some(root) = self.scopes[id].try_root_node() {
if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } {
self.drop_scope_inner(node)
}
}
}
let scope = &mut self.scopes[id.0];
let scope = &mut self.scopes[id];
// Drop all the hooks once the children are dropped
// this means we'll drop hooks bottom-up
@@ -119,7 +119,7 @@ impl VirtualDom {
scope.tasks.remove(task_id);
}
self.scopes.remove(id.0);
self.scopes.remove(id);
}
fn drop_scope_inner(&mut self, node: &VNode) {
@@ -140,7 +140,7 @@ impl VirtualDom {
/// Descend through the tree, removing any borrowed props and listeners
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
let scope = &self.scopes[scope_id.0];
let scope = &self.scopes[scope_id];
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
// run the hooks (which hold an &mut Reference)
+3 -3
View File
@@ -535,7 +535,7 @@ 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::<Rc<SuspenseContext>>() {
let boundary = match self.scopes[scope].has_context::<Rc<SuspenseContext>>() {
Some(boundary) => boundary,
_ => return created,
};
@@ -544,7 +544,7 @@ impl<'b> VirtualDom {
let new_id = self.next_element(new, parent.template.get().node_paths[idx]);
// Now connect everything to the boundary
self.scopes[scope.0].placeholder.set(Some(new_id));
self.scopes[scope].placeholder.set(Some(new_id));
// This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary
// Note that we break off dynamic mutations only - since static mutations aren't rendered immediately
@@ -583,7 +583,7 @@ impl<'b> VirtualDom {
let new_id = self.next_element(template, template.template.get().node_paths[idx]);
// Set the placeholder of the scope
self.scopes[scope.0].placeholder.set(Some(new_id));
self.scopes[scope].placeholder.set(Some(new_id));
// Since the placeholder is already in the DOM, we don't create any new nodes
self.mutations.push(AssignId {
+9 -9
View File
@@ -15,7 +15,7 @@ use DynamicNode::*;
impl<'b> VirtualDom {
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
let scope_state = &mut self.scopes[scope.0];
let scope_state = &mut self.scopes[scope];
self.scope_stack.push(scope);
unsafe {
@@ -202,7 +202,7 @@ impl<'b> VirtualDom {
right.scope.set(Some(scope_id));
// copy out the box for both
let old = self.scopes[scope_id.0].props.as_ref();
let old = self.scopes[scope_id].props.as_ref();
let new: Box<dyn AnyProps> = right.props.take().unwrap();
let new: Box<dyn AnyProps> = unsafe { std::mem::transmute(new) };
@@ -214,14 +214,14 @@ impl<'b> VirtualDom {
}
// First, move over the props from the old to the new, dropping old props in the process
self.scopes[scope_id.0].props = Some(new);
self.scopes[scope_id].props = Some(new);
// Now run the component and diff it
self.run_scope(scope_id);
self.diff_scope(scope_id);
self.dirty_scopes.remove(&DirtyScope {
height: self.scopes[scope_id.0].height,
height: self.scopes[scope_id].height,
id: scope_id,
});
}
@@ -721,7 +721,7 @@ impl<'b> VirtualDom {
Component(comp) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
RenderReturn::Aborted(_node) => todo!(),
_ => todo!(),
@@ -923,14 +923,14 @@ impl<'b> VirtualDom {
.expect("VComponents to always have a scope");
// Remove the component from the dom
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
_ => todo!(),
};
// Restore the props back to the vcomponent in case it gets rendered again
let props = self.scopes[scope.0].props.take();
let props = self.scopes[scope].props.take();
*comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
// Now drop all the resouces
@@ -945,7 +945,7 @@ impl<'b> VirtualDom {
Some(Placeholder(t)) => t.id.get().unwrap(),
Some(Component(comp)) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(t) => self.find_first_element(t),
_ => todo!("cannot handle nonstandard nodes"),
}
@@ -961,7 +961,7 @@ impl<'b> VirtualDom {
Some(Placeholder(t)) => t.id.get().unwrap(),
Some(Component(comp)) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(t) => self.find_last_element(t),
_ => todo!("cannot handle nonstandard nodes"),
}
+10 -4
View File
@@ -757,15 +757,21 @@ impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> {
}
}
impl<'a> IntoDynNode<'_> for &'a str {
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
cx.text_node(format_args!("{}", self))
impl<'a, 'b> IntoDynNode<'b> for &'a str {
fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> {
DynamicNode::Text(VText {
value: bumpalo::collections::String::from_str_in(self, cx.bump()).into_bump_str(),
id: Default::default(),
})
}
}
impl IntoDynNode<'_> for String {
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
cx.text_node(format_args!("{}", self))
DynamicNode::Text(VText {
value: cx.bump().alloc(self),
id: Default::default(),
})
}
}
+3 -3
View File
@@ -31,7 +31,7 @@ impl VirtualDom {
// If the task completes...
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
// Remove it from the scope so we dont try to double drop it when the scope dropes
let scope = &self.scopes[task.scope.0];
let scope = &self.scopes[task.scope];
scope.spawned_tasks.borrow_mut().remove(&id);
// Remove it from the scheduler
@@ -40,7 +40,7 @@ impl VirtualDom {
}
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
self.scopes[id.0]
self.scopes[id]
.consume_context::<Rc<SuspenseContext>>()
.unwrap()
}
@@ -64,7 +64,7 @@ impl VirtualDom {
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
let fiber = self.acquire_suspense_boundary(leaf.scope_id);
let scope = &self.scopes[scope_id.0];
let scope = &self.scopes[scope_id];
let arena = scope.current_frame();
let ret = arena.bump().alloc(match new_nodes {
+8 -8
View File
@@ -24,9 +24,9 @@ impl VirtualDom {
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) };
let id = ScopeId(entry.key());
let id = entry.key();
entry.insert(Box::new(ScopeState {
entry.insert(ScopeState {
parent,
id,
height,
@@ -44,13 +44,13 @@ impl VirtualDom {
shared_contexts: Default::default(),
borrowed_props: Default::default(),
attributes_to_drop: Default::default(),
}))
})
}
fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> {
let id = self.scope_stack.last().copied()?;
let scope = self.scopes.get(id.0)?;
Some(scope.as_ref())
let scope = self.scopes.get(id)?;
Some(scope)
}
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
@@ -60,9 +60,9 @@ impl VirtualDom {
self.ensure_drop_safety(scope_id);
let mut new_nodes = unsafe {
self.scopes[scope_id.0].previous_frame().bump_mut().reset();
self.scopes[scope_id].previous_frame().bump_mut().reset();
let scope = &self.scopes[scope_id.0];
let scope = &self.scopes[scope_id];
scope.hook_idx.set(0);
@@ -127,7 +127,7 @@ impl VirtualDom {
}
};
let scope = &self.scopes[scope_id.0];
let scope = &self.scopes[scope_id];
// We write on top of the previous frame and then make it the current by pushing the generation forward
let frame = scope.previous_frame();
+92
View File
@@ -10,12 +10,15 @@ use crate::{
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use bumpslab::{BumpSlab, Slot};
use rustc_hash::{FxHashMap, FxHashSet};
use slab::{Slab, VacantEntry};
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
fmt::{Arguments, Debug},
future::Future,
ops::{Index, IndexMut},
rc::Rc,
sync::Arc,
};
@@ -63,6 +66,95 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> {
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub struct ScopeId(pub usize);
/// A thin wrapper around a BumpSlab that uses ids to index into the slab.
pub(crate) struct ScopeSlab {
slab: BumpSlab<ScopeState>,
// a slab of slots of stable pointers to the ScopeState in the bump slab
entries: Slab<Slot<'static, ScopeState>>,
}
impl Drop for ScopeSlab {
fn drop(&mut self) {
// Bump slab doesn't drop its contents, so we need to do it manually
for slot in self.entries.drain() {
self.slab.remove(slot);
}
}
}
impl Default for ScopeSlab {
fn default() -> Self {
Self {
slab: BumpSlab::new(),
entries: Slab::new(),
}
}
}
impl ScopeSlab {
pub(crate) fn get(&self, id: ScopeId) -> Option<&ScopeState> {
self.entries.get(id.0).map(|slot| unsafe { &*slot.ptr() })
}
pub(crate) fn get_mut(&mut self, id: ScopeId) -> Option<&mut ScopeState> {
self.entries
.get(id.0)
.map(|slot| unsafe { &mut *slot.ptr_mut() })
}
pub(crate) fn vacant_entry(&mut self) -> ScopeSlabEntry {
let entry = self.entries.vacant_entry();
ScopeSlabEntry {
slab: &mut self.slab,
entry,
}
}
pub(crate) fn remove(&mut self, id: ScopeId) {
self.slab.remove(self.entries.remove(id.0));
}
pub(crate) fn contains(&self, id: ScopeId) -> bool {
self.entries.contains(id.0)
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &ScopeState> {
self.entries.iter().map(|(_, slot)| unsafe { &*slot.ptr() })
}
}
pub(crate) struct ScopeSlabEntry<'a> {
slab: &'a mut BumpSlab<ScopeState>,
entry: VacantEntry<'a, Slot<'static, ScopeState>>,
}
impl<'a> ScopeSlabEntry<'a> {
pub(crate) fn key(&self) -> ScopeId {
ScopeId(self.entry.key())
}
pub(crate) fn insert(self, scope: ScopeState) -> &'a ScopeState {
let slot = self.slab.push(scope);
// this is safe because the slot is only ever accessed with the lifetime of the borrow of the slab
let slot = unsafe { std::mem::transmute(slot) };
let entry = self.entry.insert(slot);
unsafe { &*entry.ptr() }
}
}
impl Index<ScopeId> for ScopeSlab {
type Output = ScopeState;
fn index(&self, id: ScopeId) -> &Self::Output {
self.get(id).unwrap()
}
}
impl IndexMut<ScopeId> for ScopeSlab {
fn index_mut(&mut self, id: ScopeId) -> &mut Self::Output {
self.get_mut(id).unwrap()
}
}
/// A component's state separate from its props.
///
/// This struct exists to provide a common interface for all scopes without relying on generics.
+11 -11
View File
@@ -5,7 +5,7 @@
use crate::{
any_props::VProps,
arena::{ElementId, ElementRef},
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg, ScopeSlab},
mutations::Mutation,
nodes::RenderReturn,
nodes::{Template, TemplateId},
@@ -177,7 +177,7 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future
pub struct VirtualDom {
// Maps a template path to a map of byteindexes to templates
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
pub(crate) scopes: Slab<Box<ScopeState>>,
pub(crate) scopes: ScopeSlab,
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
pub(crate) scheduler: Rc<Scheduler>,
@@ -258,7 +258,7 @@ impl VirtualDom {
rx,
scheduler: Scheduler::new(tx),
templates: Default::default(),
scopes: Slab::default(),
scopes: Default::default(),
elements: Default::default(),
scope_stack: Vec::new(),
dirty_scopes: BTreeSet::new(),
@@ -291,14 +291,14 @@ impl VirtualDom {
///
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
self.scopes.get(id.0).map(|f| f.as_ref())
self.scopes.get(id)
}
/// Get the single scope at the top of the VirtualDom tree that will always be around
///
/// This scope has a ScopeId of 0 and is the root of the tree
pub fn base_scope(&self) -> &ScopeState {
self.scopes.get(0).unwrap()
self.scopes.get(ScopeId(0)).unwrap()
}
/// Build the virtualdom with a global context inserted into the base scope
@@ -313,7 +313,7 @@ impl VirtualDom {
///
/// Whenever the VirtualDom "works", it will re-render this scope
pub fn mark_dirty(&mut self, id: ScopeId) {
if let Some(scope) = self.scopes.get(id.0) {
if let Some(scope) = self.scopes.get(id) {
let height = scope.height;
self.dirty_scopes.insert(DirtyScope { height, id });
}
@@ -324,7 +324,7 @@ impl VirtualDom {
/// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is
/// currently suspended.
pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
!self.scopes[id.0]
!self.scopes[id]
.consume_context::<Rc<SuspenseContext>>()
.unwrap()
.waiting_on
@@ -499,7 +499,7 @@ impl VirtualDom {
pub fn replace_template(&mut self, template: Template<'static>) {
self.register_template_first_byte_index(template);
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
for (_, scope) in &self.scopes {
for scope in self.scopes.iter() {
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
if sync.template.get().name.rsplit_once(':').unwrap().0
== template.name.rsplit_once(':').unwrap().0
@@ -583,7 +583,7 @@ impl VirtualDom {
loop {
// first, unload any complete suspense trees
for finished_fiber in self.finished_fibers.drain(..) {
let scope = &self.scopes[finished_fiber.0];
let scope = &self.scopes[finished_fiber];
let context = scope.has_context::<Rc<SuspenseContext>>().unwrap();
self.mutations
@@ -607,7 +607,7 @@ impl VirtualDom {
self.dirty_scopes.remove(&dirty);
// If the scope doesn't exist for whatever reason, then we should skip it
if !self.scopes.contains(dirty.id.0) {
if !self.scopes.contains(dirty.id) {
continue;
}
@@ -626,7 +626,7 @@ impl VirtualDom {
// If suspended leaves are present, then we should find the boundary for this scope and attach things
// No placeholder necessary since this is a diff
if !self.collected_leaves.is_empty() {
let mut boundary = self.scopes[dirty.id.0]
let mut boundary = self.scopes[dirty.id]
.consume_context::<Rc<SuspenseContext>>()
.unwrap();
+2 -2
View File
@@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"]
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
serde = "1.0.136"
serde_json = "1.0.79"
@@ -34,7 +35,6 @@ infer = "0.11.0"
dunce = "1.0.2"
slab = "0.4"
interprocess = { version = "1.1.1", optional = true }
futures-util = "0.3.25"
[target.'cfg(target_os = "ios")'.dependencies]
@@ -50,7 +50,7 @@ tokio_runtime = ["tokio"]
fullscreen = ["wry/fullscreen"]
transparent = ["wry/transparent"]
tray = ["wry/tray"]
hot-reload = ["interprocess"]
hot-reload = ["dioxus-hot-reload"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }
+5 -3
View File
@@ -9,6 +9,7 @@ use crate::Config;
use crate::WebviewHandler;
use dioxus_core::ScopeState;
use dioxus_core::VirtualDom;
use dioxus_hot_reload::HotReloadMsg;
use serde_json::Value;
use slab::Slab;
use wry::application::event::Event;
@@ -206,13 +207,12 @@ impl DesktopContext {
"method":"eval_result",
"params": (
function(){{
{}
{code}
}}
)()
}})
);
"#,
code
"#
);
if let Err(e) = self.webview.evaluate_script(&script) {
@@ -285,6 +285,8 @@ pub enum EventData {
Ipc(IpcMessage),
HotReloadEvent(HotReloadMsg),
NewWindow,
CloseWindow,
-54
View File
@@ -1,54 +0,0 @@
#![allow(dead_code)]
use dioxus_core::Template;
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
use std::io::{BufRead, BufReader};
use std::time::Duration;
use std::{sync::Arc, sync::Mutex};
fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalSocketStream> {
connection
.map_err(|error| eprintln!("Incoming connection failed: {}", error))
.ok()
}
pub(crate) fn init(proxy: futures_channel::mpsc::UnboundedSender<Template<'static>>) {
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 || {
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 || {
loop {
if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
let mut buf = String::new();
match conn.read_line(&mut buf) {
Ok(_) => {
let msg: Template<'static> =
serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
proxy.unbounded_send(msg).unwrap();
}
Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock {
break;
}
}
}
}
// give the error handler time to take the mutex
std::thread::sleep(Duration::from_millis(100));
}
});
}
+26 -4
View File
@@ -12,9 +12,6 @@ mod protocol;
mod waker;
mod webview;
#[cfg(all(feature = "hot-reload", debug_assertions))]
mod hot_reload;
pub use cfg::Config;
pub use desktop_context::{
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
@@ -111,6 +108,18 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
let proxy = event_loop.create_proxy();
// Intialize hot reloading if it is enabled
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
let proxy = proxy.clone();
dioxus_hot_reload::connect(move |template| {
let _ = proxy.send_event(UserWindowEvent(
EventData::HotReloadEvent(template),
unsafe { WindowId::dummy() },
));
});
}
// We start the tokio runtime *on this thread*
// Any future we poll later will use this runtime to spawn tasks and for IO
let rt = tokio::runtime::Builder::new_multi_thread()
@@ -176,6 +185,19 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
}
Event::UserEvent(event) => match event.0 {
EventData::HotReloadEvent(msg) => match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
for webview in webviews.values_mut() {
webview.dom.replace_template(template);
poll_vdom(webview);
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
*control_flow = ControlFlow::Exit;
}
},
EventData::CloseWindow => {
webviews.remove(&event.1);
@@ -304,5 +326,5 @@ fn send_edits(edits: Mutations, webview: &WebView) {
let serialized = serde_json::to_string(&edits).unwrap();
// todo: use SSE and binary data to send the edits with lower overhead
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized));
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
}
+2 -3
View File
@@ -11,15 +11,14 @@ fn module_loader(root_name: &str) -> String {
<script>
{INTERPRETER_JS}
let rootname = "{}";
let rootname = "{root_name}";
let root = window.document.getElementById(rootname);
if (root != null) {{
window.interpreter = new Interpreter(root);
window.ipc.postMessage(serializeIpcMessage("initialize"));
}}
</script>
"#,
root_name
"#
)
}
+5 -1
View File
@@ -18,11 +18,15 @@ dioxus-core-macro = { path = "../core-macro", version = "^0.3.0", optional = tru
dioxus-hooks = { path = "../hooks", version = "^0.3.0", optional = true }
dioxus-rsx = { path = "../rsx", version = "0.0.2", optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dioxus-hot-reload = { path = "../hot-reload", version = "0.1.0", optional = true }
[features]
default = ["macro", "hooks", "html"]
default = ["macro", "hooks", "html", "hot-reload"]
macro = ["dioxus-core-macro", "dioxus-rsx"]
html = ["dioxus-html"]
hooks = ["dioxus-hooks"]
hot-reload = ["dioxus-hot-reload"]
[dev-dependencies]
+3
View File
@@ -31,4 +31,7 @@ pub mod prelude {
#[cfg(feature = "html")]
pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes};
#[cfg(all(not(target_arch = "wasm32"), feature = "hot-reload"))]
pub use dioxus_hot_reload::{self, hot_reload_init};
}
+2 -2
View File
@@ -120,7 +120,7 @@ mod tests {
fn app(cx: Scope, name: String) -> Element {
let task = use_coroutine(cx, |mut rx: UnboundedReceiver<i32>| async move {
while let Some(msg) = rx.next().await {
println!("got message: {}", msg);
println!("got message: {msg}");
}
});
@@ -133,7 +133,7 @@ mod tests {
async fn view_task(mut rx: UnboundedReceiver<i32>) {
while let Some(msg) = rx.next().await {
println!("got message: {}", msg);
println!("got message: {msg}");
}
}
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "dioxus-hot-reload"
version = "0.1.0"
edition = "2021"
license = "MIT/Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
description = "Hot reloading utilites for Dioxus"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "hot-reloading", "watch"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-rsx = { path = "../rsx" }
dioxus-core = { path = "../core", features = ["serialize"] }
dioxus-html = { path = "../html", features = ["hot-reload-context"] }
interprocess = { version = "1.2.1" }
notify = "5.0.0"
chrono = "0.4.23"
serde_json = "1.0.91"
serde = { version = "1", features = ["derive"] }
execute = "0.2.11"
once_cell = "1.17.0"
ignore = "0.4.19"
+170
View File
@@ -0,0 +1,170 @@
# `dioxus-hot-reload`: Hot Reloading Utilites for Dioxus
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
[![Discord chat][discord-badge]][discord-url]
[crates-badge]: https://img.shields.io/crates/v/dioxus-hot-reload.svg
[crates-url]: https://crates.io/crates/dioxus-hot-reload
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/guide/) |
[API Docs](https://docs.rs/dioxus-hot-reload/latest/dioxus_hot_reload) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
Dioxus supports hot reloading for static parts of rsx macros. This enables changing the styling of your application without recompiling the rust code. This is useful for rapid iteration on the styling of your application.
Hot reloading could update the following change without recompiling:
```rust
rsx! {
div {
"Count: {count}",
}
}
```
=>
```rust
rsx! {
div {
color: "red",
font_size: "2em",
"Count: {count}",
}
}
```
But it could not update the following change:
```rust
rsx! {
div {
"Count: {count}",
}
}
```
=>
```rust
rsx! {
div {
"Count: {count*2}",
onclick: |_| println!("clicked"),
}
}
```
## Usage
> This crate implements hot reloading for native compilation targets not WASM. For hot relaoding with the web renderer, see the [dioxus-cli](https://github.com/DioxusLabs/cli) project.
Add this to the top of your main function on any renderer that supports hot reloading to start the hot reloading server:
```rust
fn main(){
hot_reload_init!();
// launch your application
}
```
By default the dev server watches on the root of the crate the macro is called in and ignores changes in the `/target` directory and any directories set in the `.gitignore` file in the root directory. To watch on custom paths pass call the `with_paths` function on the config builder:
```rust
fn main(){
hot_reload_init!(Config::new().with_paths(&["src", "examples", "assets"]));
// launch your application
}
```
By default the hot reloading server will output some logs in the console, to disable these logs call the `with_logging` function on the config builder:
```rust
fn main(){
hot_reload_init!(Config::new().with_logging(false));
// launch your application
}
```
To rebuild the application when the logic changes, you can use the `with_rebuild_command` function on the config builder. This command will be called when hot reloading fails to quickly update the rsx:
```rust
fn main(){
hot_reload_init!(Config::new().with_rebuild_command("cargo run"));
// launch your application
}
```
If you are using a namespace other than html, you can implement the [HotReloadingContext](https://docs.rs/dioxus-rsx/latest/dioxus_rsx/trait.HotReloadingContext.html) trait to provide a mapping between the rust names of your elements/attributes and the resulting strings.
You can then provide the Context to the builder to make hot reloading work with your custom namespace:
```rust
fn main(){
// Note: Use default instead of new if you are using a custom namespace
hot_reload_init!(Config::<MyContext>::default());
// launch your application
}
```
## Implementing Hot Reloading for a Custom Renderer
To add hot reloading support to your custom renderer you can use the connect function. This will connect to the dev server you just need to provide a way to transfer `Template`s to the `VirtualDom`. Once you implement this your users can use the hot_reload_init function just like any other render.
```rust
async fn launch(app: Component) {
let mut vdom = VirtualDom::new(app);
// ...
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
dioxus_hot_reload::connect(move |msg| {
let _ = tx.send(msg);
});
loop {
tokio::select! {
Some(msg) = rx.recv() => {
match msg{
HotReloadMsg::Shutdown => {
// ... shutdown the application
}
HotReloadMsg::UpdateTemplate(template) => {
// update the template in the virtual dom
vdom.replace_template(template);
}
}
}
_ = vdom.wait_for_work() => {
// ...
}
}
let mutations = vdom.render_immediate();
// apply the mutations to the dom
}
}
```
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.
+359
View File
@@ -0,0 +1,359 @@
use std::{
io::{BufRead, BufReader, Write},
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex},
};
use dioxus_core::Template;
use dioxus_rsx::{
hot_reload::{FileMap, UpdateResult},
HotReloadingContext,
};
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
pub use dioxus_html::HtmlCtx;
use serde::{Deserialize, Serialize};
/// A message the hot reloading server sends to the client
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub enum HotReloadMsg {
/// A template has been updated
#[serde(borrow = "'static")]
UpdateTemplate(Template<'static>),
/// The program needs to be recompiled, and the client should shut down
Shutdown,
}
pub struct Config<Ctx: HotReloadingContext = HtmlCtx> {
root_path: &'static str,
listening_paths: &'static [&'static str],
excluded_paths: &'static [&'static str],
log: bool,
rebuild_with: Option<Box<dyn FnMut() -> bool + Send + 'static>>,
phantom: std::marker::PhantomData<Ctx>,
}
impl<Ctx: HotReloadingContext> Default for Config<Ctx> {
fn default() -> Self {
Self {
root_path: "",
listening_paths: &[""],
excluded_paths: &["./target"],
log: true,
rebuild_with: None,
phantom: std::marker::PhantomData,
}
}
}
impl Config<HtmlCtx> {
pub const fn new() -> Self {
Self {
root_path: "",
listening_paths: &[""],
excluded_paths: &["./target"],
log: true,
rebuild_with: None,
phantom: std::marker::PhantomData,
}
}
}
impl<Ctx: HotReloadingContext> Config<Ctx> {
/// Set the root path of the project (where the Cargo.toml file is). This is automatically set by the [`hot_reload_init`] macro.
pub fn root(self, path: &'static str) -> Self {
Self {
root_path: path,
..self
}
}
/// Set whether to enable logs
pub fn with_logging(self, log: bool) -> Self {
Self { log, ..self }
}
/// Set the command to run to rebuild the project
///
/// For example to restart the application after a change is made, you could use `cargo run`
pub fn with_rebuild_command(self, rebuild_command: &'static str) -> Self {
self.with_rebuild_callback(move || {
execute::shell(rebuild_command)
.spawn()
.expect("Failed to spawn the rebuild command");
true
})
}
/// Set a callback to run to when the project needs to be rebuilt and returns if the server should shut down
///
/// For example a CLI application could rebuild the application when a change is made
pub fn with_rebuild_callback(
self,
rebuild_callback: impl FnMut() -> bool + Send + 'static,
) -> Self {
Self {
rebuild_with: Some(Box::new(rebuild_callback)),
..self
}
}
/// Set the paths to listen for changes in to trigger hot reloading. If this is a directory it will listen for changes in all files in that directory recursively.
pub fn with_paths(self, paths: &'static [&'static str]) -> Self {
Self {
listening_paths: paths,
..self
}
}
/// Sets paths to ignore changes on. This will override any paths set in the [`Config::with_paths`] method in the case of conflicts.
pub fn excluded_paths(self, paths: &'static [&'static str]) -> Self {
Self {
excluded_paths: paths,
..self
}
}
}
/// Initialize the hot reloading listener
pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
let Config {
root_path,
listening_paths,
log,
mut rebuild_with,
excluded_paths,
phantom: _,
} = cfg;
if let Ok(crate_dir) = PathBuf::from_str(root_path) {
let temp_file = std::env::temp_dir().join("@dioxusin");
let channels = Arc::new(Mutex::new(Vec::new()));
let file_map = Arc::new(Mutex::new(FileMap::<Ctx>::new(crate_dir.clone())));
if let Ok(local_socket_stream) = LocalSocketListener::bind(temp_file.as_path()) {
let aborted = Arc::new(Mutex::new(false));
// listen for connections
std::thread::spawn({
let file_map = file_map.clone();
let channels = channels.clone();
let aborted = aborted.clone();
let _ = local_socket_stream.set_nonblocking(true);
move || {
loop {
if let Ok(mut connection) = local_socket_stream.accept() {
// send any templates than have changed before the socket connected
let templates: Vec<_> = {
file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if !send_msg(
HotReloadMsg::UpdateTemplate(template),
&mut connection,
) {
continue;
}
}
channels.lock().unwrap().push(connection);
if log {
println!("Connected to hot reloading 🚀");
}
}
std::thread::sleep(std::time::Duration::from_millis(10));
if *aborted.lock().unwrap() {
break;
}
}
}
});
// watch for changes
std::thread::spawn(move || {
// try to find the gitingore file
let gitignore_file_path = crate_dir.join(".gitignore");
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
let mut last_update_time = chrono::Local::now().timestamp();
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
for path in listening_paths {
let full_path = crate_dir.join(path);
if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
if log {
println!(
"hot reloading failed to start watching {full_path:?}:\n{err:?}",
);
}
}
}
let excluded_paths = excluded_paths
.iter()
.map(|path| crate_dir.join(PathBuf::from(path)))
.collect::<Vec<_>>();
let mut rebuild = {
let aborted = aborted.clone();
let channels = channels.clone();
move || {
if let Some(rebuild_callback) = &mut rebuild_with {
if log {
println!("Rebuilding the application...");
}
let shutdown = rebuild_callback();
if shutdown {
*aborted.lock().unwrap() = true;
}
for channel in &mut *channels.lock().unwrap() {
send_msg(HotReloadMsg::Shutdown, channel);
}
return shutdown;
} else if log {
println!(
"Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes."
);
}
true
}
};
for evt in rx {
if chrono::Local::now().timestamp() > last_update_time {
if let Ok(evt) = evt {
let real_paths = evt
.paths
.iter()
.filter(|path| {
// skip non rust files
matches!(
path.extension().and_then(|p| p.to_str()),
Some("rs" | "toml" | "css" | "html" | "js")
)&&
// skip excluded paths
!excluded_paths.iter().any(|p| path.starts_with(p)) &&
// respect .gitignore
!gitignore
.matched_path_or_any_parents(path, false)
.is_ignore()
})
.collect::<Vec<_>>();
// Give time for the change to take effect before reading the file
if !real_paths.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(10));
}
let mut channels = channels.lock().unwrap();
for path in real_paths {
// if this file type cannot be hot reloaded, rebuild the application
if path.extension().and_then(|p| p.to_str()) != Some("rs")
&& rebuild()
{
return;
}
// find changes to the rsx in the file
match file_map
.lock()
.unwrap()
.update_rsx(path, crate_dir.as_path())
{
UpdateResult::UpdatedRsx(msgs) => {
for msg in msgs {
let mut i = 0;
while i < channels.len() {
let channel = &mut channels[i];
if send_msg(
HotReloadMsg::UpdateTemplate(msg),
channel,
) {
i += 1;
} else {
channels.remove(i);
}
}
}
}
UpdateResult::NeedsRebuild => {
drop(channels);
if rebuild() {
return;
}
break;
}
}
}
}
last_update_time = chrono::Local::now().timestamp();
}
}
});
}
}
}
fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {
if let Ok(msg) = serde_json::to_string(&msg) {
if channel.write_all(msg.as_bytes()).is_err() {
return false;
}
if channel.write_all(&[b'\n']).is_err() {
return false;
}
true
} else {
false
}
}
/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
std::thread::spawn(move || {
let temp_file = std::env::temp_dir().join("@dioxusin");
if let Ok(socket) = LocalSocketStream::connect(temp_file.as_path()) {
let mut buf_reader = BufReader::new(socket);
loop {
let mut buf = String::new();
match buf_reader.read_line(&mut buf) {
Ok(_) => {
let template: HotReloadMsg =
serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
f(template);
}
Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock {
break;
}
}
}
}
}
});
}
/// Start the hot reloading server with the current directory as the root
#[macro_export]
macro_rules! hot_reload_init {
() => {
#[cfg(debug_assertions)]
dioxus_hot_reload::init(dioxus_hot_reload::Config::new().root(env!("CARGO_MANIFEST_DIR")));
};
($cfg: expr) => {
#[cfg(debug_assertions)]
dioxus_hot_reload::init($cfg.root(env!("CARGO_MANIFEST_DIR")));
};
}
+1 -1
View File
@@ -25,7 +25,7 @@ fn render_template_node(node: &TemplateNode, out: &mut String) -> std::fmt::Resu
write!(out, "<{tag}")?;
for attr in *attrs {
if let TemplateAttribute::Static { name, value, .. } = attr {
write!(out, "{}=\"{}\"", name, value)?;
write!(out, "{name}=\"{value}\"")?;
}
}
for child in *children {
+43 -37
View File
@@ -445,6 +445,41 @@ class Interpreter {
}
}
function get_mouse_data(event) {
const {
altKey,
button,
buttons,
clientX,
clientY,
ctrlKey,
metaKey,
offsetX,
offsetY,
pageX,
pageY,
screenX,
screenY,
shiftKey,
} = event;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
offset_x: offsetX,
offset_y: offsetY,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
};
}
function serialize_event(event) {
switch (event.type) {
case "copy":
@@ -523,10 +558,6 @@ function serialize_event(event) {
values: {},
};
}
case "click":
case "contextmenu":
case "doubleclick":
case "dblclick":
case "drag":
case "dragend":
case "dragenter":
@@ -534,7 +565,13 @@ function serialize_event(event) {
case "dragleave":
case "dragover":
case "dragstart":
case "drop":
case "drop": {
return { mouse: get_mouse_data(event) };
}
case "click":
case "contextmenu":
case "doubleclick":
case "dblclick":
case "mousedown":
case "mouseenter":
case "mouseleave":
@@ -542,38 +579,7 @@ function serialize_event(event) {
case "mouseout":
case "mouseover":
case "mouseup": {
const {
altKey,
button,
buttons,
clientX,
clientY,
ctrlKey,
metaKey,
offsetX,
offsetY,
pageX,
pageY,
screenX,
screenY,
shiftKey,
} = event;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
offset_x: offsetX,
offset_y: offsetY,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
};
return get_mouse_data(event);
}
case "pointerdown":
case "pointermove":
+3 -1
View File
@@ -26,6 +26,7 @@ serde_json = "1.0.91"
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" }
dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
# warp
warp = { version = "0.3.3", optional = true }
@@ -51,8 +52,9 @@ salvo = { version = "0.37.7", features = ["affix", "ws"] }
tower = "0.4.13"
[features]
default = []
default = ["hot-reload"]
# actix = ["actix-files", "actix-web", "actix-ws"]
hot-reload = ["dioxus-hot-reload"]
[[example]]
name = "axum"
+1 -1
View File
@@ -46,7 +46,7 @@ async fn main() {
}),
);
println!("Listening on http://{}", addr);
println!("Listening on http://{addr}");
axum::Server::bind(&addr.to_string().parse().unwrap())
.serve(app.into_make_service())
+27
View File
@@ -103,6 +103,15 @@ pub async fn run<T>(
where
T: Send + 'static,
{
#[cfg(all(feature = "hot-reload", debug_assertions))]
let mut hot_reload_rx = {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
dioxus_hot_reload::connect(move |template| {
let _ = tx.send(template);
});
rx
};
let mut vdom = VirtualDom::new_with_props(app, props);
// todo: use an efficient binary packed format for this
@@ -122,6 +131,11 @@ where
}
loop {
#[cfg(all(feature = "hot-reload", debug_assertions))]
let hot_reload_wait = hot_reload_rx.recv();
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
let hot_reload_wait = std::future::pending();
tokio::select! {
// poll any futures or suspense
_ = vdom.wait_for_work() => {}
@@ -142,6 +156,19 @@ where
None => return Ok(()),
}
}
msg = hot_reload_wait => {
if let Some(msg) = msg {
match msg{
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(new_template) => {
vdom.replace_template(new_template);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
},
}
}
}
}
let edits = vdom
+6 -6
View File
@@ -316,7 +316,7 @@ fn creation() {
tree.add_child(parent_id, child_id);
println!("Tree: {:#?}", tree);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 2);
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(child_id), Some(1));
@@ -346,7 +346,7 @@ fn insertion() {
let after = after.id();
tree.insert_after(child, after);
println!("Tree: {:#?}", tree);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 4);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(child), Some(1));
@@ -381,7 +381,7 @@ fn deletion() {
let after = after.id();
tree.insert_after(child, after);
println!("Tree: {:#?}", tree);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 4);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(child), Some(1));
@@ -399,7 +399,7 @@ fn deletion() {
tree.remove(child);
println!("Tree: {:#?}", tree);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 3);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(before), Some(1));
@@ -415,7 +415,7 @@ fn deletion() {
tree.remove(before);
println!("Tree: {:#?}", tree);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 2);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(after), Some(1));
@@ -428,7 +428,7 @@ fn deletion() {
tree.remove(after);
println!("Tree: {:#?}", tree);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 1);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.children_ids(parent).unwrap(), &[]);
@@ -361,7 +361,7 @@ fn persist_removes() {
let mut rdom: RealDom = RealDom::new(Box::new([]));
let build = vdom.rebuild();
println!("{:#?}", build);
println!("{build:#?}");
let _to_update = rdom.apply_mutations(build);
// this will end on the node that is removed
@@ -393,7 +393,7 @@ fn persist_removes() {
vdom.mark_dirty(ScopeId(0));
let update = vdom.render_immediate();
println!("{:#?}", update);
println!("{update:#?}");
iter1.prune(&update, &rdom);
iter2.prune(&update, &rdom);
let _to_update = rdom.apply_mutations(update);
+365
View File
@@ -0,0 +1,365 @@
use dioxus::prelude::Props;
use dioxus_core::*;
use dioxus_native_core::{
node_ref::{AttributeMask, NodeView},
real_dom::RealDom,
state::{ParentDepState, State},
NodeMask, SendAnyMap,
};
use dioxus_native_core_macro::{sorted_str_slice, State};
use std::cell::Cell;
fn random_ns() -> Option<&'static str> {
let namespace = rand::random::<u8>() % 2;
match namespace {
0 => None,
1 => Some(Box::leak(
format!("ns{}", rand::random::<usize>()).into_boxed_str(),
)),
_ => unreachable!(),
}
}
fn create_random_attribute(attr_idx: &mut usize) -> TemplateAttribute<'static> {
match rand::random::<u8>() % 2 {
0 => TemplateAttribute::Static {
name: Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
value: Box::leak(format!("value{}", rand::random::<usize>()).into_boxed_str()),
namespace: random_ns(),
},
1 => TemplateAttribute::Dynamic {
id: {
let old_idx = *attr_idx;
*attr_idx += 1;
old_idx
},
},
_ => unreachable!(),
}
}
fn create_random_template_node(
dynamic_node_types: &mut Vec<DynamicNodeType>,
template_idx: &mut usize,
attr_idx: &mut usize,
depth: usize,
) -> TemplateNode<'static> {
match rand::random::<u8>() % 4 {
0 => {
let attrs = {
let attrs: Vec<_> = (0..(rand::random::<usize>() % 10))
.map(|_| create_random_attribute(attr_idx))
.collect();
Box::leak(attrs.into_boxed_slice())
};
TemplateNode::Element {
tag: Box::leak(format!("tag{}", rand::random::<usize>()).into_boxed_str()),
namespace: random_ns(),
attrs,
children: {
if depth > 4 {
&[]
} else {
let children: Vec<_> = (0..(rand::random::<usize>() % 3))
.map(|_| {
create_random_template_node(
dynamic_node_types,
template_idx,
attr_idx,
depth + 1,
)
})
.collect();
Box::leak(children.into_boxed_slice())
}
},
}
}
1 => TemplateNode::Text {
text: Box::leak(format!("{}", rand::random::<usize>()).into_boxed_str()),
},
2 => TemplateNode::DynamicText {
id: {
let old_idx = *template_idx;
*template_idx += 1;
dynamic_node_types.push(DynamicNodeType::Text);
old_idx
},
},
3 => TemplateNode::Dynamic {
id: {
let old_idx = *template_idx;
*template_idx += 1;
dynamic_node_types.push(DynamicNodeType::Other);
old_idx
},
},
_ => unreachable!(),
}
}
fn generate_paths(
node: &TemplateNode<'static>,
current_path: &[u8],
node_paths: &mut Vec<Vec<u8>>,
attr_paths: &mut Vec<Vec<u8>>,
) {
match node {
TemplateNode::Element {
children, attrs, ..
} => {
for attr in *attrs {
match attr {
TemplateAttribute::Static { .. } => {}
TemplateAttribute::Dynamic { .. } => {
attr_paths.push(current_path.to_vec());
}
}
}
for (i, child) in children.iter().enumerate() {
let mut current_path = current_path.to_vec();
current_path.push(i as u8);
generate_paths(child, &current_path, node_paths, attr_paths);
}
}
TemplateNode::Text { .. } => {}
TemplateNode::DynamicText { .. } => {
node_paths.push(current_path.to_vec());
}
TemplateNode::Dynamic { .. } => {
node_paths.push(current_path.to_vec());
}
}
}
enum DynamicNodeType {
Text,
Other,
}
fn create_random_template(name: &'static str) -> (Template<'static>, Vec<DynamicNodeType>) {
let mut dynamic_node_type = Vec::new();
let mut template_idx = 0;
let mut attr_idx = 0;
let roots = (0..(1 + rand::random::<usize>() % 5))
.map(|_| {
create_random_template_node(&mut dynamic_node_type, &mut template_idx, &mut attr_idx, 0)
})
.collect::<Vec<_>>();
assert!(!roots.is_empty());
let roots = Box::leak(roots.into_boxed_slice());
let mut node_paths = Vec::new();
let mut attr_paths = Vec::new();
for (i, root) in roots.iter().enumerate() {
generate_paths(root, &[i as u8], &mut node_paths, &mut attr_paths);
}
let node_paths = Box::leak(
node_paths
.into_iter()
.map(|v| &*Box::leak(v.into_boxed_slice()))
.collect::<Vec<_>>()
.into_boxed_slice(),
);
let attr_paths = Box::leak(
attr_paths
.into_iter()
.map(|v| &*Box::leak(v.into_boxed_slice()))
.collect::<Vec<_>>()
.into_boxed_slice(),
);
(
Template {
name,
roots,
node_paths,
attr_paths,
},
dynamic_node_type,
)
}
fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
let range = if depth > 3 { 1 } else { 3 };
match rand::random::<u8>() % range {
0 => DynamicNode::Placeholder(Default::default()),
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
key: None,
parent: Default::default(),
template: Cell::new(Template {
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
roots: &[TemplateNode::Dynamic { id: 0 }],
node_paths: &[&[0]],
attr_paths: &[],
}),
root_ids: Default::default(),
dynamic_nodes: cx.bump().alloc([cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
)]),
dynamic_attrs: &[],
})),
2 => cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
),
_ => unreachable!(),
}
}
fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
let value = match rand::random::<u8>() % 6 {
0 => AttributeValue::Text(Box::leak(
format!("{}", rand::random::<usize>()).into_boxed_str(),
)),
1 => AttributeValue::Float(rand::random()),
2 => AttributeValue::Int(rand::random()),
3 => AttributeValue::Bool(rand::random()),
4 => cx.any_value(rand::random::<usize>()),
5 => AttributeValue::None,
// Listener(RefCell<Option<ListenerCb<'a>>>),
_ => unreachable!(),
};
Attribute {
name: Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
value,
namespace: random_ns(),
mounted_element: Default::default(),
volatile: rand::random(),
}
}
static mut TEMPLATE_COUNT: usize = 0;
#[derive(PartialEq, Props)]
struct DepthProps {
depth: usize,
root: bool,
}
fn create_random_element(cx: Scope<DepthProps>) -> Element {
cx.needs_update();
let range = if cx.props.root { 2 } else { 3 };
let node = match rand::random::<usize>() % range {
0 | 1 => {
let (template, dynamic_node_types) = create_random_template(Box::leak(
format!(
"{}{}",
concat!(file!(), ":", line!(), ":", column!(), ":"),
{
unsafe {
let old = TEMPLATE_COUNT;
TEMPLATE_COUNT += 1;
old
}
}
)
.into_boxed_str(),
));
println!("{template:#?}");
let node = VNode {
key: None,
parent: None,
template: Cell::new(template),
root_ids: Default::default(),
dynamic_nodes: {
let dynamic_nodes: Vec<_> = dynamic_node_types
.iter()
.map(|ty| match ty {
DynamicNodeType::Text => DynamicNode::Text(VText {
value: Box::leak(
format!("{}", rand::random::<usize>()).into_boxed_str(),
),
id: Default::default(),
}),
DynamicNodeType::Other => {
create_random_dynamic_node(cx, cx.props.depth + 1)
}
})
.collect();
cx.bump().alloc(dynamic_nodes)
},
dynamic_attrs: cx.bump().alloc(
(0..template.attr_paths.len())
.map(|_| create_random_dynamic_attr(cx))
.collect::<Vec<_>>(),
),
};
Some(node)
}
_ => None,
};
println!("{node:#?}");
node
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BlablaState {}
/// Font style are inherited by default if not specified otherwise by some of the supported attributes.
impl ParentDepState for BlablaState {
type Ctx = ();
type DepState = (Self,);
const NODE_MASK: NodeMask =
NodeMask::new_with_attrs(AttributeMask::Static(&sorted_str_slice!(["blabla",])));
fn reduce(&mut self, _node: NodeView, _parent: Option<(&Self,)>, _ctx: &Self::Ctx) -> bool {
false
}
}
#[derive(Clone, State, Default, Debug)]
pub struct NodeState {
#[parent_dep_state(blabla)]
blabla: BlablaState,
}
// test for panics when creating random nodes and templates
#[test]
fn create() {
for _ in 0..100 {
let mut vdom = VirtualDom::new_with_props(
create_random_element,
DepthProps {
depth: 0,
root: true,
},
);
let mutations = vdom.rebuild();
let mut rdom: RealDom<NodeState> = RealDom::new();
let (to_update, _diff) = rdom.apply_mutations(mutations);
let ctx = SendAnyMap::new();
rdom.update_state(to_update, ctx);
}
}
// test for panics when diffing random nodes
// This test will change the template every render which is not very realistic, but it helps stress the system
#[test]
fn diff() {
for _ in 0..10 {
let mut vdom = VirtualDom::new_with_props(
create_random_element,
DepthProps {
depth: 0,
root: true,
},
);
let mutations = vdom.rebuild();
let mut rdom: RealDom<NodeState> = RealDom::new();
let (to_update, _diff) = rdom.apply_mutations(mutations);
let ctx = SendAnyMap::new();
rdom.update_state(to_update, ctx);
for _ in 0..10 {
let mutations = vdom.render_immediate();
let (to_update, _diff) = rdom.apply_mutations(mutations);
let ctx = SendAnyMap::new();
rdom.update_state(to_update, ctx);
}
}
}
+1 -1
View File
@@ -20,5 +20,5 @@ fn main() {
let out = dioxus_autofmt::write_block_out(body).unwrap();
println!("{}", out);
println!("{out}");
}
+1 -1
View File
@@ -193,7 +193,7 @@ pub struct FormattedSegment {
impl ToTokens for FormattedSegment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let (fmt, seg) = (&self.format_args, &self.segment);
let fmt = format!("{{0:{}}}", fmt);
let fmt = format!("{{0:{fmt}}}");
tokens.append_all(quote! {
format_args!(#fmt, #seg)
});
+4 -4
View File
@@ -60,11 +60,11 @@ impl StringCache {
..
} => {
cur_path.push(root_idx);
write!(chain, "<{}", tag)?;
write!(chain, "<{tag}")?;
for attr in *attrs {
match attr {
TemplateAttribute::Static { name, value, .. } => {
write!(chain, " {}=\"{}\"", name, value)?;
write!(chain, " {name}=\"{value}\"")?;
}
TemplateAttribute::Dynamic { id: index } => {
chain.segments.push(Segment::Attr(*index))
@@ -78,11 +78,11 @@ impl StringCache {
for child in *children {
Self::recurse(child, cur_path, root_idx, chain)?;
}
write!(chain, "</{}>", tag)?;
write!(chain, "</{tag}>")?;
}
cur_path.pop();
}
TemplateNode::Text { text } => write!(chain, "{}", text)?,
TemplateNode::Text { text } => write!(chain, "{text}")?,
TemplateNode::Dynamic { id: idx } | TemplateNode::DynamicText { id: idx } => {
chain.segments.push(Segment::Node(*idx))
}
+1 -1
View File
@@ -124,7 +124,7 @@ impl Renderer {
}
},
Segment::PreRendered(contents) => write!(buf, "{}", contents)?,
Segment::PreRendered(contents) => write!(buf, "{contents}")?,
}
}
+6 -1
View File
@@ -14,9 +14,10 @@ license = "MIT/Apache-2.0"
[dependencies]
dioxus = { path = "../dioxus", version = "^0.3.0" }
dioxus-core = { path = "../core", version = "^0.3.0" }
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
dioxus-html = { path = "../html", version = "^0.3.0" }
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
tui = "0.17.0"
crossterm = "0.23.0"
@@ -37,3 +38,7 @@ criterion = "0.3.5"
[[bench]]
name = "update"
harness = false
[features]
default = ["hot-reload"]
hot-reload = ["dioxus-hot-reload"]
+36 -6
View File
@@ -27,6 +27,7 @@ use std::{io, time::Duration};
use style_attributes::StyleModifier;
use taffy::Taffy;
pub use taffy::{geometry::Point, prelude::*};
use tokio::{select, sync::mpsc::unbounded_channel};
use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
mod config;
@@ -153,6 +154,15 @@ fn render_vdom(
.enable_all()
.build()?
.block_on(async {
#[cfg(all(feature = "hot-reload", debug_assertions))]
let mut hot_reload_rx = {
let (hot_reload_tx, hot_reload_rx) =
unbounded_channel::<dioxus_hot_reload::HotReloadMsg>();
dioxus_hot_reload::connect(move |msg| {
let _ = hot_reload_tx.send(msg);
});
hot_reload_rx
};
let mut terminal = (!cfg.headless).then(|| {
enable_raw_mode().unwrap();
let mut stdout = std::io::stdout();
@@ -238,16 +248,21 @@ fn render_vdom(
}
}
use futures::future::{select, Either};
let mut hot_reload_msg = None;
{
let wait = vdom.wait_for_work();
#[cfg(all(feature = "hot-reload", debug_assertions))]
let hot_reload_wait = hot_reload_rx.recv();
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
let hot_reload_wait = std::future::pending();
pin_mut!(wait);
match select(wait, event_reciever.next()).await {
Either::Left((_a, _b)) => {
//
}
Either::Right((evt, _o)) => {
select! {
_ = wait => {
},
evt = event_reciever.next() => {
match evt.as_ref().unwrap() {
InputEvent::UserInput(event) => match event {
TermEvent::Key(key) => {
@@ -267,6 +282,21 @@ fn render_vdom(
if let InputEvent::UserInput(evt) = evt.unwrap() {
register_event(evt);
}
},
Some(msg) = hot_reload_wait => {
hot_reload_msg = Some(msg);
}
}
}
// if we have a new template, replace the old one
if let Some(msg) = hot_reload_msg {
match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
vdom.replace_template(template);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
break;
}
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ fn rehydrates() {
.unwrap()
.body()
.unwrap()
.set_inner_html(&format!("<div id='main'>{}</div>", out));
.set_inner_html(&format!("<div id='main'>{out}</div>"));
dioxus_web::launch_cfg(app, Config::new().hydrate(true));
}