Merge pull request #16 from jkelleyrtp/jk/html-macro-bump

feat: webview basics
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2021-08-09 02:37:30 -04:00
commit f66630c935
96 files changed
+5366 -3310

No files matched your search

+17 -4
View File
@@ -37,18 +37,31 @@ mobile = ["dioxus-mobile"]
[dev-dependencies]
futures = "0.3.15"
futures-util = "0.3.16"
log = "0.4.14"
num-format = "0.4.0"
separator = "0.4.1"
serde = { version = "1.0.126", features = ["derive"] }
surf = "2.2.0"
im-rc = "15.0.0"
fxhash = "0.2.1"
anyhow = "1.0.42"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
argh = "0.1.5"
env_logger = "*"
async-std = { version = "1.9.0", features = ["attributes"] }
im-rc = "15.0.0"
rand = { version = "0.8.4", features = ["small_rng"] }
fxhash = "0.2.1"
surf = {version = "2.2.0", git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"}
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
gloo-timers = "0.2.1"
surf = {version = "2.2.0", default-features = false, features = ["wasm-client"], git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"}
[dependencies.getrandom]
version = "0.2"
features = ["js"]
[workspace]
members = [
+2 -1
View File
@@ -84,6 +84,7 @@ If you know React, then you already know Dioxus.
- Starting a new app takes zero templates or special tools - get a new app running in just seconds.
- Desktop apps running natively (no Electron!) in less than 10 lines of code.
- The most ergonomic and powerful state management of any Rust UI toolkit.
- Multithreaded asynchronous coroutine scheduler for powerful async code.
- And more! Read the full release post here.
## Get Started with...
@@ -165,7 +166,7 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an
| Custom elements | ✅ | ✅ | Define new element primitives |
| Suspense | ✅ | ✅ | schedule future render from future/promise |
| Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax |
| Re-hydration | 🛠 | ✅ | Pre-render to HTML to speed up first contentful paint |
| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint |
| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events |
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context |
+1 -1
View File
@@ -25,7 +25,7 @@ The current `RealDom` trait lives in `dioxus_core/diff`. A version of it is prov
```rust
pub trait RealDom<'a> {
fn handle_edit(&mut self, edit: DomEdit);
fn request_available_node(&mut self) -> RealDomNode;
fn request_available_node(&mut self) -> ElementId;
fn raw_node_as_any(&self) -> &mut dyn Any;
}
```
+1 -1
View File
@@ -12,7 +12,7 @@ pub static App: FC<()> = |cx| {
let mut direction = use_state(cx, || 1);
let (async_count, dir) = (count.for_async(), *direction);
let (task, _result) = cx.use_task(move || async move {
let (task, _result) = use_task(cx, move || async move {
loop {
gloo_timers::future::TimeoutFuture::new(250).await;
*async_count.get_mut() += dir;
+14
View File
@@ -0,0 +1,14 @@
use dioxus::prelude::*;
fn main() {
let g = dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory| {
use dioxus_elements::{GlobalAttributes, SvgAttributes};
__cx.element(
dioxus_elements::button,
[dioxus::events::on::onclick(__cx, move |_| {})],
[],
[],
None,
)
});
}
+86
View File
@@ -0,0 +1,86 @@
#![allow(non_snake_case)]
//! Example: Extremely nested borrowing
//! -----------------------------------
//!
//! Dioxus manages borrow lifetimes for you. This means any child may borrow from its parent. However, it is not possible
//! to hand out an &mut T to children - all props are consumed by &P, so you'd only get an &&mut T.
//!
//! How does it work?
//!
//! Dioxus will manually drop closures and props - things that borrow data before the component is ran again. This is done
//! "bottom up" from the lowest child all the way to the initiating parent. As it traverses each listener and prop, the
//! drop implementation is manually called, freeing any memory and ensuring that memory is not leaked.
//!
//! We cannot drop from the parent to the children - if the drop implementation modifies the data, downstream references
//! might be broken since we take an &mut T and and &T to the data. Instead, we work bottom up, making sure to remove any
//! potential references to the data before finally giving out an &mut T. This prevents us from mutably aliasing the data,
//! and is proven to be safe with MIRI.
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App, |c| c);
}
fn App<'a>(cx: Context<'a, ()>) -> DomTree<'a> {
let text: &'a mut Vec<String> = cx.use_hook(|_| vec![String::from("abc=def")], |f| f, |_| {});
let first = text.get_mut(0).unwrap();
cx.render(rsx! {
div {
Child1 {
text: first
}
}
})
}
#[derive(Props)]
struct C1Props<'a> {
text: &'a mut String,
}
impl<'a> Drop for C1Props<'a> {
fn drop(&mut self) {}
}
fn Child1<'a>(cx: Context<'a, C1Props>) -> DomTree<'a> {
let (left, right) = cx.text.split_once("=").unwrap();
cx.render(rsx! {
div {
Child2 { text: left }
Child2 { text: right }
}
})
}
#[derive(Props)]
struct C2Props<'a> {
text: &'a str,
}
impl<'a> Drop for C2Props<'a> {
fn drop(&mut self) {
todo!()
}
}
fn Child2<'a>(cx: Context<'a, C2Props>) -> DomTree<'a> {
cx.render(rsx! {
Child3 {
text: cx.text
}
})
}
#[derive(Props)]
struct C3Props<'a> {
text: &'a str,
}
fn Child3<'a>(cx: Context<'a, C3Props>) -> DomTree<'a> {
cx.render(rsx! {
div { "{cx.text}"}
})
}
+2 -1
View File
@@ -5,6 +5,7 @@
// use dioxus::prelude::*;
fn main() {
env_logger::init();
dioxus::desktop::launch(App, |cfg| cfg);
}
@@ -23,7 +24,7 @@ const App: FC<()> = |cx| {
let operator = use_state(cx, || None as Option<Operator>);
let display_value = use_state(cx, || "".to_string());
let clear_display = display_value.eq("0");
let clear_display = display_value == "0";
let clear_text = if clear_display { "C" } else { "AC" };
let input_digit = move |num: u8| display_value.get_mut().push_str(num.to_string().as_str());
+71
View File
@@ -0,0 +1,71 @@
//! Example: Coroutines!
//! --------------------
//!
//! Coroutines are an awesome way to write concurrent code. Dioxus heavily leverages coroutines to make sense of complex
//! ongoing asynchronous tasks. The async scheduler of Dioxus supports both single-threaded and multi-threaded coroutines,
//! so you can drop in code to run across multiple threads without blocking the main thread.
//!
//! Dioxus cannot simply abstract away the threading model for the web, unfortunately. If you want to use "web threads"
//! you either need to limit support for Chrome, or you need to use a Web Workers and message passing. This is easy enough
//! to do in your own code, and doesn't require 1st-party support from Dioxus itself.
//!
//! UseState and friends work fine with coroutines, but coroutines might be easier to use with the Dirac global state
//! management API. This lets you easily drive global state from a coroutine without having to subscribe to the state.
//!
//! For now, this example shows how to use coroutines used with use_state.
//!
//!
//! ## What is a Couroutine?
//!
//! A coroutine is a function that can be paused and resumed. It can be paused internally through "await" or externally
//! using the `TaskHandle` API. Within a coroutine, you may execute asynchonous code, that modifies values captured when
//! the coroutine was initiated. `use_state` always returns the same setter, so you don't need to worry about
fn main() {
dioxus::desktop::launch(App, |c| c);
}
use dioxus::prelude::*;
static App: FC<()> = |cx| {
let p1 = use_state(cx, || 0);
let p2 = use_state(cx, || 0);
let (mut p1_async, mut p2_async) = (p1.for_async(), p2.for_async());
let (p1_handle, _) = use_task(cx, || async move {
loop {
*p1_async.get_mut() += 1;
async_std::task::sleep(std::time::Duration::from_millis(75)).await;
}
});
let (p2_handle, _) = use_task(cx, || async move {
loop {
*p2_async.get_mut() += 1;
async_std::task::sleep(std::time::Duration::from_millis(100)).await;
}
});
cx.render(rsx! {
div { style: { width: "400px", height: "400px", position: "relative", background: "yellow" }
button { "reset", onclick: move |_| {} }
Horsey { pos: *p1, "horsey 1" }
Horsey { pos: *p2, "horsey 2" }
}
})
};
#[derive(PartialEq, Props)]
struct HorseyProps {
pos: i32,
}
static Horsey: FC<HorseyProps> = |cx| {
cx.render(rsx! {
div {
button { "pause" }
div {
{cx.children()}
}
}
})
};
+115
View File
@@ -0,0 +1,115 @@
//! Example: File Explorer
//! -------------------------
//!
//! This is a fun little desktop application that lets you explore the file system.
//!
//! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do.
use dioxus::desktop::wry::application::dpi::LogicalSize;
use dioxus::prelude::*;
use std::fs::{self, DirEntry};
fn main() {
env_logger::init();
dioxus::desktop::launch(App, |c| {
c.with_window(|w| {
w.with_resizable(false)
.with_inner_size(LogicalSize::new(800.0, 400.0))
})
})
.unwrap();
}
static App: FC<()> = |cx| {
let files = use_state(cx, || Files::new());
let file_list = files.path_names.iter().enumerate().map(|(dir_id, path)| {
rsx! (
li { a {"{path}", onclick: move |_| files.get_mut().enter_dir(dir_id), href: "#"} }
)
});
let err_disp = files.err.as_ref().map(|err| {
rsx! {
div {
code {"{err}"}
button {"x", onclick: move |_| files.get_mut().clear_err() }
}
}
});
let cur = files.current();
cx.render(rsx! {
div {
h1 {"Files: "}
h3 {"Cur dir: {cur}"}
button { "go up", onclick: move |_| files.get_mut().go_up() }
ol { {file_list} }
{err_disp}
}
})
};
// right now, this gets cloned every time. It might be a bit better to use im_rc's collections instead
#[derive(Clone)]
struct Files {
path_stack: Vec<String>,
path_names: Vec<String>,
err: Option<String>,
}
impl Files {
fn new() -> Self {
let mut files = Self {
path_stack: vec!["./".to_string()],
path_names: vec![],
err: None,
};
files.reload_path_list();
files
}
fn reload_path_list(&mut self) {
let cur_path = self.path_stack.last().unwrap();
let paths = match fs::read_dir(cur_path) {
Ok(e) => e,
Err(err) => {
let err = format!("An error occured: {:?}", err);
self.err = Some(err);
self.path_stack.pop();
return;
}
};
// clear the current state
self.clear_err();
self.path_names.clear();
for path in paths {
self.path_names
.push(path.unwrap().path().display().to_string());
}
}
fn go_up(&mut self) {
if self.path_stack.len() > 1 {
self.path_stack.pop();
}
self.reload_path_list();
}
fn enter_dir(&mut self, dir_id: usize) {
let path = &self.path_names[dir_id];
self.path_stack.push(path.clone());
self.reload_path_list();
}
fn current(&self) -> &str {
self.path_stack.last().unwrap()
}
fn clear_err(&mut self) {
self.err = None;
}
}
+33
View File
@@ -0,0 +1,33 @@
//! Example: realworld usage of hydration
//! ------------------------------------
//!
//! This example shows how to pre-render a page using dioxus SSR and then how to rehydrate it on the client side.
//!
//! To accomplish hydration on the web, you'll want to set up a slightly more sophisticated build & bundle strategy. In
//! the official docs, we have a guide for using DioxusStudio as a build tool with pre-rendering and hydration.
//!
//! In this example, we pre-render the page to HTML and then pass it into the desktop configuration. This serves as a
//! proof-of-concept for the hydration feature, but you'll probably only want to use hydration for the web.
use dioxus::prelude::*;
use dioxus::ssr;
fn main() {
let mut vdom = VirtualDom::launch_in_place(App);
let content = ssr::render_vdom(&vdom, |f| f.pre_render(true));
dioxus::desktop::launch(App, |c| c.with_prerendered(content)).unwrap();
}
static App: FC<()> = |cx| {
let mut val = use_state(cx, || 0);
cx.render(rsx! {
div {
h1 {"hello world. Count: {val}"}
button {
"click to increment"
onclick: move |_| val += 1
}
}
})
};
+10 -2
View File
@@ -15,13 +15,21 @@
//! the RefCell will panic and crash. You can use `try_get_mut` or `.modify` to avoid this problem, or just not hold two
//! RefMuts at the same time.
use dioxus::desktop::wry::application::dpi::LogicalSize;
use dioxus::events::on::*;
use dioxus::prelude::*;
const STYLE: &str = include_str!("./assets/calculator.css");
fn main() {
dioxus::desktop::launch(App, |cfg| cfg.with_title("Calculator Demo"))
.expect("failed to launch dioxus app");
env_logger::init();
dioxus::desktop::launch(App, |cfg| {
cfg.with_window(|w| {
w.with_title("Calculator Demo")
.with_resizable(false)
.with_inner_size(LogicalSize::new(320.0, 530.0))
})
})
.expect("failed to launch dioxus app");
}
static App: FC<()> = |cx| {
+1
View File
@@ -7,6 +7,7 @@
use dioxus::prelude::*;
fn main() {
env_logger::init();
dioxus::desktop::launch(App, |c| c);
}
+2 -1
View File
@@ -15,7 +15,8 @@ struct DogApi {
const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random";
pub static Example: FC<()> = |cx| {
let doggo = cx.use_suspense(
let doggo = use_suspense(
cx,
|| surf::get(ENDPOINT).recv_json::<DogApi>(),
|cx, res| match res {
Ok(res) => rsx!(in cx, img { src: "{res.message}" }),
+2 -2
View File
@@ -30,8 +30,8 @@ pub static Example: FC<()> = |cx| {
// Tasks are 'static, so we need to copy relevant items in
let (async_count, dir) = (count.for_async(), *direction);
let (task, result) = cx.use_task(move || async move {
// Count infinitely!
let (task, result) = use_task(cx, move || async move {
loop {
gloo_timers::future::TimeoutFuture::new(250).await;
*async_count.get_mut() += dir;
+1 -1
View File
@@ -7,7 +7,7 @@ pub static Example: FC<()> = |cx| {
// This is an easy/low hanging fruit to improve upon
let mut dom = VirtualDom::new(SomeApp);
dom.rebuild_in_place().unwrap();
ssr::render_vdom(&dom)
ssr::render_vdom(&dom, |c| c)
});
cx.render(rsx! {
+5 -3
View File
@@ -1,13 +1,15 @@
#![allow(non_upper_case_globals)]
use dioxus::prelude::*;
use dioxus::ssr;
fn main() {
let mut vdom = VirtualDom::new(App);
vdom.rebuild_in_place();
println!("{}", ssr::render_vdom(&vdom));
vdom.rebuild_in_place().expect("Rebuilding failed");
println!("{}", ssr::render_vdom(&vdom, |c| c));
}
const App: FC<()> = |cx| {
static App: FC<()> = |cx| {
cx.render(rsx!(
div {
h1 { "Title" }
+6 -4
View File
@@ -3,10 +3,12 @@ use dioxus::prelude::*;
fn main() {
use dioxus::desktop::wry::application::platform::macos::*;
dioxus::desktop::launch(App, |c| {
c.with_fullsize_content_view(true)
.with_titlebar_buttons_hidden(false)
.with_titlebar_transparent(true)
.with_movable_by_window_background(true)
c.with_window(|w| {
w.with_fullsize_content_view(true)
.with_titlebar_buttons_hidden(false)
.with_titlebar_transparent(true)
.with_movable_by_window_background(true)
})
});
}
+2 -1
View File
@@ -3,7 +3,7 @@ use std::cell::Cell;
use dioxus::prelude::*;
use dioxus_core::{
nodes::{VElement, VText},
RealDomNode,
ElementId,
};
fn main() {
@@ -33,6 +33,7 @@ const Example: FC<()> = |cx| {
}
"h4"
div { "h5" }
button { }
Child {}
})
};
+3 -7
View File
@@ -1,14 +1,10 @@
#![allow(non_upper_case_globals, non_snake_case)]
use dioxus::prelude::*;
use im_rc::HashMap;
use std::rc::Rc;
fn main() {
#[cfg(feature = "desktop")]
// #[cfg(not(target_arch = "wasm32"))]
dioxus::desktop::launch(App, |c| c);
#[cfg(feature = "desktop")]
dioxus::web::launch(App, |c| c);
fn main() -> anyhow::Result<()> {
dioxus::desktop::launch(App, |c| c)
}
#[derive(PartialEq)]
+27 -7
View File
@@ -8,21 +8,41 @@
//! into the native VDom instance.
//!
//! Currently, NodeRefs won't work properly, but all other event functionality will.
#![allow(non_upper_case_globals, non_snake_case)]
use dioxus::prelude::*;
use dioxus::{events::on::MouseEvent, prelude::*};
fn main() {
dioxus::desktop::launch(App, |c| c);
fn main() -> anyhow::Result<()> {
env_logger::init();
dioxus::desktop::launch(App, |c| c)
}
static App: FC<()> = |cx| {
let mut count = use_state(cx, || 0);
let state = use_state(cx, || String::from("hello"));
let clear_text = state == "hello";
dbg!("rednering parent");
cx.render(rsx! {
div {
h1 { "Hifive counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
h1 {"{state}"}
CalculatorKey { name: "key-clear", onclick: move |_| state.get_mut().push_str("hello"), "{clear_text}" }
CalculatorKey { name: "key-sign", onclick: move |_| { state.get_mut().pop(); }, "±"}
}
})
};
#[derive(Props)]
struct CalculatorKeyProps<'a> {
name: &'static str,
onclick: &'a dyn Fn(MouseEvent),
}
fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> DomTree<'a> {
cx.render(rsx! {
button {
class: "calculator-key {cx.name}"
onclick: {cx.onclick}
{cx.children()}
}
})
}
+30
View File
@@ -0,0 +1,30 @@
#![allow(non_upper_case_globals, non_snake_case)]
//! Example: Webview Renderer
//! -------------------------
//!
//! This example shows how to use the dioxus_desktop crate to build a basic desktop application.
//!
//! Under the hood, the dioxus_desktop crate bridges a native Dioxus VirtualDom with a custom prebuit application running
//! in the webview runtime. Custom handlers are provided for the webview instance to consume patches and emit user events
//! into the native VDom instance.
//!
//! Currently, NodeRefs won't work properly, but all other event functionality will.
use dioxus::prelude::*;
fn main() {
dioxus::web::launch(App, |c| c);
}
static App: FC<()> = |cx| {
let mut count = use_state(cx, || 0);
cx.render(rsx! {
div {
h1 { "Hifive counter: {count}" }
{cx.children()}
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
}
})
};
+7
View File
@@ -0,0 +1,7 @@
# core-macro
This crate implements these macros:
- `format_args_f`: for f-string interpolation inside of text blocks
- `Props`: derive macro for typed-builder with props configurations
-
-
@@ -4,6 +4,7 @@ pub mod dioxus {
pub mod prelude {
pub trait Properties {
type Builder;
const IS_STATIC: bool;
fn builder() -> Self::Builder;
unsafe fn memoize(&self, other: &Self) -> bool;
}
+5 -7
View File
@@ -13,8 +13,6 @@
//!
//!
use crate::util::is_valid_svg_tag;
use {
proc_macro::TokenStream,
proc_macro2::{Span, TokenStream as TokenStream2},
@@ -163,11 +161,11 @@ impl ToTokens for ToToksCtx<&Element> {
self.recurse(attr).to_tokens(tokens);
}
if is_valid_svg_tag(&name.to_string()) {
tokens.append_all(quote! {
.namespace(Some("http://www.w3.org/2000/svg"))
});
}
// if is_valid_svg_tag(&name.to_string()) {
// tokens.append_all(quote! {
// .namespace(Some("http://www.w3.org/2000/svg"))
// });
// }
match &self.inner.children {
MaybeExpr::Expr(expr) => tokens.append_all(quote! {
-27
View File
@@ -1,27 +0,0 @@
//! Html body
//! -------
//!
//!
//! Since both HTML and RSX serialize to the same node structure, the HTML parser uses the same types as RSX,
//! but has a different Parse implementation.
use crate::rsx::*;
use quote::ToTokens;
use syn::parse::Parse;
pub struct HtmlBody(RsxBody);
impl Parse for HtmlBody {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
todo!()
}
}
impl ToTokens for HtmlBody {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
self.0.to_tokens(tokens)
}
}
pub struct HtmlNode(BodyNode);
pub struct HtmlAmbigiousElement(AmbiguousElement);
pub struct HtmlComponent(Component);
+12 -58
View File
@@ -1,68 +1,12 @@
use proc_macro::TokenStream;
use quote::ToTokens;
use rsx::{AS_HTML, AS_RSX};
use syn::parse_macro_input;
pub(crate) mod fc;
pub(crate) mod htm;
pub(crate) mod html;
pub(crate) mod ifmt;
pub(crate) mod props;
pub(crate) mod rsx;
pub(crate) mod rsxtemplate;
pub(crate) mod util;
/// The html! macro makes it easy for developers to write jsx-style markup in their components.
/// We aim to keep functional parity with html templates.
#[proc_macro]
pub fn html(s: TokenStream) -> TokenStream {
match syn::parse::<htm::HtmlRender>(s) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
/// The html! macro makes it easy for developers to write jsx-style markup in their components.
/// We aim to keep functional parity with html templates.
#[proc_macro]
pub fn rsx_template(s: TokenStream) -> TokenStream {
match syn::parse::<rsxtemplate::RsxTemplate>(s) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
/// The html! macro makes it easy for developers to write jsx-style markup in their components.
/// We aim to keep functional parity with html templates.
#[proc_macro]
pub fn html_template(s: TokenStream) -> TokenStream {
match syn::parse::<rsxtemplate::RsxTemplate>(s) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
// #[proc_macro_attribute]
// pub fn fc(attr: TokenStream, item: TokenStream) -> TokenStream {
/// Label a function or static closure as a functional component.
/// This macro reduces the need to create a separate properties struct.
///
/// Using this macro is fun and simple
///
/// ```ignore
///
/// #[fc]
/// fn Example(cx: Context, name: &str) -> DomTree {
/// cx.render(rsx! { h1 {"hello {name}"} })
/// }
/// ```
#[proc_macro_attribute]
pub fn fc(_attr: TokenStream, item: TokenStream) -> TokenStream {
match syn::parse::<fc::FunctionComponent>(item) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
#[proc_macro]
pub fn format_args_f(input: TokenStream) -> TokenStream {
@@ -238,7 +182,17 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
/// ```
#[proc_macro]
pub fn rsx(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::RsxBody>(s) {
match syn::parse::<rsx::RsxBody<AS_RSX>>(s) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
/// The html! macro makes it easy for developers to write jsx-style markup in their components.
/// We aim to keep functional parity with html templates.
#[proc_macro]
pub fn html(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::RsxBody<AS_HTML>>(s) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
+6 -8
View File
@@ -502,7 +502,6 @@ mod field_info {
mod struct_info {
use proc_macro2::TokenStream;
use quote::__private::ext::RepToTokensExt;
use quote::quote;
use syn::parse::Error;
@@ -663,6 +662,11 @@ Finally, call `.build()` to create the instance of `{name}`.
false => quote! { self == other },
};
let is_static = match are_there_generics {
true => quote! { false },
false => quote! { true },
};
Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause {
#[doc = #builder_method_doc]
@@ -694,6 +698,7 @@ Finally, call `.build()` to create the instance of `{name}`.
impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{
type Builder = #builder_name #generics_with_empty;
const IS_STATIC: bool = #is_static;
fn builder() -> Self::Builder {
#name::builder()
}
@@ -1095,13 +1100,6 @@ Finally, call `.build()` to create the instance of `{name}`.
}
)
}
pub fn build_props_impl(&self) -> TokenStream {
// SomeProps: #name
// #builder_name
// #generics_with_empty
quote! {}
}
}
#[derive(Debug, Default)]
+50 -9
View File
@@ -15,17 +15,17 @@ use syn::{
Error, Ident, LitStr, Result, Token,
};
pub enum AmbiguousElement {
Element(Element),
Component(Component),
pub enum AmbiguousElement<const AS: HTML_OR_RSX> {
Element(Element<AS>),
Component(Component<AS>),
}
impl Parse for AmbiguousElement {
impl Parse for AmbiguousElement<AS_RSX> {
fn parse(input: ParseStream) -> Result<Self> {
// Try to parse as an absolute path and immediately defer to the componetn
if input.peek(Token![::]) {
return input
.parse::<Component>()
.parse::<Component<AS_RSX>>()
.map(|c| AmbiguousElement::Component(c));
}
@@ -34,7 +34,7 @@ impl Parse for AmbiguousElement {
if let Ok(pat) = input.fork().parse::<syn::Path>() {
if pat.segments.len() > 1 {
return input
.parse::<Component>()
.parse::<Component<AS_RSX>>()
.map(|c| AmbiguousElement::Component(c));
}
}
@@ -45,11 +45,11 @@ impl Parse for AmbiguousElement {
let first_char = name_str.chars().next().unwrap();
if first_char.is_ascii_uppercase() {
input
.parse::<Component>()
.parse::<Component<AS_RSX>>()
.map(|c| AmbiguousElement::Component(c))
} else {
input
.parse::<Element>()
.parse::<Element<AS_RSX>>()
.map(|c| AmbiguousElement::Element(c))
}
} else {
@@ -61,7 +61,48 @@ impl Parse for AmbiguousElement {
}
}
impl ToTokens for AmbiguousElement {
impl Parse for AmbiguousElement<AS_HTML> {
fn parse(input: ParseStream) -> Result<Self> {
// Try to parse as an absolute path and immediately defer to the componetn
if input.peek(Token![::]) {
return input
.parse::<Component<AS_HTML>>()
.map(|c| AmbiguousElement::Component(c));
}
// If not an absolute path, then parse the ident and check if it's a valid tag
if let Ok(pat) = input.fork().parse::<syn::Path>() {
if pat.segments.len() > 1 {
return input
.parse::<Component<AS_HTML>>()
.map(|c| AmbiguousElement::Component(c));
}
}
if let Ok(name) = input.fork().parse::<Ident>() {
let name_str = name.to_string();
let first_char = name_str.chars().next().unwrap();
if first_char.is_ascii_uppercase() {
input
.parse::<Component<AS_HTML>>()
.map(|c| AmbiguousElement::Component(c))
} else {
input
.parse::<Element<AS_HTML>>()
.map(|c| AmbiguousElement::Element(c))
}
} else {
if input.peek(LitStr) {
panic!("it's actually a litstr");
}
Err(Error::new(input.span(), "Not a valid Html tag"))
}
}
}
impl<const AS: HTML_OR_RSX> ToTokens for AmbiguousElement<AS> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
AmbiguousElement::Element(el) => el.to_tokens(tokens),
+83
View File
@@ -0,0 +1,83 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
Error, Ident, Result, Token,
};
use super::*;
pub struct RsxBody<const AS: HTML_OR_RSX> {
custom_context: Option<Ident>,
roots: Vec<BodyNode<AS>>,
}
/// The custom rusty variant of parsing rsx!
impl Parse for RsxBody<AS_RSX> {
fn parse(input: ParseStream) -> Result<Self> {
let custom_context = try_parse_custom_context(input)?;
let (_, roots, _) =
BodyParseConfig::<AS_RSX>::new_as_body().parse_component_body(&input)?;
Ok(Self {
custom_context,
roots,
})
}
}
/// The HTML variant of parsing rsx!
impl Parse for RsxBody<AS_HTML> {
fn parse(input: ParseStream) -> Result<Self> {
let custom_context = try_parse_custom_context(input)?;
let (_, roots, _) =
BodyParseConfig::<AS_HTML>::new_as_body().parse_component_body(&input)?;
Ok(Self {
custom_context,
roots,
})
}
}
fn try_parse_custom_context(input: ParseStream) -> Result<Option<Ident>> {
let res = if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) {
let _ = input.parse::<Token![in]>()?;
let name = input.parse::<Ident>()?;
input.parse::<Token![,]>()?;
Some(name)
} else {
None
};
Ok(res)
}
/// Serialize the same way, regardless of flavor
impl<const A: HTML_OR_RSX> ToTokens for RsxBody<A> {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let inner = if self.roots.len() == 1 {
let inner = &self.roots[0];
quote! {#inner}
} else {
let childs = &self.roots;
quote! { __cx.fragment_from_iter([ #(#childs),* ]) }
};
match &self.custom_context {
// The `in cx` pattern allows directly rendering
Some(ident) => out_tokens.append_all(quote! {
#ident.render(dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{
use dioxus_elements::{GlobalAttributes, SvgAttributes};
#inner
}))
}),
// Otherwise we just build the LazyNode wrapper
None => out_tokens.append_all(quote! {
dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{
use dioxus_elements::{GlobalAttributes, SvgAttributes};
#inner
})
}),
};
}
}
+187 -77
View File
@@ -19,18 +19,18 @@ use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
ext::IdentExt,
parse::{Parse, ParseBuffer, ParseStream},
token, Expr, ExprClosure, Ident, Result, Token,
token, Error, Expr, ExprClosure, Ident, Result, Token,
};
pub struct Component {
pub struct Component<const AS: HTML_OR_RSX> {
// accept any path-like argument
name: syn::Path,
body: Vec<ComponentField>,
children: Vec<BodyNode>,
body: Vec<ComponentField<AS>>,
children: Vec<BodyNode<AS>>,
manual_props: Option<Expr>,
}
impl Parse for Component {
impl Parse for Component<AS_RSX> {
fn parse(stream: ParseStream) -> Result<Self> {
// let name = s.parse::<syn::ExprPath>()?;
// todo: look into somehow getting the crate/super/etc
@@ -41,21 +41,37 @@ impl Parse for Component {
let content: ParseBuffer;
syn::braced!(content in stream);
let mut body: Vec<ComponentField> = Vec::new();
let mut children: Vec<BodyNode> = Vec::new();
let mut manual_props = None;
let cfg: BodyParseConfig<AS_RSX> = BodyParseConfig {
allow_children: true,
allow_fields: true,
allow_manual_props: true,
};
parse_component_body(
&content,
&BodyParseConfig {
allow_children: true,
allow_fields: true,
allow_manual_props: true,
},
&mut body,
&mut children,
&mut manual_props,
)?;
let (body, children, manual_props) = cfg.parse_component_body(&content)?;
Ok(Self {
name,
body,
children,
manual_props,
})
}
}
impl Parse for Component<AS_HTML> {
fn parse(stream: ParseStream) -> Result<Self> {
let name = syn::Path::parse_mod_style(stream)?;
// parse the guts
let content: ParseBuffer;
syn::braced!(content in stream);
let cfg: BodyParseConfig<AS_HTML> = BodyParseConfig {
allow_children: true,
allow_fields: true,
allow_manual_props: true,
};
let (body, children, manual_props) = cfg.parse_component_body(&content)?;
Ok(Self {
name,
@@ -66,64 +82,139 @@ impl Parse for Component {
}
}
pub struct BodyParseConfig {
pub struct BodyParseConfig<const AS: HTML_OR_RSX> {
pub allow_fields: bool,
pub allow_children: bool,
pub allow_manual_props: bool,
}
// todo: unify this body parsing for both elements and components
// both are style rather ad-hoc, though components are currently more configured
pub fn parse_component_body(
content: &ParseBuffer,
cfg: &BodyParseConfig,
body: &mut Vec<ComponentField>,
children: &mut Vec<BodyNode>,
manual_props: &mut Option<Expr>,
) -> Result<()> {
'parsing: loop {
// [1] Break if empty
if content.is_empty() {
break 'parsing;
}
if content.peek(Token![..]) {
if !cfg.allow_manual_props {
return Err(Error::new(
content.span(),
"Props spread syntax is not allowed in this context. \nMake to only use the elipsis `..` in Components.",
));
}
content.parse::<Token![..]>()?;
*manual_props = Some(content.parse::<Expr>()?);
} else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
if !cfg.allow_fields {
return Err(Error::new(
content.span(),
"Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.",
));
}
body.push(content.parse::<ComponentField>()?);
} else {
if !cfg.allow_children {
return Err(Error::new(
content.span(),
"This item is not allowed to accept children.",
));
}
children.push(content.parse::<BodyNode>()?);
}
// consume comma if it exists
// we don't actually care if there *are* commas between attrs
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
impl<const AS: HTML_OR_RSX> BodyParseConfig<AS> {
/// The configuration to parse the root
pub fn new_as_body() -> Self {
Self {
allow_children: true,
allow_fields: false,
allow_manual_props: false,
}
}
Ok(())
}
impl ToTokens for Component {
impl BodyParseConfig<AS_RSX> {
// todo: unify this body parsing for both elements and components
// both are style rather ad-hoc, though components are currently more configured
pub fn parse_component_body(
&self,
content: &ParseBuffer,
) -> Result<(
Vec<ComponentField<AS_RSX>>,
Vec<BodyNode<AS_RSX>>,
Option<Expr>,
)> {
let mut body = Vec::new();
let mut children = Vec::new();
let mut manual_props = None;
'parsing: loop {
// [1] Break if empty
if content.is_empty() {
break 'parsing;
}
if content.peek(Token![..]) {
if !self.allow_manual_props {
return Err(Error::new(
content.span(),
"Props spread syntax is not allowed in this context. \nMake to only use the elipsis `..` in Components.",
));
}
content.parse::<Token![..]>()?;
manual_props = Some(content.parse::<Expr>()?);
} else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
if !self.allow_fields {
return Err(Error::new(
content.span(),
"Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.",
));
}
body.push(content.parse::<ComponentField<AS_RSX>>()?);
} else {
if !self.allow_children {
return Err(Error::new(
content.span(),
"This item is not allowed to accept children.",
));
}
children.push(content.parse::<BodyNode<AS_RSX>>()?);
}
// consume comma if it exists
// we don't actually care if there *are* commas between attrs
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
}
}
Ok((body, children, manual_props))
}
}
impl BodyParseConfig<AS_HTML> {
// todo: unify this body parsing for both elements and components
// both are style rather ad-hoc, though components are currently more configured
pub fn parse_component_body(
&self,
content: &ParseBuffer,
) -> Result<(
Vec<ComponentField<AS_HTML>>,
Vec<BodyNode<AS_HTML>>,
Option<Expr>,
)> {
let mut body = Vec::new();
let mut children = Vec::new();
let mut manual_props = None;
'parsing: loop {
// [1] Break if empty
if content.is_empty() {
break 'parsing;
}
if content.peek(Token![..]) {
if !self.allow_manual_props {
return Err(Error::new(
content.span(),
"Props spread syntax is not allowed in this context. \nMake to only use the elipsis `..` in Components.",
));
}
content.parse::<Token![..]>()?;
manual_props = Some(content.parse::<Expr>()?);
} else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
if !self.allow_fields {
return Err(Error::new(
content.span(),
"Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.",
));
}
body.push(content.parse::<ComponentField<AS_HTML>>()?);
} else {
if !self.allow_children {
return Err(Error::new(
content.span(),
"This item is not allowed to accept children.",
));
}
children.push(content.parse::<BodyNode<AS_HTML>>()?);
}
// consume comma if it exists
// we don't actually care if there *are* commas between attrs
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
}
}
Ok((body, children, manual_props))
}
}
impl<const AS: HTML_OR_RSX> ToTokens for Component<AS> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
@@ -170,12 +261,10 @@ impl ToTokens for Component {
let key_token = match has_key {
Some(field) => {
let inners = field.content.to_token_stream();
quote! {
Some(#inners)
}
let inners = &field.content;
quote! { Some(format_args_f!(#inners)) }
}
None => quote! {None},
None => quote! { None },
};
let childs = &self.children;
@@ -188,14 +277,14 @@ impl ToTokens for Component {
#name,
#builder,
#key_token,
__cx.bump().alloc(#children)
#children
)
})
}
}
// the struct's fields info
pub struct ComponentField {
pub struct ComponentField<const AS: HTML_OR_RSX> {
name: Ident,
content: ContentField,
}
@@ -222,7 +311,28 @@ impl ToTokens for ContentField {
}
}
impl Parse for ComponentField {
impl Parse for ComponentField<AS_RSX> {
fn parse(input: ParseStream) -> Result<Self> {
let name = Ident::parse_any(input)?;
input.parse::<Token![:]>()?;
let name_str = name.to_string();
let content = if name_str.starts_with("on") {
if input.peek(token::Brace) {
let content;
syn::braced!(content in input);
ContentField::OnHandlerRaw(content.parse()?)
} else {
ContentField::OnHandler(input.parse()?)
}
} else {
ContentField::ManExpr(input.parse::<Expr>()?)
};
Ok(Self { name, content })
}
}
impl Parse for ComponentField<AS_HTML> {
fn parse(input: ParseStream) -> Result<Self> {
let name = Ident::parse_any(input)?;
input.parse::<Token![:]>()?;
@@ -244,7 +354,7 @@ impl Parse for ComponentField {
}
}
impl ToTokens for ComponentField {
impl<const AS: HTML_OR_RSX> ToTokens for ComponentField<AS> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let ComponentField { name, content, .. } = self;
tokens.append_all(quote! {
+162 -38
View File
@@ -11,35 +11,16 @@ use syn::{
// =======================================
// Parse the VNode::Element type
// =======================================
pub struct Element {
pub struct Element<const AS: HTML_OR_RSX> {
name: Ident,
key: Option<AttrType>,
attributes: Vec<ElementAttr>,
listeners: Vec<ElementAttr>,
children: Vec<BodyNode>,
key: Option<LitStr>,
attributes: Vec<ElementAttr<AS>>,
listeners: Vec<ElementAttr<AS>>,
children: Vec<BodyNode<AS>>,
is_static: bool,
}
impl ToTokens for Element {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
let attr = &self.attributes;
let childs = &self.children;
let listeners = &self.listeners;
tokens.append_all(quote! {
__cx.element(
dioxus_elements::#name,
__cx.bump().alloc([ #(#listeners),* ]),
__cx.bump().alloc([ #(#attr),* ]),
__cx.bump().alloc([ #(#childs),* ]),
None,
)
});
}
}
impl Parse for Element {
impl Parse for Element<AS_RSX> {
fn parse(stream: ParseStream) -> Result<Self> {
let name = Ident::parse(stream)?;
@@ -47,9 +28,9 @@ impl Parse for Element {
let content: ParseBuffer;
syn::braced!(content in stream);
let mut attributes: Vec<ElementAttr> = vec![];
let mut listeners: Vec<ElementAttr> = vec![];
let mut children: Vec<BodyNode> = vec![];
let mut attributes: Vec<ElementAttr<AS_RSX>> = vec![];
let mut listeners: Vec<ElementAttr<AS_RSX>> = vec![];
let mut children: Vec<BodyNode<AS_RSX>> = vec![];
let mut key = None;
'parsing: loop {
@@ -59,7 +40,7 @@ impl Parse for Element {
}
if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
parse_element_body(
parse_rsx_element_field(
&content,
&mut attributes,
&mut listeners,
@@ -67,7 +48,7 @@ impl Parse for Element {
name.clone(),
)?;
} else {
children.push(content.parse::<BodyNode>()?);
children.push(content.parse::<BodyNode<AS_RSX>>()?);
}
// consume comma if it exists
@@ -88,10 +69,153 @@ impl Parse for Element {
}
}
impl Parse for Element<AS_HTML> {
fn parse(stream: ParseStream) -> Result<Self> {
let l_tok = stream.parse::<Token![<]>()?;
let el_name = Ident::parse(stream)?;
// parse the guts
// let content: ParseBuffer;
// syn::braced!(content in stream);
let mut attributes: Vec<ElementAttr<AS_HTML>> = vec![];
let mut listeners: Vec<ElementAttr<AS_HTML>> = vec![];
let mut children: Vec<BodyNode<AS_HTML>> = vec![];
let mut key = None;
// loop {
// if stream.peek(Token![>]) {
// break;
// } else {
// }
// }
while !stream.peek(Token![>]) {
// self-closing
if stream.peek(Token![/]) {
stream.parse::<Token![/]>()?;
stream.parse::<Token![>]>()?;
return Ok(Self {
name: el_name,
key: None,
attributes,
is_static: false,
listeners,
children,
});
}
let name = Ident::parse_any(stream)?;
let name_str = name.to_string();
stream.parse::<Token![=]>()?;
if name_str.starts_with("on") {
let inner;
syn::braced!(inner in stream);
let toks = inner.parse::<Expr>()?;
let ty = AttrType::EventTokens(toks);
listeners.push(ElementAttr {
element_name: el_name.clone(),
name,
value: ty,
namespace: None,
})
} else {
match name_str.as_str() {
"style" => {}
"key" => {}
"classes" | "namespace" | "ref" | _ => {
let ty = if stream.peek(LitStr) {
let rawtext = stream.parse::<LitStr>().unwrap();
AttrType::BumpText(rawtext)
} else {
// like JSX, we expect raw expressions
let inner;
syn::braced!(inner in stream);
let toks = inner.parse::<Expr>()?;
AttrType::FieldTokens(toks)
};
attributes.push(ElementAttr {
element_name: el_name.clone(),
name,
value: ty,
namespace: None,
})
}
}
};
}
stream.parse::<Token![>]>()?;
// closing element
stream.parse::<Token![<]>()?;
stream.parse::<Token![/]>()?;
let close = Ident::parse_any(stream)?;
if close.to_string() != el_name.to_string() {
return Err(Error::new_spanned(
close,
"closing element does not match opening",
));
}
stream.parse::<Token![>]>()?;
// 'parsing: loop {
// // if stream.peek(Token![>]) {}
// // // [1] Break if empty
// // if content.is_empty() {
// // break 'parsing;
// // }
// if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
// parse_element_body(
// &content,
// &mut attributes,
// &mut listeners,
// &mut key,
// name.clone(),
// )?;
// } else {
// children.push(stream.parse::<BodyNode<AS_HTML>>()?);
// }
// }
Ok(Self {
key,
name: el_name,
attributes,
children,
listeners,
is_static: false,
})
}
}
impl<const AS: HTML_OR_RSX> ToTokens for Element<AS> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
let attr = &self.attributes;
let childs = &self.children;
let listeners = &self.listeners;
let key = match &self.key {
Some(ty) => quote! { Some(format_args_f!(#ty)) },
None => quote! { None },
};
tokens.append_all(quote! {
__cx.element(
dioxus_elements::#name,
[ #(#listeners),* ],
[ #(#attr),* ],
[ #(#childs),* ],
#key,
)
});
}
}
/// =======================================
/// Parse a VElement's Attributes
/// =======================================
struct ElementAttr {
struct ElementAttr<const AS: HTML_OR_RSX> {
element_name: Ident,
name: Ident,
value: AttrType,
@@ -108,14 +232,14 @@ enum AttrType {
// We parse attributes and dump them into the attribute vec
// This is because some tags might be namespaced (IE style)
// These dedicated tags produce multiple name-spaced attributes
fn parse_element_body(
fn parse_rsx_element_field(
stream: ParseStream,
attrs: &mut Vec<ElementAttr>,
listeners: &mut Vec<ElementAttr>,
key: &mut Option<AttrType>,
attrs: &mut Vec<ElementAttr<AS_RSX>>,
listeners: &mut Vec<ElementAttr<AS_RSX>>,
key: &mut Option<LitStr>,
element_name: Ident,
) -> Result<()> {
let mut name = Ident::parse_any(stream)?;
let name = Ident::parse_any(stream)?;
let name_str = name.to_string();
stream.parse::<Token![:]>()?;
@@ -178,7 +302,7 @@ fn parse_element_body(
return Ok(());
}
"key" => {
*key = Some(AttrType::BumpText(stream.parse::<LitStr>()?));
*key = Some(stream.parse::<LitStr>()?);
return Ok(());
}
"classes" => {
@@ -218,7 +342,7 @@ fn parse_element_body(
Ok(())
}
impl ToTokens for ElementAttr {
impl<const AS: HTML_OR_RSX> ToTokens for ElementAttr<AS> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let el_name = &self.element_name;
let name_str = self.name.to_string();
+27 -6
View File
@@ -10,7 +10,7 @@
use syn::parse::ParseBuffer;
use super::AmbiguousElement;
use super::{AmbiguousElement, AS_HTML, AS_RSX, HTML_OR_RSX};
use {
proc_macro::TokenStream,
@@ -23,10 +23,11 @@ use {
},
};
pub struct Fragment {
children: Vec<AmbiguousElement>,
pub struct Fragment<const AS: HTML_OR_RSX> {
children: Vec<AmbiguousElement<AS>>,
}
impl Parse for Fragment {
impl Parse for Fragment<AS_RSX> {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<Ident>()?;
@@ -36,7 +37,7 @@ impl Parse for Fragment {
let content: ParseBuffer;
syn::braced!(content in input);
while !content.is_empty() {
content.parse::<AmbiguousElement>()?;
content.parse::<AmbiguousElement<AS_RSX>>()?;
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
@@ -46,7 +47,27 @@ impl Parse for Fragment {
}
}
impl ToTokens for Fragment {
impl Parse for Fragment<AS_HTML> {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<Ident>()?;
let children = Vec::new();
// parse the guts
let content: ParseBuffer;
syn::braced!(content in input);
while !content.is_empty() {
content.parse::<AmbiguousElement<AS_HTML>>()?;
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
}
}
Ok(Self { children })
}
}
impl<const AS: HTML_OR_RSX> ToTokens for Fragment<AS> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let childs = &self.children;
let children = quote! {
+5 -89
View File
@@ -12,6 +12,7 @@
//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful.
mod ambiguous;
mod body;
mod component;
mod element;
mod fragment;
@@ -19,97 +20,12 @@ mod node;
// Re-export the namespaces into each other
pub use ambiguous::*;
pub use body::*;
pub use component::*;
pub use element::*;
pub use fragment::*;
pub use node::*;
use crate::util::is_valid_tag;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
Error, Ident, LitStr, Result, Token,
};
pub struct RsxBody {
custom_context: Option<Ident>,
roots: Vec<BodyNode>,
}
impl Parse for RsxBody {
fn parse(input: ParseStream) -> Result<Self> {
// if input.peek(LitStr) {
// return input.parse::<LitStr>()?.parse::<RsxRender>();
// }
// try to parse the first ident and comma
let custom_context =
if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) {
let _ = input.parse::<Token![in]>()?;
let name = input.parse::<Ident>()?;
if is_valid_tag(&name.to_string()) {
return Err(Error::new(
input.span(),
"Custom context cannot be an html element name",
));
} else {
input.parse::<Token![,]>().unwrap();
Some(name)
}
} else {
None
};
let mut body = Vec::new();
let mut children = Vec::new();
let mut manual_props = None;
parse_component_body(
input,
&BodyParseConfig {
allow_children: true,
allow_fields: false,
allow_manual_props: false,
},
&mut body,
&mut children,
&mut manual_props,
)?;
Ok(Self {
roots: children,
custom_context,
})
}
}
impl ToTokens for RsxBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let inner = if self.roots.len() == 1 {
let inner = &self.roots[0];
quote! {#inner}
} else {
let childs = &self.roots;
quote! { __cx.fragment_from_iter([ #(#childs),* ]) }
};
match &self.custom_context {
// The `in cx` pattern allows directly rendering
Some(ident) => out_tokens.append_all(quote! {
#ident.render(dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{
use dioxus_elements::GlobalAttributes;
#inner
}))
}),
// Otherwise we just build the LazyNode wrapper
None => out_tokens.append_all(quote! {
dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{
use dioxus_elements::GlobalAttributes;
#inner
})
}),
};
}
}
pub type HTML_OR_RSX = bool;
pub const AS_HTML: bool = true;
pub const AS_RSX: bool = false;
+27 -5
View File
@@ -10,13 +10,13 @@ use syn::{
// ==============================================
// Parse any div {} as a VElement
// ==============================================
pub enum BodyNode {
Element(AmbiguousElement),
pub enum BodyNode<const AS: HTML_OR_RSX> {
Element(AmbiguousElement<AS>),
Text(TextNode),
RawExpr(Expr),
}
impl Parse for BodyNode {
impl Parse for BodyNode<AS_RSX> {
fn parse(stream: ParseStream) -> Result<Self> {
// Supposedly this approach is discouraged due to inability to return proper errors
// TODO: Rework this to provide more informative errors
@@ -31,11 +31,33 @@ impl Parse for BodyNode {
return Ok(BodyNode::Text(stream.parse::<TextNode>()?));
}
Ok(BodyNode::Element(stream.parse::<AmbiguousElement>()?))
Ok(BodyNode::Element(
stream.parse::<AmbiguousElement<AS_RSX>>()?,
))
}
}
impl Parse for BodyNode<AS_HTML> {
fn parse(stream: ParseStream) -> Result<Self> {
// Supposedly this approach is discouraged due to inability to return proper errors
// TODO: Rework this to provide more informative errors
if stream.peek(token::Brace) {
let content;
syn::braced!(content in stream);
return Ok(BodyNode::RawExpr(content.parse::<Expr>()?));
}
if stream.peek(LitStr) {
return Ok(BodyNode::Text(stream.parse::<TextNode>()?));
}
Ok(BodyNode::Element(
stream.parse::<AmbiguousElement<AS_HTML>>()?,
))
}
}
impl ToTokens for BodyNode {
impl<const AS: HTML_OR_RSX> ToTokens for BodyNode<AS> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match &self {
BodyNode::Element(el) => el.to_tokens(tokens),
-179
View File
@@ -1,179 +0,0 @@
// use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use std::collections::hash_set::HashSet;
use syn::{parse::ParseBuffer, Expr};
pub fn try_parse_bracketed(stream: &ParseBuffer) -> syn::Result<Expr> {
let content;
syn::braced!(content in stream);
content.parse()
}
/// rsx! and html! macros support the html namespace as well as svg namespace
static HTML_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
"a",
"abbr",
"address",
"area",
"article",
"aside",
"audio",
"b",
"base",
"bdi",
"bdo",
"big",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"cite",
"code",
"col",
"colgroup",
"command",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"div",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"head",
"header",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"keygen",
"label",
"legend",
"li",
"link",
"main",
"map",
"mark",
"menu",
"menuitem",
"meta",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"param",
"picture",
"pre",
"progress",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"script",
"section",
"select",
"small",
"source",
"span",
"strong",
"style",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"track",
"u",
"ul",
"var",
"video",
"wbr",
]
.iter()
.cloned()
.collect()
});
static SVG_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
// SVTG
"svg", "path", "g", "text",
]
.iter()
.cloned()
.collect()
});
// these tags are reserved by dioxus for any reason
// They might not all be used
static RESERVED_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
// a fragment
"fragment",
]
.iter()
.cloned()
.collect()
});
/// Whether or not this tag is valid
///
/// ```
/// use html_validation::is_valid_tag;
///
/// assert_eq!(is_valid_tag("br"), true);
///
/// assert_eq!(is_valid_tag("random"), false);
/// ```
pub fn is_valid_tag(tag: &str) -> bool {
is_valid_html_tag(tag) || is_valid_svg_tag(tag) || is_valid_reserved_tag(tag)
}
pub fn is_valid_html_tag(tag: &str) -> bool {
HTML_TAGS.contains(tag)
}
pub fn is_valid_svg_tag(tag: &str) -> bool {
SVG_TAGS.contains(tag)
}
pub fn is_valid_reserved_tag(tag: &str) -> bool {
RESERVED_TAGS.contains(tag)
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1 -1
View File
@@ -1,3 +1,3 @@
{
"rust-analyzer.inlayHints.enable": false
"rust-analyzer.inlayHints.enable": true
}
+12 -4
View File
@@ -31,15 +31,23 @@ log = "0.4"
# # Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
# Backs scopes and unique keys
slotmap = "1.0.3"
appendlist = "1.4.0"
futures-util = "0.3.15"
smallvec = "1.6.1"
slab = "0.4.3"
futures-channel = "0.3.16"
[dev-dependencies]
anyhow = "1.0.42"
async-std = { version = "1.9.0", features = ["attributes"] }
dioxus-html = { path = "../html" }
[features]
default = ["serialize"]
serialize = ["slotmap/serde", "serde"]
serialize = ["serde"]
+6 -4
View File
@@ -2,18 +2,18 @@
This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website.
We reserve the "dioxus" name and aggregate all the various renderers under it. If you want just a single dioxus renderer, then chose from "dioxus-web", "dioxus-desktop", etc.
To build new apps with Dioxus or to extend the ecosystem with new hooks or components, use the `Dioxus` crate with the appropriate feature flags.
## Internals
Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts:
- React: hooks, concurrency, suspense
- Dodrio: bump allocation, double buffering, and source code for NodeBuilder
- Dodrio: bump allocation, double buffering, and some diffing architecture
- Percy: html! macro architecture, platform-agnostic edits
- Yew: passion and inspiration ❤️
- InfernoJS: approach to fragments and node diffing
- InfernoJS: approach to keyed diffing
- Preact: approach for normalization and ref
- Yew: passion and inspiration ❤️
Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. Some unique features include:
@@ -21,6 +21,8 @@ Dioxus-core leverages some really cool techniques and hits a very high level of
- suspended nodes (task/fiber endpoints) for asynchronous vnodes
- custom memory allocator for vnodes and all text content
- support for fragments w/ lazy normalization
- slab allocator for scopes
- mirrored-slab approach for remote vdoms
There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, it is likely that zero allocations will need to be performed once the app has been mounted. Only when new components are added to the dom will allocations occur - and only en mass. The space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components.
+2 -4
View File
@@ -1,9 +1,7 @@
fn main() {}
use dioxus::*;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
fn main() {}
pub static Example: FC<()> = |cx| {
let list = (0..10).map(|f| LazyNodes::new(move |f| todo!()));
+2 -2
View File
@@ -21,7 +21,7 @@ const App: FC<()> = |cx| {
};
const Task: FC<()> = |cx| {
let (task, res) = cx.use_task(|| async { true });
let (task, res) = use_task(cx, || async { true });
// task.pause();
// task.restart();
// task.stop();
@@ -29,7 +29,7 @@ const Task: FC<()> = |cx| {
//
let _s = cx.use_task(|| async { "hello world".to_string() });
let _s = use_task(cx, || async { "hello world".to_string() });
todo!()
};
+1
View File
@@ -67,6 +67,7 @@ impl PartialEq for ChildProps {
}
impl Properties for ChildProps {
type Builder = ();
const IS_STATIC: bool = true;
fn builder() -> Self::Builder {
()
}
+2 -1
View File
@@ -4,7 +4,8 @@ use dioxus_core::prelude::*;
fn App(cx: Context<()>) -> DomTree {
//
let vak = cx.use_suspense(
let vak = use_suspense(
cx,
|| async {},
|c, res| {
//
+150 -41
View File
@@ -1,53 +1,118 @@
use std::cell::{RefCell, RefMut};
use std::fmt::Display;
use std::{cell::UnsafeCell, rc::Rc};
use crate::heuristics::*;
use crate::innerlude::*;
use slotmap::SlotMap;
use futures_util::stream::FuturesUnordered;
use fxhash::{FxHashMap, FxHashSet};
use slab::Slab;
use smallvec::SmallVec;
// slotmap::new_key_type! {
// // A dedicated key type for the all the scopes
// pub struct ScopeId;
// }
// #[cfg(feature = "serialize", serde::Serialize)]
// #[cfg(feature = "serialize", serde::Serialize)]
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ScopeId(pub usize);
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ElementId(pub usize);
impl Display for ElementId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl ElementId {
pub fn as_u64(self) -> u64 {
self.0 as u64
}
}
type Shared<T> = Rc<RefCell<T>>;
type TaskReceiver = futures_channel::mpsc::UnboundedReceiver<EventTrigger>;
type TaskSender = futures_channel::mpsc::UnboundedSender<EventTrigger>;
/// These are resources shared among all the components and the virtualdom itself
#[derive(Clone)]
pub struct SharedArena {
pub components: Rc<UnsafeCell<ScopeMap>>,
}
pub type ScopeMap = SlotMap<ScopeId, Scope>;
pub struct SharedResources {
pub components: Rc<UnsafeCell<Slab<Scope>>>,
enum MutStatus {
Immut,
Mut,
pub(crate) heuristics: Shared<HeuristicsEngine>,
///
pub task_sender: TaskSender,
pub task_receiver: Shared<TaskReceiver>,
pub async_tasks: Shared<FuturesUnordered<FiberTask>>,
/// We use a SlotSet to keep track of the keys that are currently being used.
/// However, we don't store any specific data since the "mirror"
pub raw_elements: Rc<RefCell<Slab<()>>>,
pub task_setter: Rc<dyn Fn(ScopeId)>,
}
impl SharedArena {
pub fn new(arena: ScopeMap) -> Self {
let components = Rc::new(UnsafeCell::new(arena));
SharedArena { components }
impl SharedResources {
pub fn new() -> Self {
// preallocate 2000 elements and 20 scopes to avoid dynamic allocation
let components: Rc<UnsafeCell<Slab<Scope>>> =
Rc::new(UnsafeCell::new(Slab::with_capacity(100)));
// elements are super cheap - the value takes no space
let raw_elements = Slab::with_capacity(2000);
let (sender, receiver) = futures_channel::mpsc::unbounded();
let heuristics = HeuristicsEngine::new();
// we allocate this task setter once to save us from having to allocate later
let task_setter = {
let queue = sender.clone();
let components = components.clone();
Rc::new(move |idx: ScopeId| {
let comps = unsafe { &*components.get() };
if let Some(scope) = comps.get(idx.0) {
queue
.unbounded_send(EventTrigger::new(
VirtualEvent::ScheduledUpdate {
height: scope.height,
},
idx,
None,
EventPriority::High,
))
.expect("The event queu receiver should *never* be dropped");
}
}) as Rc<dyn Fn(ScopeId)>
};
Self {
components,
async_tasks: Rc::new(RefCell::new(FuturesUnordered::new())),
task_receiver: Rc::new(RefCell::new(receiver)),
task_sender: sender,
heuristics: Rc::new(RefCell::new(heuristics)),
raw_elements: Rc::new(RefCell::new(raw_elements)),
task_setter,
}
}
/// THIS METHOD IS CURRENTLY UNSAFE
/// THERE ARE NO CHECKS TO VERIFY THAT WE ARE ALLOWED TO DO THIS
pub fn get(&self, idx: ScopeId) -> Option<&Scope> {
/// this is unsafe because the caller needs to track which other scopes it's already using
pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> {
let inner = unsafe { &*self.components.get() };
inner.get(idx)
inner.get(idx.0)
}
/// THIS METHOD IS CURRENTLY UNSAFE
/// THERE ARE NO CHECKS TO VERIFY THAT WE ARE ALLOWED TO DO THIS
pub fn get_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
/// this is unsafe because the caller needs to track which other scopes it's already using
pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
let inner = unsafe { &mut *self.components.get() };
inner.get_mut(idx)
}
fn inner(&self) -> &ScopeMap {
todo!()
}
fn inner_mut(&mut self) -> &mut ScopeMap {
todo!()
}
/// THIS METHOD IS CURRENTLY UNSAFE
/// THERE ARE NO CHECKS TO VERIFY THAT WE ARE ALLOWED TO DO THIS
pub fn with<T>(&self, f: impl FnOnce(&mut ScopeMap) -> T) -> Result<T> {
let inner = unsafe { &mut *self.components.get() };
Ok(f(inner))
// todo!()
inner.get_mut(idx.0)
}
pub fn with_scope<'b, O: 'static>(
@@ -70,12 +135,56 @@ impl SharedArena {
pub fn try_remove(&self, id: ScopeId) -> Result<Scope> {
let inner = unsafe { &mut *self.components.get() };
inner
.remove(id)
.ok_or_else(|| Error::FatalInternal("Scope not found"))
Ok(inner.remove(id.0))
// .try_remove(id.0)
// .ok_or_else(|| Error::FatalInternal("Scope not found"))
}
unsafe fn inner_unchecked<'s>() -> &'s mut ScopeMap {
todo!()
pub fn reserve_node(&self) -> ElementId {
ElementId(self.raw_elements.borrow_mut().insert(()))
}
/// return the id, freeing the space of the original node
pub fn collect_garbage(&self, id: ElementId) {
self.raw_elements.borrow_mut().remove(id.0);
}
pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId {
let g = unsafe { &mut *self.components.get() };
let entry = g.vacant_entry();
let id = ScopeId(entry.key());
entry.insert(f(id));
id
}
pub fn schedule_update(&self) -> Rc<dyn Fn(ScopeId)> {
self.task_setter.clone()
}
pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
self.async_tasks.borrow_mut().push(task);
TaskHandle {}
}
pub fn make_trigger_key(&self, trigger: &EventTrigger) -> EventKey {
let height = self
.get_scope(trigger.originator)
.map(|f| f.height)
.unwrap();
EventKey {
height,
originator: trigger.originator,
priority: trigger.priority,
}
}
}
pub struct TaskHandle {}
impl TaskHandle {
pub fn toggle(&self) {}
pub fn start(&self) {}
pub fn stop(&self) {}
pub fn restart(&self) {}
}
+75 -47
View File
@@ -1,10 +1,10 @@
use crate::innerlude::*;
use bumpalo::Bump;
use std::cell::Cell;
use std::cell::RefCell;
pub struct ActiveFrame {
// We use a "generation" for users of contents in the bump frames to ensure their data isn't broken
pub generation: RefCell<usize>,
pub generation: Cell<usize>,
// The double-buffering situation that we will use
pub frames: [BumpFrame; 2],
@@ -12,79 +12,107 @@ pub struct ActiveFrame {
pub struct BumpFrame {
pub bump: Bump,
pub head_node: VNode<'static>,
pub(crate) head_node: VNode<'static>,
// used internally for debugging
_name: &'static str,
}
impl ActiveFrame {
pub fn new() -> Self {
Self::from_frames(
BumpFrame {
bump: Bump::new(),
head_node: NodeFactory::static_text(""),
},
BumpFrame {
bump: Bump::new(),
head_node: NodeFactory::static_text(""),
},
)
}
pub fn from_frames(a: BumpFrame, b: BumpFrame) -> Self {
let frame_a = BumpFrame {
bump: Bump::new(),
head_node: NodeFactory::unstable_place_holder(),
_name: "wip",
};
let frame_b = BumpFrame {
bump: Bump::new(),
head_node: NodeFactory::unstable_place_holder(),
_name: "fin",
};
Self {
generation: 0.into(),
frames: [a, b],
frames: [frame_a, frame_b],
}
}
pub fn cur_frame(&self) -> &BumpFrame {
match *self.generation.borrow() & 1 == 0 {
pub unsafe fn reset_wip_frame(&mut self) {
self.wip_frame_mut().bump.reset()
}
pub fn update_head_node<'a>(&mut self, node: VNode<'a>) {
self.wip_frame_mut().head_node = unsafe { std::mem::transmute(node) };
}
/// The "work in progress frame" represents the frame that is currently being worked on.
pub fn wip_frame(&self) -> &BumpFrame {
match self.generation.get() & 1 == 0 {
true => &self.frames[0],
false => &self.frames[1],
}
}
pub fn cur_frame_mut(&mut self) -> &mut BumpFrame {
match *self.generation.borrow() & 1 == 0 {
pub fn wip_frame_mut(&mut self) -> &mut BumpFrame {
match self.generation.get() & 1 == 0 {
true => &mut self.frames[0],
false => &mut self.frames[1],
}
}
pub fn current_head_node<'b>(&'b self) -> &'b VNode<'b> {
let raw_node = match *self.generation.borrow() & 1 == 0 {
/// The finished frame represents the frame that has been "finished" and cannot be modified again
pub fn finished_frame(&self) -> &BumpFrame {
match self.generation.get() & 1 == 1 {
true => &self.frames[0],
false => &self.frames[1],
};
// Give out our self-referential item with our own borrowed lifetime
unsafe {
let unsafe_head = &raw_node.head_node;
let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head);
safe_node
}
}
pub fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> {
let raw_node = match *self.generation.borrow() & 1 != 0 {
true => &self.frames[0],
false => &self.frames[1],
};
// Give out our self-referential item with our own borrowed lifetime
unsafe {
let unsafe_head = &raw_node.head_node;
let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head);
safe_node
pub fn finished_frame_mut(&mut self) -> &mut BumpFrame {
match self.generation.get() & 1 == 1 {
true => &mut self.frames[0],
false => &mut self.frames[1],
}
}
/// Give out our self-referential item with our own borrowed lifetime
pub fn fin_head<'b>(&'b self) -> &'b VNode<'b> {
let cur_head = &self.finished_frame().head_node;
unsafe { std::mem::transmute::<&VNode<'static>, &VNode<'b>>(cur_head) }
}
pub fn old_frame_mut(&mut self) -> &mut BumpFrame {
match *self.generation.borrow() & 1 == 0 {
true => &mut self.frames[1],
false => &mut self.frames[0],
}
/// Give out our self-referential item with our own borrowed lifetime
pub fn wip_head<'b>(&'b self) -> &'b VNode<'b> {
let cur_head = &self.wip_frame().head_node;
unsafe { std::mem::transmute::<&VNode<'static>, &VNode<'b>>(cur_head) }
}
pub fn cycle_frame(&mut self) {
*self.generation.borrow_mut() += 1;
self.generation.set(self.generation.get() + 1);
}
}
#[cfg(test)]
mod tests {
//! These tests are bad. I don't have a good way of properly testing the ActiveFrame stuff
use super::*;
#[test]
fn test_bump_frame() {
let mut frames = ActiveFrame::new();
// just cycle a few times and make sure we get the right frames out
for _ in 0..5 {
let fin = frames.finished_frame();
let wip = frames.wip_frame();
assert_eq!(wip._name, "wip");
assert_eq!(fin._name, "fin");
frames.cycle_frame();
let fin = frames.finished_frame();
let wip = frames.wip_frame();
assert_eq!(wip._name, "fin");
assert_eq!(fin._name, "wip");
frames.cycle_frame();
}
assert_eq!(frames.generation.get(), 10);
}
}
-91
View File
@@ -1,91 +0,0 @@
/// This iterator iterates through a list of virtual children and only returns real children (Elements or Text).
///
/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like
/// "InsertBefore".
struct RealChildIterator<'a> {
scopes: &'a SharedArena,
// Heuristcally we should never bleed into 5 completely nested fragments/components
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
}
impl<'a> RealChildIterator<'a> {
fn new(starter: &'a VNode<'a>, scopes: &'a SharedArena) -> Self {
Self {
scopes,
stack: smallvec::smallvec![(0, starter)],
}
}
}
impl<'a> Iterator for RealChildIterator<'a> {
type Item = &'a VNode<'a>;
fn next(&mut self) -> Option<&'a VNode<'a>> {
let mut should_pop = false;
let mut returned_node = None;
let mut should_push = None;
while returned_node.is_none() {
if let Some((count, node)) = self.stack.last_mut() {
match node {
// We can only exit our looping when we get "real" nodes
// This includes fragments and components when they're empty (have a single root)
VNode::Element(_) | VNode::Text(_) => {
// We've recursed INTO an element/text
// We need to recurse *out* of it and move forward to the next
should_pop = true;
returned_node = Some(&**node);
}
// If we get a fragment we push the next child
VNode::Fragment(frag) => {
let subcount = *count as usize;
if frag.children.len() == 0 {
should_pop = true;
returned_node = Some(&**node);
}
if subcount >= frag.children.len() {
should_pop = true;
} else {
should_push = Some(&frag.children[subcount]);
}
}
// Immediately abort suspended nodes - can't do anything with them yet
// VNode::Suspended => should_pop = true,
VNode::Suspended { real } => todo!(),
// For components, we load their root and push them onto the stack
VNode::Component(sc) => {
let scope = self.scopes.try_get(sc.ass_scope.get().unwrap()).unwrap();
// Simply swap the current node on the stack with the root of the component
*node = scope.root();
}
}
} else {
// If there's no more items on the stack, we're done!
return None;
}
if should_pop {
self.stack.pop();
if let Some((id, _)) = self.stack.last_mut() {
*id += 1;
}
should_pop = false;
}
if let Some(push) = should_push {
self.stack.push((0, push));
should_push = None;
}
}
returned_node
}
}
+44 -2
View File
@@ -5,11 +5,43 @@
//! if the type suppports PartialEq. The Properties trait is used by the rsx! and html! macros to generate the type-safe builder
//! that ensures compile-time required and optional fields on cx.
use crate::innerlude::{Context, DomTree, LazyNodes, VNode, FC};
use crate::innerlude::{Context, DomTree, LazyNodes, FC};
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus
/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the
/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe.
///
/// If your props are 'static, then Dioxus will require that they also be PartialEq for the derived memoize strategy. However,
/// if your props borrow data, then the memoization strategy will simply default to "false" and the PartialEq will be ignored.
/// This tends to be useful when props borrow something that simply cannot be compared (IE a reference to a closure);
///
/// By default, the memoization strategy is very conservative, but can be tuned to be more aggressive manually. However,
/// this is only safe if the props are 'static - otherwise you might borrow references after-free.
///
/// We strongly suggest that any changes to memoization be done at the "PartialEq" level for 'static props. Additionally,
/// we advise the use of smart pointers in cases where memoization is important.
///
/// ## Example
///
/// For props that are 'static:
/// ```rust ignore
/// #[derive(Props, PartialEq)]
/// struct MyProps {
/// data: String
/// }
/// ```
///
/// For props that borrow:
///
/// ```rust ignore
/// #[derive(Props)]
/// struct MyProps<'a >{
/// data: &'a str
/// }
/// ```
pub trait Properties: Sized {
type Builder;
const IS_STATIC: bool = false;
const IS_STATIC: bool;
fn builder() -> Self::Builder;
/// Memoization can only happen if the props are 'static
@@ -50,6 +82,16 @@ pub fn fc_to_builder<T: Properties>(_: FC<T>) -> T::Builder {
///
/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
/// Try to avoid nesting fragments if you can. Infinitely nested Fragments *will* cause diffing to crash.
///
/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
///
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
///
/// ```rust
/// rsx!{
/// Fragment { key: "abc" }
/// }
/// ```
#[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>(cx: Context<'a, ()>) -> DomTree<'a> {
cx.render(LazyNodes::new(move |f| f.fragment_from_iter(cx.children())))
+104 -352
View File
@@ -1,25 +1,24 @@
use crate::innerlude::*;
//! Public APIs for managing component state, tasks, and lifecycles.
use futures_util::FutureExt;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
future::Future,
marker::PhantomData,
ops::Deref,
rc::{Rc, Weak},
};
use crate::innerlude::*;
use std::{any::TypeId, ops::Deref, rc::Rc};
/// Components in Dioxus use the "Context" object to interact with their lifecycle.
/// This lets components schedule updates, integrate hooks, and expose their context via the context api.
///
/// Properties passed down from the parent component are also directly accessible via the exposed "props" field.
/// This lets components access props, schedule updates, integrate hooks, and expose shared state.
///
/// Note: all of these methods are *imperative* - they do not act as hooks! They are meant to be used by hooks
/// to provide complex behavior. For instance, calling "add_shared_state" on every render is considered a leak. This method
/// exists for the `use_provide_state` hook to provide a shared state object.
///
/// For the most part, the only method you should be using regularly is `render`.
///
/// ## Example
///
/// ```ignore
/// #[derive(Properties)]
/// struct Props {
/// name: String
///
/// }
///
/// fn example(cx: Context<Props>) -> VNode {
@@ -28,16 +27,6 @@ use std::{
/// }
/// }
/// ```
///
/// ## Available Methods:
/// - render
/// - use_hook
/// - use_task
/// - use_suspense
/// - submit_task
/// - children
/// - use_effect
///
pub struct Context<'src, T> {
pub props: &'src T,
pub scope: &'src Scope,
@@ -98,13 +87,22 @@ impl<'src, P> Context<'src, P> {
/// })
/// }
/// ```
pub fn children(&self) -> &'src [VNode<'src>] {
self.scope.child_nodes
pub fn children(&self) -> ScopeChildren<'src> {
self.scope.child_nodes()
}
/// Create a subscription that schedules a future render for the reference component
///
/// ## Notice: you should prefer using prepare_update and get_scope_id
///
pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
self.scope.event_channel.clone()
let cb = self.scope.vdom.schedule_update();
let id = self.get_scope_id();
Rc::new(move || cb(id))
}
pub fn prepare_update(&self) -> Rc<dyn Fn(ScopeId)> {
self.scope.vdom.schedule_update()
}
pub fn schedule_effect(&self) -> Rc<dyn Fn() + 'static> {
@@ -115,7 +113,8 @@ impl<'src, P> Context<'src, P> {
todo!()
}
/// Get's this context's ScopeId
/// Get's this component's unique identifier.
///
pub fn get_scope_id(&self) -> ScopeId {
self.scope.our_arena_idx.clone()
}
@@ -139,16 +138,88 @@ impl<'src, P> Context<'src, P> {
self,
lazy_nodes: LazyNodes<'src, F>,
) -> DomTree<'src> {
let scope_ref = self.scope;
let listener_id = &scope_ref.listener_idx;
Some(lazy_nodes.into_vnode(NodeFactory {
scope_ref,
listener_id,
}))
let bump = &self.scope.frames.wip_frame().bump;
Some(lazy_nodes.into_vnode(NodeFactory { bump }))
}
/// `submit_task` will submit the future to be polled.
///
/// This is useful when you have some async task that needs to be progressed.
///
/// This method takes ownership over the task you've provided, and must return (). This means any work that needs to
/// happen must occur within the future or scheduled for after the future completes (through schedule_update )
///
/// ## Explanation
/// Dioxus will step its internal event loop if the future returns if the future completes while waiting.
///
/// Tasks can't return anything, but they can be controlled with the returned handle
///
/// Tasks will only run until the component renders again. Because `submit_task` is valid for the &'src lifetime, it
/// is considered "stable"
///
///
///
pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
self.scope.vdom.submit_task(task)
}
/// Add a state globally accessible to child components via tree walking
pub fn add_shared_state<T: 'static>(self, val: T) {
self.scope
.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(val))
.map(|_| {
log::warn!("A shared state was replaced with itself. This is does not result in a panic, but is probably not what you are trying to do");
});
}
/// Walk the tree to find a shared state with the TypeId of the generic type
///
pub fn consume_shared_state<T: 'static>(self) -> Option<Rc<T>> {
let mut scope = Some(self.scope);
let mut parent = None;
let ty = TypeId::of::<T>();
while let Some(inner) = scope {
log::debug!(
"Searching {:#?} for valid shared_context",
inner.our_arena_idx
);
let shared_ctx = {
let shared_contexts = inner.shared_contexts.borrow();
log::debug!(
"This component has {} shared contexts",
shared_contexts.len()
);
shared_contexts.get(&ty).map(|f| f.clone())
};
if let Some(shared_cx) = shared_ctx {
log::debug!("found matching cx");
let rc = shared_cx
.clone()
.downcast::<T>()
.expect("Should not fail, already validated the type from the hashmap");
parent = Some(rc);
break;
} else {
match inner.parent_idx {
Some(parent_id) => {
scope = unsafe { inner.vdom.get_scope(parent_id) };
}
None => break,
}
}
}
parent
}
/// Store a value between renders
///
/// This is *the* foundational hook for all other hooks.
///
/// - Initializer: closure used to create the initial hook state
/// - Runner: closure used to output a value every time the hook is used
/// - Cleanup: closure used to teardown the hook once the dom is cleaned up
@@ -192,323 +263,4 @@ Any function prefixed with "use" should not be called conditionally.
runner(self.scope.hooks.next::<State>().expect(ERR_MSG))
}
/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
///
/// This is a hook, so it may not be called conditionally!
///
/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
/// so don't put it in a conditional.
///
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
/// the context via Rc/Weak.
///
///
///
pub fn use_provide_context<T, F>(self, init: F) -> &'src Rc<T>
where
T: 'static,
F: FnOnce() -> T,
{
let ty = TypeId::of::<T>();
let contains_key = self.scope.shared_contexts.borrow().contains_key(&ty);
let is_initialized = self.use_hook(
|_| false,
|s| {
let i = s.clone();
*s = true;
i
},
|_| {},
);
match (is_initialized, contains_key) {
// Do nothing, already initialized and already exists
(true, true) => {}
// Needs to be initialized
(false, false) => {
log::debug!("Initializing context...");
let initialized = Rc::new(init());
let p = self
.scope
.shared_contexts
.borrow_mut()
.insert(ty, initialized);
log::info!(
"There are now {} shared contexts for scope {:?}",
self.scope.shared_contexts.borrow().len(),
self.scope.our_arena_idx,
);
}
_ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
};
self.use_context::<T>()
}
/// There are hooks going on here!
pub fn use_context<T: 'static>(self) -> &'src Rc<T> {
self.try_use_context().unwrap()
}
/// Uses a context, storing the cached value around
///
/// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a
/// context was added later. This allows using another hook as a fallback
///
pub fn try_use_context<T: 'static>(self) -> Option<&'src Rc<T>> {
struct UseContextHook<C> {
par: Option<Rc<C>>,
}
self.use_hook(
move |_| {
let mut scope = Some(self.scope);
let mut par = None;
let ty = TypeId::of::<T>();
while let Some(inner) = scope {
log::debug!(
"Searching {:#?} for valid shared_context",
inner.our_arena_idx
);
let shared_ctx = {
let shared_contexts = inner.shared_contexts.borrow();
log::debug!(
"This component has {} shared contexts",
shared_contexts.len()
);
shared_contexts.get(&ty).map(|f| f.clone())
};
if let Some(shared_cx) = shared_ctx {
log::debug!("found matching cx");
let rc = shared_cx
.clone()
.downcast::<T>()
.expect("Should not fail, already validated the type from the hashmap");
par = Some(rc);
break;
} else {
match inner.parent_idx {
Some(parent_id) => {
scope = inner.arena_link.get(parent_id);
}
None => break,
}
}
}
//
UseContextHook { par }
},
move |hook| hook.par.as_ref(),
|_| {},
)
}
/// `submit_task` will submit the future to be polled.
///
/// This is useful when you have some async task that needs to be progressed.
///
/// This method takes ownership over the task you've provided, and must return (). This means any work that needs to
/// happen must occur within the future or scheduled for after the future completes (through schedule_update )
///
/// ## Explanation
/// Dioxus will step its internal event loop if the future returns if the future completes while waiting.
///
/// Tasks can't return anything, but they can be controlled with the returned handle
///
/// Tasks will only run until the component renders again. Because `submit_task` is valid for the &'src lifetime, it
/// is considered "stable"
///
///
///
pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
(self.scope.task_submitter)(task);
TaskHandle { _p: PhantomData {} }
}
/// Awaits the given task, forcing the component to re-render when the value is ready.
///
///
///
///
pub fn use_task<Out, Fut, Init>(
self,
task_initializer: Init,
) -> (TaskHandle<'src>, &'src Option<Out>)
where
Out: 'static,
Fut: Future<Output = Out> + 'static,
Init: FnOnce() -> Fut + 'src,
{
struct TaskHook<T> {
task_dump: Rc<RefCell<Option<T>>>,
value: Option<T>,
}
// whenever the task is complete, save it into th
self.use_hook(
move |hook_idx| {
let task_fut = task_initializer();
let task_dump = Rc::new(RefCell::new(None));
let slot = task_dump.clone();
let update = self.schedule_update();
let originator = self.scope.our_arena_idx.clone();
self.submit_task(Box::pin(task_fut.then(move |output| async move {
*slot.as_ref().borrow_mut() = Some(output);
update();
EventTrigger {
event: VirtualEvent::AsyncEvent { hook_idx },
originator,
priority: EventPriority::Low,
real_node_id: None,
}
})));
TaskHook {
task_dump,
value: None,
}
},
|hook| {
if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() {
hook.value = Some(val);
}
(TaskHandle { _p: PhantomData }, &hook.value)
},
|_| {},
)
}
}
pub(crate) struct SuspenseHook {
pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
pub callback: SuspendedCallback,
pub dom_node_id: Rc<Cell<RealDomNode>>,
}
type SuspendedCallback = Box<dyn for<'a> Fn(SuspendedContext<'a>) -> DomTree<'a>>;
impl<'src, P> Context<'src, P> {
/// Asynchronously render new nodes once the given future has completed.
///
/// # Easda
///
///
///
///
/// # Example
///
///
pub fn use_suspense<Out, Fut, Cb>(
self,
task_initializer: impl FnOnce() -> Fut,
user_callback: Cb,
) -> DomTree<'src>
where
Fut: Future<Output = Out> + 'static,
Out: 'static,
Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> DomTree<'a> + 'static,
{
self.use_hook(
move |hook_idx| {
let value = Rc::new(RefCell::new(None));
let dom_node_id = Rc::new(RealDomNode::empty_cell());
let domnode = dom_node_id.clone();
let slot = value.clone();
let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| {
let v: std::cell::Ref<Option<Box<dyn Any>>> = slot.as_ref().borrow();
match v.as_ref() {
Some(a) => {
let v: &dyn Any = a.as_ref();
let real_val = v.downcast_ref::<Out>().unwrap();
user_callback(ctx, real_val)
}
None => {
//
Some(VNode {
dom_id: RealDomNode::empty_cell(),
key: None,
kind: VNodeKind::Suspended {
node: domnode.clone(),
},
})
}
}
});
let originator = self.scope.our_arena_idx.clone();
let task_fut = task_initializer();
let domnode = dom_node_id.clone();
let slot = value.clone();
self.submit_task(Box::pin(task_fut.then(move |output| async move {
// When the new value arrives, set the hooks internal slot
// Dioxus will call the user's callback to generate new nodes outside of the diffing system
*slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
EventTrigger {
event: VirtualEvent::SuspenseEvent { hook_idx, domnode },
originator,
priority: EventPriority::Low,
real_node_id: None,
}
})));
SuspenseHook {
value,
callback,
dom_node_id,
}
},
move |hook| {
let cx = Context {
scope: &self.scope,
props: &(),
};
let csx = SuspendedContext { inner: cx };
(&hook.callback)(csx)
},
|_| {},
)
}
}
pub struct SuspendedContext<'a> {
pub(crate) inner: Context<'a, ()>,
}
impl<'src> SuspendedContext<'src> {
pub fn render<F: FnOnce(NodeFactory<'src>) -> VNode<'src>>(
self,
lazy_nodes: LazyNodes<'src, F>,
) -> DomTree<'src> {
let scope_ref = self.inner.scope;
let listener_id = &scope_ref.listener_idx;
Some(lazy_nodes.into_vnode(NodeFactory {
scope_ref,
listener_id,
}))
}
}
#[derive(Clone, Copy)]
pub struct TaskHandle<'src> {
_p: PhantomData<&'src ()>,
}
impl<'src> TaskHandle<'src> {
pub fn toggle(&self) {}
pub fn start(&self) {}
pub fn stop(&self) {}
pub fn restart(&self) {}
}
+1062 -786
View File
File diff suppressed because it is too large. Load diff
+35 -124
View File
@@ -5,128 +5,7 @@
//!
//!
use crate::{innerlude::ScopeId, RealDomNode};
/// The `DomEditor` provides an imperative interface for the Diffing algorithm to plan out its changes.
///
/// However, the DomEditor only builds a change list - it does not apply them. In contrast with the "RealDom", the DomEditor
/// is cancellable and flushable. At any moment in time, Dioxus may choose to completely clear the edit list and start over.
///
/// This behavior is used in the cooperative scheduling algorithm.
pub struct DomEditor<'real, 'bump> {
pub edits: &'real mut Vec<DomEdit<'bump>>,
}
use DomEdit::*;
impl<'real, 'bump> DomEditor<'real, 'bump> {
pub fn new(edits: &'real mut Vec<DomEdit<'bump>>) -> Self {
Self { edits }
}
// Navigation
pub(crate) fn push(&mut self, root: RealDomNode) {
let id = root.as_u64();
self.edits.push(PushRoot { id });
}
#[inline]
pub(crate) fn pop(&mut self) {
self.edits.push(PopRoot {});
}
// Add Nodes to the dom
// add m nodes from the stack
#[inline]
pub(crate) fn append_children(&mut self, many: u32) {
self.edits.push(AppendChildren { many });
}
// replace the n-m node on the stack with the m nodes
// ends with the last element of the chain on the top of the stack
#[inline]
pub(crate) fn replace_with(&mut self, many: u32) {
self.edits.push(ReplaceWith { many });
}
// Remove Nodesfrom the dom
#[inline]
pub(crate) fn remove(&mut self) {
self.edits.push(Remove);
}
#[inline]
pub(crate) fn remove_all_children(&mut self) {
self.edits.push(RemoveAllChildren);
}
// Create
#[inline]
pub(crate) fn create_text_node(&mut self, text: &'bump str, id: RealDomNode) {
let id = id.as_u64();
self.edits.push(CreateTextNode { text, id });
}
pub(crate) fn create_element(
&mut self,
tag: &'static str,
ns: Option<&'static str>,
id: RealDomNode,
) {
let id = id.as_u64();
match ns {
Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }),
None => self.edits.push(CreateElement { id, tag }),
}
}
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
pub(crate) fn create_placeholder(&mut self, id: RealDomNode) {
let id = id.as_u64();
self.edits.push(CreatePlaceholder { id });
}
// events
pub(crate) fn new_event_listener(
&mut self,
event: &'static str,
scope: ScopeId,
element_id: usize,
realnode: RealDomNode,
) {
self.edits.push(NewEventListener {
scope,
event_name: event,
element_id,
mounted_node_id: realnode.as_u64(),
});
}
#[inline]
pub(crate) fn remove_event_listener(&mut self, event: &'static str) {
self.edits.push(RemoveEventListener { event });
}
// modify
#[inline]
pub(crate) fn set_text(&mut self, text: &'bump str) {
self.edits.push(SetText { text });
}
#[inline]
pub(crate) fn set_attribute(
&mut self,
field: &'static str,
value: &'bump str,
ns: Option<&'static str>,
) {
self.edits.push(SetAttribute { field, value, ns });
}
#[inline]
pub(crate) fn remove_attribute(&mut self, name: &'static str) {
self.edits.push(RemoveAttribute { name });
}
}
use crate::innerlude::ScopeId;
/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the
/// network or through FFI boundaries.
@@ -145,7 +24,17 @@ pub enum DomEdit<'bump> {
many: u32,
},
ReplaceWith {
many: u32,
// the first n elements
n: u32,
// the last m elements
m: u32,
},
InsertAfter {
n: u32,
},
InsertBefore {
n: u32,
},
Remove,
RemoveAllChildren,
@@ -169,7 +58,6 @@ pub enum DomEdit<'bump> {
event_name: &'static str,
scope: ScopeId,
mounted_node_id: u64,
element_id: usize,
},
RemoveEventListener {
event: &'static str,
@@ -186,3 +74,26 @@ pub enum DomEdit<'bump> {
name: &'static str,
},
}
impl DomEdit<'_> {
pub fn is(&self, id: &'static str) -> bool {
match self {
DomEdit::InsertAfter { .. } => id == "InsertAfter",
DomEdit::InsertBefore { .. } => id == "InsertBefore",
DomEdit::PushRoot { .. } => id == "PushRoot",
DomEdit::PopRoot => id == "PopRoot",
DomEdit::AppendChildren { .. } => id == "AppendChildren",
DomEdit::ReplaceWith { .. } => id == "ReplaceWith",
DomEdit::Remove => id == "Remove",
DomEdit::RemoveAllChildren => id == "RemoveAllChildren",
DomEdit::CreateTextNode { .. } => id == "CreateTextNode",
DomEdit::CreateElement { .. } => id == "CreateElement",
DomEdit::CreateElementNs { .. } => id == "CreateElementNs",
DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder",
DomEdit::NewEventListener { .. } => id == "NewEventListener",
DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener",
DomEdit::SetText { .. } => id == "SetText",
DomEdit::SetAttribute { .. } => id == "SetAttribute",
DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute",
}
}
}
+159 -61
View File
@@ -4,9 +4,15 @@
//! 3rd party renderers are responsible for converting their native events into these virtual event types. Events might
//! be heavy or need to interact through FFI, so the events themselves are designed to be lazy.
use std::{cell::Cell, rc::Rc};
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use crate::innerlude::{RealDomNode, ScopeId};
use crate::{
innerlude::{ElementId, ScopeId},
VNode,
};
#[derive(Debug)]
pub struct EventTrigger {
@@ -14,7 +20,7 @@ pub struct EventTrigger {
pub originator: ScopeId,
/// The optional real node associated with the trigger
pub real_node_id: Option<RealDomNode>,
pub real_node_id: Option<ElementId>,
/// The type of event
pub event: VirtualEvent,
@@ -23,14 +29,25 @@ pub struct EventTrigger {
pub priority: EventPriority,
}
impl EventTrigger {
pub fn new_from_task(originator: ScopeId, hook_idx: usize) -> Self {
Self {
originator,
event: VirtualEvent::AsyncEvent { hook_idx },
priority: EventPriority::Low,
real_node_id: None,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct EventKey {
/// The originator of the event trigger
pub originator: ScopeId,
/// The priority of the event
pub priority: EventPriority,
/// The height of the scope (used for ordering)
pub height: u32,
// TODO: add the time that the event was queued
}
impl PartialOrd for EventKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
todo!()
}
}
impl Ord for EventKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
todo!()
}
}
@@ -41,17 +58,18 @@ impl EventTrigger {
/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
///
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
pub enum EventPriority {
/// "Immediate" work will interrupt whatever work is currently being done and force its way through. This type of work
/// is typically reserved for small changes to single elements.
/// Garbage collection is a type of work than can be scheduled around other work, but must be completed in a specific
/// order. The GC must be run for a component before any other future work for that component is run. Otherwise,
/// we will leak slots in our slab.
///
/// The primary user of the "Immediate" priority is the `Signal` API which performs surgical mutations to the DOM.
Immediate,
/// Garbage collection mixes with the safety aspects of the virtualdom so it's very important to get it done before
/// other work.
GarbageCollection,
/// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work.
///
///
/// This is typically reserved for things like user interaction.
High,
@@ -72,7 +90,7 @@ impl EventTrigger {
pub fn new(
event: VirtualEvent,
scope: ScopeId,
mounted_dom_id: Option<RealDomNode>,
mounted_dom_id: Option<ElementId>,
priority: EventPriority,
) -> Self {
Self {
@@ -84,8 +102,47 @@ impl EventTrigger {
}
}
#[derive(Debug)]
pub enum VirtualEvent {
/// Generated during diffing to signal that a component's nodes to be given back
///
/// Typically has a high priority
///
/// If an event is scheduled for a component that has "garbage", that garabge will be cleaned up before the event can
/// be processed.
GarbageCollection,
/// A type of "immediate" event scheduled by components
///
/// Usually called through "set_state"
ScheduledUpdate {
height: u32,
},
// Whenever a task is ready (complete) Dioxus produces this "AsyncEvent"
//
// Async events don't necessarily propagate into a scope being ran. It's up to the event itself
// to force an update for itself.
//
// Most async events should have a low priority.
//
// This type exists for the task/concurrency system to signal that a task is ready.
// However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its
// own re-run.
AsyncEvent {
should_rerender: bool,
},
// Suspense events are a type of async event generated when suspended nodes are ready to be processed.
//
// they have the lowest priority
SuspenseEvent {
hook_idx: usize,
domnode: Rc<Cell<Option<ElementId>>>,
},
// image event has conflicting method types
// ImageEvent(event_data::ImageEvent),
// Real events
ClipboardEvent(on::ClipboardEvent),
CompositionEvent(on::CompositionEvent),
@@ -102,25 +159,60 @@ pub enum VirtualEvent {
ToggleEvent(on::ToggleEvent),
MouseEvent(on::MouseEvent),
PointerEvent(on::PointerEvent),
}
impl VirtualEvent {
pub fn is_input_event(&self) -> bool {
match self {
VirtualEvent::ClipboardEvent(_)
| VirtualEvent::CompositionEvent(_)
| VirtualEvent::KeyboardEvent(_)
| VirtualEvent::FocusEvent(_)
| VirtualEvent::FormEvent(_)
| VirtualEvent::SelectionEvent(_)
| VirtualEvent::TouchEvent(_)
| VirtualEvent::UIEvent(_)
| VirtualEvent::WheelEvent(_)
| VirtualEvent::MediaEvent(_)
| VirtualEvent::AnimationEvent(_)
| VirtualEvent::TransitionEvent(_)
| VirtualEvent::ToggleEvent(_)
| VirtualEvent::MouseEvent(_)
| VirtualEvent::PointerEvent(_) => true,
// image event has conflicting method types
// ImageEvent(event_data::ImageEvent),
VirtualEvent::GarbageCollection
| VirtualEvent::ScheduledUpdate { .. }
| VirtualEvent::AsyncEvent { .. }
| VirtualEvent::SuspenseEvent { .. } => false,
}
}
}
// Whenever a task is ready (complete) Dioxus produces this "AsyncEvent"
//
// Async events don't necessarily propagate into a scope being ran. It's up to the event itself
// to force an update for itself.
AsyncEvent {
hook_idx: usize,
},
impl std::fmt::Debug for VirtualEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
VirtualEvent::ClipboardEvent(_) => "ClipboardEvent",
VirtualEvent::CompositionEvent(_) => "CompositionEvent",
VirtualEvent::KeyboardEvent(_) => "KeyboardEvent",
VirtualEvent::FocusEvent(_) => "FocusEvent",
VirtualEvent::FormEvent(_) => "FormEvent",
VirtualEvent::SelectionEvent(_) => "SelectionEvent",
VirtualEvent::TouchEvent(_) => "TouchEvent",
VirtualEvent::UIEvent(_) => "UIEvent",
VirtualEvent::WheelEvent(_) => "WheelEvent",
VirtualEvent::MediaEvent(_) => "MediaEvent",
VirtualEvent::AnimationEvent(_) => "AnimationEvent",
VirtualEvent::TransitionEvent(_) => "TransitionEvent",
VirtualEvent::ToggleEvent(_) => "ToggleEvent",
VirtualEvent::MouseEvent(_) => "MouseEvent",
VirtualEvent::PointerEvent(_) => "PointerEvent",
VirtualEvent::GarbageCollection => "GarbageCollection",
VirtualEvent::ScheduledUpdate { .. } => "SetStateEvent",
VirtualEvent::AsyncEvent { .. } => "AsyncEvent",
VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent",
};
// These are more intrusive than the rest
SuspenseEvent {
hook_idx: usize,
domnode: Rc<Cell<RealDomNode>>,
},
OtherEvent,
f.debug_struct("VirtualEvent").field("type", &name).finish()
}
}
pub mod on {
@@ -134,11 +226,12 @@ pub mod on {
//!
#![allow(unused)]
use std::{fmt::Debug, ops::Deref, rc::Rc};
use bumpalo::boxed::Box as BumpBox;
use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
use crate::{
innerlude::NodeFactory,
innerlude::{Attribute, Listener, RealDomNode, VNode},
innerlude::{Attribute, ElementId, Listener, VNode},
};
use std::cell::Cell;
@@ -156,7 +249,6 @@ pub mod on {
)* ) => {
$(
$(#[$attr])*
#[derive(Debug)]
pub struct $wrapper(pub Rc<dyn $eventdata>);
// todo: derefing to the event is fine (and easy) but breaks some IDE stuff like (go to source)
@@ -178,14 +270,20 @@ pub mod on {
where F: FnMut($wrapper) + 'a
{
let bump = &c.bump();
let cb: &mut dyn FnMut(VirtualEvent) = bump.alloc(move |evt: VirtualEvent| match evt {
VirtualEvent::$wrapper(event) => callback(event),
_ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!")
});
let callback: BumpBox<dyn FnMut(VirtualEvent) + 'a> = unsafe { BumpBox::from_raw(cb) };
let event_name = stringify!($name);
let shortname: &'static str = &event_name[2..];
Listener {
event: stringify!($name),
mounted_node: bump.alloc(Cell::new(RealDomNode::empty())),
scope: c.scope_ref.our_arena_idx,
callback: bump.alloc(move |evt: VirtualEvent| match evt {
VirtualEvent::$wrapper(event) => callback(event),
_ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!")
}),
event: shortname,
mounted_node: Cell::new(None),
callback: RefCell::new(Some(callback)),
}
}
)*
@@ -504,15 +602,15 @@ pub mod on {
fn time_stamp(&self) -> usize;
}
pub trait ClipboardEventInner: Debug + GenericEventInner {
pub trait ClipboardEventInner {
// DOMDataTransfer clipboardData
}
pub trait CompositionEventInner: Debug {
pub trait CompositionEventInner {
fn data(&self) -> String;
}
pub trait KeyboardEventInner: Debug {
pub trait KeyboardEventInner {
fn char_code(&self) -> u32;
/// Get the key code as an enum Variant.
@@ -569,15 +667,15 @@ pub mod on {
fn get_modifier_state(&self, key_code: usize) -> bool;
}
pub trait FocusEventInner: Debug {
pub trait FocusEventInner {
/* DOMEventInnerTarget relatedTarget */
}
pub trait FormEventInner: Debug {
pub trait FormEventInner {
fn value(&self) -> String;
}
pub trait MouseEventInner: Debug {
pub trait MouseEventInner {
fn alt_key(&self) -> bool;
fn button(&self) -> i16;
fn buttons(&self) -> u16;
@@ -594,7 +692,7 @@ pub mod on {
fn get_modifier_state(&self, key_code: &str) -> bool;
}
pub trait PointerEventInner: Debug {
pub trait PointerEventInner {
// Mouse only
fn alt_key(&self) -> bool;
fn button(&self) -> usize;
@@ -621,9 +719,9 @@ pub mod on {
fn is_primary(&self) -> bool;
}
pub trait SelectionEventInner: Debug {}
pub trait SelectionEventInner {}
pub trait TouchEventInner: Debug {
pub trait TouchEventInner {
fn alt_key(&self) -> bool;
fn ctrl_key(&self) -> bool;
fn meta_key(&self) -> bool;
@@ -634,37 +732,37 @@ pub mod on {
// touches: DOMTouchList,
}
pub trait UIEventInner: Debug {
pub trait UIEventInner {
// DOMAbstractView view
fn detail(&self) -> i32;
}
pub trait WheelEventInner: Debug {
pub trait WheelEventInner {
fn delta_mode(&self) -> i32;
fn delta_x(&self) -> i32;
fn delta_y(&self) -> i32;
fn delta_z(&self) -> i32;
}
pub trait MediaEventInner: Debug {}
pub trait MediaEventInner {}
pub trait ImageEventInner: Debug {
pub trait ImageEventInner {
// load error
}
pub trait AnimationEventInner: Debug {
pub trait AnimationEventInner {
fn animation_name(&self) -> String;
fn pseudo_element(&self) -> String;
fn elapsed_time(&self) -> f32;
}
pub trait TransitionEventInner: Debug {
pub trait TransitionEventInner {
fn property_name(&self) -> String;
fn pseudo_element(&self) -> String;
fn elapsed_time(&self) -> f32;
}
pub trait ToggleEventInner: Debug {}
pub trait ToggleEventInner {}
pub use util::KeyCode;
mod util {
+14 -5
View File
@@ -1,14 +1,23 @@
use std::collections::HashMap;
use fxhash::FxHashMap;
use crate::FC;
pub(crate) struct HeuristicsEngine {
heuristics: HashMap<FcSlot, Heuristic>,
/// Provides heuristics to the "SharedResources" object for improving allocation performance.
///
/// This heueristic engine records the memory footprint of bump arenas and hook lists for each component. These records are
/// then used later on to optimize the initial allocation for future components. This helps save large allocations later on
/// that would slow down the diffing and initializion process.
///
///
pub struct HeuristicsEngine {
heuristics: FxHashMap<FcSlot, Heuristic>,
}
pub(crate) type FcSlot = *const ();
pub type FcSlot = *const ();
pub(crate) struct Heuristic {
pub struct Heuristic {
hooks: u32,
bump_size: u64,
}
@@ -16,7 +25,7 @@ pub(crate) struct Heuristic {
impl HeuristicsEngine {
pub(crate) fn new() -> Self {
Self {
heuristics: HashMap::new(),
heuristics: FxHashMap::default(),
}
}
+258
View File
@@ -0,0 +1,258 @@
//! Built-in hooks
//!
//! This module contains all the low-level built-in hooks that require 1st party support to work.
//!
//! Hooks:
//! - use_hook
//! - use_state_provider
//! - use_state_consumer
//! - use_task
//! - use_suspense
use crate::innerlude::*;
use futures_util::FutureExt;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
future::Future,
rc::Rc,
};
/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
///
/// This is a hook, so it may not be called conditionally!
///
/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
/// so don't put it in a conditional.
///
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
/// the context via Rc/Weak.
///
///
///
pub fn use_provide_state<'src, Pr, T, F>(cx: Context<'src, Pr>, init: F) -> &'src Rc<T>
where
T: 'static,
F: FnOnce() -> T,
{
let ty = TypeId::of::<T>();
let contains_key = cx.scope.shared_contexts.borrow().contains_key(&ty);
let is_initialized = cx.use_hook(
|_| false,
|s| {
let i = s.clone();
*s = true;
i
},
|_| {},
);
match (is_initialized, contains_key) {
// Do nothing, already initialized and already exists
(true, true) => {}
// Needs to be initialized
(false, false) => {
log::debug!("Initializing context...");
cx.add_shared_state(init());
log::info!(
"There are now {} shared contexts for scope {:?}",
cx.scope.shared_contexts.borrow().len(),
cx.scope.our_arena_idx,
);
}
_ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
};
use_consume_state::<T, _>(cx)
}
/// There are hooks going on here!
pub fn use_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> &'src Rc<T> {
use_try_consume_state::<T, _>(cx).unwrap()
}
/// Uses a context, storing the cached value around
///
/// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a
/// context was added later. This allows using another hook as a fallback
///
pub fn use_try_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> Option<&'src Rc<T>> {
struct UseContextHook<C>(Option<Rc<C>>);
cx.use_hook(
move |_| UseContextHook(cx.consume_shared_state::<T>()),
move |hook| hook.0.as_ref(),
|_| {},
)
}
/// Awaits the given task, forcing the component to re-render when the value is ready.
///
///
///
///
pub fn use_task<'src, Out, Fut, Init, P>(
cx: Context<'src, P>,
task_initializer: Init,
) -> (&'src TaskHandle, &'src Option<Out>)
where
Out: 'static,
Fut: Future<Output = Out> + 'static,
Init: FnOnce() -> Fut + 'src,
{
struct TaskHook<T> {
handle: TaskHandle,
task_dump: Rc<RefCell<Option<T>>>,
value: Option<T>,
}
// whenever the task is complete, save it into th
cx.use_hook(
move |_| {
let task_fut = task_initializer();
let task_dump = Rc::new(RefCell::new(None));
let slot = task_dump.clone();
let updater = cx.prepare_update();
let update_id = cx.get_scope_id();
let originator = cx.scope.our_arena_idx.clone();
let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move {
*slot.as_ref().borrow_mut() = Some(output);
updater(update_id);
EventTrigger {
event: VirtualEvent::AsyncEvent {
should_rerender: false,
},
originator,
priority: EventPriority::Low,
real_node_id: None,
}
})));
TaskHook {
task_dump,
value: None,
handle,
}
},
|hook| {
if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() {
hook.value = Some(val);
}
(&hook.handle, &hook.value)
},
|_| {},
)
}
/// Asynchronously render new nodes once the given future has completed.
///
/// # Easda
///
///
///
///
/// # Example
///
///
pub fn use_suspense<'src, Out, Fut, Cb, P>(
cx: Context<'src, P>,
task_initializer: impl FnOnce() -> Fut,
user_callback: Cb,
) -> DomTree<'src>
where
Fut: Future<Output = Out> + 'static,
Out: 'static,
Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> DomTree<'a> + 'static,
{
cx.use_hook(
move |hook_idx| {
let value = Rc::new(RefCell::new(None));
let dom_node_id = Rc::new(empty_cell());
let domnode = dom_node_id.clone();
let slot = value.clone();
let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| {
let v: std::cell::Ref<Option<Box<dyn Any>>> = slot.as_ref().borrow();
match v.as_ref() {
Some(a) => {
let v: &dyn Any = a.as_ref();
let real_val = v.downcast_ref::<Out>().unwrap();
user_callback(ctx, real_val)
}
None => {
//
Some(VNode {
key: None,
kind: VNodeKind::Suspended(VSuspended {
node: domnode.clone(),
}),
})
}
}
});
let originator = cx.scope.our_arena_idx.clone();
let task_fut = task_initializer();
let domnode = dom_node_id.clone();
let slot = value.clone();
cx.submit_task(Box::pin(task_fut.then(move |output| async move {
// When the new value arrives, set the hooks internal slot
// Dioxus will call the user's callback to generate new nodes outside of the diffing system
*slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
EventTrigger {
event: VirtualEvent::SuspenseEvent { hook_idx, domnode },
originator,
priority: EventPriority::Low,
real_node_id: None,
}
})));
SuspenseHook {
value,
callback,
dom_node_id,
}
},
move |hook| {
let cx = Context {
scope: &cx.scope,
props: &(),
};
let csx = SuspendedContext { inner: cx };
(&hook.callback)(csx)
},
|_| {},
)
}
pub(crate) struct SuspenseHook {
pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
pub callback: SuspendedCallback,
pub dom_node_id: Rc<Cell<Option<ElementId>>>,
}
type SuspendedCallback = Box<dyn for<'a> Fn(SuspendedContext<'a>) -> DomTree<'a>>;
pub struct SuspendedContext<'a> {
pub(crate) inner: Context<'a, ()>,
}
impl<'src> SuspendedContext<'src> {
pub fn render<F: FnOnce(NodeFactory<'src>) -> VNode<'src>>(
self,
lazy_nodes: LazyNodes<'src, F>,
) -> DomTree<'src> {
let scope_ref = self.inner.scope;
let bump = &self.inner.scope.frames.wip_frame().bump;
Some(lazy_nodes.into_vnode(NodeFactory { bump }))
}
}
+7 -5
View File
@@ -1,4 +1,5 @@
#![allow(non_snake_case)]
#![doc = include_str!("../README.md")]
//! Dioxus Core
//! ----------
//!
@@ -10,14 +11,15 @@
//!
pub use crate::innerlude::{
format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, EventTrigger, LazyNodes,
NodeFactory, Properties, RealDom, RealDomNode, ScopeId, VNode, VNodeKind, VirtualDom,
VirtualEvent, FC,
format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority,
EventTrigger, LazyNodes, NodeFactory, Properties, ScopeId, SuspendedContext, VNode, VNodeKind,
VirtualDom, VirtualEvent, FC,
};
pub mod prelude {
pub use crate::component::{fc_to_builder, Fragment, Properties};
pub use crate::context::Context;
pub use crate::hooks::*;
pub use crate::innerlude::{DioxusElement, DomTree, LazyNodes, NodeFactory, FC};
pub use crate::nodes::VNode;
pub use crate::VirtualDom;
@@ -36,9 +38,9 @@ pub(crate) mod innerlude {
pub use crate::events::*;
pub use crate::heuristics::*;
pub use crate::hooklist::*;
pub use crate::hooks::*;
pub use crate::nodes::*;
pub use crate::scope::*;
pub use crate::tasks::*;
pub use crate::util::*;
pub use crate::virtual_dom::*;
@@ -63,9 +65,9 @@ pub mod error;
pub mod events;
pub mod heuristics;
pub mod hooklist;
pub mod hooks;
pub mod nodes;
pub mod scope;
pub mod signals;
pub mod tasks;
pub mod util;
pub mod virtual_dom;
+232 -116
View File
@@ -5,24 +5,40 @@
//! These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick.
use crate::{
events::VirtualEvent,
innerlude::{Context, DomTree, Properties, RealDomNode, Scope, ScopeId, FC},
innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC},
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
cell::Cell,
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
marker::PhantomData,
mem::ManuallyDrop,
rc::Rc,
};
pub struct VNode<'src> {
pub kind: VNodeKind<'src>,
pub(crate) dom_id: Cell<RealDomNode>,
pub(crate) key: Option<&'src str>,
}
impl VNode<'_> {
fn key(&self) -> Option<&str> {
impl<'src> VNode<'src> {
pub fn key(&self) -> Option<&'src str> {
self.key
}
pub fn direct_id(&self) -> ElementId {
self.try_direct_id().unwrap()
}
pub fn try_direct_id(&self) -> Option<ElementId> {
match &self.kind {
VNodeKind::Text(el) => el.dom_id.get(),
VNodeKind::Element(el) => el.dom_id.get(),
VNodeKind::Anchor(el) => el.dom_id.get(),
VNodeKind::Fragment(_) => None,
VNodeKind::Component(_) => None,
VNodeKind::Suspended(_) => None,
}
}
}
/// Tools for the base unit of the virtual dom - the VNode
@@ -39,18 +55,24 @@ pub enum VNodeKind<'src> {
Component(&'src VComponent<'src>),
Suspended { node: Rc<Cell<RealDomNode>> },
Suspended(VSuspended),
Anchor(VAnchor),
}
pub struct VAnchor {
pub dom_id: Cell<Option<ElementId>>,
}
pub struct VText<'src> {
pub text: &'src str,
pub dom_id: Cell<Option<ElementId>>,
pub is_static: bool,
}
pub struct VFragment<'src> {
pub children: &'src [VNode<'src>],
pub is_static: bool,
pub is_error: bool,
}
pub trait DioxusElement {
@@ -70,6 +92,7 @@ pub struct VElement<'a> {
// tag is always static
pub tag_name: &'static str,
pub namespace: Option<&'static str>,
pub dom_id: Cell<Option<ElementId>>,
pub static_listeners: bool,
pub listeners: &'a [Listener<'a>],
@@ -103,11 +126,19 @@ pub struct Listener<'bump> {
/// The type of event to listen for.
pub(crate) event: &'static str,
pub scope: ScopeId,
pub mounted_node: Cell<Option<ElementId>>,
pub mounted_node: &'bump mut Cell<RealDomNode>,
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(VirtualEvent) + 'bump>>>,
}
pub(crate) callback: &'bump dyn FnMut(VirtualEvent),
impl Listener<'_> {
// serialize the listener event stuff to a string
pub fn serialize(&self) {
//
}
pub fn deserialize() {
//
}
}
/// Virtual Components for custom user-defined components
@@ -121,8 +152,12 @@ pub struct VComponent<'src> {
pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>,
pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
pub is_static: bool,
pub can_memoize: bool,
// a pointer into the bump arena (given by the 'src lifetime)
pub(crate) raw_props: *const (),
@@ -130,28 +165,53 @@ pub struct VComponent<'src> {
pub(crate) user_fc: *const (),
}
pub struct VSuspended {
pub node: Rc<Cell<Option<ElementId>>>,
}
/// This struct provides an ergonomic API to quickly build VNodes.
///
/// NodeFactory is used to build VNodes in the component's memory space.
/// This struct adds metadata to the final VNode about listeners, attributes, and children
#[derive(Copy, Clone)]
pub struct NodeFactory<'a> {
pub scope_ref: &'a Scope,
pub listener_id: &'a Cell<usize>,
pub(crate) bump: &'a Bump,
}
impl<'a> NodeFactory<'a> {
pub fn new(bump: &'a Bump) -> NodeFactory<'a> {
NodeFactory { bump }
}
#[inline]
pub fn bump(&self) -> &'a bumpalo::Bump {
&self.scope_ref.cur_frame().bump
&self.bump
}
pub fn render_directly<F>(&self, lazy_nodes: LazyNodes<'a, F>) -> DomTree<'a>
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
Some(lazy_nodes.into_vnode(NodeFactory { bump: self.bump }))
}
pub fn unstable_place_holder() -> VNode<'static> {
VNode {
key: None,
kind: VNodeKind::Text(VText {
text: "",
dom_id: empty_cell(),
is_static: true,
}),
}
}
/// Used in a place or two to make it easier to build vnodes from dummy text
pub fn static_text(text: &'static str) -> VNode {
pub fn static_text(&self, text: &'static str) -> VNode<'a> {
VNode {
dom_id: RealDomNode::empty_cell(),
key: None,
kind: VNodeKind::Text(VText {
dom_id: empty_cell(),
text,
is_static: true,
}),
@@ -178,20 +238,28 @@ impl<'a> NodeFactory<'a> {
pub fn text(&self, args: Arguments) -> VNode<'a> {
let (text, is_static) = self.raw_text(args);
VNode {
dom_id: RealDomNode::empty_cell(),
key: None,
kind: VNodeKind::Text(VText { text, is_static }),
kind: VNodeKind::Text(VText {
text,
is_static,
dom_id: empty_cell(),
}),
}
}
pub fn element(
pub fn element<L, A, V>(
&self,
el: impl DioxusElement,
listeners: &'a mut [Listener<'a>],
attributes: &'a [Attribute<'a>],
children: &'a [VNode<'a>],
key: Option<&'a str>,
) -> VNode<'a> {
listeners: L,
attributes: A,
children: V,
key: Option<Arguments>,
) -> VNode<'a>
where
L: 'a + AsRef<[Listener<'a>]>,
A: 'a + AsRef<[Attribute<'a>]>,
V: 'a + AsRef<[VNode<'a>]>,
{
self.raw_element(
el.tag_name(),
el.namespace(),
@@ -202,28 +270,32 @@ impl<'a> NodeFactory<'a> {
)
}
pub fn raw_element(
pub fn raw_element<L, A, V>(
&self,
tag: &'static str,
namespace: Option<&'static str>,
listeners: &'a mut [Listener],
attributes: &'a [Attribute],
children: &'a [VNode<'a>],
key: Option<&'a str>,
) -> VNode<'a> {
// We take the references directly from the bump arena
// TODO: this code shouldn't necessarily be here of all places
// It would make more sense to do this in diffing
listeners: L,
attributes: A,
children: V,
key: Option<Arguments>,
) -> VNode<'a>
where
L: 'a + AsRef<[Listener<'a>]>,
A: 'a + AsRef<[Attribute<'a>]>,
V: 'a + AsRef<[VNode<'a>]>,
{
let listeners: &'a L = self.bump().alloc(listeners);
let listeners = listeners.as_ref();
let mut queue = self.scope_ref.listeners.borrow_mut();
for listener in listeners.iter_mut() {
let mounted = listener.mounted_node as *mut _;
let callback = listener.callback as *const _ as *mut _;
queue.push((mounted, callback))
}
let attributes: &'a A = self.bump().alloc(attributes);
let attributes = attributes.as_ref();
let children: &'a V = self.bump().alloc(children);
let children = children.as_ref();
let key = key.map(|f| self.raw_text(f).0);
VNode {
dom_id: RealDomNode::empty_cell(),
key,
kind: VNodeKind::Element(self.bump().alloc(VElement {
tag_name: tag,
@@ -231,6 +303,7 @@ impl<'a> NodeFactory<'a> {
listeners,
attributes,
children,
dom_id: empty_cell(),
// todo: wire up more constization
static_listeners: false,
@@ -242,11 +315,10 @@ impl<'a> NodeFactory<'a> {
pub fn suspended() -> VNode<'static> {
VNode {
dom_id: RealDomNode::empty_cell(),
key: None,
kind: VNodeKind::Suspended {
node: Rc::new(RealDomNode::empty_cell()),
},
kind: VNodeKind::Suspended(VSuspended {
node: Rc::new(empty_cell()),
}),
}
}
@@ -283,31 +355,44 @@ impl<'a> NodeFactory<'a> {
}
}
pub fn component<P>(
pub fn component<P, V>(
&self,
component: FC<P>,
props: P,
key: Option<&'a str>,
children: &'a [VNode<'a>],
key: Option<Arguments>,
children: V,
) -> VNode<'a>
where
P: Properties + 'a,
V: 'a + AsRef<[VNode<'a>]>,
{
// TODO
// It's somewhat wrong to go about props like this
let bump = self.bump();
// We don't want the fat part of the fat pointer
// This function does static dispatch so we don't need any VTable stuff
let props = self.bump().alloc(props);
let raw_props = props as *const P as *const ();
let children: &'a V = bump.alloc(children);
let children = children.as_ref();
let props = bump.alloc(props);
let raw_props = props as *mut P as *mut ();
let user_fc = component as *const ();
let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(self.bump().alloc_with(|| {
let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(bump.alloc_with(|| {
move |other: &VComponent| {
if user_fc == other.user_fc {
let real_other = unsafe { &*(other.raw_props as *const _ as *const P) };
let props_memoized = unsafe { props.memoize(&real_other) };
// Safety
// - We guarantee that FC<P> is the same by function pointer
// - Because FC<P> is the same, then P must be the same (even with generics)
// - Non-static P are autoderived to memoize as false
// - This comparator is only called on a corresponding set of bumpframes
let props_memoized = unsafe {
let real_other: &P = &*(other.raw_props as *const _ as *const P);
props.memoize(&real_other)
};
// It's only okay to memoize if there are no children and the props can be memoized
// Implementing memoize is unsafe and done automatically with the props trait
match (props_memoized, children.len() == 0) {
(true, true) => true,
_ => false,
@@ -318,18 +403,36 @@ impl<'a> NodeFactory<'a> {
}
}));
// create a closure to drop the props
let mut has_dropped = false;
let drop_props: &mut dyn FnMut() = bump.alloc_with(|| {
move || unsafe {
if !has_dropped {
let real_other = raw_props as *mut _ as *mut P;
let b = BumpBox::from_raw(real_other);
std::mem::drop(b);
has_dropped = true;
}
}
});
let drop_props = unsafe { BumpBox::from_raw(drop_props) };
let is_static = children.len() == 0 && P::IS_STATIC && key.is_none();
let key = key.map(|f| self.raw_text(f).0);
VNode {
key,
dom_id: Cell::new(RealDomNode::empty()),
kind: VNodeKind::Component(self.bump().alloc_with(|| VComponent {
kind: VNodeKind::Component(bump.alloc_with(|| VComponent {
user_fc,
comparator,
raw_props,
children,
caller: NodeFactory::create_component_caller(component, raw_props),
is_static,
drop_props: RefCell::new(Some(drop_props)),
can_memoize: P::IS_STATIC,
ass_scope: Cell::new(None),
})),
}
@@ -357,38 +460,14 @@ impl<'a> NodeFactory<'a> {
unsafe { std::mem::transmute::<_, Captured<'static>>(caller) }
}
pub fn fragment_from_iter(
self,
node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>,
) -> VNode<'a> {
let mut nodes = bumpalo::collections::Vec::new_in(self.bump());
pub fn fragment_from_iter(self, node_iter: impl IntoVNodeList<'a>) -> VNode<'a> {
let children = node_iter.into_vnode_list(self);
for node in node_iter.into_iter() {
nodes.push(node.into_vnode(self));
}
if cfg!(debug_assertions) {
if nodes.len() > 1 {
if nodes.last().unwrap().key().is_none() {
log::error!(
r#"
Warning: Each child in an array or iterator should have a unique "key" prop.
Not providing a key will lead to poor performance with lists.
See docs.rs/dioxus for more information.
---
To help you identify where this error is coming from, we've generated a backtrace.
"#,
);
}
}
}
VNode {
dom_id: RealDomNode::empty_cell(),
key: None,
kind: VNodeKind::Fragment(VFragment {
children: nodes.into_bump_slice(),
children,
is_static: false,
is_error: false,
}),
}
}
@@ -413,6 +492,72 @@ pub trait IntoVNode<'a> {
fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a>;
}
pub trait IntoVNodeList<'a> {
fn into_vnode_list(self, cx: NodeFactory<'a>) -> &'a [VNode<'a>];
}
impl<'a, T, V> IntoVNodeList<'a> for T
where
T: IntoIterator<Item = V>,
V: IntoVNode<'a>,
{
fn into_vnode_list(self, cx: NodeFactory<'a>) -> &'a [VNode<'a>] {
let mut nodes = bumpalo::collections::Vec::new_in(cx.bump());
for node in self.into_iter() {
nodes.push(node.into_vnode(cx));
}
if cfg!(debug_assertions) {
if nodes.len() > 1 {
if nodes.last().unwrap().key().is_none() {
log::error!(
r#"
Warning: Each child in an array or iterator should have a unique "key" prop.
Not providing a key will lead to poor performance with lists.
See docs.rs/dioxus for more information.
---
To help you identify where this error is coming from, we've generated a backtrace.
"#,
);
}
}
}
if nodes.len() == 0 {
nodes.push(VNode {
kind: VNodeKind::Anchor(VAnchor {
dom_id: empty_cell(),
}),
key: None,
});
}
nodes.into_bump_slice()
}
}
pub struct ScopeChildren<'a>(pub &'a [VNode<'a>]);
impl Copy for ScopeChildren<'_> {}
impl<'a> Clone for ScopeChildren<'a> {
fn clone(&self) -> Self {
ScopeChildren(self.0)
}
}
impl ScopeChildren<'_> {
pub unsafe fn extend_lifetime(self) -> ScopeChildren<'static> {
std::mem::transmute(self)
}
pub unsafe fn unextend_lfetime<'a>(self) -> ScopeChildren<'a> {
std::mem::transmute(self)
}
}
impl<'a> IntoVNodeList<'a> for ScopeChildren<'a> {
fn into_vnode_list(self, _: NodeFactory<'a>) -> &'a [VNode<'a>] {
self.0
}
}
// For the case where a rendered VNode is passed into the rsx! macro through curly braces
impl<'a> IntoIterator for VNode<'a> {
type Item = VNode<'a>;
@@ -429,37 +574,6 @@ impl<'a> IntoVNode<'a> for VNode<'a> {
}
}
// For the case where a rendered VNode is by reference passed into the rsx! macro through curly braces
// This behavior is designed for the cx.children method where child nodes are passed by reference.
//
// Designed to support indexing
impl<'a> IntoVNode<'a> for &VNode<'a> {
fn into_vnode(self, _: NodeFactory<'a>) -> VNode<'a> {
let kind = match &self.kind {
VNodeKind::Element(element) => VNodeKind::Element(element),
VNodeKind::Text(old) => VNodeKind::Text(VText {
text: old.text,
is_static: old.is_static,
}),
VNodeKind::Fragment(fragment) => VNodeKind::Fragment(VFragment {
children: fragment.children,
is_static: fragment.is_static,
is_error: false,
}),
VNodeKind::Component(component) => VNodeKind::Component(component),
// todo: it doesn't make much sense to pass in suspended nodes
// I think this is right but I'm not too sure.
VNodeKind::Suspended { node } => VNodeKind::Suspended { node: node.clone() },
};
VNode {
kind,
dom_id: self.dom_id.clone(),
key: self.key.clone(),
}
}
}
/// A concrete type provider for closures that build VNode structures.
///
/// This struct wraps lazy structs that build VNode trees Normally, we cannot perform a blanket implementation over
@@ -535,7 +649,7 @@ impl<'a> IntoVNode<'a> for Option<VNode<'a>> {
impl IntoVNode<'_> for &'static str {
fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> {
NodeFactory::static_text(self)
cx.static_text(self)
}
}
impl IntoVNode<'_> for Arguments<'_> {
@@ -553,8 +667,10 @@ impl Debug for NodeFactory<'_> {
impl Debug for VNode<'_> {
fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match &self.kind {
VNodeKind::Element(el) => write!(s, "element, {}", el.tag_name),
VNodeKind::Text(t) => write!(s, "text, {}", t.text),
VNodeKind::Element(el) => write!(s, "VElement {{ name: {} }}", el.tag_name),
VNodeKind::Text(t) => write!(s, "VText {{ text: {} }}", t.text),
VNodeKind::Anchor(a) => write!(s, "VAnchor"),
VNodeKind::Fragment(_) => write!(s, "fragment"),
VNodeKind::Suspended { .. } => write!(s, "suspended"),
VNodeKind::Component(_) => write!(s, "component"),
+136 -88
View File
@@ -1,6 +1,9 @@
use crate::innerlude::*;
use bumpalo::boxed::Box as BumpBox;
use fxhash::FxHashSet;
use std::{
any::{Any, TypeId},
borrow::BorrowMut,
cell::{Cell, RefCell},
collections::{HashMap, HashSet},
future::Future,
@@ -18,40 +21,32 @@ use std::{
/// We expose the `Scope` type so downstream users can traverse the Dioxus VirtualDOM for whatever
/// usecase they might have.
pub struct Scope {
// Book-keeping about the arena
// Book-keeping about our spot in the arena
pub(crate) parent_idx: Option<ScopeId>,
pub(crate) descendents: RefCell<HashSet<ScopeId>>,
pub(crate) our_arena_idx: ScopeId,
pub(crate) height: u32,
pub(crate) descendents: RefCell<FxHashSet<ScopeId>>,
// Nodes
// an internal, highly efficient storage of vnodes
// lots of safety condsiderations
pub(crate) frames: ActiveFrame,
pub(crate) child_nodes: &'static [VNode<'static>],
pub(crate) caller: Rc<WrappedCaller>,
pub(crate) child_nodes: ScopeChildren<'static>,
pub(crate) pending_garbage: RefCell<Vec<*const VNode<'static>>>,
// Listeners
pub(crate) listeners: RefCell<Vec<(*mut Cell<RealDomNode>, *mut dyn FnMut(VirtualEvent))>>,
pub(crate) listener_idx: Cell<usize>,
pub(crate) listeners: RefCell<Vec<*const Listener<'static>>>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
// State
pub(crate) hooks: HookList,
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
// Events
pub(crate) event_channel: Rc<dyn Fn() + 'static>,
// Tasks
pub(crate) task_submitter: TaskSubmitter,
// A reference to the list of components.
// This lets us traverse the component list whenever we need to access our parent or children.
pub(crate) arena_link: SharedArena,
// A reference to the resources shared by all the comonents
pub(crate) vdom: SharedResources,
}
// The type of the channel function
type EventChannel = Rc<dyn Fn()>;
// The type of closure that wraps calling components
pub type WrappedCaller = dyn for<'b> Fn(&'b Scope) -> DomTree<'b>;
@@ -68,42 +63,52 @@ impl Scope {
// Therefore, their lifetimes are connected exclusively to the virtual dom
pub fn new<'creator_node>(
caller: Rc<WrappedCaller>,
arena_idx: ScopeId,
parent: Option<ScopeId>,
height: u32,
event_channel: EventChannel,
arena_link: SharedArena,
child_nodes: &'creator_node [VNode<'creator_node>],
task_submitter: TaskSubmitter,
child_nodes: ScopeChildren,
vdom: SharedResources,
) -> Self {
let child_nodes = unsafe { std::mem::transmute(child_nodes) };
let child_nodes = unsafe { child_nodes.extend_lifetime() };
// insert ourself as a descendent of the parent
// when the parent is removed, this map will be traversed, and we will also be cleaned up.
if let Some(parent) = &parent {
let parent = unsafe { vdom.get_scope(*parent) }.unwrap();
parent.descendents.borrow_mut().insert(arena_idx);
}
Self {
child_nodes,
caller,
parent_idx: parent,
our_arena_idx: arena_idx,
height,
event_channel,
arena_link,
task_submitter,
listener_idx: Default::default(),
vdom,
frames: ActiveFrame::new(),
hooks: Default::default(),
shared_contexts: Default::default(),
listeners: Default::default(),
borrowed_props: Default::default(),
descendents: Default::default(),
pending_garbage: Default::default(),
}
}
pub(crate) fn update_caller<'creator_node>(&mut self, caller: Rc<WrappedCaller>) {
self.caller = caller;
}
pub(crate) fn update_children<'creator_node>(
pub(crate) fn update_scope_dependencies<'creator_node>(
&mut self,
child_nodes: &'creator_node [VNode<'creator_node>],
caller: Rc<WrappedCaller>,
child_nodes: ScopeChildren,
) {
let child_nodes = unsafe { std::mem::transmute(child_nodes) };
self.caller = caller;
// let child_nodes = unsafe { std::mem::transmute(child_nodes) };
let child_nodes = unsafe { child_nodes.extend_lifetime() };
self.child_nodes = child_nodes;
}
@@ -112,19 +117,21 @@ impl Scope {
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
// This is a very dangerous operation
let next_frame = self.frames.old_frame_mut();
next_frame.bump.reset();
self.listeners.borrow_mut().clear();
self.ensure_drop_safety();
// Safety:
// - We dropped the listeners, so no more &mut T can be used while these are held
// - All children nodes that rely on &mut T are replaced with a new reference
unsafe { self.hooks.reset() };
self.listener_idx.set(0);
// Safety:
// - We've dropped all references to the wip bump frame
unsafe { self.frames.reset_wip_frame() };
// Cast the caller ptr from static to one with our own reference
let c3: &WrappedCaller = self.caller.as_ref();
let render: &WrappedCaller = self.caller.as_ref();
match c3(self) {
match render(self) {
None => {
// the user's component failed. We avoid cycling to the next frame
log::error!("Running your component failed! It will no longer receive events.");
@@ -132,13 +139,59 @@ impl Scope {
}
Some(new_head) => {
// the user's component succeeded. We can safely cycle to the next frame
self.frames.old_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
self.frames.cycle_frame();
log::debug!("Cycle okay");
Ok(())
}
}
}
/// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from
/// causuing UB in our tree.
///
/// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components
/// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove
/// any possible references to the data in the hook list.
///
/// Refrences to hook data can only be stored in listeners and component props. During diffing, we make sure to log
/// all listeners and borrowed props so we can clear them here.
fn ensure_drop_safety(&mut self) {
// make sure all garabge is collected before trying to proceed with anything else
debug_assert!(
self.pending_garbage.borrow().is_empty(),
"clean up your garabge please"
);
// 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 Referrence)
// right now, we don't drop
let vdom = &self.vdom;
self.borrowed_props
.get_mut()
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|comp| {
// First drop the component's undropped references
let scope_id = comp.ass_scope.get().unwrap();
let scope = unsafe { vdom.get_scope_mut(scope_id) }.unwrap();
scope.ensure_drop_safety();
// Now, drop our own reference
let mut dropper = comp.drop_props.borrow_mut().take().unwrap();
dropper();
});
// Now that all the references are gone, we can safely drop our own references in our listeners.
self.listeners
.get_mut()
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|listener| {
listener.callback.borrow_mut().take();
});
}
// A safe wrapper around calling listeners
// calling listeners will invalidate the list of listeners
// The listener list will be completely drained because the next frame will write over previous listeners
@@ -162,63 +215,58 @@ impl Scope {
let listners = self.listeners.borrow_mut();
let raw_listener = listners.iter().find(|(domptr, _)| {
let search = unsafe { &**domptr };
let search_id = search.get();
log::info!("searching listener {:#?}", search_id);
match real_node_id {
Some(e) => search_id == e,
None => false,
let raw_listener = listners.iter().find(|lis| {
let search = unsafe { &***lis };
let search_id = search.mounted_node.get();
log::info!(
"searching listener {:#?} for real {:?}",
search_id,
real_node_id
);
match (real_node_id, search_id) {
(Some(e), Some(search_id)) => search_id == e,
_ => false,
}
});
match raw_listener {
Some((_node, listener)) => unsafe {
// TODO: Don'tdo a linear scan! Do a hashmap lookup! It'll be faster!
let listener_fn = &mut **listener;
listener_fn(event);
},
None => todo!(),
if let Some(raw_listener) = raw_listener {
let listener = unsafe { &**raw_listener };
// log::info!(
// "calling listener {:?}, {:?}",
// listener.event,
// // listener.scope
// );
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
(cb)(event);
}
} else {
log::warn!("An event was triggered but there was no listener to handle it");
}
Ok(())
}
pub(crate) fn submit_task(&self, task: FiberTask) {
log::debug!("Task submitted into scope");
(self.task_submitter)(task);
pub fn root(&self) -> &VNode {
self.frames.fin_head()
}
#[inline]
pub(crate) fn next_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
self.frames.current_head_node()
pub fn child_nodes<'a>(&'a self) -> ScopeChildren {
unsafe { self.child_nodes.unextend_lfetime() }
}
#[inline]
pub(crate) fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
self.frames.prev_head_node()
}
#[inline]
pub(crate) fn cur_frame(&self) -> &BumpFrame {
self.frames.cur_frame()
}
/// Get the root VNode of this component
#[inline]
pub fn root<'a>(&'a self) -> &'a VNode<'a> {
&self.frames.cur_frame().head_node
}
}
pub fn errored_fragment() -> VNode<'static> {
VNode {
dom_id: RealDomNode::empty_cell(),
key: None,
kind: VNodeKind::Fragment(VFragment {
children: &[],
is_static: false,
is_error: true,
}),
pub fn consume_garbage(&self) -> Vec<&VNode> {
let mut garbage = self.pending_garbage.borrow_mut();
garbage
.drain(..)
.map(|node| {
// safety: scopes cannot cycle without their garbage being collected. these nodes are safe
let node: &VNode<'static> = unsafe { &*node };
let node: &VNode = unsafe { std::mem::transmute(node) };
node
})
.collect::<Vec<_>>()
}
}
-137
View File
@@ -1,137 +0,0 @@
//! The TaskQueue serves as a centralized async store for all tasks in Dioxus.
//! When a component renders, it may submit an async task to the queue.
//!
//! Then the task complete, it is emitted from the virtual dom in the event loop, which is then fed back into the virtualdom
//! as an event trigger.
//!
//! When a component is scheduled to re-render, the awaing task must be dumped from the queue.
//!
//! This is all pretty unsafe stuff.
//! The major invariant here is that tasks that enter the queue may be invalidated during transitions.
use std::{
cell::Cell,
sync::{Arc, RwLock},
};
use futures_util::{stream::FuturesUnordered, Future, Stream, StreamExt};
use slotmap::{DefaultKey, SlotMap};
use crate::innerlude::{EventTrigger, FiberTask, ScopeId};
pub type TaskSubmitter = Arc<dyn Fn(FiberTask)>;
pub struct TaskQueue {
slots: Arc<RwLock<FuturesUnordered<FiberTask>>>,
// slots: Arc<RwLock<SlotMap<DefaultKey, DTask>>>,
submitter: TaskSubmitter,
}
impl TaskQueue {
pub fn new() -> Self {
let slots = Arc::new(RwLock::new(FuturesUnordered::new()));
let slots2 = slots.clone();
let submitter = Arc::new(move |task| {
let mut slots = slots2.write().unwrap();
log::debug!("Task submitted into global task queue");
slots.push(task);
});
Self { slots, submitter }
}
pub fn new_submitter(&self) -> TaskSubmitter {
self.submitter.clone()
}
pub fn submit_task(&mut self, task: FiberTask) {
self.slots.write().unwrap().push(task);
// TaskHandle { key }
}
pub fn is_empty(&self) -> bool {
self.slots.read().unwrap().is_empty()
}
pub fn len(&self) -> usize {
self.slots.read().unwrap().len()
}
pub async fn next(&mut self) -> Option<EventTrigger> {
let mut slots = self.slots.write().unwrap();
slots.next().await
}
}
// impl Stream for TaskQueue {
// type Item = EventTrigger;
// /// We can never be finished polling
// fn poll_next(
// self: Pin<&mut Self>,
// cx: &mut std::task::Context<'_>,
// ) -> std::task::Poll<Option<Self::Item>> {
// // let yield_every = self.len();
// // let mut polled = 0;
// let mut slots = self.slots.write().unwrap();
// for (_key, slot) in slots.iter_mut() {
// if slot.dead.get() {
// continue;
// }
// let r = slot.fut;
// // let fut = unsafe { &mut *r };
// // use futures::{future::Future, poll, FutureExt};
// let f2 = fut.as_mut();
// let w = cx.waker();
// let mut cx = Context::from_waker(&w);
// // Pin::new_unchecked(pointer)
// // use std::future::Future;
// match f2.poll(&mut cx) {
// Poll::Ready(_) => {
// let trigger = EventTrigger::new_from_task(slot.originator);
// slot.dead.set(true);
// return Poll::Ready(Some(trigger));
// }
// Poll::Pending => continue,
// }
// }
// // we tried polling every active task.
// // give up and relinquish controlto the parent
// // We have polled a large number of futures in a row without yielding.
// // To ensure we do not starve other tasks waiting on the executor,
// // we yield here, but immediately wake ourselves up to continue.
// // cx.waker().wake_by_ref();
// return Poll::Pending;
// }
// }
pub struct TaskHandle {
key: DefaultKey,
}
pub struct DTask {
fut: FiberTask,
originator: ScopeId,
dead: Cell<bool>,
}
impl DTask {
pub fn new(fut: FiberTask, originator: ScopeId) -> Self {
Self {
fut,
originator,
dead: Cell::new(false),
}
}
pub fn debug_new(fut: FiberTask) -> Self {
let originator = ScopeId::default();
Self {
fut,
originator,
dead: Cell::new(false),
}
}
}
+21 -94
View File
@@ -1,101 +1,28 @@
use std::{
cell::{Cell, RefCell, RefMut},
rc::Rc,
};
use futures_util::StreamExt;
use slotmap::{DefaultKey, Key, KeyData};
use std::cell::Cell;
use crate::innerlude::*;
#[derive(PartialEq, Debug, Clone, Default)]
pub struct EventQueue {
pub queue: Rc<RefCell<Vec<HeightMarker>>>,
// create a cell with a "none" value
#[inline]
pub fn empty_cell() -> Cell<Option<ElementId>> {
Cell::new(None)
}
impl EventQueue {
pub fn new_channel(&self, height: u32, idx: ScopeId) -> Rc<dyn Fn()> {
let inner = self.clone();
let marker = HeightMarker { height, idx };
Rc::new(move || {
log::debug!("channel updated {:#?}", marker);
inner.queue.as_ref().borrow_mut().push(marker)
})
}
// /// A helper type that lets scopes be ordered by their height
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
// pub struct HeightMarker {
// pub idx: ScopeId,
// pub height: u32,
// }
pub fn sort_unstable(&self) {
self.queue.borrow_mut().sort_unstable()
}
// impl Ord for HeightMarker {
// fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// self.height.cmp(&other.height)
// }
// }
pub fn borrow_mut(&self) -> RefMut<Vec<HeightMarker>> {
self.queue.borrow_mut()
}
}
/// A helper type that lets scopes be ordered by their height
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HeightMarker {
pub idx: ScopeId,
pub height: u32,
}
impl Ord for HeightMarker {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.height.cmp(&other.height)
}
}
impl PartialOrd for HeightMarker {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
/// The `RealDomNode` is an ID handle that corresponds to a foreign DOM node.
///
/// "u64" was chosen for two reasons
/// - 0 cost hashing
/// - use with slotmap and other versioned slot arenas
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RealDomNode(pub u64);
impl RealDomNode {
#[inline]
pub fn empty() -> Self {
Self(u64::MIN)
}
#[inline]
pub fn empty_cell() -> Cell<Self> {
Cell::new(Self::empty())
}
#[inline]
pub fn from_u64(id: u64) -> Self {
Self(id)
}
#[inline]
pub fn as_u64(&self) -> u64 {
self.0
}
}
pub struct DebugDom {
counter: u64,
}
impl DebugDom {
pub fn new() -> Self {
Self { counter: 0 }
}
}
impl<'a> RealDom<'a> for DebugDom {
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
todo!()
}
fn request_available_node(&mut self) -> RealDomNode {
self.counter += 1;
RealDomNode::from_u64(self.counter)
}
}
// impl PartialOrd for HeightMarker {
// fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// Some(self.cmp(other))
// }
// }
+304 -191
View File
@@ -18,22 +18,20 @@
//!
//! This module includes just the barebones for a complete VirtualDOM API.
//! Additional functionality is defined in the respective files.
#![allow(unreachable_code)]
use futures_util::StreamExt;
use fxhash::FxHashMap;
use crate::tasks::TaskQueue;
use crate::{arena::SharedArena, innerlude::*};
use crate::hooks::{SuspendedContext, SuspenseHook};
use crate::{arena::SharedResources, innerlude::*};
use slotmap::DefaultKey;
use slotmap::SlotMap;
use std::any::Any;
use std::any::TypeId;
use std::cell::RefCell;
use std::cell::{Ref, RefCell, RefMut};
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet};
use std::pin::Pin;
slotmap::new_key_type! {
pub struct ScopeId;
}
/// An integrated virtual node system that progresses events and diffs UI trees.
/// Differences are converted into patches which a renderer can use to draw the UI.
///
@@ -49,34 +47,22 @@ pub struct VirtualDom {
///
/// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
/// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
pub components: SharedArena,
pub shared: SharedResources,
/// The index of the root component
/// Should always be the first (gen=0, id=0)
pub base_scope: ScopeId,
pub triggers: RefCell<Vec<EventTrigger>>,
active_fibers: Vec<Fiber<'static>>,
/// All components dump their updates into a queue to be processed
pub event_queue: EventQueue,
pub tasks: TaskQueue,
heuristics: HeuristicsEngine,
root_props: std::pin::Pin<Box<dyn std::any::Any>>,
/// Type of the original props. This is stored as TypeId so VirtualDom does not need to be generic.
///
/// Whenver props need to be updated, an Error will be thrown if the new props do not
/// match the props used to create the VirtualDom.
// for managing the props that were used to create the dom
#[doc(hidden)]
_root_prop_type: std::any::TypeId,
#[doc(hidden)]
_root_props: std::pin::Pin<Box<dyn std::any::Any>>,
}
// ======================================
// Public Methods for the VirtualDom
// ======================================
impl VirtualDom {
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
///
@@ -142,45 +128,30 @@ impl VirtualDom {
/// let dom = VirtualDom::new(Example);
/// ```
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
let components = SharedArena::new(SlotMap::<ScopeId, Scope>::with_key());
let components = SharedResources::new();
let root_props: Pin<Box<dyn Any>> = Box::pin(root_props);
let props_ptr = root_props.as_ref().downcast_ref::<P>().unwrap() as *const P;
// Build a funnel for hooks to send their updates into. The `use_hook` method will call into the update funnel.
let event_queue = EventQueue::default();
let _event_queue = event_queue.clone();
let link = components.clone();
let tasks = TaskQueue::new();
let submitter = tasks.new_submitter();
let base_scope = components
.with(|arena| {
arena.insert_with_key(move |myidx| {
let event_channel = _event_queue.new_channel(0, myidx);
let caller = NodeFactory::create_component_caller(root, props_ptr as *const _);
Scope::new(caller, myidx, None, 0, event_channel, link, &[], submitter)
})
})
.unwrap();
let base_scope = components.insert_scope_with_key(move |myidx| {
let caller = NodeFactory::create_component_caller(root, props_ptr as *const _);
Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), link)
});
Self {
base_scope,
event_queue,
components,
root_props,
tasks,
heuristics: HeuristicsEngine::new(),
triggers: Default::default(),
_root_props: root_props,
shared: components,
active_fibers: Vec::new(),
_root_prop_type: TypeId::of::<P>(),
}
}
pub fn launch_in_place(root: FC<()>) -> Self {
let mut s = Self::new(root);
s.rebuild_in_place();
s.rebuild_in_place().unwrap();
s
}
@@ -188,10 +159,18 @@ impl VirtualDom {
///
pub fn launch_with_props_in_place<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
let mut s = Self::new_with_props(root, root_props);
s.rebuild_in_place();
s.rebuild_in_place().unwrap();
s
}
pub fn base_scope(&self) -> &Scope {
unsafe { self.shared.get_scope(self.base_scope).unwrap() }
}
pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> {
unsafe { self.shared.get_scope(id) }
}
/// Rebuilds the VirtualDOM from scratch, but uses a "dummy" RealDom.
///
/// Used in contexts where a real copy of the structure doesn't matter, and the VirtualDOM is the source of truth.
@@ -201,191 +180,292 @@ impl VirtualDom {
/// This method uses the `DebugDom` under the hood - essentially making the VirtualDOM's diffing patches a "no-op".
///
/// SSR takes advantage of this by using Dioxus itself as the source of truth, and rendering from the tree directly.
pub fn rebuild_in_place(&mut self) -> Result<()> {
let mut realdom = DebugDom::new();
let mut edits = Vec::new();
self.rebuild(&mut realdom, &mut edits)
pub fn rebuild_in_place(&mut self) -> Result<Vec<DomEdit>> {
todo!();
// let mut realdom = DebugDom::new();
// let mut edits = Vec::new();
// self.rebuild(&mut realdom, &mut edits)?;
// Ok(edits)
}
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch
///
/// Currently this doesn't do what we want it to do
///
/// The diff machine expects the RealDom's stack to be the root of the application
pub fn rebuild<'s, Dom: RealDom<'s>>(
&'s mut self,
realdom: &mut Dom,
edits: &mut Vec<DomEdit<'s>>,
) -> Result<()> {
let mut diff_machine = DiffMachine::new(
edits,
realdom,
&self.components,
self.base_scope,
self.event_queue.clone(),
&self.tasks,
);
///
/// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed
/// through "run"
///
pub fn rebuild<'s>(&'s mut self) -> Result<Vec<DomEdit<'s>>> {
let mut edits = Vec::new();
let mutations = Mutations { edits: Vec::new() };
let mut diff_machine = DiffMachine::new(mutations, self.base_scope, &self.shared);
let cur_component = self.components.get_mut(self.base_scope).unwrap();
let cur_component = diff_machine
.get_scope_mut(&self.base_scope)
.expect("The base scope should never be moved");
// We run the component. If it succeeds, then we can diff it and add the changes to the dom.
if cur_component.run_scope().is_ok() {
let meta = diff_machine.create(cur_component.next_frame());
diff_machine.edits.append_children(meta.added_to_stack);
let meta = diff_machine.create_vnode(cur_component.frames.fin_head());
diff_machine.edit_append_children(meta.added_to_stack);
} else {
// todo: should this be a hard error?
log::warn!(
"Component failed to run succesfully during rebuild.
This does not result in a failed rebuild, but indicates a logic failure within your app."
);
}
Ok(())
Ok(edits)
}
// async fn select_next_event(&mut self) -> Option<EventTrigger> {
// let mut receiver = self.shared.task_receiver.borrow_mut();
// // drain the in-flight events so that we can sort them out with the current events
// while let Ok(Some(trigger)) = receiver.try_next() {
// log::info!("retrieving event from receiver");
// let key = self.shared.make_trigger_key(&trigger);
// self.pending_events.insert(key, trigger);
// }
// if self.pending_events.is_empty() {
// // Continuously poll the future pool and the event receiver for work
// let mut tasks = self.shared.async_tasks.borrow_mut();
// let tasks_tasks = tasks.next();
// let mut receiver = self.shared.task_receiver.borrow_mut();
// let reciv_task = receiver.next();
// futures_util::pin_mut!(tasks_tasks);
// futures_util::pin_mut!(reciv_task);
// let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await {
// futures_util::future::Either::Left((trigger, _)) => trigger,
// futures_util::future::Either::Right((trigger, _)) => trigger,
// }
// .unwrap();
// let key = self.shared.make_trigger_key(&trigger);
// self.pending_events.insert(key, trigger);
// }
// // pop the most important event off
// let key = self.pending_events.keys().next().unwrap().clone();
// let trigger = self.pending_events.remove(&key).unwrap();
// Some(trigger)
// }
/// Runs the virtualdom immediately, not waiting for any suspended nodes to complete.
///
///
///
///
///
pub fn queue_event(&self, trigger: EventTrigger) -> Result<()> {
let mut triggers = self.triggers.borrow_mut();
triggers.push(trigger);
Ok(())
/// This method will not wait for any suspended tasks, completely skipping over
pub fn run_immediate<'s>(&'s mut self) -> Result<Mutations<'s>> {
//
todo!()
}
/// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered.
///
/// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual
/// dom to completion, tagging components that need updates, compressing events together, and finally emitting a single
/// change list.
/// Runs the virtualdom with no time limit.
///
/// If implementing an external renderer, this is the perfect method to combine with an async event loop that waits on
/// listeners, something like this:
/// If there are pending tasks, they will be progressed before returning. This is useful when rendering an application
/// that has suspended nodes or suspended tasks. Be warned - any async tasks running forever will prevent this method
/// from completing. Consider using `run` and specifing a deadline.
pub async fn run_unbounded<'s>(&'s mut self) -> Result<Mutations<'s>> {
self.run_with_deadline(|| false).await
}
/// Run the virtualdom with a time limit.
///
/// ```ignore
/// while let Ok(event) = receiver.recv().await {
/// let edits = self.internal_dom.progress_with_event(event)?;
/// for edit in &edits {
/// patch_machine.handle_edit(edit);
/// }
/// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline,
/// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will
/// exhaust the deadline working on them.
///
/// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent
/// "jank". It will try to finish whatever work it has by the deadline to free up time for other work.
///
/// Due to platform differences in how time is handled, this method accepts a closure that must return true when the
/// deadline is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room
/// into the deadline closure manually.
///
/// The deadline is checked before starting to diff components. This strikes a balance between the overhead of checking
/// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then
/// the screen will "jank" up. In debug, this will trigger an alert.
///
/// # Example
///
/// ```no_run
/// let mut dom = VirtualDom::new(|cx| cx.render(rsx!( div {"hello"} )));
/// loop {
/// let started = std::time::Instant::now();
/// let deadline = move || std::time::Instant::now() - started > std::time::Duration::from_millis(16);
///
/// let mutations = dom.run_with_deadline(deadline).await;
/// apply_mutations(mutations);
/// }
/// ```
///
/// Note: this method is not async and does not provide suspense-like functionality. It is up to the renderer to provide the
/// executor and handlers for suspense as show in the example.
///
/// ```ignore
/// let (sender, receiver) = channel::new();
/// sender.send(EventTrigger::start());
///
/// let mut dom = VirtualDom::new();
/// dom.suspense_handler(|event| sender.send(event));
///
/// while let Ok(diffs) = dom.progress_with_event(receiver.recv().await) {
/// render(diffs);
/// }
///
/// ```
//
// Developer notes:
// ----
// This method has some pretty complex safety guarantees to uphold.
// We interact with bump arenas, raw pointers, and use UnsafeCell to get a partial borrow of the arena.
// The final EditList has edits that pull directly from the Bump Arenas which add significant complexity
// in crafting a 100% safe solution with traditional lifetimes. Consider this method to be internally unsafe
// but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework.
//
// A good project would be to remove all unsafe from this crate and move the unsafety into safer abstractions.
pub async fn progress_with_event<'s, Dom: RealDom<'s>>(
pub async fn run_with_deadline<'s>(
&'s mut self,
realdom: &'_ mut Dom,
edits: &mut Vec<DomEdit<'s>>,
) -> Result<()> {
let trigger = self.triggers.borrow_mut().pop().expect("failed");
mut deadline_exceeded: impl FnMut() -> bool,
) -> Result<Mutations<'s>> {
let cur_component = self.base_scope;
let mut diff_machine = DiffMachine::new(
edits,
realdom,
&self.components,
trigger.originator,
self.event_queue.clone(),
&self.tasks,
);
let mut diff_machine =
DiffMachine::new(Mutations { edits: Vec::new() }, cur_component, &self.shared);
match &trigger.event {
VirtualEvent::OtherEvent => todo!(),
/*
Strategy:
1. Check if there are any events in the receiver.
2. If there are, process them and create a new fiber.
3. If there are no events, then choose a fiber to work on.
4. If there are no fibers, then wait for the next event from the receiver.
5. While processing a fiber, periodically check if we're out of time
6. If we are almost out of time, then commit our edits to the realdom
7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported)
*/
// Nothing yet
VirtualEvent::AsyncEvent { .. } => {}
// 1. Consume any pending events and create new fibers
let mut receiver = self.shared.task_receiver.borrow_mut();
while let Ok(Some(trigger)) = receiver.try_next() {
// todo: cache the fibers
let mut fiber = Fiber::new();
// Suspense Events! A component's suspended node is updated
VirtualEvent::SuspenseEvent { hook_idx, domnode } => {
let scope = self.components.get_mut(trigger.originator).unwrap();
match &trigger.event {
// If any input event is received, then we need to create a new fiber
VirtualEvent::ClipboardEvent(_)
| VirtualEvent::CompositionEvent(_)
| VirtualEvent::KeyboardEvent(_)
| VirtualEvent::FocusEvent(_)
| VirtualEvent::FormEvent(_)
| VirtualEvent::SelectionEvent(_)
| VirtualEvent::TouchEvent(_)
| VirtualEvent::UIEvent(_)
| VirtualEvent::WheelEvent(_)
| VirtualEvent::MediaEvent(_)
| VirtualEvent::AnimationEvent(_)
| VirtualEvent::TransitionEvent(_)
| VirtualEvent::ToggleEvent(_)
| VirtualEvent::MouseEvent(_)
| VirtualEvent::PointerEvent(_) => {
if let Some(scope) = self.shared.get_scope_mut(trigger.originator) {
scope.call_listener(trigger)?;
}
}
// safety: we are sure that there are no other references to the inner content of this hook
let hook = unsafe { scope.hooks.get_mut::<SuspenseHook>(*hook_idx) }.unwrap();
VirtualEvent::AsyncEvent { .. } => {
while let Ok(Some(event)) = receiver.try_next() {
fiber.pending_scopes.push(event.originator);
}
}
let cx = Context { scope, props: &() };
let scx = SuspendedContext { inner: cx };
// These shouldn't normally be received, but if they are, it's done because some task set state manually
// Instead of batching the results,
VirtualEvent::ScheduledUpdate { height: u32 } => {}
// generate the new node!
let nodes: Option<VNode<'s>> = (&hook.callback)(scx);
let nodes = nodes.unwrap_or_else(|| errored_fragment());
let nodes = scope.cur_frame().bump.alloc(nodes);
// Suspense Events! A component's suspended node is updated
VirtualEvent::SuspenseEvent { hook_idx, domnode } => {
// Safety: this handler is the only thing that can mutate shared items at this moment in tim
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
// push the old node's root onto the stack
diff_machine.edits.push(domnode.get());
// safety: we are sure that there are no other references to the inner content of suspense hooks
let hook = unsafe { scope.hooks.get_mut::<SuspenseHook>(*hook_idx) }.unwrap();
// push these new nodes onto the diff machines stack
let meta = diff_machine.create(&*nodes);
let cx = Context { scope, props: &() };
let scx = SuspendedContext { inner: cx };
// replace the placeholder with the new nodes we just pushed on the stack
diff_machine.edits.replace_with(meta.added_to_stack);
}
// generate the new node!
let nodes: Option<VNode> = (&hook.callback)(scx);
match nodes {
None => {
log::warn!(
"Suspense event came through, but there were no generated nodes >:(."
);
}
Some(nodes) => {
// allocate inside the finished frame - not the WIP frame
let nodes = scope.frames.finished_frame().bump.alloc(nodes);
// This is the "meat" of our cooperative scheduler
// As updates flow in, we re-evalute the event queue and decide if we should be switching the type of work
//
// We use the reconciler to request new IDs and then commit/uncommit the IDs when the scheduler is finished
_ => {
self.components
.get_mut(trigger.originator)
.map(|f| f.call_listener(trigger));
// push the old node's root onto the stack
let real_id = domnode.get().ok_or(Error::NotMounted)?;
diff_machine.edit_push_root(real_id);
// Now, there are events in the queue
let mut updates = self.event_queue.queue.as_ref().borrow_mut();
// push these new nodes onto the diff machines stack
let meta = diff_machine.create_vnode(&*nodes);
// Order the nodes by their height, we want the nodes with the smallest depth on top
// This prevents us from running the same component multiple times
updates.sort_unstable();
// replace the placeholder with the new nodes we just pushed on the stack
diff_machine.edit_replace_with(1, meta.added_to_stack);
}
}
}
log::debug!("There are: {:#?} updates to be processed", updates.len());
// Collecting garabge is not currently interruptible.
//
// In the future, it could be though
VirtualEvent::GarbageCollection => {
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
// Iterate through the triggered nodes (sorted by height) and begin to diff them
for update in updates.drain(..) {
log::debug!("Running updates for: {:#?}", update);
let mut garbage_list = scope.consume_garbage();
// Make sure this isn't a node we've already seen, we don't want to double-render anything
// If we double-renderer something, this would cause memory safety issues
if diff_machine.seen_nodes.contains(&update.idx) {
continue;
let mut scopes_to_kill = Vec::new();
while let Some(node) = garbage_list.pop() {
match &node.kind {
VNodeKind::Text(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Anchor(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Suspended(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Element(el) => {
self.shared.collect_garbage(node.direct_id());
for child in el.children {
garbage_list.push(child);
}
}
VNodeKind::Fragment(frag) => {
for child in frag.children {
garbage_list.push(child);
}
}
VNodeKind::Component(comp) => {
// TODO: run the hook destructors and then even delete the scope
let scope_id = comp.ass_scope.get().unwrap();
let scope = self.get_scope(scope_id).unwrap();
let root = scope.root();
garbage_list.push(root);
scopes_to_kill.push(scope_id);
}
}
}
// Now, all the "seen nodes" are nodes that got notified by running this listener
diff_machine.seen_nodes.insert(update.idx.clone());
// Start a new mutable borrow to components
// We are guaranteeed that this scope is unique because we are tracking which nodes have modified
let cur_component = self.components.get_mut(update.idx).unwrap();
if cur_component.run_scope().is_ok() {
let (old, new) = (cur_component.old_frame(), cur_component.next_frame());
diff_machine.diff_node(old, new);
for scope in scopes_to_kill {
// oy kill em
log::debug!("should be removing scope {:#?}", scope);
}
}
}
}
Ok(())
while !deadline_exceeded() {
let mut receiver = self.shared.task_receiver.borrow_mut();
// no messages to receive, just work on the fiber
}
Ok(diff_machine.edits)
}
pub fn base_scope(&self) -> &Scope {
self.components.get(self.base_scope).unwrap()
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender<EventTrigger> {
self.shared.task_sender.clone()
}
fn get_scope_mut(&mut self, id: ScopeId) -> Option<&mut Scope> {
unsafe { self.shared.get_scope_mut(id) }
}
}
@@ -393,3 +473,36 @@ impl VirtualDom {
// These impls are actually wrong. The DOM needs to have a mutex implemented.
unsafe impl Sync for VirtualDom {}
unsafe impl Send for VirtualDom {}
struct Fiber<'a> {
// scopes that haven't been updated yet
pending_scopes: Vec<ScopeId>,
pending_nodes: Vec<*const VNode<'a>>,
// WIP edits
edits: Vec<DomEdit<'a>>,
started: bool,
completed: bool,
}
impl Fiber<'_> {
fn new() -> Self {
Self {
pending_scopes: Vec::new(),
pending_nodes: Vec::new(),
edits: Vec::new(),
started: false,
completed: false,
}
}
}
/// The "Mutations" object holds the changes that need to be made to the DOM.
pub struct Mutations<'s> {
// todo: apply node refs
// todo: apply effects
pub edits: Vec<DomEdit<'s>>,
}
+19
View File
@@ -0,0 +1,19 @@
use futures_channel::mpsc::unbounded;
#[async_std::test]
async fn channels() {
let (sender, mut receiver) = unbounded::<u32>();
// drop(sender);
match receiver.try_next() {
Ok(a) => {
dbg!(a);
}
Err(no) => {
dbg!(no);
}
}
sender.unbounded_send(1).unwrap();
}
+340
View File
@@ -0,0 +1,340 @@
//! Diffing Tests
//! -------------
//!
//! These should always compile and run, but the result is not validated for each test.
//! TODO: Validate the results beyond visual inspection.
use bumpalo::Bump;
use anyhow::{Context, Result};
use dioxus::{
arena::SharedResources,
diff::{CreateMeta, DiffMachine},
prelude::*,
DomEdit,
};
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
struct TestDom {
bump: Bump,
resources: SharedResources,
}
impl TestDom {
fn new() -> TestDom {
let bump = Bump::new();
let resources = SharedResources::new();
TestDom { bump, resources }
}
fn new_factory<'a>(&'a self) -> NodeFactory<'a> {
NodeFactory::new(&self.bump)
}
fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
use dioxus_core::nodes::{IntoVNode, IntoVNodeList};
lazy_nodes.into_vnode(NodeFactory::new(&self.bump))
}
fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Vec<DomEdit<'a>> {
let mut edits = Vec::new();
let mut machine = DiffMachine::new_headless(&mut edits, &self.resources);
machine.diff_node(old, new);
edits
}
fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> (CreateMeta, Vec<DomEdit<'a>>)
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let mut edits = Vec::new();
let mut machine = DiffMachine::new_headless(&mut edits, &self.resources);
let meta = machine.create_vnode(old);
(meta, edits)
}
fn lazy_diff<'a, F1, F2>(
&'a self,
left: LazyNodes<'a, F1>,
right: LazyNodes<'a, F2>,
) -> (Vec<DomEdit<'a>>, Vec<DomEdit<'a>>)
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
F2: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let new = self.bump.alloc(self.render(right));
let mut create_edits = Vec::new();
let mut machine = DiffMachine::new_headless(&mut create_edits, &self.resources);
machine.create_vnode(old);
let mut edits = Vec::new();
let mut machine = DiffMachine::new_headless(&mut edits, &self.resources);
machine.diff_node(old, new);
(create_edits, edits)
}
}
#[test]
fn diffing_works() {}
/// Should push the text node onto the stack and modify it
#[test]
fn html_and_rsx_generate_the_same_output() {
let dom = TestDom::new();
let edits = dom.lazy_diff(
rsx! ( div { "Hello world" } ),
rsx! ( div { "Goodbye world" } ),
);
dbg!(edits);
}
/// Should result in 3 elements on the stack
#[test]
fn fragments_create_properly() {
let dom = TestDom::new();
let (meta, edits) = dom.create(rsx! {
div { "Hello a" }
div { "Hello b" }
div { "Hello c" }
});
assert!(&edits[0].is("CreateElement"));
assert!(&edits[3].is("CreateElement"));
assert!(&edits[6].is("CreateElement"));
assert_eq!(meta.added_to_stack, 3);
dbg!(edits);
}
/// Should result in the creation of an anchor (placeholder) and then a replacewith
#[test]
fn empty_fragments_create_anchors() {
let dom = TestDom::new();
let left = rsx!({ (0..0).map(|f| rsx! { div {}}) });
let right = rsx!({ (0..1).map(|f| rsx! { div {}}) });
let edits = dom.lazy_diff(left, right);
dbg!(edits);
}
/// Should result in the creation of an anchor (placeholder) and then a replacewith m=5
#[test]
fn empty_fragments_create_many_anchors() {
let dom = TestDom::new();
let left = rsx!({ (0..0).map(|f| rsx! { div {}}) });
let right = rsx!({ (0..5).map(|f| rsx! { div {}}) });
let edits = dom.lazy_diff(left, right);
dbg!(edits);
}
/// Should result in the creation of an anchor (placeholder) and then a replacewith
/// Includes child nodes inside the fragment
#[test]
fn empty_fragments_create_anchors_with_many_children() {
let dom = TestDom::new();
let left = rsx!({ (0..0).map(|f| rsx! { div {} }) });
let right = rsx!({
(0..5).map(|f| {
rsx! { div { "hello" }}
})
});
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
let last_edit = edits.1.last().unwrap();
assert!(last_edit.is("ReplaceWith"));
}
/// Should result in every node being pushed and then replaced with an anchor
#[test]
fn many_items_become_fragment() {
let dom = TestDom::new();
let left = rsx!({
(0..2).map(|f| {
rsx! { div { "hello" }}
})
});
let right = rsx!({ (0..0).map(|f| rsx! { div {} }) });
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
/// Should result in no edits
#[test]
fn two_equal_fragments_are_equal() {
let dom = TestDom::new();
let left = rsx!({
(0..2).map(|f| {
rsx! { div { "hello" }}
})
});
let right = rsx!({
(0..2).map(|f| {
rsx! { div { "hello" }}
})
});
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
assert!(edits.1.is_empty());
}
/// Should result the creation of more nodes appended after the old last node
#[test]
fn two_fragments_with_differrent_elements_are_differet() {
let dom = TestDom::new();
let left = rsx!(
{(0..2).map(|f| {rsx! { div { }}})}
p {}
);
let right = rsx!(
{(0..5).map(|f| {rsx! { h1 { }}})}
p {}
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
/// Should result in multiple nodes destroyed - with changes to the first nodes
#[test]
fn two_fragments_with_differrent_elements_are_differet_shorter() {
let dom = TestDom::new();
let left = rsx!(
{(0..5).map(|f| {rsx! { div { }}})}
p {}
);
let right = rsx!(
{(0..2).map(|f| {rsx! { h1 { }}})}
p {}
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
/// Should result in multiple nodes destroyed - with no changes
#[test]
fn two_fragments_with_same_elements_are_differet() {
let dom = TestDom::new();
let left = rsx!(
{(0..2).map(|f| {rsx! { div { }}})}
p {}
);
let right = rsx!(
{(0..5).map(|f| {rsx! { div { }}})}
p {}
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
// Similar test from above, but with extra child nodes
#[test]
fn two_fragments_with_same_elements_are_differet_shorter() {
let dom = TestDom::new();
let left = rsx!(
{(0..5).map(|f| {rsx! { div { }}})}
p {"e"}
);
let right = rsx!(
{(0..2).map(|f| {rsx! { div { }}})}
p {"e"}
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
/// should result in the removal of elements
#[test]
fn keyed_diffing_order() {
let dom = TestDom::new();
let left = rsx!(
{(0..5).map(|f| {rsx! { div { key: "{f}" }}})}
p {"e"}
);
let right = rsx!(
{(0..2).map(|f| {rsx! { div { key: "{f}" }}})}
p {"e"}
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
#[test]
fn fragment_keys() {
let r = 1;
let p = rsx! {
Fragment { key: "asd {r}" }
};
}
/// Should result in moves, but not removals or additions
#[test]
fn keyed_diffing_out_of_order() {
let dom = TestDom::new();
// 0, 1, 2, 3, 4, 5, 6, 7, 8,
let left = rsx!({
(0..3).chain(3..6).chain(6..9).map(|f| {
rsx! { div { key: "{f}" }}
})
});
// 0, 1, 2, 6, 5, 4, 3, 7, 8, 9
let right = rsx!({
(0..3).chain((3..7).rev()).chain(7..10).map(|f| {
rsx! { div { key: "{f}" }}
})
});
// LIS: 3, 7, 8,
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
#[test]
fn controlled_keyed_diffing_out_of_order() {
let dom = TestDom::new();
let left = [4, 5, 6, 7];
let left = rsx!({
left.iter().map(|f| {
rsx! { div { key: "{f}" "{f}" }}
})
});
// 0, 1, 2, 6, 5, 4, 3, 7, 8, 9
let right = [0, 5, 9, 6, 4];
let right = rsx!({
right.iter().map(|f| {
rsx! { div { key: "{f}" "{f}" }}
})
});
// LIS: 3, 7, 8,
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
+32
View File
@@ -0,0 +1,32 @@
use bumpalo::Bump;
use anyhow::{Context, Result};
use dioxus::{
arena::SharedResources,
diff::{CreateMeta, DiffMachine},
prelude::*,
DomEdit,
};
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
#[async_std::test]
async fn event_queue_works() {
static App: FC<()> = |cx| {
cx.render(rsx! {
div { "hello world" }
})
};
let mut dom = VirtualDom::new(App);
let edits = dom.rebuild().unwrap();
async_std::task::spawn_local(async move {
match dom.run_unbounded().await {
Err(_) => todo!(),
Ok(mutations) => {
//
}
}
});
}
+99
View File
@@ -0,0 +1,99 @@
//! Rebuilding tests
//! ----------------
//!
//! This tests module ensures that the initial build of the virtualdom is correct.
//! This does not include dynamic tests or the diffing algorithm itself.
//!
//! It does prove that mounting works properly and the correct edit streams are generated.
//!
//! Don't have a good way to validate, everything is done manually ATM
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
#[test]
fn app_runs() {
static App: FC<()> = |cx| {
//
cx.render(rsx!( div{"hello"} ))
};
let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild_in_place().unwrap();
dbg!(edits);
}
#[test]
fn fragments_work() {
static App: FC<()> = |cx| {
cx.render(rsx!(
div{"hello"}
div{"goodbye"}
))
};
let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild_in_place().unwrap();
// should result in a final "appendchildren n=2"
dbg!(edits);
}
#[test]
fn lists_work() {
static App: FC<()> = |cx| {
cx.render(rsx!(
h1 {"hello"}
{(0..6).map(|f| rsx!(span{ "{f}" }))}
))
};
let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild_in_place().unwrap();
dbg!(edits);
}
#[test]
fn conditional_rendering() {
static App: FC<()> = |cx| {
cx.render(rsx!(
h1 {"hello"}
{true.then(|| rsx!(span{ "a" }))}
{false.then(|| rsx!(span{ "b" }))}
))
};
let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild_in_place().unwrap();
dbg!(&edits);
// the "false" fragment should generate an empty placeholder to re-visit
assert!(edits[edits.len() - 2].is("CreatePlaceholder"));
}
#[test]
fn child_components() {
static App: FC<()> = |cx| {
cx.render(rsx!(
{true.then(|| rsx!(Child { }))}
{false.then(|| rsx!(Child { }))}
))
};
static Child: FC<()> = |cx| {
cx.render(rsx!(
h1 {"hello"}
h1 {"goodbye"}
))
};
let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild_in_place().unwrap();
dbg!(edits);
}
#[test]
fn suspended_works() {
static App: FC<()> = |cx| {
let title = use_suspense(cx, || async { "bob" }, |cx, f| cx.render(rsx! { "{f}"}));
cx.render(rsx!("hello" { title }))
};
let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild_in_place().unwrap();
dbg!(edits);
}
+3 -2
View File
@@ -11,7 +11,7 @@ license = "MIT/Apache-2.0"
[dependencies]
# web-view = { git = "https://github.com/Boscop/web-view" }
dioxus-core = { path = "../core", version = "0.1.2", features = ["serialize"] }
anyhow = "1.0.38"
anyhow = "1.0"
argh = "0.1.4"
serde = "1.0.120"
serde_json = "1.0.61"
@@ -20,12 +20,13 @@ log = "0.4.13"
fern = { version = "0.6.0", features = ["colored"] }
html-escape = "0.2.9"
wry = "0.11.0"
async-std = { version = "1.9.0", features = ["attributes"] }
[dev-dependencies]
dioxus-html = { path = "../html" }
tide = "0.15.0"
tide-websockets = "0.3.0"
async-std = { version = "1.9.0", features = ["attributes"] }
# thiserror = "1.0.23"
# log = "0.4.13"
+2 -11
View File
@@ -1,8 +1,9 @@
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_html as dioxus_elements;
fn main() {
dioxus_desktop::launch(App, |f| f.with_maximized(true)).expect("Failed");
dioxus_desktop::launch(App, |f| f.with_window(|w| w.with_maximized(true))).expect("Failed");
}
static App: FC<()> = |cx| {
@@ -13,13 +14,3 @@ static App: FC<()> = |cx| {
}
))
};
mod dioxus_elements {
use super::*;
pub struct div;
impl DioxusElement for div {
const TAG_NAME: &'static str = "div";
const NAME_SPACE: Option<&'static str> = None;
}
pub trait GlobalAttributes {}
}
+45
View File
@@ -0,0 +1,45 @@
use std::ops::{Deref, DerefMut};
use dioxus_core::DomEdit;
use wry::{
application::{
error::OsError,
event_loop::EventLoopWindowTarget,
menu::MenuBar,
window::{Fullscreen, Icon, Window, WindowBuilder},
},
webview::{RpcRequest, RpcResponse},
};
pub struct DesktopConfig<'a> {
pub window: WindowBuilder,
pub(crate) manual_edits: Option<DomEdit<'a>>,
pub(crate) pre_rendered: Option<String>,
}
impl DesktopConfig<'_> {
/// Initializes a new `WindowBuilder` with default values.
#[inline]
pub fn new() -> Self {
Self {
window: Default::default(),
pre_rendered: None,
manual_edits: None,
}
}
pub fn with_prerendered(&mut self, content: String) -> &mut Self {
self.pre_rendered = Some(content);
self
}
pub fn with_window(&mut self, f: impl FnOnce(WindowBuilder) -> WindowBuilder) -> &mut Self {
// gots to do a swap because the window builder only takes itself as muy self
// I wish more people knew about returning &mut Self
let mut builder = WindowBuilder::default();
std::mem::swap(&mut self.window, &mut builder);
builder = f(builder);
std::mem::swap(&mut self.window, &mut builder);
self
}
}
+15 -21
View File
@@ -1,43 +1,37 @@
//! webview dom
use dioxus_core::{DomEdit, RealDom, RealDomNode, ScopeId};
use DomEdit::*;
use dioxus_core::{DomEdit, RealDom};
pub struct WebviewRegistry {}
// pub struct WebviewRegistry {}
impl WebviewRegistry {
pub fn new() -> Self {
Self {}
}
}
// impl WebviewRegistry {
// pub fn new() -> Self {
// Self {}
// }
// }
pub struct WebviewDom<'bump> {
pub edits: Vec<DomEdit<'bump>>,
pub node_counter: u64,
pub registry: WebviewRegistry,
// pub registry: WebviewRegistry,
}
impl WebviewDom<'_> {
pub fn new(registry: WebviewRegistry) -> Self {
pub fn new() -> Self {
Self {
edits: Vec::new(),
node_counter: 0,
registry,
// registry,
}
}
// Finish using the dom (for its edit list) and give back the node and event registry
pub fn consume(self) -> WebviewRegistry {
self.registry
}
// // Finish using the dom (for its edit list) and give back the node and event registry
// pub fn consume(self) -> WebviewRegistry {
// self.registry
// }
}
impl<'bump> RealDom<'bump> for WebviewDom<'bump> {
impl RealDom for WebviewDom<'_> {
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
todo!()
// self.edits.push(PushRoot { root });
}
fn request_available_node(&mut self) -> RealDomNode {
self.node_counter += 1;
RealDomNode::from_u64(self.node_counter)
}
}
+85
View File
@@ -0,0 +1,85 @@
//! Convert a serialized event to an event Trigger
//!
use std::rc::Rc;
use dioxus_core::{
events::{
on::{MouseEvent, MouseEventInner},
VirtualEvent,
},
ElementId, EventPriority, EventTrigger, ScopeId,
};
#[derive(serde::Serialize, serde::Deserialize)]
struct ImEvent {
event: String,
mounted_dom_id: u64,
scope: u64,
}
pub fn trigger_from_serialized(val: serde_json::Value) -> EventTrigger {
let mut data: Vec<ImEvent> = serde_json::from_value(val).unwrap();
let data = data.drain(..).next().unwrap();
let event = VirtualEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent)));
let scope = ScopeId(data.scope as usize);
let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize));
let priority = EventPriority::High;
EventTrigger::new(event, scope, mounted_dom_id, priority)
}
#[derive(Debug)]
struct WebviewMouseEvent;
impl MouseEventInner for WebviewMouseEvent {
fn alt_key(&self) -> bool {
todo!()
}
fn button(&self) -> i16 {
todo!()
}
fn buttons(&self) -> u16 {
todo!()
}
fn client_x(&self) -> i32 {
todo!()
}
fn client_y(&self) -> i32 {
todo!()
}
fn ctrl_key(&self) -> bool {
todo!()
}
fn meta_key(&self) -> bool {
todo!()
}
fn page_x(&self) -> i32 {
todo!()
}
fn page_y(&self) -> i32 {
todo!()
}
fn screen_x(&self) -> i32 {
todo!()
}
fn screen_y(&self) -> i32 {
todo!()
}
fn shift_key(&self) -> bool {
todo!()
}
fn get_modifier_state(&self, key_code: &str) -> bool {
todo!()
}
}
+207 -131
View File
@@ -2,143 +2,219 @@
<html>
<head>
<script>
class Interpreter {
constructor(root) {
this.stack = [root];
this.listeners = {};
this.lastNodeWasText = false;
this.nodes = {
0: root
};
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
}
class OPTABLE {
PushRoot(self, edit) {
const id = edit.root;
const node = self.nodes[id];
self.stack.push(node);
}
AppendChildren(self, edit) {
let root = self.stack[self.stack.length - (edit.many + 1)];
for (let i = 0; i < edit.many; i++) {
console.log("popping ", i, edit.many);
let node = self.pop();
root.appendChild(node);
}
}
ReplaceWith(self, edit) {
let root = self.stack[self.stack.length - (edit.many + 1)];
let els = [];
for (let i = 0; i < edit.many; i++) {
els.push(self.pop());
}
root.replaceWith(...els);
}
Remove(self, edit) {
const node = self.stack.pop();
node.remove();
}
RemoveAllChildren(self, edit) {}
CreateTextNode(self, edit) {
self.stack.push(document.createTextNode(edit.text));
}
CreateElement(self, edit) {
const tagName = edit.tag;
console.log(`creating element: `, edit);
self.stack.push(document.createElement(tagName));
}
CreateElementNs(self, edit) {
const tagName = edit.tag;
console.log(`creating namespaced element: `, edit);
self.stack.push(document.createElementNS(edit.ns, edit.tag));
}
CreatePlaceholder(self, edit) {
const a = `self.stack.push(document.createElement("pre"))`;
self.stack.push(document.createComment("vroot"));
}
NewEventListener(self, edit) {}
RemoveEventListener(self, edit) {}
SetText(self, edit) {
self.top().textContent = edit.text;
}
SetAttribute(self, edit) {
const name = edit.field;
const value = edit.value;
const ns = edit.ns;
const node = self.top(self.stack);
if (ns == "style") {
node.style[name] = value;
} else if (ns !== undefined) {
node.setAttributeNS(ns, name, value);
} else {
node.setAttribute(name, value);
}
if ((name === "value", self)) {
node.value = value;
}
if ((name === "checked", self)) {
node.checked = true;
}
if ((name === "selected", self)) {
node.selected = true;
}
}
RemoveAttribute(self, edit) {
const name = edit.field;
const node = self.top(self.stack);
node.removeAttribute(name);
if ((name === "value", self)) {
node.value = null;
}
if ((name === "checked", self)) {
node.checked = false;
}
if ((name === "selected", self)) {
node.selected = false;
}
}
}
const op_table = new OPTABLE();
async function initialize() {
const reply = await rpc.call('initiate');
const interpreter = new Interpreter(window.document.getElementById("_dioxusroot"));
console.log(reply);
for (let x = 0; x < reply.length; x++) {
let edit = reply[x];
console.log(edit);
op_table[edit.type](interpreter, edit);
}
console.log("stack completed: ", interpreter.stack);
}
console.log("initializing...");
initialize();
</script>
</head>
<body>
<div id="_dioxusroot">
</div>
</body>
<script>
class Interpreter {
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {
"onclick": {}
};
this.lastNodeWasText = false;
this.nodes = [root, root, root, root];
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
PushRoot(edit) {
const id = edit.id;
const node = this.nodes[id];
console.log("pushing root ", node, "with id", id);
this.stack.push(node);
}
PopRoot(edit) {
this.stack.pop();
}
AppendChildren(edit) {
let root = this.stack[this.stack.length - (edit.many + 1)];
for (let i = 0; i < edit.many; i++) {
console.log("popping ", i, edit.many);
let node = this.pop();
root.appendChild(node);
}
}
ReplaceWith(edit) {
let root = this.stack[this.stack.length - (edit.many + 1)];
let els = [];
for (let i = 0; i < edit.many; i++) {
els.push(this.pop());
}
root.replaceWith(...els);
}
Remove(edit) {
const node = this.stack.pop();
node.remove();
}
RemoveAllChildren(edit) {}
CreateTextNode(edit) {
const node = document.createTextNode(edit.text);
this.nodes[edit.id] = node;
this.stack.push(node);
}
CreateElement(edit) {
const tagName = edit.tag;
const el = document.createElement(tagName);
this.nodes[edit.id] = el;
console.log(`creating element: `, edit);
this.stack.push(el);
}
CreateElementNs(edit) {
const tagName = edit.tag;
console.log(`creating namespaced element: `, edit);
this.stack.push(document.createElementNS(edit.ns, edit.tag));
}
CreatePlaceholder(edit) {
const a = `this.stack.push(document.createElement(" pre"))`;
this.stack.push(document.createComment("vroot"));
}
NewEventListener(edit) {
const element_id = edit.element_id;
const event_name = edit.event_name;
const mounted_node_id = edit.mounted_node_id;
const scope = edit.scope;
const element = this.top();
element.setAttribute(`dioxus-event-${event_name}`, `${scope}.${mounted_node_id}`);
console.log("listener map is", this.listeners);
if (this.listeners[event_name] === undefined) {
console.log("adding listener!");
this.listeners[event_name] = "bla";
this.root.addEventListener(event_name, (event) => {
const target = event.target;
const type = event.type;
const val = target.getAttribute(`dioxus-event-${event_name}`);
const fields = val.split(".");
const scope_id = parseInt(fields[0]);
const real_id = parseInt(fields[1]);
console.log(`parsed event with scope_id ${scope_id} and real_id ${real_id}`);
rpc.call('user_event', {
event: event_name,
scope: scope_id,
mounted_dom_id: real_id,
}).then((reply) => {
console.log(reply);
this.stack.push(this.root);
let edits = reply.edits;
for (let x = 0; x < edits.length; x++) {
let edit = edits[x];
console.log(edit);
let f = this[edit.type];
f.call(this, edit);
}
console.log("initiated");
}).catch((err) => {
console.log("failed to initiate", err);
});
});
}
}
RemoveEventListener(edit) {}
SetText(edit) {
this.top().textContent = edit.text;
}
SetAttribute(edit) {
const name = edit.field;
const value = edit.value;
const ns = edit.ns;
const node = this.top(this.stack);
if (ns == "style") {
node.style[name] = value;
} else if (ns !== undefined) {
node.setAttributeNS(ns, name, value);
} else {
node.setAttribute(name, value);
}
if (name === "value") {
node.value = value;
}
if (name === "checked") {
node.checked = true;
}
if (name === "selected") {
node.selected = true;
}
}
RemoveAttribute(edit) {
const name = edit.field;
const node = this.top(this.stack);
node.removeAttribute(name);
if (name === "value") {
node.value = null;
}
if (name === "checked") {
node.checked = false;
}
if (name === "selected") {
node.selected = false;
}
}
}
async function initialize() {
const reply = await rpc.call('initiate');
let root = window.document.getElementById("_dioxusroot");
const interpreter = new Interpreter(root);
console.log(reply);
let pre_rendered = reply.pre_rendered;
if (pre_rendered !== undefined) {
root.innerHTML = pre_rendered;
}
const edits = reply.edits;
for (let x = 0; x < edits.length; x++) {
let edit = edits[x];
console.log(edit);
let f = interpreter[edit.type];
f.call(interpreter, edit);
}
console.log("stack completed: ", interpreter.stack);
}
console.log("initializing...");
initialize();
</script>
</html>
+130 -121
View File
@@ -3,7 +3,9 @@ use std::ops::{Deref, DerefMut};
use std::sync::mpsc::channel;
use std::sync::{Arc, RwLock};
use cfg::DesktopConfig;
use dioxus_core::*;
use serde::{Deserialize, Serialize};
pub use wry;
use wry::application::event::{Event, WindowEvent};
@@ -15,21 +17,24 @@ use wry::{
webview::{RpcRequest, RpcResponse},
};
mod cfg;
mod dom;
mod escape;
mod events;
use events::*;
static HTML_CONTENT: &'static str = include_str!("./index.html");
pub fn launch(
root: FC<()>,
builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
) -> anyhow::Result<()> {
launch_with_props(root, (), builder)
}
pub fn launch_with_props<P: Properties + 'static>(
root: FC<P>,
props: P,
builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
) -> anyhow::Result<()> {
WebviewRenderer::run(root, props, builder)
}
@@ -44,11 +49,17 @@ enum RpcEvent<'a> {
Initialize { edits: Vec<DomEdit<'a>> },
}
#[derive(Serialize)]
struct Response<'a> {
pre_rendered: Option<String>,
edits: Vec<DomEdit<'a>>,
}
impl<T: Properties + 'static> WebviewRenderer<T> {
pub fn run(
root: FC<T>,
props: T,
user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
user_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
) -> anyhow::Result<()> {
Self::run_with_edits(root, props, user_builder, None)
}
@@ -56,73 +67,131 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
pub fn run_with_edits(
root: FC<T>,
props: T,
user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
user_builder: impl for<'a, 'b> FnOnce(&'a mut DesktopConfig<'b>) -> &'a mut DesktopConfig<'b>,
redits: Option<Vec<DomEdit<'static>>>,
) -> anyhow::Result<()> {
log::info!("hello edits");
let event_loop = EventLoop::new();
let window = user_builder(WindowBuilder::new()).build(&event_loop)?;
let mut cfg = DesktopConfig::new();
user_builder(&mut cfg);
let vir = VirtualDom::new_with_props(root, props);
let DesktopConfig {
window,
manual_edits,
pre_rendered,
} = cfg;
let window = window.build(&event_loop)?;
let mut vir = VirtualDom::new_with_props(root, props);
let channel = vir.get_event_sender();
struct WebviewBridge {}
// impl RealDom for WebviewBridge {
// fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
// todo!()
// }
// fn must_commit(&self) -> bool {
// false
// }
// fn commit_edits<'a>(&mut self, edits: &mut Vec<DomEdit<'a>>) {}
// fn wait_until_ready<'s>(
// &'s mut self,
// ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + 's>> {
// //
// Box::pin(async {
// //
// })
// }
// }
let mut real_dom = WebviewBridge {};
// async_std::task::spawn_local(vir.run(&mut real_dom));
// todo: combine these or something
let vdom = Arc::new(RwLock::new(vir));
let registry = Arc::new(RwLock::new(Some(WebviewRegistry::new())));
// let registry = Arc::new(RwLock::new(Some(WebviewRegistry::new())));
let webview = WebViewBuilder::new(window)?
// .with_visible(false)
// .with_transparent(true)
.with_url(&format!("data:text/html,{}", HTML_CONTENT))?
.with_rpc_handler(move |_window: &Window, mut req: RpcRequest| {
match req.method.as_str() {
"initiate" => {
let edits = if let Some(edits) = &redits {
serde_json::to_value(edits).unwrap()
} else {
let mut lock = vdom.write().unwrap();
let mut reg_lock = registry.write().unwrap();
// Create the thin wrapper around the registry to collect the edits into
let mut real = dom::WebviewDom::new(reg_lock.take().unwrap());
// Serialize the edit stream
let edits = {
let mut edits = Vec::new();
lock.rebuild(&mut real, &mut edits).unwrap();
serde_json::to_value(edits).unwrap()
};
// Give back the registry into its slot
*reg_lock = Some(real.consume());
edits
};
// Return the edits into the webview runtime
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
}
"user_event" => {
let mut lock = vdom.write().unwrap();
let mut reg_lock = registry.write().unwrap();
// Create the thin wrapper around the registry to collect the edits into
let mut real = dom::WebviewDom::new(reg_lock.take().unwrap());
// Serialize the edit stream
let edits = {
let mut edits = Vec::new();
lock.rebuild(&mut real, &mut edits).unwrap();
serde_json::to_value(edits).unwrap()
};
// Give back the registry into its slot
*reg_lock = Some(real.consume());
// Return the edits into the webview runtime
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
}
_ => todo!("this message failed"),
}
})
// .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| {
// match req.method.as_str() {
// "initiate" => {
// let edits = if let Some(edits) = &redits {
// serde_json::to_value(edits).unwrap()
// } else {
// let mut lock = vdom.write().unwrap();
// // let mut reg_lock = registry.write().unwrap();
// // Create the thin wrapper around the registry to collect the edits into
// let mut real = dom::WebviewDom::new();
// let pre = pre_rendered.clone();
// let response = match pre {
// Some(content) => {
// lock.rebuild_in_place().unwrap();
// Response {
// edits: Vec::new(),
// pre_rendered: Some(content),
// }
// }
// None => {
// //
// let edits = {
// // let mut edits = Vec::new();
// todo!()
// // lock.rebuild(&mut real, &mut edits).unwrap();
// // edits
// };
// Response {
// edits,
// pre_rendered: None,
// }
// }
// };
// serde_json::to_value(&response).unwrap()
// };
// // Return the edits into the webview runtime
// Some(RpcResponse::new_result(req.id.take(), Some(edits)))
// }
// "user_event" => {
// log::debug!("User event received");
// // let registry = registry.clone();
// let vdom = vdom.clone();
// let response = async_std::task::block_on(async move {
// let mut lock = vdom.write().unwrap();
// // let mut reg_lock = registry.write().unwrap();
// // a deserialized event
// let data = req.params.unwrap();
// log::debug!("Data: {:#?}", data);
// let event = trigger_from_serialized(data);
// // lock.queue_event(event);
// // Create the thin wrapper around the registry to collect the edits into
// let mut real = dom::WebviewDom::new();
// // Serialize the edit stream
// //
// let mut edits = Vec::new();
// // lock.run(&mut real, &mut edits)
// // .await
// // .expect("failed to progress");
// let response = Response {
// edits,
// pre_rendered: None,
// };
// let response = serde_json::to_value(&response).unwrap();
// // Give back the registry into its slot
// // *reg_lock = Some(real.consume());
// // Return the edits into the webview runtime
// Some(RpcResponse::new_result(req.id.take(), Some(response)))
// });
// response
// // spawn a task to clean up the garbage
// }
// _ => todo!("this message failed"),
// }
// })
.build()?;
event_loop.run(move |event, _, control_flow| {
@@ -138,33 +207,17 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
}
_ => {}
},
Event::MainEventsCleared => {
webview.resize();
// window.request_redraw();
}
_ => {} // Event::WindowEvent { event, .. } => {
// //
// match event {
// WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
// _ => {}
// }
// }
// _ => {
// // let _ = webview.resize();
// }
_ => {}
}
});
}
/// Create a new text-renderer instance from a functional component root.
/// Automatically progresses the creation of the VNode tree to completion.
///
/// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom`
// pub fn new(root: FC<T>, builder: impl FnOnce() -> WVResult<WebView<'static, ()>>) -> Self {
// Self { root }
// }
/// Create a new text renderer from an existing Virtual DOM.
/// This will progress the existing VDom's events to completion.
pub fn from_vdom() -> Self {
@@ -181,47 +234,3 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
todo!()
}
}
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::dom::WebviewRegistry;
#[derive(Debug, Serialize, Deserialize)]
struct MessageParameters {
message: String,
}
fn HANDLER(window: &Window, mut req: RpcRequest) -> Option<RpcResponse> {
let mut response = None;
if &req.method == "fullscreen" {
if let Some(params) = req.params.take() {
if let Ok(mut args) = serde_json::from_value::<Vec<bool>>(params) {
if !args.is_empty() {
if args.swap_remove(0) {
window.set_fullscreen(Some(Fullscreen::Borderless(None)));
} else {
window.set_fullscreen(None);
}
};
response = Some(RpcResponse::new_result(req.id.take(), None));
}
}
} else if &req.method == "send-parameters" {
if let Some(params) = req.params.take() {
if let Ok(mut args) = serde_json::from_value::<Vec<MessageParameters>>(params) {
let result = if !args.is_empty() {
let msg = args.swap_remove(0);
Some(Value::String(format!("Hello, {}!", msg.message)))
} else {
// NOTE: in the real-world we should send an error response here!
None
};
// Must always send a response as this is a `call()`
response = Some(RpcResponse::new_result(req.id.take(), result));
}
}
}
response
}
+3 -3
View File
@@ -52,7 +52,7 @@ use std::{
pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T, P>(
cx: Context<'a, P>,
initial_state_fn: F,
) -> UseState<T> {
) -> UseState<'a, T> {
cx.use_hook(
move |_| UseStateInner {
current_val: initial_state_fn(),
@@ -212,8 +212,8 @@ impl<'a, T: Copy + Div<T, Output = T>> DivAssign<T> for UseState<'a, T> {
self.set(self.inner.current_val.div(rhs));
}
}
impl<'a, T: PartialEq<T>> PartialEq<T> for UseState<'a, T> {
fn eq(&self, other: &T) -> bool {
impl<'a, V, T: PartialEq<V>> PartialEq<V> for UseState<'a, T> {
fn eq(&self, other: &V) -> bool {
self.get() == other
}
}
+30
View File
@@ -0,0 +1,30 @@
trait AsAttributeValue: Sized {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a>;
}
enum AttributeValue<'a> {
Int(i32),
Float(f32),
Str(&'a str),
Bool(bool),
}
impl<'b> AsAttributeValue for Arguments<'b> {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> {
todo!()
}
}
impl AsAttributeValue for &'static str {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> {
todo!()
}
}
impl AsAttributeValue for f32 {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> {
todo!()
}
}
impl AsAttributeValue for i32 {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> {
todo!()
}
}
+1 -30
View File
@@ -1,3 +1,4 @@
#![allow(non_snake_case)]
//! # Dioxus Namespace for HTML
//!
//! This crate provides a set of compile-time correct HTML elements that can be used with the Rsx and Html macros.
@@ -2100,33 +2101,3 @@ summary
slot
template
*/
trait AsAttributeValue: Sized {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a>;
}
enum AttributeValue<'a> {
Int(i32),
Float(f32),
Str(&'a str),
Bool(bool),
}
impl<'b> AsAttributeValue for Arguments<'b> {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> {
todo!()
}
}
impl AsAttributeValue for &'static str {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> {
todo!()
}
}
impl AsAttributeValue for f32 {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> {
todo!()
}
}
impl AsAttributeValue for i32 {
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> {
todo!()
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.42"
anyhow = "1.0"
# cacao = { git = "https://github.com/ryanmcgrath/cacao" }
dioxus-core = { path = "../core", version = "0.1.2" }
log = "0.4.14"
+2 -7
View File
@@ -1,6 +1,6 @@
//! webview dom
use dioxus_core::{DomEdit, RealDom, RealDomNode, ScopeId};
use dioxus_core::{DomEdit, ElementId, RealDom, ScopeId};
use DomEdit::*;
pub struct WebviewRegistry {}
@@ -30,14 +30,9 @@ impl WebviewDom<'_> {
self.registry
}
}
impl<'bump> RealDom<'bump> for WebviewDom<'bump> {
impl<'bump> RealDom for WebviewDom<'bump> {
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
todo!()
// self.edits.push(PushRoot { root });
}
fn request_available_node(&mut self) -> RealDomNode {
self.node_counter += 1;
RealDomNode::from_u64(self.node_counter)
}
}
+3 -2
View File
@@ -118,7 +118,7 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
// Serialize the edit stream
let edits = {
let mut edits = Vec::new();
lock.rebuild(&mut real, &mut edits).unwrap();
lock.rebuild(&mut edits).unwrap();
serde_json::to_value(edits).unwrap()
};
@@ -140,7 +140,7 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
// Serialize the edit stream
let edits = {
let mut edits = Vec::new();
lock.rebuild(&mut real, &mut edits).unwrap();
lock.rebuild(&mut edits).unwrap();
serde_json::to_value(edits).unwrap()
};
@@ -185,6 +185,7 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
});
}
}
// brad johnson go chat
fn main() {
init_logging();
+1 -1
View File
@@ -17,7 +17,7 @@ tide-websockets = "*"
thiserror = "1.0.23"
log = "0.4.13"
fern = { version = "0.6.0", features = ["colored"] }
anyhow = "1.0.38"
anyhow = "1.0"
argh = "0.1.4"
serde = "1.0.120"
serde_json = "1.0.61"
+6
View File
@@ -16,3 +16,9 @@ vdom.rebuild_in_place();
let text = dioxus_ssr::render_root(&vdom);
assert_eq!(text, "<div>hello world!</div>")
```
## Pre-rendering
+10
View File
@@ -0,0 +1,10 @@
//! Example: realworld usage of hydration
//!
//!
use dioxus::virtual_dom::VirtualDom;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_hooks::use_state;
use dioxus_html as dioxus_elements;
fn main() {}
+1 -1
View File
@@ -22,7 +22,7 @@ async fn main() -> Result<(), std::io::Error> {
let dom = VirtualDom::launch_with_props_in_place(Example, ExampleProps { initial_name });
Ok(Response::builder(200)
.body(format!("{}", dioxus_ssr::render_vdom(&dom)))
.body(format!("{}", dioxus_ssr::render_vdom(&dom, |c| c)))
.content_type(tide::http::mime::HTML)
.build())
});
+5 -2
View File
@@ -15,8 +15,11 @@ fn main() {
let mut dom = VirtualDom::new(App);
dom.rebuild_in_place().expect("failed to run virtualdom");
file.write_fmt(format_args!("{}", TextRenderer::from_vdom(&dom)))
.unwrap();
file.write_fmt(format_args!(
"{}",
TextRenderer::from_vdom(&dom, Default::default())
))
.unwrap();
}
pub static App: FC<()> = |cx| {
+113 -51
View File
@@ -1,3 +1,7 @@
//!
//!
//!
//!
//! This crate demonstrates how to implement a custom renderer for Dioxus VNodes via the `TextRenderer` renderer.
//! The `TextRenderer` consumes a Dioxus Virtual DOM, progresses its event queue, and renders the VNodes to a String.
//!
@@ -12,8 +16,11 @@ use dioxus_core::*;
pub fn render_vnode(vnode: &VNode, string: &mut String) {}
pub fn render_vdom(dom: &VirtualDom) -> String {
format!("{:}", TextRenderer::from_vdom(dom))
pub fn render_vdom(dom: &VirtualDom, cfg: impl FnOnce(SsrConfig) -> SsrConfig) -> String {
format!(
"{:}",
TextRenderer::from_vdom(dom, cfg(SsrConfig::default()))
)
}
pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
@@ -21,35 +28,12 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
"{:}",
TextRenderer {
cfg: SsrConfig::default(),
root: vdom.components.get(scope).unwrap().root(),
root: vdom.get_scope(scope).unwrap().root(),
vdom: Some(vdom)
}
))
}
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,
// 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 Default for SsrConfig {
fn default() -> Self {
Self {
indent: false,
newline: false,
_skip_components: false,
}
}
}
/// A configurable text renderer for the Dioxus VirtualDOM.
///
///
@@ -60,7 +44,7 @@ impl Default for SsrConfig {
///
/// ## Example
/// ```ignore
/// const App: FC<()> = |cx| cx.render(rsx!(div { "hello world" }));
/// static App: FC<()> = |cx| cx.render(rsx!(div { "hello world" }));
/// let mut vdom = VirtualDom::new(App);
/// vdom.rebuild_in_place();
///
@@ -81,11 +65,11 @@ impl Display for TextRenderer<'_> {
}
impl<'a> TextRenderer<'a> {
pub fn from_vdom(vdom: &'a VirtualDom) -> Self {
pub fn from_vdom(vdom: &'a VirtualDom, cfg: SsrConfig) -> Self {
Self {
cfg,
root: vdom.base_scope().root(),
vdom: Some(vdom),
cfg: SsrConfig::default(),
}
}
@@ -99,6 +83,15 @@ impl<'a> TextRenderer<'a> {
}
write!(f, "{}", text.text)?
}
VNodeKind::Anchor(anchor) => {
//
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
}
}
write!(f, "<!-- -->")?;
}
VNodeKind::Element(el) => {
if self.cfg.indent {
for _ in 0..il {
@@ -132,6 +125,21 @@ impl<'a> TextRenderer<'a> {
}
}
// we write the element's id as a data attribute
//
// when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add
// them interpreter's stack
match (self.cfg.pre_render, node.try_direct_id()) {
(true, Some(id)) => {
write!(f, " dio_el=\"{}\"", id)?;
//
for listener in el.listeners {
// write the listeners
}
}
_ => {}
}
match self.cfg.newline {
true => write!(f, ">\n")?,
false => write!(f, ">")?,
@@ -162,9 +170,14 @@ impl<'a> TextRenderer<'a> {
}
VNodeKind::Component(vcomp) => {
let idx = vcomp.ass_scope.get().unwrap();
if let Some(vdom) = self.vdom {
let new_node = vdom.components.get(idx).unwrap().root();
self.html_render(new_node, f, il + 1)?;
match (self.vdom, self.cfg.skip_components) {
(Some(vdom), false) => {
let new_node = vdom.get_scope(idx).unwrap().root();
self.html_render(new_node, f, il + 1)?;
}
_ => {
// render the component by name
}
}
}
VNodeKind::Suspended { .. } => {
@@ -175,6 +188,52 @@ impl<'a> TextRenderer<'a> {
}
}
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 Default for SsrConfig {
fn default() -> Self {
Self {
indent: false,
pre_render: false,
newline: false,
skip_components: false,
}
}
}
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
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -182,15 +241,14 @@ mod tests {
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_html as dioxus_elements;
use dioxus_html::GlobalAttributes;
const SIMPLE_APP: FC<()> = |cx| {
static SIMPLE_APP: FC<()> = |cx| {
cx.render(rsx!(div {
"hello world!"
}))
};
const SLIGHTLY_MORE_COMPLEX: FC<()> = |cx| {
static SLIGHTLY_MORE_COMPLEX: FC<()> = |cx| {
cx.render(rsx! {
div {
title: "About W3Schools"
@@ -209,14 +267,14 @@ mod tests {
})
};
const NESTED_APP: FC<()> = |cx| {
static NESTED_APP: FC<()> = |cx| {
cx.render(rsx!(
div {
SIMPLE_APP {}
}
))
};
const FRAGMENT_APP: FC<()> = |cx| {
static FRAGMENT_APP: FC<()> = |cx| {
cx.render(rsx!(
div { "f1" }
div { "f2" }
@@ -229,21 +287,28 @@ mod tests {
fn to_string_works() {
let mut dom = VirtualDom::new(SIMPLE_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dbg!(render_vdom(&dom));
dbg!(render_vdom(&dom, |c| c));
}
#[test]
fn hydration() {
let mut dom = VirtualDom::new(NESTED_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dbg!(render_vdom(&dom, |c| c.pre_render(true)));
}
#[test]
fn nested() {
let mut dom = VirtualDom::new(NESTED_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dbg!(render_vdom(&dom));
dbg!(render_vdom(&dom, |c| c));
}
#[test]
fn fragment_app() {
let mut dom = VirtualDom::new(FRAGMENT_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dbg!(render_vdom(&dom));
dbg!(render_vdom(&dom, |c| c));
}
#[test]
@@ -256,26 +321,23 @@ mod tests {
let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX);
dom.rebuild_in_place().expect("failed to run virtualdom");
file.write_fmt(format_args!("{}", TextRenderer::from_vdom(&dom)))
.unwrap();
file.write_fmt(format_args!(
"{}",
TextRenderer::from_vdom(&dom, SsrConfig::default())
))
.unwrap();
}
#[test]
fn styles() {
const STLYE_APP: FC<()> = |cx| {
//
static STLYE_APP: FC<()> = |cx| {
cx.render(rsx! {
div {
style: {
color: "blue",
font_size: "46px"
}
}
div { style: { color: "blue", font_size: "46px" } }
})
};
let mut dom = VirtualDom::new(STLYE_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dbg!(render_vdom(&dom));
dbg!(render_vdom(&dom, |c| c));
}
}
+5 -4
View File
@@ -22,18 +22,18 @@ generational-arena = "0.2.8"
wasm-bindgen-test = "0.3.21"
once_cell = "1.8"
async-channel = "1.6.1"
anyhow = "1.0.41"
slotmap = "1.0.3"
anyhow = "1.0"
futures-util = "0.3.15"
[dependencies.web-sys]
version = "0.3.50"
version = "0.3.51"
features = [
"Comment",
"Attr",
"Document",
"Element",
"CssStyleDeclaration",
"HtmlElement",
"HtmlInputElement",
"HtmlSelectElement",
@@ -57,6 +57,7 @@ features = [
"PointerEvent",
"FocusEvent",
"CompositionEvent",
"ClipboardEvent",
"DocumentType",
"CharacterData",
"SvgElement",
@@ -79,7 +80,7 @@ uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] }
dioxus-hooks = { path = "../hooks" }
gloo-timers = { version = "0.2.1", features = ["futures"] }
serde = { version = "1.0.126", features = ["derive"] }
surf = { version = "2.2.0", default-features = false, features = [
surf = { git = "https://github.com/http-rs/surf", rev = "1ffaba8873", default-features = false, features = [
"wasm-client",
] }
# wasm-timer = "0.2.5"
+2 -1
View File
@@ -31,7 +31,8 @@ const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random/";
static App: FC<()> = |cx| {
let state = use_state(cx, || 0);
let dog_node = cx.use_suspense(
let dog_node = use_suspense(
cx,
|| surf::get(ENDPOINT).recv_json::<DogApi>(),
|cx, res| match res {
Ok(res) => rsx!(in cx, img { src: "{res.message}" }),
+45
View File
@@ -0,0 +1,45 @@
//! Basic example that renders a simple VNode to the browser.
use dioxus::events::on::MouseEvent;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
// use wasm_timer;
use std::future::Future;
use std::{pin::Pin, time::Duration};
use dioxus::prelude::*;
use dioxus_web::*;
fn main() {
// Setup logging
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
// Run the app
dioxus_web::launch(App, |c| c)
}
static App: FC<()> = |cx| {
let mut count = use_state(cx, || 0);
cx.render(rsx! {
div {
button {
"add"
onclick: move |_| count += 1
}
ul {
{(0..*count).map(|f| rsx!{
li { "a - {f}" }
li { "b - {f}" }
li { "c - {f}" }
})}
}
}
})
};
+68
View File
@@ -0,0 +1,68 @@
//! Example: Webview Renderer
//! -------------------------
//!
//! This example shows how to use the dioxus_desktop crate to build a basic desktop application.
//!
//! Under the hood, the dioxus_desktop crate bridges a native Dioxus VirtualDom with a custom prebuit application running
//! in the webview runtime. Custom handlers are provided for the webview instance to consume patches and emit user events
//! into the native VDom instance.
//!
//! Currently, NodeRefs won't work properly, but all other event functionality will.
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
// #[cfg]
fn main() {
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
dioxus_web::launch(App, |c| c);
// env_logger::init();
// dioxus::web::launch(App, |c| c);
}
static App: FC<()> = |cx| {
dbg!("rednering parent");
cx.render(rsx! {
div {
But {
h1 {"he"}
}
// But {
// h1 {"llo"}
// }
// But {
// h1 {"world"}
// }
}
})
};
static But: FC<()> = |cx| {
let mut count = use_state(cx, || 0);
// let d = Dropper { name: "asd" };
// let handler = move |_| {
// dbg!(d.name);
// };
cx.render(rsx! {
div {
h1 { "Hifive counter: {count}" }
{cx.children()}
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
// button { onclick: {handler}, "Down low!" }
}
})
};
// struct Dropper {
// name: &'static str,
// }
// impl Drop for Dropper {
// fn drop(&mut self) {
// dbg!("dropped");
// }
// }
+30
View File
@@ -0,0 +1,30 @@
pub struct WebConfig {
pub(crate) hydrate: bool,
pub(crate) rootname: String,
}
impl Default for WebConfig {
fn default() -> Self {
Self {
hydrate: false,
rootname: "dioxusroot".to_string(),
}
}
}
impl WebConfig {
/// Enable SSR hydration
///
/// This enables Dioxus to pick up work from a pre-renderd HTML file. Hydration will completely skip over any async
/// work and suspended nodes.
///
/// Dioxus will load up all the elements with the `dio_el` data attribute into memory when the page is loaded.
///
pub fn hydrate(mut self, f: bool) -> Self {
self.hydrate = f;
self
}
pub fn rootname(mut self, name: impl Into<String>) -> Self {
self.rootname = name.into();
self
}
}
@@ -2,24 +2,28 @@ use std::{collections::HashMap, rc::Rc, sync::Arc};
use dioxus_core::{
events::{EventTrigger, VirtualEvent},
DomEdit, RealDomNode, ScopeId,
DomEdit, ElementId, ScopeId,
};
use fxhash::FxHashMap;
use slotmap::{DefaultKey, Key, KeyData};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node,
window, CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
HtmlOptionElement, Node, NodeList,
};
use crate::{nodeslab::NodeSlab, WebConfig};
pub struct WebsysDom {
pub stack: Stack,
nodes: slotmap::SlotMap<DefaultKey, Option<Node>>,
stack: Stack,
/// A map from ElementID (index) to Node
nodes: NodeSlab,
document: Document,
root: Element,
event_receiver: async_channel::Receiver<EventTrigger>,
trigger: Arc<dyn Fn(EventTrigger)>,
sender_callback: Rc<dyn Fn(EventTrigger)>,
// map of listener types to number of those listeners
// This is roughly a delegater
@@ -34,42 +38,45 @@ pub struct WebsysDom {
last_node_was_text: bool,
}
impl WebsysDom {
pub fn new(root: Element) -> Self {
let document = window()
.expect("must have access to the window")
.document()
.expect("must have access to the Document");
pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc<dyn Fn(EventTrigger)>) -> Self {
let document = load_document();
let (sender, receiver) = async_channel::unbounded::<EventTrigger>();
let mut nodes = NodeSlab::new(2000);
let mut listeners = FxHashMap::default();
let sender_callback = Arc::new(move |ev| {
let c = sender.clone();
wasm_bindgen_futures::spawn_local(async move {
c.send(ev).await.unwrap();
});
});
// re-hydrate the page - only supports one virtualdom per page
if cfg.hydrate {
// Load all the elements into the arena
let node_list: NodeList = document.query_selector_all("dio_el").unwrap();
let len = node_list.length() as usize;
let mut nodes = slotmap::SlotMap::with_capacity(1000);
for x in 0..len {
let node: Node = node_list.get(x as u32).unwrap();
let el: &Element = node.dyn_ref::<Element>().unwrap();
let id: String = el.get_attribute("dio_el").unwrap();
let id = id.parse::<usize>().unwrap();
nodes[id] = Some(node);
}
let root_id = nodes.insert(Some(root.clone().dyn_into::<Node>().unwrap()));
// Load all the event listeners into our listener register
// TODO
}
let mut stack = Stack::with_capacity(10);
let root_node = root.clone().dyn_into::<Node>().unwrap();
stack.push(root_node);
Self {
stack: Stack::with_capacity(10),
stack,
nodes,
listeners: FxHashMap::default(),
listeners,
document,
event_receiver: receiver,
trigger: sender_callback,
sender_callback,
root,
last_node_was_text: false,
}
}
pub async fn wait_for_event(&mut self) -> Option<EventTrigger> {
let v = self.event_receiver.recv().await.unwrap();
Some(v)
}
pub fn process_edits(&mut self, edits: &mut Vec<DomEdit>) {
for edit in edits.drain(..) {
log::info!("Handling edit: {:#?}", edit);
@@ -77,7 +84,7 @@ impl WebsysDom {
DomEdit::PushRoot { id: root } => self.push(root),
DomEdit::PopRoot => self.pop(),
DomEdit::AppendChildren { many } => self.append_children(many),
DomEdit::ReplaceWith { many } => self.replace_with(many),
DomEdit::ReplaceWith { n, m } => self.replace_with(n, m),
DomEdit::Remove => self.remove(),
DomEdit::RemoveAllChildren => self.remove_all_children(),
DomEdit::CreateTextNode { text, id } => self.create_text_node(text, id),
@@ -88,24 +95,28 @@ impl WebsysDom {
event_name: event,
scope,
mounted_node_id: node,
element_id: idx,
} => self.new_event_listener(event, scope, idx, node),
} => self.new_event_listener(event, scope, node),
DomEdit::RemoveEventListener { event } => todo!(),
DomEdit::SetText { text } => self.set_text(text),
DomEdit::SetAttribute { field, value, ns } => self.set_attribute(field, value, ns),
DomEdit::RemoveAttribute { name } => self.remove_attribute(name),
DomEdit::InsertAfter { n } => self.insert_after(n),
DomEdit::InsertBefore { n } => self.insert_before(n),
}
}
}
fn push(&mut self, root: u64) {
let key = DefaultKey::from(KeyData::from_ffi(root));
let domnode = self.nodes.get_mut(key);
let key = root as usize;
let domnode = &self.nodes[key];
let domnode = domnode.unwrap().as_mut().unwrap();
// .expect(&format!("Failed to pop know root: {:#?}", key))
// .unwrap();
let real_node: Node = match domnode {
Some(n) => n.clone(),
None => todo!(),
};
self.stack.push(domnode.clone());
self.stack.push(real_node);
}
// drop the node off the stack
fn pop(&mut self) {
@@ -143,37 +154,55 @@ impl WebsysDom {
}
}
fn replace_with(&mut self, many: u32) {
fn replace_with(&mut self, n: u32, m: u32) {
log::debug!("Called [`replace_with`]");
let new_node = self.stack.pop();
let old_node = self.stack.pop();
// TODO: use different-sized replace withs
if many == 1 {
if old_node.has_type::<Element>() {
old_node
.dyn_ref::<Element>()
.unwrap()
.replace_with_with_node_1(&new_node)
.unwrap();
} else if old_node.has_type::<web_sys::CharacterData>() {
old_node
.dyn_ref::<web_sys::CharacterData>()
.unwrap()
.replace_with_with_node_1(&new_node)
.unwrap();
} else if old_node.has_type::<web_sys::DocumentType>() {
old_node
.dyn_ref::<web_sys::DocumentType>()
.unwrap()
.replace_with_with_node_1(&new_node)
.unwrap();
} else {
panic!("Cannot replace node: {:?}", old_node);
}
let mut new_nodes = vec![];
for _ in 0..m {
new_nodes.push(self.stack.pop());
}
self.stack.push(new_node);
let mut old_nodes = vec![];
for _ in 0..n {
old_nodes.push(self.stack.pop());
}
for node in &old_nodes[1..] {
node.dyn_ref::<Element>().unwrap().remove();
}
let old = old_nodes[0].clone();
let arr: js_sys::Array = new_nodes.iter().collect();
let el = old.dyn_into::<Element>().unwrap();
el.replace_with_with_node(&arr).unwrap();
// let arr = js_sys::Array::from();
// TODO: use different-sized replace withs
// if m == 1 {
// if old_node.has_type::<Element>() {
// old_node
// .dyn_ref::<Element>()
// .unwrap()
// .replace_with_with_node_1(&new_node)
// .unwrap();
// } else if old_node.has_type::<web_sys::CharacterData>() {
// old_node
// .dyn_ref::<web_sys::CharacterData>()
// .unwrap()
// .replace_with_with_node_1(&new_node)
// .unwrap();
// } else if old_node.has_type::<web_sys::DocumentType>() {
// old_node
// .dyn_ref::<web_sys::DocumentType>()
// .unwrap()
// .replace_with_with_node_1(&new_node)
// .unwrap();
// } else {
// panic!("Cannot replace node: {:?}", old_node);
// }
// }
// self.stack.push(new_node);
}
fn remove(&mut self) {
@@ -187,7 +216,8 @@ impl WebsysDom {
}
fn create_placeholder(&mut self, id: u64) {
self.create_element("pre", None, id)
self.create_element("pre", None, id);
self.set_attribute("hidden", "", None);
}
fn create_text_node(&mut self, text: &str, id: u64) {
// let nid = self.node_counter.next();
@@ -198,11 +228,9 @@ impl WebsysDom {
.dyn_into::<Node>()
.unwrap();
let id = id as usize;
self.stack.push(textnode.clone());
*self
.nodes
.get_mut(DefaultKey::from(KeyData::from_ffi(id)))
.unwrap() = Some(textnode);
self.nodes[id] = Some(textnode);
}
fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
@@ -221,24 +249,18 @@ impl WebsysDom {
.dyn_into::<Node>()
.unwrap(),
};
let id = DefaultKey::from(KeyData::from_ffi(id));
let id = id as usize;
self.stack.push(el.clone());
*self.nodes.get_mut(id).unwrap() = Some(el);
self.nodes[id] = Some(el);
// let nid = self.node_counter.?next();
// let nid = self.nodes.insert(el).data().as_ffi();
// log::debug!("Called [`create_element`]: {}, {:?}", tag, nid);
// RealDomNode::new(nid)
// ElementId::new(nid)
}
fn new_event_listener(
&mut self,
event: &'static str,
scope: ScopeId,
_element_id: usize,
real_id: u64,
) {
let (_on, event) = event.split_at(2);
fn new_event_listener(&mut self, event: &'static str, scope: ScopeId, real_id: u64) {
// let (_on, event) = event.split_at(2);
let event = wasm_bindgen::intern(event);
// attach the correct attributes to the element
@@ -252,19 +274,24 @@ impl WebsysDom {
.dyn_ref::<Element>()
.expect(&format!("not an element: {:?}", el));
let scope_id = scope.data().as_ffi();
// let scope_id = scope.data().as_ffi();
let scope_id = scope.0 as u64;
el.set_attribute(
&format!("dioxus-event-{}", event),
&format!("{}.{}", scope_id, real_id),
)
.unwrap();
el.set_attribute(&format!("dioxus-event"), &format!("{}", event))
.unwrap();
// Register the callback to decode
if let Some(entry) = self.listeners.get_mut(event) {
entry.0 += 1;
} else {
let trigger = self.trigger.clone();
let trigger = self.sender_callback.clone();
let handler = Closure::wrap(Box::new(move |event: &web_sys::Event| {
// "Result" cannot be received from JS
@@ -293,25 +320,21 @@ impl WebsysDom {
}
fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>) {
if name == "class" {
if let Some(el) = self.stack.top().dyn_ref::<Element>() {
match ns {
Some("http://www.w3.org/2000/svg") => {
//
if let Some(el) = self.stack.top().dyn_ref::<web_sys::SvgElement>() {
let r: web_sys::SvgAnimatedString = el.class_name();
r.set_base_val(value);
// el.set_class_name(value);
}
}
_ => {
if let Some(el) = self.stack.top().dyn_ref::<Element>() {
el.set_class_name(value);
}
// inline style support
Some("style") => {
let el = el.dyn_ref::<HtmlElement>().unwrap();
let style_dc: CssStyleDeclaration = el.style();
style_dc.set_property(name, value).unwrap();
}
_ => el.set_attribute(name, value).unwrap(),
}
} else {
if let Some(el) = self.stack.top().dyn_ref::<Element>() {
el.set_attribute(name, value).unwrap();
match name {
"value" => {}
"checked" => {}
"selected" => {}
_ => {}
}
}
}
@@ -338,17 +361,46 @@ impl WebsysDom {
}
}
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
todo!()
fn insert_after(&mut self, n: u32) {
let mut new_nodes = vec![];
for _ in 0..n {
new_nodes.push(self.stack.pop());
}
let after = self.stack.top().clone();
let arr: js_sys::Array = new_nodes.iter().collect();
let el = after.dyn_into::<Element>().unwrap();
el.after_with_node(&arr).unwrap();
// let mut old_nodes = vec![];
// for _ in 0..n {
// old_nodes.push(self.stack.pop());
// }
// let el = self.stack.top();
}
fn insert_before(&mut self, n: u32) {
let n = n as usize;
let root = self
.stack
.list
.get(self.stack.list.len() - n)
.unwrap()
.clone();
for _ in 0..n {
let el = self.stack.pop();
root.insert_before(&el, None).unwrap();
}
}
}
impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
fn request_available_node(&mut self) -> RealDomNode {
let key = self.nodes.insert(None);
log::debug!("making new key: {:#?}", key);
RealDomNode(key.data().as_ffi())
}
impl<'a> dioxus_core::diff::RealDom for WebsysDom {
// fn request_available_node(&mut self) -> ElementId {
// let key = self.nodes.insert(None);
// log::debug!("making new key: {:#?}", key);
// ElementId(key.data().as_ffi())
// }
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
todo!()
@@ -395,30 +447,140 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
use dioxus_core::events::on::*;
match event.type_().as_str() {
"copy" | "cut" | "paste" => {
// let evt: web_sys::ClipboardEvent = event.clone().dyn_into().unwrap();
todo!()
struct WebsysClipboardEvent();
impl ClipboardEventInner for WebsysClipboardEvent {}
VirtualEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent())))
}
"compositionend" | "compositionstart" | "compositionupdate" => {
let evt: web_sys::CompositionEvent = event.clone().dyn_into().unwrap();
todo!()
struct WebsysCompositionEvent(web_sys::CompositionEvent);
impl CompositionEventInner for WebsysCompositionEvent {
fn data(&self) -> String {
todo!()
}
}
VirtualEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt))))
}
"keydown" | "keypress" | "keyup" => {
struct Event(web_sys::KeyboardEvent);
impl KeyboardEventInner for Event {
fn char_code(&self) -> u32 {
todo!()
}
fn key_code(&self) -> KeyCode {
todo!()
}
fn ctrl_key(&self) -> bool {
todo!()
}
fn key(&self) -> String {
todo!()
}
fn locale(&self) -> String {
todo!()
}
fn location(&self) -> usize {
todo!()
}
fn meta_key(&self) -> bool {
todo!()
}
fn repeat(&self) -> bool {
todo!()
}
fn shift_key(&self) -> bool {
todo!()
}
fn which(&self) -> usize {
todo!()
}
fn get_modifier_state(&self, key_code: usize) -> bool {
todo!()
}
}
let evt: web_sys::KeyboardEvent = event.clone().dyn_into().unwrap();
todo!()
VirtualEvent::KeyboardEvent(KeyboardEvent(Rc::new(Event(evt))))
}
"focus" | "blur" => {
struct Event(web_sys::FocusEvent);
impl FocusEventInner for Event {}
let evt: web_sys::FocusEvent = event.clone().dyn_into().unwrap();
todo!()
VirtualEvent::FocusEvent(FocusEvent(Rc::new(Event(evt))))
}
"change" => {
let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ");
// struct Event(web_sys::Event);
// impl GenericEventInner for Event {
// fn bubbles(&self) -> bool {
// todo!()
// }
// fn cancel_bubble(&self) {
// todo!()
// }
// fn cancelable(&self) -> bool {
// todo!()
// }
// fn composed(&self) -> bool {
// todo!()
// }
// fn composed_path(&self) -> String {
// todo!()
// }
// fn current_target(&self) {
// todo!()
// }
// fn default_prevented(&self) -> bool {
// todo!()
// }
// fn event_phase(&self) -> usize {
// todo!()
// }
// fn is_trusted(&self) -> bool {
// todo!()
// }
// fn prevent_default(&self) {
// todo!()
// }
// fn stop_immediate_propagation(&self) {
// todo!()
// }
// fn stop_propagation(&self) {
// todo!()
// }
// fn target(&self) {
// todo!()
// }
// fn time_stamp(&self) -> usize {
// todo!()
// }
// }
// let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ");
// VirtualEvent::Event(GenericEvent(Rc::new(Event(evt))))
todo!()
// VirtualEvent::FormEvent(FormEvent {value:})
}
"input" | "invalid" | "reset" | "submit" => {
@@ -442,15 +604,6 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
})
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
// let p2 = evt.data_transfer();
// let value: Option<String> = (&evt).data();
// let value = val;
// let value = value.unwrap_or_default();
// let value = (&evt).data().expect("No data to unwrap");
// todo - this needs to be a "controlled" event
// these events won't carry the right data with them
todo!()
// VirtualEvent::FormEvent(FormEvent { value })
}
@@ -507,26 +660,6 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
}
}
VirtualEvent::MouseEvent(MouseEvent(Rc::new(CustomMouseEvent(evt))))
// MouseEvent(Box::new(RawMouseEvent {
// alt_key: evt.alt_key(),
// button: evt.button() as i32,
// buttons: evt.buttons() as i32,
// client_x: evt.client_x(),
// client_y: evt.client_y(),
// ctrl_key: evt.ctrl_key(),
// meta_key: evt.meta_key(),
// page_x: evt.page_x(),
// page_y: evt.page_y(),
// screen_x: evt.screen_x(),
// screen_y: evt.screen_y(),
// shift_key: evt.shift_key(),
// get_modifier_state: GetModifierKey(Box::new(|f| {
// // evt.get_modifier_state(f)
// todo!("This is not yet implemented properly, sorry :(");
// })),
// }))
// todo!()
// VirtualEvent::MouseEvent()
}
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
@@ -555,6 +688,15 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap();
todo!()
}
"animationstart" | "animationend" | "animationiteration" => {
let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap();
todo!()
}
"transitionend" => {
let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap();
todo!()
}
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
@@ -567,23 +709,15 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
todo!()
}
"animationstart" | "animationend" | "animationiteration" => {
let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap();
todo!()
}
"transitionend" => {
let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap();
todo!()
}
"toggle" => {
// not required to construct anything special beyond standard event stuff (target)
// let evt: web_sys::ToggleEvent = event.clone().dyn_into().unwrap();
todo!()
}
_ => VirtualEvent::OtherEvent,
_ => {
todo!()
}
}
}
@@ -628,12 +762,24 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
// Call the trigger
log::debug!("decoded scope_id: {}, node_id: {:#?}", gi_id, real_id);
let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into();
let triggered_scope = gi_id;
// let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into();
log::debug!("Triggered scope is {:#?}", triggered_scope);
Ok(EventTrigger::new(
virtual_event_from_websys_event(event),
triggered_scope,
Some(RealDomNode::from_u64(real_id)),
ScopeId(triggered_scope as usize),
Some(ElementId(real_id as usize)),
dioxus_core::events::EventPriority::High,
))
}
pub fn prepare_websys_dom() -> Element {
load_document().get_element_by_id("dioxusroot").unwrap()
}
pub fn load_document() -> Document {
web_sys::window()
.expect("should have access to the Window")
.document()
.expect("should have access to the Document")
}
+90 -66
View File
@@ -2,38 +2,92 @@
//! --------------
//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys.
/*
From Google's guide on rAF and rIC:
--------
If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
which is a potential performance bottleneck.
Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
and as such we could easily go past the deadline the browser provided.
The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
Essentially:
------------
- Do the VDOM work during the idlecallback
- Do DOM work in the next requestAnimationFrame callback
*/
use std::rc::Rc;
pub use crate::cfg::WebConfig;
use crate::dom::load_document;
use dioxus::prelude::{Context, Properties, VNode};
use dioxus::virtual_dom::VirtualDom;
pub use dioxus_core as dioxus;
use dioxus_core::error::Result;
use dioxus_core::{events::EventTrigger, prelude::FC};
use futures_util::{pin_mut, Stream, StreamExt};
use fxhash::FxHashMap;
use web_sys::{window, Document, Element, Event, Node};
use js_sys::Iterator;
use web_sys::{window, Document, Element, Event, Node, NodeList};
mod cache;
mod new;
mod cfg;
mod dom;
mod nodeslab;
/// Launches the VirtualDOM from the specified component function.
///
/// This method will block the thread with `spawn_local`
///
/// # Example
///
///
///
pub fn launch<F>(root: FC<()>, config: F)
where
F: FnOnce(()),
F: FnOnce(WebConfig) -> WebConfig,
{
wasm_bindgen_futures::spawn_local(run(root))
launch_with_props(root, (), config)
}
/// Launches the VirtualDOM from the specified component function and props.
///
/// This method will block the thread with `spawn_local`
///
/// # Example
///
///
pub fn launch_with_props<T, F>(root: FC<T>, root_props: T, config: F)
where
T: Properties + 'static,
F: FnOnce(()),
F: FnOnce(WebConfig) -> WebConfig,
{
wasm_bindgen_futures::spawn_local(run_with_props(root, root_props))
let config = config(WebConfig::default());
let fut = run_with_props(root, root_props, config);
wasm_bindgen_futures::spawn_local(async {
match fut.await {
Ok(_) => log::error!("Your app completed running... somehow?"),
Err(e) => log::error!("Your app crashed! {}", e),
}
});
}
/// This method is the primary entrypoint for Websys Dioxus apps. Will panic if an error occurs while rendering.
/// See DioxusErrors for more information on how these errors could occour.
///
/// # Example
///
/// ```ignore
/// fn main() {
/// wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example));
@@ -42,73 +96,43 @@ where
///
/// Run the app to completion, panicing if any error occurs while rendering.
/// Pairs well with the wasm_bindgen async handler
pub async fn run(root: FC<()>) {
run_with_props(root, ()).await;
}
pub async fn run_with_props<T: Properties + 'static>(
root: FC<T>,
root_props: T,
cfg: WebConfig,
) -> Result<()> {
let mut dom = VirtualDom::new_with_props(root, root_props);
pub async fn run_with_props<T: Properties + 'static>(root: FC<T>, root_props: T) {
let dom = VirtualDom::new_with_props(root, root_props);
event_loop(dom).await.expect("Event loop failed");
}
let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap();
pub async fn event_loop(mut internal_dom: VirtualDom) -> dioxus_core::error::Result<()> {
use wasm_bindgen::JsCast;
let tasks = dom.get_event_sender();
let root = prepare_websys_dom();
let root_node = root.clone().dyn_into::<Node>().unwrap();
let mut real = RealDomWebsys {};
let mut websys_dom = crate::new::WebsysDom::new(root.clone());
websys_dom.stack.push(root_node.clone());
websys_dom.stack.push(root_node);
let mut edits = Vec::new();
internal_dom.rebuild(&mut websys_dom, &mut edits)?;
websys_dom.process_edits(&mut edits);
log::info!("Going into event loop");
loop {
let trigger = {
let real_queue = websys_dom.wait_for_event();
if internal_dom.tasks.is_empty() {
log::info!("tasks is empty, waiting for dom event to trigger soemthing");
real_queue.await
} else {
log::info!("tasks is not empty, waiting for either tasks or event system");
let task_queue = (&mut internal_dom.tasks).next();
pin_mut!(real_queue);
pin_mut!(task_queue);
match futures_util::future::select(real_queue, task_queue).await {
futures_util::future::Either::Left((trigger, _)) => trigger,
futures_util::future::Either::Right((trigger, _)) => trigger,
}
}
};
if let Some(real_trigger) = trigger {
log::info!("event received");
internal_dom.queue_event(real_trigger)?;
let mut edits = Vec::new();
internal_dom
.progress_with_event(&mut websys_dom, &mut edits)
.await?;
websys_dom.process_edits(&mut edits);
}
// initialize the virtualdom first
if cfg.hydrate {
dom.rebuild_in_place()?;
}
// should actually never return from this, should be an error, rustc just cant see it
let mut websys_dom = dom::WebsysDom::new(
root_el,
cfg,
Rc::new(move |event| tasks.unbounded_send(event).unwrap()),
);
dom.run_with_deadline(&mut websys_dom).await?;
Ok(())
}
fn prepare_websys_dom() -> Element {
web_sys::window()
.expect("should have access to the Window")
.document()
.expect("should have access to the Document")
.get_element_by_id("dioxusroot")
.unwrap()
struct HydrationNode {
id: usize,
node: Node,
}
struct RealDomWebsys {}
impl dioxus::RealDom for RealDomWebsys {
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
todo!()
}
}
+37
View File
@@ -0,0 +1,37 @@
use std::ops::{Index, IndexMut};
use web_sys::Node;
pub struct NodeSlab {
nodes: Vec<Option<Node>>,
}
impl NodeSlab {
pub fn new(capacity: usize) -> NodeSlab {
let mut nodes = Vec::with_capacity(capacity);
for x in 0..5 {
nodes.push(None);
}
NodeSlab { nodes }
}
}
impl Index<usize> for NodeSlab {
type Output = Option<Node>;
fn index(&self, index: usize) -> &Self::Output {
&self.nodes[index]
}
}
impl IndexMut<usize> for NodeSlab {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
if index >= self.nodes.capacity() * 3 {
panic!("Trying to mutate an element way too far out of bounds");
}
if index + 1 > self.nodes.len() {
self.nodes.resize_with(index + 1, || None);
}
&mut self.nodes[index]
}
}