Merge pull request #9 from jkelleyrtp/jk/no-more-patch-machine

Remove patch machine
This commit is contained in:
Jonathan Kelley 2021-06-25 12:16:27 -04:00 committed by GitHub
commit 8c37e4947e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 2885 additions and 1874 deletions

View file

@ -57,3 +57,5 @@ dom
textarea
noderefs
wasm
7ns
attr

View file

@ -52,7 +52,8 @@ members = [
"packages/core-macro",
"packages/core",
"packages/html-namespace",
# "packages/web",
"packages/web",
# "packages/cli",
# "packages/atoms",
# "packages/ssr",
# "packages/docsite",

View file

@ -89,3 +89,61 @@ TypeScript is a great addition to JavaScript, but comes with a lot of tweaking f
- various macros (`html!`, `rsx!`) for fast template iteration
And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. Dioxus also works on the server, on the web, on mobile, on desktop - and it runs completely natively so performance is never an issue.
# Parity with React
Dioxus is heavily inspired by React, but we want your transition to feel like an upgrade. Dioxus is _most_ of the way there, but missing a few key features. This parity table does not necessarily include important ecosystem crates like code blocks, markdown, resizing hooks, etc.
### Phase 1: The Basics
| Feature | Dioxus | React | Notes for Dioxus |
| ---------------------- | ------ | ----- | ------------------------------------------------ |
| Conditional Rendering | ✅ | ✅ | if/then to hide/show component |
| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! |
| Keyed Components | ✅ | ✅ | advanced diffing with keys |
| Web | ✅ | ✅ | renderer for web browser |
| Desktop (webview) | ✅ | ✅ | renderer for desktop |
| Context | ✅ | ✅ | share state through the tree |
| Hook | ✅ | ✅ | memory cells in components |
| SSR | ✅ | ✅ | render directly to string |
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
| Component Children | ✅ | ✅ | cx.children() as a list of nodes |
| Null components | ✅ | ✅ | allow returning no components |
| No-div components | ✅ | ✅ | components that render components |
| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root |
| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax |
| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs |
| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context |
| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] |
| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups[2] |
- [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API.
- [2] Would like to solve this in a more general way. Something like attribute groups that's not styling-specific.
### Phase 2: Advanced Toolkits
| Feature | Dioxus | React | Notes for Dioxus |
| --------------------- | ------ | ----- | ------------------------------------------ |
| 1st class router | 👀 | ✅ | Hook built on top of history |
| Assets | 👀 | ✅ | include css/svg/img url statically |
| Integrated classnames | 🛠 | ❓ | built-in `classnames` |
| Suspense | 👀 | 🛠 | schedule future render from future/promise |
| Transition | 👀 | 🛠 | High-level control over suspense |
| Animation | 👀 | ✅ | Spring-style animations |
| Mobile | 👀 | ✅ | Render with cacao |
| Desktop (native) | 👀 | ✅ | Render with native desktop |
| 3D Renderer | 👀 | ✅ | react-three-fiber |
### Phase 3: Additional Complexity
| Feature | Dioxus | React | Notes for Dioxus |
| -------------------- | ------ | ----- | ------------------------------------ |
| Portal | ❓ | ✅ | cast elements through tree |
| Error/Panic boundary | ❓ | ✅ | catch panics and display custom BSOD |
| Code-splitting | 👀 | ✅ | Make bundle smaller/lazy |
| LiveView | 👀 | ❓ | Example for SSR + WASM apps |
- ✅ = implemented and working
- 🛠 = actively being worked on
- 👀 = not yet implemented or being worked on
- ❓ = not sure if will or can implement

View file

@ -0,0 +1,171 @@
# Custom Renderer
Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer.
Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but only really require implementation of the `RealDom` trait for things to function properly.
## The specifics:
Implementing the renderer is fairly straightforward. The renderer needs to:
1. Handle the stream of edits generated by updates to the virtual DOM
2. Register listeners and pass events into the virtual DOM's event system
3. Progress the virtual DOM with an async executor (or disable the suspense API and use `progress_sync`)
Essentially, your renderer needs to implement the `RealDom` trait and generate `EventTrigger` objects to update the VirtualDOM. From there, you'll have everything needed to render the VirtualDOM to the screen.
Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves.
For reference, check out the WebSys renderer as a starting point for your custom renderer.
## Trait implementation
The current `RealDom` trait lives in `dioxus_core/diff`. A version of it is provided here:
```rust
pub trait RealDom {
// Navigation
fn push_root(&mut self, root: RealDomNode);
// Add Nodes to the dom
fn append_child(&mut self);
fn replace_with(&mut self);
// Remove Nodes from the dom
fn remove(&mut self);
fn remove_all_children(&mut self);
// Create
fn create_text_node(&mut self, text: &str) -> RealDomNode;
fn create_element(&mut self, tag: &str, namespace: Option<&str>) -> RealDomNode;
// Events
fn new_event_listener(
&mut self,
event: &str,
scope: ScopeIdx,
element_id: usize,
realnode: RealDomNode,
);
fn remove_event_listener(&mut self, event: &str);
// modify
fn set_text(&mut self, text: &str);
fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool);
fn remove_attribute(&mut self, name: &str);
// node ref
fn raw_node_as_any_mut(&self) -> &mut dyn Any;
}
```
This trait defines what the Dioxus VirtualDOM expects a "RealDom" abstraction to implement for the Dioxus diffing mechanism to function properly. The Dioxus diffing mechanism operates as a [stack machine](https://en.wikipedia.org/wiki/Stack_machine) where the "push_root" method pushes a new "real" DOM node onto the stack and "append_child" and "replace_with" both remove nodes from the stack. When the RealDOM creates new nodes, it must return the `RealDomNode` type... which is just an abstraction over u32. We strongly recommend the use of `nohash-hasher`'s IntMap for managing the mapping of `RealDomNode` (ids) to their corresponding real node. For an IntMap of 1M+ nodes, an index time takes about 7ns which is very performant when compared to the traditional hasher.
## Event loop
Like most GUIs, Dioxus relies on an event loop to progress the VirtualDOM. The VirtualDOM itself can produce events as well, so it's important that your custom renderer can handle those too.
The code for the WebSys implementation is straightforward, so we'll add it here to demonstrate how simple an event loop is:
```rust
pub async fn run(&mut self) -> dioxus_core::error::Result<()> {
// Push the body element onto the WebsysDom's stack machine
let mut websys_dom = crate::new::WebsysDom::new(prepare_websys_dom().first_child().unwrap());
websys_dom.stack.push(root_node);
// Rebuild or hydrate the virtualdom
self.internal_dom.rebuild(&mut websys_dom)?;
// Wait for updates from the real dom and progress the virtual dom
while let Some(trigger) = websys_dom.wait_for_event().await {
websys_dom.stack.push(body_element.first_child().unwrap());
self.internal_dom
.progress_with_event(&mut websys_dom, trigger)?;
}
}
```
It's important that you decode the real events from your event system into Dioxus' synthetic event system (synthetic meaning abstracted). This simply means matching your event type and creating a Dioxus `VirtualEvent` type. Your custom event must implement the corresponding event trait:
```rust
fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
match event.type_().as_str() {
"keydown" | "keypress" | "keyup" => {
struct CustomKeyboardEvent(web_sys::KeyboardEvent);
impl dioxus::events::KeyboardEvent for CustomKeyboardEvent {
fn char_code(&self) -> usize { self.0.char_code() }
fn ctrl_key(&self) -> bool { self.0.ctrl_key() }
fn key(&self) -> String { self.0.key() }
fn key_code(&self) -> usize { self.0.key_code() }
fn locale(&self) -> String { self.0.locale() }
fn location(&self) -> usize { self.0.location() }
fn meta_key(&self) -> bool { self.0.meta_key() }
fn repeat(&self) -> bool { self.0.repeat() }
fn shift_key(&self) -> bool { self.0.shift_key() }
fn which(&self) -> usize { self.0.which() }
fn get_modifier_state(&self, key_code: usize) -> bool { self.0.get_modifier_state() }
}
VirtualEvent::KeyboardEvent(Rc::new(event.clone().dyn_into().unwrap()))
}
_ => todo!()
```
## Custom raw elements
If you need to go as far as relying on custom elements for your renderer - you totally can. This still enables you to use Dioxus' reactive nature, component system, shared state, and other features, but will ultimately generate different nodes. All attributes and listeners for the HTML and SVG namespace are shuttled through helper structs that essentially compile away (pose no runtime overhead). You can drop in your own elements any time you want, with little hassle. However, you must be absolutely sure your renderer can handle the new type, or it will crash and burn.
For example, the `div` element is (approximately!) defined as such:
```rust
struct div(NodeBuilder);
impl<'a> div {
#[inline]
fn new(factory: &NodeFactory<'a>) -> Self {
Self(factory.new_element("div"))
}
#[inline]
fn onclick(mut self, callback: impl Fn(MouseEvent) + 'a) -> Self {
self.0.add_listener("onclick", |evt: VirtualEvent| match evt {
MouseEvent(evt) => callback(evt),
_ => {}
});
self
}
// etc
}
```
The rsx! and html! macros just use the `div` struct as a compile-time guard around the NodeFactory.
## Compatibility
Forewarning: not every hook and service will work on your platform. Dioxus wraps things that need to be cross-platform in "synthetic" types. However, downcasting to a native type might fail if the types don't match.
There are three opportunities for platform incompatibilities to break your program:
1. When downcasting elements via `Ref.to_native<T>()`
2. When downcasting events via `Event.to_native<T>()`
3. Calling platform-specific APIs that don't exist
The best hooks will properly detect the target platform and still provide functionality, failing gracefully when a platform is not supported. We encourage - and provide - an indication to the user on what platforms a hook supports. For issues 1 and 2, these return a result as to not cause panics on unsupported platforms. When designing your hooks, we recommend propagating this error upwards into user facing code, making it obvious that this particular service is not supported.
This particular code _will panic_ due to the unwrap. Try to avoid these types of patterns.
```rust
let div_ref = use_node_ref(&cx);
cx.render(rsx!{
div { ref: div_ref, class: "custom class",
button { "click me to see my parent's class"
onclick: move |_| if let Some(div_ref) = div_ref {
panic!("Div class is {}", div_ref.to_native::<web_sys::Element>().unwrap().class())
}
}
}
})
```
## Conclusion
That should be it! You should have nearly all the knowledge required on how to implement your own renderer. We're super interested in seeing Dioxus apps brought to custom desktop renderers, mobile renderer, video game UI, and even augmented reality! If you're interesting in contributing to any of the these projects, don't be afraid to reach out or join the community.

View file

@ -1,30 +0,0 @@
# Custom Renderer
Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer.
Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but provide no trait or explicit interface to follow.
Implementing the renderer is fairly straightforward. The renderer needs to:
1. Handle the stream of edit events generated by updates to the virtual DOM
2. Register listeners and pass events into the virtual DOM's event system
3. Progress the virtual DOM with an async executor (or disable the suspense API and use `progress_sync`)
Essentially, your renderer needs to understand the EditEvent type and provide a callback for injecting events. From there, you'll have everything needed to render the VirtualDOM to the screen.
Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves.
For inspiration, check out the source code for the various renderers we support:
- WebSys
- Morph
## Compatibility
Forewarning: not every hook and service will work on your platform. Dioxus wraps things that need to be cross-platform in "synthetic" types. However, downcasting to a native type might fail if the types don't match.
There are three opportunities for platform incompatibilities to break your program:
1. When downcasting elements via `Ref.to_native<T>()`
2. When downcasting events via `Event.to_native<T>()`
3. Calling platform-specific APIs that don't exist
The best hooks will properly detect the target platform and still provide functionality, failing gracefully when a platform is not supported. We encourage - and provide - an indication to the user on what platforms a hook supports. For issues 1 and 2, these return a result as to not cause panics on unsupported platforms. When designing your hooks, we recommend propagating this error upwards into user facing code, making it obvious that this particular service is not supported.

176
examples/antipatterns.rs Normal file
View file

@ -0,0 +1,176 @@
//! Example: Antipatterns
//! ---------------------
//!
//! This example shows what *not* to do and provides a reason why a given pattern is considered an "AntiPattern". Most
//! anti-patterns are considered wrong to due performance reasons or violate the "rules" of Dioxus. These rules are
//! borrowed from other successful UI frameworks, and Dioxus is more focused on providing a familiar, ergonomic interface
//! rather than building new harder-to-misuse patterns.
//!
//! In this list we showcase:
//! - Not adding keys for iterators
//! - Heavily nested fragments
//! - Understadning ordering of set_state
//! - Naming conventions
//! - Rules of hooks
//!
//! Feel free to file a PR or Issue if you run into another antipattern that you think users of Dioxus should know about.
use dioxus::prelude::*;
fn main() {}
/// Antipattern: Iterators without keys
/// -----------------------------------
///
/// This is considered an anti-pattern for performance reasons. Dioxus must diff your current and old layout and must
/// take a slower path if it can't correlate old elements with new elements. Lists are particularly susceptible to the
/// "slow" path, so you're strongly encouraged to provide a unique ID stable between renders. Additionally, providing
/// the *wrong* keys is even worse. Keys should be:
/// - Unique
/// - Stable
/// - Predictable
///
/// Dioxus will log an error in the console if it detects that your iterator does not properly generate keys
#[derive(PartialEq, Props)]
struct NoKeysProps {
data: std::collections::HashMap<u32, String>,
}
static AntipatternNoKeys: FC<NoKeysProps> = |cx| {
// WRONG: Make sure to add keys!
rsx!(in cx, ul {
{cx.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))}
});
// Like this:
rsx!(in cx, ul {
{cx.data.iter().map(|(k, v)| rsx!(li { key: "{k}", "List item: {v}" }))}
})
};
/// Antipattern: Deeply nested fragments
/// ------------------------------------
///
/// This particular antipattern is not necessarily an antipattern in other frameworks but does has a performance impact
/// in Dioxus apps. Fragments don't mount a physical element to the dom immediately, so Dioxus must recurse into its
/// children to find a physical dom node. This process is called "normalization". Other frameworks perform an agressive
/// mutative normalization while Dioxus keeps your VNodes immutable. This means that deepely nested fragments make Dioxus
/// perform unnecessary work. Prefer one or two levels of fragments / nested components until presenting a true dom element.
///
/// Only Component and Fragment nodes are susceptible to this issue. Dioxus mitigates this with components by providing
/// an API for registering shared state without the ContextProvider pattern.
static AntipatternNestedFragments: FC<()> = |cx| {
// Try to avoid heavily nesting fragments
rsx!(in cx,
Fragment {
Fragment {
Fragment {
Fragment {
Fragment {
div { "Finally have a real node!" }
}
}
}
}
}
)
};
/// Antipattern: Using state after its been updated
/// -----------------------------------------------
///
/// This is an antipattern in other frameworks, but less so in Dioxus. However, it's important to highlight that use_state
/// does *not* work the same way as it does in React. Rust provides explicit guards against mutating shared data - a huge
/// problem in JavaScript land. With Rust and Dioxus, it's nearly impossible to misuse `use_state` - you simply can't
/// accidentally modify the state you've received!
///
/// However, calling set_state will *not* update the current version of state in the component. This should be easy to
/// recognize from the function signature, but Dioxus will not update the "live" version of state. Calling `set_state`
/// merely places a new value in the queue and schedules the component for a future update.
static AntipaternRelyingOnSetState: FC<()> = |cx| {
let (state, set_state) = use_state(&cx, || "Hello world");
set_state("New state");
// This will return false! `state` will *still* be "Hello world"
assert!(state == &"New state");
todo!()
};
/// Antipattern: Capitalization
/// ---------------------------
///
/// This antipattern is enforced to retain parity with other frameworks and provide useful IDE feedback, but is less
/// critical than other potential misues. In short:
/// - Only raw elements may start with a lowercase character
/// - All components must start with an uppercase character
///
/// IE: the following component will be rejected when attempted to be used in the rsx! macro
static antipattern_component: FC<()> = |cx| todo!();
/// Antipattern: Misusing hooks
/// ---------------------------
///
/// This pattern is an unfortunate one where Dioxus supports the same behavior as in other frameworks. Dioxus supports
/// "hooks" - IE "memory cells" that allow a value to be stored between renders. This allows other hooks to tap into
/// a components "memory" without explicitly adding all of its data to a struct definition. In Dioxus, hooks are allocated
/// with a bump arena and then immediately sealed.
///
/// This means that hooks may not be misued:
/// - Called out of order
/// - Called in a conditional
/// - Called in loops or callbacks
///
/// For the most part, Rust helps with rule #3 but does not save you from misusing rule #1 or #2. Dioxus will panic
/// if hooks do not downcast the same data between renders. This is validated by TypeId - and eventually - a custom key.
#[derive(PartialEq, Props)]
struct MisuedHooksProps {
should_render_state: bool,
}
static AntipatternMisusedHooks: FC<MisuedHooksProps> = |cx| {
if cx.should_render_state {
// do not place a hook in the conditional!
// prefer to move it out of the conditional
let (state, set_state) = use_state(&cx, || "hello world");
rsx!(in cx, div { "{state}" })
} else {
rsx!(in cx, div { "Not rendering state" })
}
};
/// Antipattern: Downcasting refs and panicing
/// ------------------------------------------
///
/// Occassionally it's useful to get the ref of an element to handle it directly. Elements support downcasting to
/// Dioxus's virtual element types as well as their true native counterparts. Downcasting to Dioxus' virtual elements
/// will never panic, but downcasting to native elements will fail if on an unsupported platform. We recommend avoiding
/// publishing hooks and components that deply rely on control over elements using their native `ref`, preferring to
/// use their Dioxus Virtual Element counterpart instead.
// This particular code *will panic* due to the unwrap. Try to avoid these types of patterns.
/// ---------------------------------
/// TODO: Get this to compile properly
/// let div_ref = use_node_ref(&cx);
///
/// cx.render(rsx!{
/// div { ref: div_ref, class: "custom class",
/// button { "click me to see my parent's class"
/// onclick: move |_| if let Some(div_ref) = div_ref {
/// panic!("Div class is {}", div_ref.to_native::<web_sys::Element>().unwrap().class())
/// }
/// }
/// }
/// })
static _example: FC<()> = |cx| todo!();
/// Antipattern: publishing components and hooks with all features enabled
/// ----------------------------------------------------------------------
///
/// The `dioxus` crate combines a bunch of useful utilities together (like the rsx! and html! macros, hooks, and more).
/// However, when publishing your custom hook or component, we highly advise using only the `core` feature on the dioxus
/// crate. This makes your crate compile faster, makes it more stable, and avoids bringing in incompatible libraries that
/// might make it not compile on unsupported platforms.
///
/// We don't have a code snippet for this, but just prefer to use this line:
/// dioxus = { version = "*", features = ["core"]}
/// instead of this one:
/// dioxus = { version = "*", features = ["web", "desktop", "full"]}
/// in your Cargo.toml
///
/// This will only include the `core` dioxus crate which is relatively slim and fast to compile and avoids target-specific
/// libraries.
static __example: FC<()> = |cx| todo!();

View file

@ -1,143 +0,0 @@
#![allow(unused)]
/*
This example shows how to encapsulate sate in dioxus components with the reducer pattern.
This pattern is very useful when a single component can handle many types of input that can
be represented by an enum. This particular pattern is very powerful in rust where ADTs can simplify
much of the traditional reducer boilerplate.
*/
fn main() {}
use dioxus::prelude::*;
use std::future::Future;
enum Actions {
Pause,
Play,
}
struct SomeState {
is_playing: bool,
}
impl SomeState {
fn new() -> Self {
Self { is_playing: false }
}
fn reduce(&mut self, action: Actions) {
match action {
Actions::Pause => self.is_playing = false,
Actions::Play => self.is_playing = true,
}
}
fn is_playing(&self) -> &'static str {
match self.is_playing {
true => "currently playing!",
false => "not currently playing",
}
}
}
pub static ExampleReducer: FC<()> = |ctx| {
let (state, reduce) = use_reducer(&ctx, SomeState::new, SomeState::reduce);
let is_playing = state.is_playing();
ctx.render(rsx! {
div {
h1 {"Select an option"}
h3 {"The radio is... {is_playing}!"}
button {
"Pause"
onclick: move |_| reduce(Actions::Pause)
}
button {
"Play"
onclick: move |_| reduce(Actions::Play)
}
}
})
};
/*
*/
struct AppContext {
name: String,
}
enum KindaState {
Ready,
Complete,
Erred,
}
static EnumReducer: FC<()> = |ctx| {
let (state, reduce) = use_reducer(&ctx, || KindaState::Ready, |cur, new| *cur = new);
let status = match state {
KindaState::Ready => "Ready",
KindaState::Complete => "Complete",
KindaState::Erred => "Erred",
};
ctx.render(rsx! {
div {
h1 {"{status}"}
button {
"Set Ready"
onclick: move |_| reduce(KindaState::Ready)
}
button {
"Set Complete"
onclick: move |_| reduce(KindaState::Complete)
}
button {
"Set Erred"
onclick: move |_| reduce(KindaState::Erred)
}
ul {
{(0..10).map(|f| {
rsx!{
li {
"hello there!"
}
}
})}
}
}
})
};
/// Demonstrate how the DebugRenderer can be used to unit test components without needing a browser
/// These tests can run locally.
/// They use the "compare" method of the debug renderer to do partial tree compares for succint
#[test]
fn ensure_it_works_properly() -> dioxus::error::Result<()> {
let mut test = DebugRenderer::new(EnumReducer);
test.compare(rsx! { div { h1 {"Ready"} } })?;
test.trigger_listener(1)?;
test.compare(rsx! { div { h1 {"Ready"} } })?;
test.trigger_listener(2)?;
test.compare(rsx! { div { h1 {"Complete"} } })?;
test.trigger_listener(3)?;
test.compare(rsx! { div { h1 {"Erred"} } })?;
Ok(())
}

View file

@ -2,13 +2,7 @@
use dioxus_core::prelude::*;
fn main() {
Some(10)
.map(|f| f * 5)
.map(|f| f / 3)
.map(|f| f * 5)
.map(|f| f / 3);
}
fn main() {}
static Example: FC<()> = |ctx| {
let (name, set_name) = use_state(&ctx, || "...?");

56
examples/reducer.rs Normal file
View file

@ -0,0 +1,56 @@
//! Example: Reducer Pattern
//! -----------------
//! This example shows how to encapsulate sate in dioxus components with the reducer pattern.
//! This pattern is very useful when a single component can handle many types of input that can
//! be represented by an enum.
fn main() {}
use dioxus::prelude::*;
pub static ExampleReducer: FC<()> = |ctx| {
let (state, reduce) = use_reducer(&ctx, PlayerState::new, PlayerState::reduce);
let is_playing = state.is_playing();
ctx.render(rsx! {
div {
h1 {"Select an option"}
h3 {"The radio is... {is_playing}!"}
button {
"Pause"
onclick: move |_| reduce(PlayerAction::Pause)
}
button {
"Play"
onclick: move |_| reduce(PlayerAction::Play)
}
}
})
};
enum PlayerAction {
Pause,
Play,
}
struct PlayerState {
is_playing: bool,
}
impl PlayerState {
fn new() -> Self {
Self { is_playing: false }
}
fn reduce(&mut self, action: PlayerAction) {
match action {
PlayerAction::Pause => self.is_playing = false,
PlayerAction::Play => self.is_playing = true,
}
}
fn is_playing(&self) -> &'static str {
match self.is_playing {
true => "currently playing!",
false => "not currently playing",
}
}
}

View file

@ -5,10 +5,6 @@
//!
//! A full in-depth reference guide is available at: https://www.notion.so/rsx-macro-basics-ef6e367dec124f4784e736d91b0d0b19
//!
//! ## Topics
//!
//!
//!
//! ### Elements
//! - Create any element from its tag
//! - Accept compile-safe attributes for each tag
@ -46,23 +42,119 @@ fn main() {
dioxus::webview::launch(Example);
}
/// When trying to return "nothing" to Dioxus, you'll need to specify the type parameter or Rust will be sad.
/// This type alias specifices the type for you so you don't need to write "None as Option<()>"
const NONE_ELEMENT: Option<()> = None;
use baller::Baller;
use dioxus_core::prelude::*;
static Example: FC<()> = |ctx| {
ctx.render(rsx! {
static Example: FC<()> = |cx| {
let formatting = "formatting!";
let formatting_tuple = ("a", "b");
let lazy_fmt = format_args!("lazily formatted text");
cx.render(rsx! {
div {
// Elements
div {}
h1 {"Some text"}
h1 {"Some text with {formatting}"}
h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"}
h2 {
"Multiple"
"Text"
"Blocks"
"Use comments as separators in html"
}
div {
h1 {"multiple"}
h2 {"nested"}
h3 {"elements"}
}
div {
class: "my special div"
h1 {"Headers and attributes!"}
}
div {
// pass simple rust expressions in
class: lazy_fmt,
id: format_args!("attributes can be passed lazily with std::fmt::Arguments"),
div {
class: {
const WORD: &str = "expressions";
format_args!("Arguments can be passed in through curly braces for complex {}", WORD)
}
}
}
// Expressions can be used in element position too:
{rsx!(p { "More templating!" })}
{html!(<p>"Even HTML templating!!"</p>)}
// Iterators
{(0..10).map(|i| rsx!(li { "{i}" }))}
{{
let data = std::collections::HashMap::<&'static str, &'static str>::new();
// Iterators *should* have keys when you can provide them.
// Keys make your app run faster. Make sure your keys are stable, unique, and predictable.
// Using an "ID" associated with your data is a good idea.
data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" }))
}}
// Matching
// Matching will throw a Rust error about "no two closures are the same type"
// To fix this, call "render" method or use the "in" syntax to produce VNodes.
// There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros)
{match true {
true => rsx!(in cx, h1 {"Top text"}),
false => cx.render(rsx!( h1 {"Bottom text"}))
}}
// ==============
// Components
// ==============
// Conditional rendering
// Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals.
// You can convert a bool condition to rsx! with .then and .or
{true.then(|| rsx!(div {}))}
// True conditions need to be rendered (same reasons as matching)
{if true {
rsx!(in cx, h1 {"Top text"})
} else {
cx.render(rsx!( h1 {"Bottom text"}))
}}
// returning "None" is a bit noisy... but rare in practice
{None as Option<()>}
// Use the Dioxus type-alias for less noise
{NONE_ELEMENT}
// can also just use empty fragments
Fragment {}
// Fragments let you insert groups of nodes without a parent.
// This lets you make components that insert elements as siblings without a container.
div {"A"}
Fragment {
div {"B"}
div {"C"}
Fragment {
"D"
Fragment {
"heavily nested fragments is an antipattern"
"they cause Dioxus to do unnecessary work"
"don't use them carelessly if you can help it"
}
}
}
// Components
// Can accept any paths
crate::baller::Baller {}
// Notice how you still get syntax highlighting and IDE support :)
Baller {}
baller::Baller { }
crate::baller::Baller {}
// Can take properties
Taller { a: "asd" }
@ -70,11 +162,14 @@ static Example: FC<()> = |ctx| {
// Can take optional properties
Taller { a: "asd" }
// Can pass in props directly
Taller { a: "asd" /* ..{props}*/ }
// Can pass in props directly as an expression
{{
let props = TallerProps {a: "hello"};
rsx!(Taller { ..props })
}}
// Can take children
Taller { a: "asd", div {} }
Taller { a: "asd", div {"hello world!"} }
}
})
};
@ -83,7 +178,8 @@ mod baller {
use super::*;
pub struct BallerProps {}
pub fn Baller(ctx: Context<()>) -> VNode {
/// This component totally balls
pub fn Baller(cx: Context<()>) -> VNode {
todo!()
}
}
@ -93,6 +189,7 @@ pub struct TallerProps {
a: &'static str,
}
/// This component is taller than most :)
pub fn Taller(ctx: Context<TallerProps>) -> VNode {
let b = true;
todo!()

View file

@ -6,22 +6,26 @@
//! Under the hood, the dioxus_webview 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::webview::launch(|ctx| {
let (count, set_count) = use_state(&ctx, || 0);
ctx.render(rsx! {
div {
h1 { "Dioxus Desktop Demo" }
p { "Count is {count}" }
button {
"Click to increment"
onclick: |_| set_count(count + 1)
}
}
})
});
dioxus::webview::launch(App);
}
static App: FC<()> = |cx| {
let (count, set_count) = use_state(&cx, || 0);
cx.render(rsx! {
div {
h1 { "Dioxus Desktop Demo" }
p { "Count is {count}" }
button {
"Click to increment"
onclick: move |_| set_count(count + 1)
}
}
})
};

View file

@ -1,51 +1,31 @@
# Parity with React
Sorted by priority
| Feature | Dioxus | React | Notes |
| ---------------------- | ------ | ----- | ------------------------------------------------ |
| ----- Phase 1 ----- | ----- | ----- | ----- |
| Conditional Rendering | ✅ | ✅ | if/then to hide/show component |
| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! |
| Keyed Components | ✅ | ✅ | advanced diffing with keys |
| Web | ✅ | ✅ | renderer for web browser |
| Desktop (webview) | ✅ | ✅ | renderer for desktop |
| Context | ✅ | ✅ | share state through the tree |
| Hook | ✅ | ✅ | memory cells in components |
| SSR | ✅ | ✅ | render directly to string |
| Runs natively | ✅ | 👀 | runs as a portable binary w/ extra tooling |
| Component Children | ✅ | ✅ | cx.children() as a list of nodes |
| Null components | ✅ | ✅ | allow returning no components |
| No-div components | ✅ | ✅ | components that render components |
| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root |
| NodeRef | 👀 | ✅ | gain direct access to nodes |
| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs |
| CSS/Inline Styles | 🛠 | ✅ | syntax for inline/conditional styles |
| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context |
| ----- Phase 2 ----- | ----- | ----- | ----- |
| 1st class router | 👀 | ✅ | Hook built on top of history |
| Assets | 👀 | ✅ | include css/svg/img url statically |
| Integrated classnames | 🛠 | 👀 | built-in `classnames` |
| Suspense | 👀 | 👀 | schedule future render from future/promise |
| Transition | 👀 | 👀 | High-level control over suspense |
| Animation | 👀 | ✅ | Spring-style animations |
| Mobile | 👀 | ✅ | Render with cacao |
| Desktop (native) | 👀 | ✅ | Render with native desktop |
| 3D Renderer | 👀 | ✅ | react-three-fiber |
| ----- Phase 3 ----- | ----- | ----- | ----- |
| Portal | 👀 | ✅ | cast elements through tree |
| Error/Panic boundary | 👀 | ✅ | catch panics and display custom BSOD |
| Code-splitting | 👀 | ✅ | Make bundle smaller/lazy |
| LiveView | 👀 | 👀 | Example for SSR + WASM apps |
Parity has moved to the homepage
## Required services:
---
Gloo is covering a lot of these. We want to build hooks around these, and provide examples on how to use them.
Gloo is covering a lot of these. We want to build hooks around these and provide examples on how to use them.
https://github.com/rustwasm/gloo
If the gloo service doesn't exist, then we need to contribute to the project
For example, resize observer would function like this:
```rust
static Example: FC<()> = |cx| {
let observer = use_resize_observer();
cx.render(rsx!(
div { ref: observer.node_ref
"Size, x: {observer.x} y: {observer.y}"
}
))
}
```
However, resize observing is _not_ cross-platform, so this hook (internally) needs to abstract over the rendering platform.
For other services, we shell out to gloo. If the gloo service doesn't exist, then we need to contribute to the project to make sure it exists.
| Service | Hook examples | Current Projects |
| ---------------------------- | ------------- | ---------------- |

View file

@ -438,6 +438,8 @@ abstract the real dom
```rust
struct VirtualDom<Dom: RealDom>
trait RealDom {
type Node: RealNode;
fn get_node(&self, id: u32) -> &Self::Node;
@ -454,7 +456,8 @@ trait RealNode {
fn set_attr(&mut self, name, value);
fn set_class(&mut self);
fn remove_attr(&mut self);
fn downcast_mut<T>(&mut self) -> Option<&mut T>;
// We can't have a generic type in trait objects, so instead we provide the inner as Any
fn raw_node_as_any_mut(&mut self) -> &mut dyn Any;
}
impl VirtualDom<Dom: RealDom> {

View file

@ -2,7 +2,7 @@
- [] Transition away from names and towards compile-time safe tags
- [] Fix diffing of fragments
- [] Properly integrate memoization to prevent safety issues with children
- [] Understand the issue with callbacks (outdated generations)
- [] Understand and fix the issue with callbacks (outdated generations)
- [] Fix examples for core, web, ssr, and general
- [] Finish up documentation
- [] Polish the Recoil (Dirac?) API
@ -16,3 +16,5 @@
- [] ...some how deserialize (hydrate) the dom state?
- [] Implement controlled inputs for select and textarea
- [] ...somehow... noderefs....
use_state(&cx, )

View file

@ -11,13 +11,13 @@ description = "CLI tool for developing, testing, and publishing Dioxus apps"
[dependencies]
thiserror = "1.0.23"
log = "0.4.13"
fern = { version = "0.6.0", features = ["colored"] }
fern = { version="0.6.0", features=["colored"] }
wasm-bindgen-cli-support = "0.2.73"
anyhow = "1.0.38"
argh = "0.1.4"
serde = "1.0.120"
serde_json = "1.0.61"
async-std = { version = "1.9.0", features = ["attributes"] }
async-std = { version="1.9.0", features=["attributes"] }
tide = "0.15.0"
fs_extra = "1.2.0"

View file

@ -131,7 +131,7 @@ fn gen_page(module: &str) -> String {
<!-- Note the usage of `type=module` here as this is an ES6 module -->
<script type="module">
import init from "{}";
init();
init("./wasm/module_bg.wasm");
</script>
</body>
</html>

View file

@ -2,18 +2,21 @@ fn main() {}
pub mod dioxus {
pub mod prelude {
pub unsafe trait Properties {
pub trait Properties {
type Builder;
const CAN_BE_MEMOIZED: bool;
fn builder() -> Self::Builder;
unsafe fn memoize(&self, other: &Self) -> bool;
}
}
}
#[derive(dioxus_core_macro::Props)]
/// This implementation should require a "PartialEq" because it memoizes (no external references)
#[derive(PartialEq, dioxus_core_macro::Props)]
struct SomeProps {
a: String,
}
/// This implementation does not require a "PartialEq" because it does not memoize
#[derive(dioxus_core_macro::Props)]
struct SomePropsTwo<'a> {
a: &'a str,

View file

@ -502,6 +502,7 @@ mod field_info {
mod struct_info {
use proc_macro2::TokenStream;
use quote::__private::ext::RepToTokensExt;
use quote::quote;
use syn::parse::Error;
@ -569,6 +570,13 @@ mod struct_info {
ref builder_name,
..
} = *self;
// we're generating stuff that goes into unsafe code here
// we use the heuristic: are there *any* generic parameters?
// If so, then they might have non-static lifetimes and we can't compare two generic things that *might borrow*
// Therefore, we will generate code that shortcircuits the "comparison" in memoization
let are_there_generics = self.generics.params.len() > 0;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
let all_fields_param = syn::GenericParam::Type(
syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(),
@ -650,6 +658,11 @@ Finally, call `.build()` to create the instance of `{name}`.
.extend(predicates.predicates.clone());
}
let can_memoize = match are_there_generics {
true => quote! { false },
false => quote! { self == other },
};
Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause {
#[doc = #builder_method_doc]
@ -679,12 +692,14 @@ Finally, call `.build()` to create the instance of `{name}`.
}
}
unsafe impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{
impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{
type Builder = #builder_name #generics_with_empty;
const CAN_BE_MEMOIZED: bool = true;
fn builder() -> Self::Builder {
#name::builder()
}
unsafe fn memoize(&self, other: &Self) -> bool {
#can_memoize
}
}
})

View file

@ -27,21 +27,23 @@ pub struct Component {
name: syn::Path,
body: Vec<ComponentField>,
children: Vec<Node>,
manual_props: Option<Expr>,
}
impl Parse for Component {
fn parse(s: ParseStream) -> Result<Self> {
fn parse(stream: ParseStream) -> Result<Self> {
// let name = s.parse::<syn::ExprPath>()?;
// todo: look into somehow getting the crate/super/etc
let name = syn::Path::parse_mod_style(s)?;
let name = syn::Path::parse_mod_style(stream)?;
// parse the guts
let content: ParseBuffer;
syn::braced!(content in s);
syn::braced!(content in stream);
let mut body: Vec<ComponentField> = Vec::new();
let mut children: Vec<Node> = Vec::new();
let mut manual_props = None;
'parsing: loop {
// [1] Break if empty
@ -49,15 +51,10 @@ impl Parse for Component {
break 'parsing;
}
if content.peek(token::Brace) && content.peek2(Token![...]) {
let inner: ParseBuffer;
syn::braced!(inner in content);
if inner.peek(Token![...]) {
todo!("Inline props not yet supported");
}
}
if content.peek(Ident) && content.peek2(Token![:]) {
if content.peek(Token![..]) {
content.parse::<Token![..]>()?;
manual_props = Some(content.parse::<Expr>()?);
} else if content.peek(Ident) && content.peek2(Token![:]) {
body.push(content.parse::<ComponentField>()?);
} else {
children.push(content.parse::<Node>()?);
@ -74,6 +71,7 @@ impl Parse for Component {
name,
body,
children,
manual_props,
})
}
}
@ -82,8 +80,13 @@ impl ToTokens for Component {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
let mut builder = quote! {
fc_to_builder(#name)
let using_manual_override = self.manual_props.is_some();
let mut builder = {
match &self.manual_props {
Some(manual_props) => quote! { #manual_props },
None => quote! { fc_to_builder(#name) },
}
};
let mut has_key = None;
@ -92,13 +95,18 @@ impl ToTokens for Component {
if field.name.to_string() == "key" {
has_key = Some(field);
} else {
builder.append_all(quote! {#field});
match using_manual_override {
true => panic!("Currently we don't support manual props and prop fields. Choose either manual props or prop fields"),
false => builder.append_all(quote! {#field}),
}
}
}
builder.append_all(quote! {
.build()
});
if !using_manual_override {
builder.append_all(quote! {
.build()
});
}
let key_token = match has_key {
Some(field) => {

View file

@ -1,2 +1,8 @@
Dodrio
VDoms
dom
virtualdom
ns
nohasher
Preact
vnodes

View file

@ -11,13 +11,13 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi
[dependencies]
# todo: use wast for faster load/compile
dioxus-core-macro = { path = "../core-macro", version = "0.1.1" }
dioxus-core-macro = { path="../core-macro", version="0.1.1" }
# Backs scopes and graphs between parent and children
generational-arena = { version = "0.2.8" }
generational-arena = { version="0.2.8" }
# Bumpalo backs the VNode creation
bumpalo = { version = "3.6.0", features = ["collections", "boxed"] }
# Bumpalo is used as a micro heap backing each component
bumpalo = { version="3.6.0", features=["collections", "boxed"] }
# custom error type
thiserror = "1"
@ -25,6 +25,9 @@ thiserror = "1"
# faster hashmaps
fxhash = "0.2.1"
# even *faster* hashmaps for index-based types
nohash-hasher = "0.2.0"
# Used in diffing
longest-increasing-subsequence = "0.1.0"
@ -32,7 +35,10 @@ longest-increasing-subsequence = "0.1.0"
log = "0.4"
# Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
serde = { version="1", features=["derive"], optional=true }
smallvec = "1.6.1"
[features]
default = []

View file

@ -12,6 +12,19 @@ Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus
- Dodrio: bump allocation, double buffering, and source code for NodeBuilder
- Percy: html! macro architecture, platform-agnostic edits
- Yew: passion and inspiration ❤️
- InfernoJS: approach to fragments and node diffing
- Preact: approach for normalization and ref
Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. Some unique features include:
- managed lifetimes for borrowed data
- suspended nodes (task/fiber endpoints) for asynchronous vnodes
- custom memory allocator for vnodes and all text content
- support for fragments w/ lazy normalization
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.
All in all, Dioxus treats memory as an incredibly valuable resource. Combined with the memory-efficient footprint of WASM compilation, Dioxus apps can scale to thousands of components and still stay snappy and respect your RAM usage.
## Goals
@ -31,40 +44,36 @@ We have big goals for Dioxus. The final implementation must:
- Support lazy VNodes (ie VNodes that are not actually created when the html! macro is used)
- Support advanced diffing strategies (patience, Meyers, etc)
## Design Quirks
- Use of "Context" as a way of mitigating threading issues and the borrow checker. (JS relies on globals)
- html! is lazy - needs to be used with a partner function to actually allocate the html. (Good be a good thing or a bad thing)
```rust
let text = TextRenderer::render(html! {<div>"hello world"</div>});
// <div>hello world</div>
```
```rust
fn main() {
tide::new()
.get("blah", serve_app("../"))
.get("blah", ws_handler(serve_app))
rsx!{ "this is a text node" }
rsx!{
div {}
"asd"
div {}
div {}
}
rsx!{
div {
a {}
b {}
c {}
Container {
Container {
Container {
Container {
Container {
div {}
}
}
}
}
}
}
}
fn serve_app(ctx: &Context<()>) -> VNode {
let livecontext = LiveContext::new()
.with_handler("graph", graph_component)
.with_handler("graph", graph_component)
.with_handler("graph", graph_component)
.with_handler("graph", graph_component)
.with_handler("graph", graph_component)
.with_handler("graph", graph_component)
.build();
ctx.render(html! {
<LiveContext ctx={livecontext}>
<App />
</ LiveContext>
})
}
```

View file

@ -1,23 +1,57 @@
# This module includes all life-cycle related mechanics, including the virtual DOM, scopes, properties, and lifecycles.
---
The VirtualDom is designed as so:
VDOM contains:
- An arena of component scopes.
- A scope contains
- lifecycle data
- hook data
- A scope contains
- lifecycle data
- hook data
- Event queue
- An event
- An event
A VDOM is
- constructed from anything that implements "component"
A "Component" is anything (normally functions) that can be ran with a context to produce VNodes
- Must implement properties-builder trait which produces a properties builder
A Context
- Is a consumable struct
- Made of references to properties
- Holds a reference (lockable) to the underlying scope
- Is partially thread-safe
- Made of references to properties
- Holds a reference (lockable) to the underlying scope
- Is partially thread-safe
# How to interact with the real dom?
## idea: use only u32
pros:
- allows for 4,294,967,295 nodes (enough)
- u32 is relatively small
- doesn't add type noise
- allows virtualdom to stay completely generic
cons:
- cost of querying individual nodes (about 7ns per node query for all sizes w/ nohasher)
- old IDs need to be manually freed when subtrees are destroyed
- can be collected as garbage after every render
- loss of ids between renders........................
- each new render doesn't know which node the old one was connected to unless it is visited
- When are nodes _not_ visited during diffing?
- They are predetermined to be removed (a parent was probed)
- something with keys?
- I think all nodes must be visited between diffs
-
## idea: leak raw nodes and then reclaim them on drop
## idea: bind

View file

@ -63,10 +63,12 @@ impl PartialEq for ChildProps {
false
}
}
unsafe impl Properties for ChildProps {
impl Properties for ChildProps {
type Builder = ();
const CAN_BE_MEMOIZED: bool = false;
fn builder() -> Self::Builder {
()
}
unsafe fn memoize(&self, other: &Self) -> bool {
self == other
}
}

View file

@ -7,21 +7,27 @@
use crate::innerlude::FC;
pub unsafe trait Properties: PartialEq + Sized {
pub trait Properties: Sized {
type Builder;
const CAN_BE_MEMOIZED: bool;
fn builder() -> Self::Builder;
/// Memoization can only happen if the props are 'static
/// The user must know if their props are static, but if they make a mistake, UB happens
/// Therefore it's unsafe to memeoize.
unsafe fn memoize(&self, other: &Self) -> bool;
}
unsafe impl Properties for () {
const CAN_BE_MEMOIZED: bool = true;
impl Properties for () {
type Builder = EmptyBuilder;
fn builder() -> Self::Builder {
EmptyBuilder {}
}
unsafe fn memoize(&self, _other: &Self) -> bool {
true
}
}
// We allow components to use the () generic parameter if they have no props. This impl enables the "build" method
// that the macros use to anonymously complete prop construction.
pub struct EmptyBuilder;
impl EmptyBuilder {
#[inline]
@ -30,6 +36,8 @@ impl EmptyBuilder {
}
}
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
/// to initialize a component's props.
pub fn fc_to_builder<T: Properties>(_: FC<T>) -> T::Builder {
T::builder()
}
@ -39,9 +47,10 @@ pub fn fc_to_builder<T: Properties>(_: FC<T>) -> T::Builder {
///
/// Fragments capture a series of children without rendering extra nodes.
///
///
///
pub static Fragment: FC<()> = |ctx| {
/// 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.
#[allow(non_upper_case_globals)]
pub const Fragment: FC<()> = |ctx| {
use crate::prelude::*;
ctx.render(LazyNodes::new(move |c| {
crate::nodebuilder::vfragment(c, None, ctx.children())

View file

@ -3,6 +3,7 @@
//!
//! Renderers don't actually need to own the virtual dom (it's up to the implementer).
use crate::innerlude::RealDom;
use crate::{events::EventTrigger, virtual_dom::VirtualDom};
use crate::{innerlude::Result, prelude::*};
@ -15,7 +16,7 @@ impl DebugRenderer {
///
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
/// The root component can access things like routing in its context.
pub fn new(root: impl for<'a> Fn(Context<'a, ()>) -> VNode + 'static) -> Self {
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root, ())
}
@ -23,10 +24,7 @@ impl DebugRenderer {
/// 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_with_props<T: Properties + 'static>(
root: impl for<'a> Fn(Context<'a, T>) -> VNode + 'static,
root_props: T,
) -> Self {
pub fn new_with_props<T: Properties + 'static>(root: FC<T>, root_props: T) -> Self {
Self::from_vdom(VirtualDom::new_with_props(root, root_props))
}
@ -40,7 +38,7 @@ impl DebugRenderer {
Ok(())
}
pub fn step(&mut self, machine: &mut DiffMachine) -> Result<()> {
pub fn step<Dom: RealDom>(&mut self, machine: &mut DiffMachine<Dom>) -> Result<()> {
Ok(())
}
@ -70,6 +68,27 @@ impl DebugRenderer {
pub fn trigger_listener(&mut self, id: usize) -> Result<()> {
Ok(())
}
pub fn render_nodes<'a, F>(&self, other: LazyNodes<'a, F>) -> Result<()>
where
F: for<'b> FnOnce(&'b NodeCtx<'a>) -> VNode<'a> + 'a,
{
Ok(())
}
}
pub struct DebugVNodeSource {
bump: Bump,
}
impl DebugVNodeSource {
fn new() -> Self {
Self { bump: Bump::new() }
}
fn render_nodes(&self) -> VNode {
// let ctx = NodeCtx
todo!()
}
}
#[cfg(test)]

File diff suppressed because it is too large Load diff

View file

@ -6,20 +6,20 @@
use std::rc::Rc;
use crate::innerlude::ScopeIdx;
use crate::{innerlude::ScopeIdx, virtual_dom::RealDomNode};
#[derive(Debug)]
pub struct EventTrigger {
pub component_id: ScopeIdx,
pub listener_id: usize,
pub real_node_id: RealDomNode,
pub event: VirtualEvent,
}
impl EventTrigger {
pub fn new(event: VirtualEvent, scope: ScopeIdx, id: usize) -> Self {
pub fn new(event: VirtualEvent, scope: ScopeIdx, mounted_dom_id: RealDomNode) -> Self {
Self {
component_id: scope,
listener_id: id,
real_node_id: mounted_dom_id,
event,
}
}
@ -44,28 +44,36 @@ pub enum VirtualEvent {
MouseEvent(Rc<dyn on::MouseEvent>),
PointerEvent(Rc<dyn on::PointerEvent>),
// image event has conflicting method types
// ImageEvent(event_data::ImageEvent),
OtherEvent,
}
pub mod on {
//! This module defines the synthetic events that all Dioxus apps enable. No matter the platform, every dioxus renderer
//! will implement the same events and same behavior (bubbling, cancelation, etc).
//!
//! Synthetic events are immutable and wrapped in Arc. It is the intention for Dioxus renderers to re-use the underyling
//! Arc allocation through "get_mut"
//!
//!
//!
#![allow(unused)]
use std::{fmt::Debug, ops::Deref, rc::Rc};
use crate::{
builder::ElementBuilder,
innerlude::{Attribute, Listener, VNode},
virtual_dom::NodeCtx,
builder::NodeCtx,
innerlude::{Attribute, Listener, RealDomNode, VNode},
};
use std::cell::Cell;
use super::VirtualEvent;
macro_rules! event_directory {
( $( $eventdata:ident: [ $( $name:ident )* ]; )* ) => {
$(
$(
pub fn $name<'a>(
c: &'_ NodeCtx<'a>,
@ -74,7 +82,7 @@ pub mod on {
let bump = &c.bump();
Listener {
event: stringify!($name),
id: *c.listener_id.borrow(),
mounted_node: bump.alloc(Cell::new(RealDomNode::empty())),
scope: c.scope_ref.arena_idx,
callback: bump.alloc(move |evt: VirtualEvent| match evt {
VirtualEvent::$eventdata(event) => callback(event),
@ -121,31 +129,31 @@ pub mod on {
/// Returns whether or not a specific event is a bubbling event
fn bubbles(&self) -> bool;
/// Sets or returns whether the event should propagate up the hierarchy or not
fn cancelBubble(&self) -> ();
fn cancel_bubble(&self);
/// Returns whether or not an event can have its default action prevented
fn cancelable(&self) -> bool;
/// Returns whether the event is composed or not
fn composed(&self) -> bool;
/// Returns the event's path
fn composedPath(&self) -> ();
fn composed_path(&self) -> String;
/// Returns the element whose event listeners triggered the event
fn currentTarget(&self) -> ();
fn current_target(&self);
/// Returns whether or not the preventDefault method was called for the event
fn defaultPrevented(&self) -> ();
fn default_prevented(&self) -> bool;
/// Returns which phase of the event flow is currently being evaluated
fn eventPhase(&self) -> ();
fn event_phase(&self) -> usize;
/// Returns whether or not an event is trusted
fn isTrusted(&self) -> ();
fn is_trusted(&self) -> bool;
/// Cancels the event if it is cancelable, meaning that the default action that belongs to the event will
fn preventDefault(&self) -> ();
fn prevent_default(&self);
/// Prevents other listeners of the same event from being called
fn stopImmediatePropagation(&self) -> ();
fn stop_immediate_propagation(&self);
/// Prevents further propagation of an event during event flow
fn stopPropagation(&self) -> ();
fn stop_propagation(&self);
/// Returns the element that triggered the event
fn target(&self) -> ();
fn target(&self);
/// Returns the time (in milliseconds relative to the epoch) at which the event was created
fn timeStamp(&self) -> usize;
fn time_stamp(&self) -> usize;
}
pub trait ClipboardEvent: Debug {
@ -180,8 +188,8 @@ pub mod on {
pub trait MouseEvent: Debug {
fn alt_key(&self) -> bool;
fn button(&self) -> i32;
fn buttons(&self) -> i32;
fn button(&self) -> i16;
fn buttons(&self) -> u16;
fn client_x(&self) -> i32;
fn client_y(&self) -> i32;
fn ctrl_key(&self) -> bool;
@ -191,7 +199,7 @@ pub mod on {
fn screen_x(&self) -> i32;
fn screen_y(&self) -> i32;
fn shift_key(&self) -> bool;
fn get_modifier_state(&self, key_code: usize) -> bool;
fn get_modifier_state(&self, key_code: &str) -> bool;
}
pub trait PointerEvent: Debug {

View file

@ -13,8 +13,7 @@ pub mod component; // Logic for extending FC
pub mod debug_renderer;
pub mod diff;
pub mod patch; // An "edit phase" described by transitions and edit operations // Test harness for validating that lifecycles and diffs work appropriately
// the diffing algorithm that builds the ChangeList
pub mod error; // Error type we expose to the renderers
pub mod events; // Manages the synthetic event API
pub mod hooks; // Built-in hooks
@ -30,14 +29,12 @@ pub mod builder {
pub(crate) mod innerlude {
pub use crate::component::*;
pub use crate::debug_renderer::*;
pub use crate::diff::*;
pub use crate::error::*;
pub use crate::events::*;
pub use crate::hooks::*;
pub use crate::nodebuilder::*;
pub use crate::nodes::*;
pub use crate::patch::*;
pub use crate::virtual_dom::*;
pub type FC<P> = fn(Context<P>) -> VNode;
@ -59,7 +56,7 @@ pub mod prelude {
pub use crate::nodebuilder::LazyNodes;
pub use crate::nodebuilder::ChildrenList;
pub use crate::virtual_dom::NodeCtx;
pub use crate::nodebuilder::NodeCtx;
// pub use nodes::iterables::IterableNodes;
/// This type alias is an internal way of abstracting over the static functions that represent components.
pub use crate::innerlude::FC;
@ -77,6 +74,6 @@ pub mod prelude {
pub use crate::diff::DiffMachine;
pub use crate::virtual_dom::ScopeIdx;
pub use crate::debug_renderer::DebugRenderer;
// pub use crate::debug_renderer::DebugRenderer;
pub use crate::hooks::*;
}

View file

@ -1,13 +1,20 @@
//! Helpers for building virtual DOM VNodes.
use std::{any::Any, borrow::BorrowMut, fmt::Arguments, intrinsics::transmute, u128};
use std::{
any::Any,
borrow::BorrowMut,
cell::{Cell, RefCell},
fmt::Arguments,
intrinsics::transmute,
u128,
};
use crate::{
events::VirtualEvent,
innerlude::{Properties, VComponent, FC},
nodes::{Attribute, Listener, NodeKey, VNode},
prelude::{VElement, VFragment},
virtual_dom::NodeCtx,
virtual_dom::{RealDomNode, Scope},
};
/// A virtual DOM element builder.
@ -351,12 +358,11 @@ where
/// ```
pub fn on(self, event: &'static str, callback: impl Fn(VirtualEvent) + 'a) -> Self {
let bump = &self.ctx.bump();
let listener = Listener {
event,
callback: bump.alloc(callback),
id: *self.ctx.listener_id.borrow(),
scope: self.ctx.scope_ref.arena_idx,
mounted_node: bump.alloc(Cell::new(RealDomNode::empty())),
};
self.add_listener(listener)
}
@ -365,7 +371,8 @@ where
self.listeners.push(listener);
// bump the context id forward
*self.ctx.listener_id.borrow_mut() += 1;
let id = self.ctx.listener_id.get();
self.ctx.listener_id.set(id + 1);
// Add this listener to the context list
// This casts the listener to a self-referential pointer
@ -376,7 +383,7 @@ where
.scope_ref
.listeners
.borrow_mut()
.push(r.callback as *const _);
.push((r.mounted_node as *const _, r.callback as *const _));
});
self
@ -492,10 +499,26 @@ where
/// .finish();
/// ```
pub fn iter_child(mut self, nodes: impl IntoIterator<Item = impl IntoVNode<'a>>) -> Self {
let len_before = self.children.len();
for item in nodes {
let child = item.into_vnode(&self.ctx);
self.children.push(child);
}
if cfg!(debug_assertions) {
if self.children.len() > len_before + 1 {
if self.children.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.
"#,
);
}
}
}
self
}
}
@ -535,17 +558,6 @@ impl<'a, F> VNodeBuilder<'a, LazyNodes<'a, F>> for LazyNodes<'a, F> where
{
}
// Cover the cases where nodes are pre-rendered.
// Likely used by enums.
// ----
// let nodes = ctx.render(rsx!{ ... };
// rsx! { {nodes } }
// impl<'a> IntoVNode<'a> for VNode {
// fn into_vnode(self, _ctx: &NodeCtx<'a>) -> VNode<'a> {
// self.root
// }
// }
// Wrap the the node-builder closure in a concrete type.
// ---
// This is a bit of a hack to implement the IntoVNode trait for closure types.
@ -597,12 +609,14 @@ where
impl<'a> IntoVNode<'a> for () {
fn into_vnode(self, ctx: &NodeCtx<'a>) -> VNode<'a> {
todo!();
VNode::Suspended
}
}
impl<'a> IntoVNode<'a> for Option<()> {
fn into_vnode(self, ctx: &NodeCtx<'a>) -> VNode<'a> {
todo!();
VNode::Suspended
}
}
@ -715,3 +729,45 @@ impl<'a, 'b> ChildrenList<'a, 'b> {
self.children.into_bump_slice()
}
}
// NodeCtx 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(Clone)]
pub struct NodeCtx<'a> {
pub scope_ref: &'a Scope,
pub listener_id: Cell<usize>,
// pub listener_id: RefCell<usize>,
}
impl<'a> NodeCtx<'a> {
#[inline]
pub fn bump(&self) -> &'a bumpalo::Bump {
&self.scope_ref.cur_frame().bump
}
/// Create some text that's allocated along with the other vnodes
pub fn text(&self, args: Arguments) -> VNode<'a> {
text3(self.bump(), args)
}
/// Create an element builder
pub fn element<'b, Listeners, Attributes, Children>(
&'b self,
tag_name: &'static str,
) -> ElementBuilder<
'a,
'b,
bumpalo::collections::Vec<'a, Listener<'a>>,
bumpalo::collections::Vec<'a, Attribute<'a>>,
bumpalo::collections::Vec<'a, VNode<'a>>,
> {
ElementBuilder::new(self, tag_name)
}
}
use std::fmt::Debug;
impl Debug for NodeCtx<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}

View file

@ -6,15 +6,14 @@
use crate::{
events::VirtualEvent,
innerlude::{Context, Properties, Scope, ScopeIdx, FC},
nodebuilder::text3,
virtual_dom::NodeCtx,
// support::NodeCtx,
nodebuilder::{text3, NodeCtx},
virtual_dom::RealDomNode,
};
use bumpalo::Bump;
use std::{
any::Any,
cell::RefCell,
fmt::{Arguments, Debug},
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
marker::PhantomData,
rc::Rc,
};
@ -29,7 +28,7 @@ pub enum VNode<'src> {
Element(&'src VElement<'src>),
/// A text node (node type `TEXT_NODE`).
Text(&'src str),
Text(VText<'src>),
/// A fragment is a "virtual position" in the DOM
/// Fragments may have children and keys
@ -49,7 +48,7 @@ impl<'a> Clone for VNode<'a> {
fn clone(&self) -> Self {
match self {
VNode::Element(element) => VNode::Element(element),
VNode::Text(text) => VNode::Text(text),
VNode::Text(old) => VNode::Text(old.clone()),
VNode::Fragment(fragment) => VNode::Fragment(fragment),
VNode::Component(component) => VNode::Component(component),
VNode::Suspended => VNode::Suspended,
@ -81,6 +80,7 @@ impl<'a> VNode<'a> {
attributes,
children,
namespace,
dom_id: Cell::new(RealDomNode::empty()),
});
VNode::Element(element)
}
@ -88,7 +88,10 @@ impl<'a> VNode<'a> {
/// Construct a new text node with the given text.
#[inline]
pub fn text(text: &'a str) -> VNode<'a> {
VNode::Text(text)
VNode::Text(VText {
text,
dom_id: Cell::new(RealDomNode::empty()),
})
}
pub fn text_args(bump: &'a Bump, args: Arguments) -> VNode<'a> {
@ -98,7 +101,7 @@ impl<'a> VNode<'a> {
#[inline]
pub(crate) fn key(&self) -> NodeKey {
match &self {
VNode::Text(_) => NodeKey::NONE,
VNode::Text { .. } => NodeKey::NONE,
VNode::Element(e) => e.key,
VNode::Fragment(frag) => frag.key,
VNode::Component(c) => c.key,
@ -107,6 +110,48 @@ impl<'a> VNode<'a> {
VNode::Suspended => NodeKey::NONE,
}
}
fn get_child(&self, id: u32) -> Option<&'a VNode<'a>> {
todo!()
}
pub fn is_real(&self) -> bool {
match self {
VNode::Element(_) => true,
VNode::Text(_) => true,
VNode::Fragment(_) => false,
VNode::Suspended => false,
VNode::Component(_) => false,
}
}
pub fn get_mounted_id(&self) -> Option<RealDomNode> {
match self {
VNode::Element(el) => Some(el.dom_id.get()),
VNode::Text(te) => Some(te.dom_id.get()),
VNode::Fragment(_) => todo!(),
VNode::Suspended => todo!(),
VNode::Component(_) => todo!(),
}
}
}
impl Debug for VNode<'_> {
fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match self {
VNode::Element(el) => write!(s, "element, {}", el.tag_name),
VNode::Text(t) => write!(s, "text, {}", t.text),
VNode::Fragment(_) => write!(s, "fragment"),
VNode::Suspended => write!(s, "suspended"),
VNode::Component(_) => write!(s, "component"),
}
}
}
#[derive(Clone)]
pub struct VText<'src> {
pub text: &'src str,
pub dom_id: Cell<RealDomNode>,
}
// ========================================================
@ -120,6 +165,7 @@ pub struct VElement<'a> {
pub attributes: &'a [Attribute<'a>],
pub children: &'a [VNode<'a>],
pub namespace: Option<&'a str>,
pub dom_id: Cell<RealDomNode>,
}
/// An attribute on a DOM node, such as `id="my-thing"` or
@ -167,11 +213,14 @@ pub struct Listener<'bump> {
/// The type of event to listen for.
pub(crate) event: &'static str,
/// Which scope?
/// This might not actually be relevant
pub scope: ScopeIdx,
pub id: usize,
pub mounted_node: &'bump Cell<RealDomNode>,
/// The callback to invoke when the event happens.
pub(crate) callback: &'bump (dyn Fn(VirtualEvent)),
pub(crate) callback: &'bump dyn Fn(VirtualEvent),
}
/// The key for keyed children.
@ -224,7 +273,8 @@ pub type VCompAssociatedScope = Option<ScopeIdx>;
pub struct VComponent<'src> {
pub key: NodeKey<'src>,
pub stable_addr: RefCell<StableScopeAddres>,
pub mounted_root: Cell<RealDomNode>,
pub ass_scope: RefCell<VCompAssociatedScope>,
// pub comparator: Rc<dyn Fn(&VComponent) -> bool + 'src>,
@ -243,96 +293,76 @@ pub struct VComponent<'src> {
}
impl<'a> VComponent<'a> {
// use the type parameter on props creation and move it into a portable context
// this lets us keep scope generic *and* downcast its props when we need to:
// - perform comparisons when diffing (memoization)
// TODO: lift the requirement that props need to be static
// we want them to borrow references... maybe force implementing a "to_static_unsafe" trait
/// When the rsx! macro is called, it will check if the CanMemo flag is set to true (from the Props impl)
/// If it is set to true, then this method will be called which implements automatic memoization.
///
/// If the CanMemo is `false`, then the macro will call the backup method which always defaults to "false"
pub fn new<P: Properties + 'a>(
// bump: &'a Bump,
ctx: &NodeCtx<'a>,
component: FC<P>,
// props: bumpalo::boxed::Box<'a, P>,
props: P,
key: Option<&'a str>,
children: &'a [VNode<'a>],
) -> Self {
// pub fn new<P: Properties + 'a>(component: FC<P>, props: P, key: Option<&'a str>) -> Self {
// let bad_props = unsafe { transmogrify(props) };
let bump = ctx.bump();
let caller_ref = component as *const ();
let props = bump.alloc(props);
let user_fc = component as *const ();
let props = bump.alloc(props);
let raw_props = props as *const P as *const ();
let comparator: Option<&dyn Fn(&VComponent) -> bool> = {
if P::CAN_BE_MEMOIZED {
Some(bump.alloc(move |other: &VComponent| {
// Safety:
// We are guaranteed that the props will be of the same type because
// there is no way to create a VComponent other than this `new` method.
//
// Therefore, if the render functions are identical (by address), then so will be
// props type paramter (because it is the same render function). Therefore, we can be
// sure
if caller_ref == other.user_fc {
// let g = other.raw_ctx.downcast_ref::<P>().unwrap();
let real_other = unsafe { &*(other.raw_props as *const _ as *const P) };
&props == &real_other
} else {
false
Some(bump.alloc(move |other: &VComponent| {
// Safety:
// ------
//
// Invariants:
// - Component function pointers are the same
// - Generic properties on the same function pointer are the same
// - Lifetime of P borrows from its parent
// - The parent scope still exists when method is called
// - Casting from T to *const () is portable
//
// Explanation:
// We are guaranteed that the props will be of the same type because
// there is no way to create a VComponent other than this `new` method.
//
// Therefore, if the render functions are identical (by address), then so will be
// props type paramter (because it is the same render function). Therefore, we can be
// sure that it is safe to interperet the previous props raw pointer as the same props
// type. From there, we can call the props' "memoize" method to see if we can
// avoid re-rendering the component.
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) };
match (props_memoized, children.len() == 0) {
(true, true) => true,
_ => false,
}
}))
} else {
None
}
} else {
false
}
}))
};
// let prref: &'a P = props.as_ref();
// let r = create_closure(component, raw_props);
// let caller: Rc<dyn for<'g> Fn(&'g Scope) -> VNode<'g>> = Rc::new(move |scope| {
// // r(scope);
// //
// // let props2 = bad_props;
// // props.as_ref();
// // let ctx = Context {
// // props: prref,
// // scope,
// // };
// // let ctx: Context<'g, P> = todo!();
// // todo!()
// // let r = component(ctx);
// todo!()
// });
let caller = create_closure(component, raw_props);
// let caller: Rc<dyn Fn(&Scope) -> VNode> = Rc::new(create_closure(component, raw_props));
let key = match key {
Some(key) => NodeKey::new(key),
None => NodeKey(None),
};
// raw_props: Box::new(props),
// comparator: Rc::new(props_comparator),
Self {
key,
ass_scope: RefCell::new(None),
user_fc: caller_ref,
user_fc,
comparator,
raw_props,
children,
caller,
stable_addr: RefCell::new(None),
ass_scope: RefCell::new(None),
key: match key {
Some(key) => NodeKey::new(key),
None => NodeKey(None),
},
caller: create_closure(component, raw_props),
mounted_root: Cell::new(RealDomNode::empty()),
}
}
}
type Captured<'a> = Rc<dyn for<'r> Fn(&'r Scope) -> VNode<'r> + 'a>;
fn create_closure<'a, P: Properties + 'a>(
fn create_closure<'a, P: 'a>(
component: FC<P>,
raw_props: *const (),
) -> Rc<dyn for<'r> Fn(&'r Scope) -> VNode<'r>> {
@ -369,3 +399,163 @@ impl<'a> VFragment<'a> {
Self { key, children }
}
}
/// This method converts a list of nested real/virtual nodes into a stream of nodes that are definitely associated
/// with the real dom. The only types of nodes that may be returned are text, elemets, and components.
///
/// Components *are* considered virtual, but this iterator can't necessarily handle them without the scope arena.
///
/// Why?
/// ---
/// Fragments are seen as virtual nodes but are actually a list of possibly-real nodes.
/// JS implementations normalize their node lists when fragments are present. Here, we just create a new iterator
/// that iterates through the recursive nesting of fragments.
///
/// Fragments are stupid and I wish we didn't need to support them.
///
/// This iterator only supports 3 levels of nested fragments
///
pub fn iterate_real_nodes<'a>(nodes: &'a [VNode<'a>]) -> RealNodeIterator<'a> {
RealNodeIterator::new(nodes)
}
pub struct RealNodeIterator<'a> {
nodes: &'a [VNode<'a>],
// this node is always a "real" node
// the index is "what sibling # is it"
// IE in a list of children on a fragment, the node will be a text node that's the 5th sibling
node_stack: Vec<(&'a VNode<'a>, u32)>,
}
impl<'a> RealNodeIterator<'a> {
// We immediately descend to the first real node we can find
fn new(nodes: &'a [VNode<'a>]) -> Self {
let mut node_stack = Vec::new();
if nodes.len() > 0 {
let mut cur_node = nodes.get(0).unwrap();
loop {
node_stack.push((cur_node, 0_u32));
if !cur_node.is_real() {
cur_node = cur_node.get_child(0).unwrap();
} else {
break;
}
}
}
Self { nodes, node_stack }
}
// // advances the cursor to the next element, panicing if we're on the 3rd level and still finding fragments
// fn advance_cursor(&mut self) {
// let (mut cur_node, mut cur_id) = self.node_stack.last().unwrap();
// while !cur_node.is_real() {
// match cur_node {
// VNode::Element(_) | VNode::Text(_) => todo!(),
// VNode::Suspended => todo!(),
// VNode::Component(_) => todo!(),
// VNode::Fragment(frag) => {
// let p = frag.children;
// }
// }
// }
// }
fn next_node(&mut self) -> bool {
let (mut cur_node, cur_id) = self.node_stack.last_mut().unwrap();
match cur_node {
VNode::Fragment(frag) => {
//
if *cur_id + 1 > frag.children.len() as u32 {
self.node_stack.pop();
let next = self.node_stack.last_mut();
return false;
}
*cur_id += 1;
true
}
VNode::Element(_) => todo!(),
VNode::Text(_) => todo!(),
VNode::Suspended => todo!(),
VNode::Component(_) => todo!(),
}
}
fn get_current_node(&self) -> Option<&VNode<'a>> {
self.node_stack.last().map(|(node, id)| match node {
VNode::Element(_) => todo!(),
VNode::Text(_) => todo!(),
VNode::Fragment(_) => todo!(),
VNode::Suspended => todo!(),
VNode::Component(_) => todo!(),
})
}
}
impl<'a> Iterator for RealNodeIterator<'a> {
type Item = &'a VNode<'a>;
fn next(&mut self) -> Option<Self::Item> {
todo!()
// let top_idx = self.nesting_idxs.get_mut(0).unwrap();
// let node = &self.nodes.get_mut(*top_idx as usize);
// if node.is_none() {
// return None;
// }
// let node = node.unwrap();
// match node {
// VNode::Element(_) | VNode::Text(_) => {
// *top_idx += 1;
// return Some(node);
// }
// VNode::Suspended => todo!(),
// // we need access over the scope map
// VNode::Component(_) => todo!(),
// VNode::Fragment(frag) => {
// let nest_idx = self.nesting_idxs.get_mut(1).unwrap();
// let node = &frag.children.get_mut(*nest_idx as usize);
// match node {
// VNode::Element(_) | VNode::Text(_) => {
// *nest_idx += 1;
// return Some(node);
// }
// VNode::Fragment(_) => todo!(),
// VNode::Suspended => todo!(),
// VNode::Component(_) => todo!(),
// }
// }
// }
}
}
mod tests {
use crate::debug_renderer::DebugRenderer;
use crate::nodebuilder::LazyNodes;
use crate as dioxus;
use dioxus::prelude::*;
#[test]
fn iterate_nodes() {
let rs = rsx! {
Fragment {
Fragment {
Fragment {
Fragment {
h1 {"abc1"}
}
h2 {"abc2"}
}
h3 {"abc3"}
}
h4 {"abc4"}
}
};
}
}

View file

@ -1,695 +0,0 @@
//! Changelist
//! ----------
//!
//! This module exposes the "changelist" object which allows 3rd party implementors to handle diffs to the virtual dom.
//!
//! # Design
//! ---
//! In essence, the changelist object connects a diff of two vdoms to the actual edits required to update the output renderer.
//!
//! This abstraction relies on the assumption that the final renderer accepts a tree of elements. For most target platforms,
//! this is an appropriate abstraction .
//!
//! During the diff phase, the change list is built. Once the diff phase is over, the change list is finished and returned back
//! to the renderer. The renderer is responsible for propogating the updates to the final display.
//!
//! Because the change list references data internal to the vdom, it needs to be consumed by the renderer before the vdom
//! can continue to work. This means once a change list is generated, it should be consumed as fast as possible, otherwise the
//! dom will be blocked from progressing. This is enforced by lifetimes on the returend changelist object.
//!
//! # Known Issues
//! ----
//! - stack machine approach does not work when 3rd party extensions inject elements (breaking our view of the dom) - solvable by the renderer
use crate::innerlude::ScopeIdx;
pub type EditList<'src> = Vec<Edit<'src>>;
/// The `Edit` represents a single modifcation of the renderer tree.
/// todo @jon, go through and make certain fields static. tag names should be known at compile time
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
#[derive(Debug)]
pub enum Edit<'src_bump> {
// ========================================================
// Common Ops: The most common operation types
// ========================================================
SetText {
text: &'src_bump str,
},
SetClass {
class_name: &'src_bump str,
},
CreateTextNode {
text: &'src_bump str,
},
CreateElement {
// todo - make static?
tag_name: &'src_bump str,
},
CreateElementNs {
// todo - make static?
tag_name: &'src_bump str,
// todo - make static?
ns: &'src_bump str,
},
// ========================================================
// Attributes
// ========================================================
SetAttribute {
name: &'src_bump str,
value: &'src_bump str,
},
RemoveAttribute {
name: &'src_bump str,
},
RemoveChild {
n: u32,
},
// ============================================================
// Event Listeners: Event types and IDs used to update the VDOM
// ============================================================
NewListener {
// todo - make static?
event: &'src_bump str,
scope: ScopeIdx,
id: usize,
},
UpdateListener {
// todo - make static?
event: &'src_bump str,
scope: ScopeIdx,
id: usize,
},
RemoveListener {
// todo - make static?
event: &'src_bump str,
},
// ========================================================
// Cached Roots: The mount point for individual components
// Allows quick traversal to cached entrypoints
// ========================================================
// push a known node on to the stack
TraverseToKnown {
node: u32,
// node: ScopeIdx,
},
// Add the current top of the stack to the known nodes
MakeKnown {
node: u32,
// node: ScopeIdx,
},
// Remove the current top of the stack from the known nodes
RemoveKnown,
// ========================================================
// Stack OPs: Operations for manipulating the stack machine
// ========================================================
PushReverseChild {
n: u32,
},
PopPushChild {
n: u32,
},
Pop,
AppendChild,
RemoveSelfAndNextSiblings {},
ReplaceWith,
SaveChildrenToTemporaries {
temp: u32,
start: u32,
end: u32,
},
PushChild {
n: u32,
},
PushTemporary {
temp: u32,
},
InsertBefore,
PopPushReverseChild {
n: u32,
},
}
/// The edit machine represents a stream of differences between two component trees.
///
/// This struct is interesting in that it keeps track of differences by borrowing
/// from the source rather than writing to a new buffer. This means that the virtual dom
/// *cannot* be updated while this machine is in existence without "unsafe".
///
/// This unsafety is handled by methods on the virtual dom and is not exposed via lib code.
pub struct EditMachine<'lock> {
pub traversal: Traversal,
next_temporary: u32,
forcing_new_listeners: bool,
pub cur_height: u32,
// // if the current node is a "known" node
// // any actions that modify this node should update the mapping
// current_known: Option<u32>,
pub emitter: EditList<'lock>,
}
impl<'lock> EditMachine<'lock> {
pub fn new() -> Self {
Self {
// current_known: None,
traversal: Traversal::new(),
cur_height: 0,
next_temporary: 0,
forcing_new_listeners: false,
emitter: EditList::<'lock>::default(),
}
}
}
// ===================================
// Traversal Methods
// ===================================
impl<'src> EditMachine<'src> {
pub fn go_down(&mut self) {
self.traversal.down();
}
pub fn go_down_to_child(&mut self, index: usize) {
self.traversal.down();
self.traversal.sibling(index);
}
pub fn go_down_to_reverse_child(&mut self, index: usize) {
self.traversal.down();
self.traversal.reverse_sibling(index);
}
pub fn go_up(&mut self) {
self.traversal.up();
}
pub fn go_to_sibling(&mut self, index: usize) {
self.traversal.sibling(index);
}
pub fn go_to_temp_sibling(&mut self, temp: u32) {
self.traversal.up();
self.traversal.down_to_temp(temp);
}
pub fn go_down_to_temp_child(&mut self, temp: u32) {
self.traversal.down_to_temp(temp);
}
pub fn commit_traversal(&mut self) {
if self.traversal.is_committed() {
return;
}
for mv in self.traversal.commit() {
match mv {
MoveTo::Parent => self.emitter.push(Edit::Pop {}),
MoveTo::Child(n) => self.emitter.push(Edit::PushChild { n }),
MoveTo::ReverseChild(n) => self.emitter.push(Edit::PushReverseChild { n }),
MoveTo::Sibling(n) => self.emitter.push(Edit::PopPushChild { n }),
MoveTo::ReverseSibling(n) => self.emitter.push(Edit::PopPushReverseChild { n }),
MoveTo::TempChild(temp) => self.emitter.push(Edit::PushTemporary { temp }),
}
}
}
pub fn traversal_is_committed(&self) -> bool {
self.traversal.is_committed()
}
}
// ===================================
// Stack methods
// ===================================
impl<'a> EditMachine<'a> {
pub fn next_temporary(&self) -> u32 {
self.next_temporary
}
pub fn set_next_temporary(&mut self, next_temporary: u32) {
self.next_temporary = next_temporary;
}
pub fn save_children_to_temporaries(&mut self, start: usize, end: usize) -> u32 {
debug_assert!(self.traversal_is_committed());
debug_assert!(start < end);
let temp_base = self.next_temporary;
self.next_temporary = temp_base + (end - start) as u32;
self.emitter.push(Edit::SaveChildrenToTemporaries {
temp: temp_base,
start: start as u32,
end: end as u32,
});
temp_base
}
pub fn push_temporary(&mut self, temp: u32) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::PushTemporary { temp });
}
pub fn remove_child(&mut self, child: usize) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::RemoveChild { n: child as u32 })
}
pub fn insert_before(&mut self) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::InsertBefore {})
}
pub fn set_text(&mut self, text: &'a str) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::SetText { text });
}
pub fn remove_self_and_next_siblings(&mut self) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::RemoveSelfAndNextSiblings {});
}
pub fn replace_with(&mut self) {
debug_assert!(self.traversal_is_committed());
// debug!("emit: replace_with()");
// if let Some(id) = self.current_known {
// // update mapping
// self.emitter.push(Edit::MakeKnown{node: id});
// self.current_known = None;
// }
// self.emitter.replace_with();
self.emitter.push(Edit::ReplaceWith {});
}
pub fn set_attribute(&mut self, name: &'a str, value: &'a str, is_namespaced: bool) {
debug_assert!(self.traversal_is_committed());
if name == "class" && !is_namespaced {
self.emitter.push(Edit::SetClass { class_name: value });
} else {
self.emitter.push(Edit::SetAttribute { name, value });
}
}
pub fn remove_attribute(&mut self, name: &'a str) {
self.emitter.push(Edit::RemoveAttribute { name });
}
pub fn append_child(&mut self) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::AppendChild {});
}
pub fn create_text_node(&mut self, text: &'a str) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::CreateTextNode { text });
}
pub fn create_element(&mut self, tag_name: &'a str) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::CreateElement { tag_name });
}
pub fn create_element_ns(&mut self, tag_name: &'a str, ns: &'a str) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::CreateElementNs { tag_name, ns });
}
pub fn push_force_new_listeners(&mut self) -> bool {
let old = self.forcing_new_listeners;
self.forcing_new_listeners = true;
old
}
pub fn pop_force_new_listeners(&mut self, previous: bool) {
debug_assert!(self.forcing_new_listeners);
self.forcing_new_listeners = previous;
}
pub fn new_event_listener(&mut self, event: &'a str, scope: ScopeIdx, id: usize) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::NewListener { event, scope, id });
}
pub fn update_event_listener(&mut self, event: &'a str, scope: ScopeIdx, id: usize) {
debug_assert!(self.traversal_is_committed());
if self.forcing_new_listeners {
self.new_event_listener(event, scope, id);
return;
}
self.emitter.push(Edit::NewListener { event, scope, id });
}
pub fn remove_event_listener(&mut self, event: &'a str) {
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::RemoveListener { event });
}
pub fn save_known_root(&mut self, id: u32) {
log::debug!("emit: save_known_root({:?})", id);
self.emitter.push(Edit::MakeKnown { node: id })
}
pub fn load_known_root(&mut self, id: u32) {
log::debug!("emit: TraverseToKnown({:?})", id);
self.emitter.push(Edit::TraverseToKnown { node: id })
}
}
// Keeps track of where we are moving in a DOM tree, and shortens traversal
// paths between mutations to their minimal number of operations.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MoveTo {
/// Move from the current node up to its parent.
Parent,
/// Move to the current node's n^th child.
Child(u32),
/// Move to the current node's n^th from last child.
ReverseChild(u32),
/// Move to the n^th sibling. Not relative from the current
/// location. Absolute indexed within all of the current siblings.
Sibling(u32),
/// Move to the n^th from last sibling. Not relative from the current
/// location. Absolute indexed within all of the current siblings.
ReverseSibling(u32),
/// Move down to the given saved temporary child.
TempChild(u32),
}
#[derive(Debug)]
pub struct Traversal {
uncommitted: Vec<MoveTo>,
}
impl Traversal {
/// Construct a new `Traversal` with its internal storage backed by the
/// given bump arena.
pub fn new() -> Traversal {
Traversal {
uncommitted: Vec::with_capacity(32),
}
}
/// Move the traversal up in the tree.
pub fn up(&mut self) {
match self.uncommitted.last() {
Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
self.uncommitted.pop();
self.uncommitted.push(MoveTo::Parent);
}
Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) | Some(MoveTo::ReverseChild(_)) => {
self.uncommitted.pop();
// And we're back at the parent.
}
_ => {
self.uncommitted.push(MoveTo::Parent);
}
}
}
/// Move the traversal down in the tree to the first child of the current
/// node.
pub fn down(&mut self) {
if let Some(&MoveTo::Parent) = self.uncommitted.last() {
self.uncommitted.pop();
self.sibling(0);
} else {
self.uncommitted.push(MoveTo::Child(0));
}
}
/// Move the traversal to the n^th sibling.
pub fn sibling(&mut self, index: usize) {
let index = index as u32;
match self.uncommitted.last_mut() {
Some(MoveTo::Sibling(ref mut n)) | Some(MoveTo::Child(ref mut n)) => {
*n = index;
}
Some(MoveTo::ReverseSibling(_)) => {
self.uncommitted.pop();
self.uncommitted.push(MoveTo::Sibling(index));
}
Some(MoveTo::TempChild(_)) | Some(MoveTo::ReverseChild(_)) => {
self.uncommitted.pop();
self.uncommitted.push(MoveTo::Child(index))
}
_ => {
self.uncommitted.push(MoveTo::Sibling(index));
}
}
}
/// Move the the n^th from last sibling.
pub fn reverse_sibling(&mut self, index: usize) {
let index = index as u32;
match self.uncommitted.last_mut() {
Some(MoveTo::ReverseSibling(ref mut n)) | Some(MoveTo::ReverseChild(ref mut n)) => {
*n = index;
}
Some(MoveTo::Sibling(_)) => {
self.uncommitted.pop();
self.uncommitted.push(MoveTo::ReverseSibling(index));
}
Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) => {
self.uncommitted.pop();
self.uncommitted.push(MoveTo::ReverseChild(index))
}
_ => {
self.uncommitted.push(MoveTo::ReverseSibling(index));
}
}
}
/// Go to the given saved temporary.
pub fn down_to_temp(&mut self, temp: u32) {
match self.uncommitted.last() {
Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
self.uncommitted.pop();
}
Some(MoveTo::Parent)
| Some(MoveTo::TempChild(_))
| Some(MoveTo::Child(_))
| Some(MoveTo::ReverseChild(_))
| None => {
// Can't remove moves to parents since we rely on their stack
// pops.
}
}
self.uncommitted.push(MoveTo::TempChild(temp));
}
/// Are all the traversal's moves committed? That is, are there no moves
/// that have *not* been committed yet?
#[inline]
pub fn is_committed(&self) -> bool {
self.uncommitted.is_empty()
}
/// Commit this traversals moves and return the optimized path from the last
/// commit.
#[inline]
pub fn commit(&mut self) -> std::vec::Drain<'_, MoveTo> {
self.uncommitted.drain(..)
}
#[inline]
pub fn reset(&mut self) {
self.uncommitted.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_traversal() {
fn t<F>(f: F) -> Box<dyn FnMut(&mut Traversal)>
where
F: 'static + FnMut(&mut Traversal),
{
Box::new(f) as _
}
for (mut traverse, expected_moves) in vec![
(
t(|t| {
t.down();
}),
vec![MoveTo::Child(0)],
),
(
t(|t| {
t.up();
}),
vec![MoveTo::Parent],
),
(
t(|t| {
t.sibling(42);
}),
vec![MoveTo::Sibling(42)],
),
(
t(|t| {
t.down();
t.up();
}),
vec![],
),
(
t(|t| {
t.down();
t.sibling(2);
t.up();
}),
vec![],
),
(
t(|t| {
t.down();
t.sibling(3);
}),
vec![MoveTo::Child(3)],
),
(
t(|t| {
t.down();
t.sibling(4);
t.sibling(8);
}),
vec![MoveTo::Child(8)],
),
(
t(|t| {
t.sibling(1);
t.sibling(1);
}),
vec![MoveTo::Sibling(1)],
),
(
t(|t| {
t.reverse_sibling(3);
}),
vec![MoveTo::ReverseSibling(3)],
),
(
t(|t| {
t.down();
t.reverse_sibling(3);
}),
vec![MoveTo::ReverseChild(3)],
),
(
t(|t| {
t.down();
t.reverse_sibling(3);
t.up();
}),
vec![],
),
(
t(|t| {
t.down();
t.reverse_sibling(3);
t.reverse_sibling(6);
}),
vec![MoveTo::ReverseChild(6)],
),
(
t(|t| {
t.up();
t.reverse_sibling(3);
t.reverse_sibling(6);
}),
vec![MoveTo::Parent, MoveTo::ReverseSibling(6)],
),
(
t(|t| {
t.up();
t.sibling(3);
t.sibling(6);
}),
vec![MoveTo::Parent, MoveTo::Sibling(6)],
),
(
t(|t| {
t.sibling(3);
t.sibling(6);
t.up();
}),
vec![MoveTo::Parent],
),
(
t(|t| {
t.reverse_sibling(3);
t.reverse_sibling(6);
t.up();
}),
vec![MoveTo::Parent],
),
(
t(|t| {
t.down();
t.down_to_temp(3);
}),
vec![MoveTo::Child(0), MoveTo::TempChild(3)],
),
(
t(|t| {
t.down_to_temp(3);
t.sibling(5);
}),
vec![MoveTo::Child(5)],
),
(
t(|t| {
t.down_to_temp(3);
t.reverse_sibling(5);
}),
vec![MoveTo::ReverseChild(5)],
),
(
t(|t| {
t.down_to_temp(3);
t.up();
}),
vec![],
),
(
t(|t| {
t.sibling(2);
t.up();
t.down_to_temp(3);
}),
vec![MoveTo::Parent, MoveTo::TempChild(3)],
),
(
t(|t| {
t.up();
t.down_to_temp(3);
}),
vec![MoveTo::Parent, MoveTo::TempChild(3)],
),
] {
let mut traversal = Traversal::new();
traverse(&mut traversal);
let actual_moves: Vec<_> = traversal.commit().collect();
assert_eq!(actual_moves, expected_moves);
}
}
}

View file

@ -24,7 +24,7 @@ use bumpalo::Bump;
use generational_arena::Arena;
use std::{
any::{Any, TypeId},
cell::RefCell,
cell::{Cell, RefCell},
collections::{HashMap, HashSet, VecDeque},
fmt::Debug,
future::Future,
@ -63,6 +63,17 @@ pub struct VirtualDom {
_root_prop_type: std::any::TypeId,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RealDomNode(pub u32);
impl RealDomNode {
pub fn new(id: u32) -> Self {
Self(id)
}
pub fn empty() -> Self {
Self(u32::MIN)
}
}
// ======================================
// Public Methods for the VirtualDom
// ======================================
@ -98,7 +109,7 @@ impl VirtualDom {
///
/// let dom = VirtualDom::new(Example);
/// ```
pub fn new(root: impl Fn(Context<()>) -> VNode + 'static) -> Self {
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root, ())
}
@ -130,10 +141,7 @@ impl VirtualDom {
///
/// let dom = VirtualDom::new(Example);
/// ```
pub fn new_with_props<P: Properties + 'static>(
root: impl Fn(Context<P>) -> VNode + 'static,
root_props: P,
) -> Self {
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
let components = ScopeArena::new(Arena::new());
// Normally, a component would be passed as a child in the RSX macro which automatically produces OpaqueComponents
@ -174,11 +182,17 @@ impl VirtualDom {
_root_prop_type: TypeId::of::<P>(),
}
}
}
// ======================================
// Private Methods for the VirtualDom
// ======================================
impl VirtualDom {
/// 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
pub fn rebuild<'s>(&'s mut self) -> Result<EditList<'s>> {
pub fn rebuild<'s, Dom: RealDom>(&'s mut self, realdom: &mut Dom) -> Result<()> {
let mut diff_machine = DiffMachine::new(
realdom,
self.components.clone(),
self.base_scope,
self.event_queue.clone(),
@ -195,14 +209,8 @@ impl VirtualDom {
self.progress_completely(&mut diff_machine)?;
Ok(diff_machine.consume())
Ok(())
}
}
// ======================================
// Private Methods for the VirtualDom
// ======================================
impl VirtualDom {
/// 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
@ -246,26 +254,34 @@ impl VirtualDom {
// 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 fn progress_with_event(&mut self, event: EventTrigger) -> Result<EditList> {
let id = event.component_id.clone();
pub fn progress_with_event<Dom: RealDom>(
&mut self,
realdom: &mut Dom,
trigger: EventTrigger,
) -> Result<()> {
let id = trigger.component_id.clone();
self.components.try_get_mut(id)?.call_listener(event)?;
self.components.try_get_mut(id)?.call_listener(trigger)?;
let mut diff_machine =
DiffMachine::new(self.components.clone(), id, self.event_queue.clone());
let mut diff_machine = DiffMachine::new(
realdom,
self.components.clone(),
id,
self.event_queue.clone(),
);
self.progress_completely(&mut diff_machine)?;
Ok(diff_machine.consume())
Ok(())
}
/// Consume the event queue, descending depth-first.
/// Only ever run each component once.
///
/// The DiffMachine logs its progress as it goes which might be useful for certain types of renderers.
pub(crate) fn progress_completely<'s>(
pub(crate) fn progress_completely<'s, Dom: RealDom>(
&'s mut self,
diff_machine: &'_ mut DiffMachine<'s>,
diff_machine: &'_ mut DiffMachine<'s, Dom>,
) -> Result<()> {
// Add this component to the list of components that need to be difed
// #[allow(unused_assignments)]
@ -302,7 +318,9 @@ impl VirtualDom {
cur_component.run_scope()?;
// diff_machine.change_list.load_known_root(1);
diff_machine.diff_node(cur_component.old_frame(), cur_component.next_frame());
let (old, new) = (cur_component.old_frame(), cur_component.next_frame());
// let (old, new) = cur_component.get_frames_mut();
diff_machine.diff_node(old, new);
// cur_height = cur_component.height;
@ -376,7 +394,12 @@ pub struct Scope {
// - is self-refenrential and therefore needs to point into the bump
// Stores references into the listeners attached to the vnodes
// NEEDS TO BE PRIVATE
pub(crate) listeners: RefCell<Vec<*const dyn Fn(VirtualEvent)>>,
pub(crate) listeners: RefCell<Vec<(*const Cell<RealDomNode>, *const dyn Fn(VirtualEvent))>>,
// pub(crate) listeners: RefCell<nohash_hasher::IntMap<u32, *const dyn Fn(VirtualEvent)>>,
// pub(crate) listeners: RefCell<Vec<*const dyn Fn(VirtualEvent)>>,
// pub(crate) listeners: RefCell<Vec<*const dyn Fn(VirtualEvent)>>,
// NoHashMap<RealDomNode, <*const dyn Fn(VirtualEvent)>
// pub(crate) listeners: RefCell<Vec<*const dyn Fn(VirtualEvent)>>,
}
// We need to pin the hook so it doesn't move as we initialize the list of hooks
@ -429,7 +452,7 @@ impl Scope {
let child_nodes = unsafe { std::mem::transmute(child_nodes) };
Self {
child_nodes: child_nodes,
child_nodes,
caller,
parent,
arena_idx,
@ -469,12 +492,12 @@ impl Scope {
self.frames.next().bump.reset();
// Remove all the outdated listeners
//
self.listeners
.try_borrow_mut()
.ok()
.ok_or(Error::FatalInternal("Borrowing listener failed"))?
.drain(..);
self.listeners.borrow_mut().clear();
// self.listeners
// .try_borrow_mut()
// .ok()
// .ok_or(Error::FatalInternal("Borrowing listener failed"))?
// .drain(..);
*self.hookidx.borrow_mut() = 0;
@ -508,25 +531,35 @@ impl Scope {
// The listener list will be completely drained because the next frame will write over previous listeners
pub fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> {
let EventTrigger {
listener_id, event, ..
real_node_id,
event,
..
} = trigger;
//
unsafe {
// Convert the raw ptr into an actual object
// This operation is assumed to be safe
let listener_fn = self
.listeners
.try_borrow()
.ok()
.ok_or(Error::FatalInternal("Borrowing listener failed"))?
.get(listener_id as usize)
.ok_or(Error::FatalInternal("Event should exist if triggered"))?
.as_ref()
.ok_or(Error::FatalInternal("Raw event ptr is invalid"))?;
// Run the callback with the user event
// todo: implement scanning for outdated events
// Convert the raw ptr into an actual object
// This operation is assumed to be safe
log::debug!("Calling listeners! {:?}", self.listeners.borrow().len());
let listners = self.listeners.borrow();
let (_, listener) = listners
.iter()
.find(|(domptr, _)| {
let p = unsafe { &**domptr };
p.get() == real_node_id
})
.expect(&format!(
"Failed to find real node with ID {:?}",
real_node_id
));
// TODO: Don'tdo a linear scan! Do a hashmap lookup! It'll be faster!
unsafe {
let listener_fn = &**listener;
listener_fn(event);
}
Ok(())
}
@ -745,7 +778,7 @@ Any function prefixed with "use" should not be called conditionally.
}
/// There are hooks going on here!
fn use_context<T: 'static>(&self) -> &'src Rc<T> {
fn use_context<T: 'static>(&self) -> &'src T {
self.try_use_context().unwrap()
}
@ -858,26 +891,6 @@ impl PartialOrd for HeightMarker {
}
}
// NodeCtx 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(Clone)]
pub struct NodeCtx<'a> {
pub scope_ref: &'a Scope,
pub listener_id: RefCell<usize>,
}
impl<'a> NodeCtx<'a> {
pub fn bump(&self) -> &'a Bump {
&self.scope_ref.cur_frame().bump
}
}
impl Debug for NodeCtx<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
#[derive(Debug, PartialEq, Hash)]
pub struct ContextId {
// Which component is the scope in

View file

@ -0,0 +1,30 @@
//! POC: Planning the layout of a single element type
//!
//!
//! The ultimate goal with a dedicated namespace is three-fold:
//! - compile-time correct templates preventing misuse of elemnents
//! - deep integration of DSL with IDE
//!
//!
//!
struct NodeCtx {}
struct div<'a>(&NodeCtx);
impl<'a> div<'a> {
fn new(cx: &NodeCtx) -> Self {
div(cx)
}
}
fn main() {}
fn factory(
// this is your mom
cx: &NodeCtx,
) {
div::new(cx);
rsx! {
div {}
}
}

View file

@ -23,20 +23,15 @@ wasm-bindgen-test = "0.3.21"
once_cell = "1.7.2"
atoms = { path="../atoms" }
# wasm-bindgen = "0.2.70"
# futures = "0.3.12"
# html-validation = { path = "../html-validation", version = "0.1.1" }
async-channel = "1.6.1"
wee_alloc = "0.4.5"
# futures-lite = "1.11.3"
nohash-hasher = "0.2.0"
anyhow = "1.0.41"
[dependencies.web-sys]
version = "0.3.50"
features = [
"Comment",
"Document",
# "DataTransfer",
"Element",
"HtmlElement",
"HtmlInputElement",
@ -63,24 +58,16 @@ features = [
"DocumentType",
"CharacterData",
"HtmlOptionElement",
]
[profile.release]
lto = true
opt-level = 's'
# debug = true
# [profile.release]
[lib]
crate-type = ["cdylib", "rlib"]
[dev-dependencies]
im-rc = "15.0.0"
rand = { version="0.8.4", features=["small_rng"] }
uuid = { version="0.8.2", features=["v4", "wasm-bindgen"] }
[[example]]
name = "todomvc"
path = "./examples/todomvc/main.rs"

View file

@ -1,5 +1,6 @@
//! Basic example that renders a simple VNode to the browser.
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_web::*;
@ -50,15 +51,15 @@ static DocExamples: FC<()> = |ctx| {
}
};
rsx! {
div {}
h1 {}
{""}
"asbasd"
dioxus::Fragment {
//
}
}
// rsx! {
// div {}
// h1 {}
// {""}
// "asbasd"
// dioxus::Fragment {
// //
// }
// }
ctx.render(rsx! {
div {

View file

@ -2,16 +2,17 @@
//! --------------------
//! This example demonstrates how to use the raw context api for sharing state throughout the VirtualDOM Tree.
//! A custom context must be its own unique type - otherwise use_context will fail. A context may be c
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
use dioxus_core::prelude::*;
use dioxus_core as dioxus;
use dioxus_web::WebsysRenderer;
fn main() {
@ -20,7 +21,6 @@ fn main() {
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example));
}
#[derive(Debug)]
struct CustomContext([&'static str; 3]);
@ -46,7 +46,6 @@ static Example: FC<()> = |ctx| {
})
};
#[derive(Props, PartialEq)]
struct ButtonProps {
id: u8,

View file

@ -1,5 +1,6 @@
use std::rc::Rc;
use dioxus_core as dioxus;
use dioxus_web::{dioxus::prelude::*, WebsysRenderer};
fn main() {

View file

@ -1,3 +1,4 @@
use dioxus_core as dioxus;
use dioxus_web::{dioxus::prelude::*, WebsysRenderer};
fn main() {
@ -10,7 +11,7 @@ fn App(ctx: Context<()>) -> VNode {
ctx.render(rsx! {
main { class: "dark:bg-gray-800 bg-white relative h-screen"
NavBar {}
{(0..10).map(|f| rsx!{ Landing {} })}
{(0..10).map(|f| rsx!(Landing { key: "{f}" }))}
}
})
}
@ -61,7 +62,7 @@ fn Landing(ctx: Context<()>) -> VNode {
div { class: "container mx-auto px-6 flex flex-col justify-between items-center relative py-8"
div { class: "flex flex-col"
h1 { class: "font-light w-full uppercase text-center text-4xl sm:text-5xl dark:text-white text-gray-800"
"The React Framework for Production"
"The Dioxus Framework for Production"
}
h2{ class: "font-light max-w-2xl mx-auto w-full text-xl dark:text-white text-gray-500 text-center py-8"
"Next.js gives you the best developer experience with all the features you need for production: \n

View file

@ -8,10 +8,6 @@ fn main() {
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App));
}
// Use `wee_alloc` as the global allocator.
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
fn App(ctx: Context<()>) -> VNode {
let cansee = use_state_new(&ctx, || false);
rsx! { in ctx,

View file

@ -5,17 +5,17 @@ use dioxus_core::prelude::*;
fn main() {}
fn autocomplete() {
let handler = move |evt| {
let r = evt.alt_key();
if evt.alt_key() {}
};
// let handler = move |evt| {
// let r = evt.alt_key();
// if evt.alt_key() {}
// };
let g = rsx! {
button {
button {
onclick: {handler}
}
}
// let g = rsx! {
// button {
// button {
// onclick: {handler}
// }
// }
};
// };
}

View file

@ -0,0 +1,184 @@
//! JS Framework Benchmark
//! ----------------------
//!
//! This example is used in the JS framework benchmarking tool to compare Dioxus' performance with other frontend frameworks.
//!
//!
//!
use std::rc::Rc;
use dioxus::events::on::MouseEvent;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_web::WebsysRenderer;
fn main() {
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App));
}
// We use a special immutable hashmap to make hashmap operations efficient
type RowList = im_rc::HashMap<usize, Rc<str>, nohash_hasher::BuildNoHashHasher<usize>>;
static App: FC<()> = |cx| {
let (items, set_items) = use_state(&cx, || RowList::default());
let (selection, set_selection) = use_state(&cx, || None as Option<usize>);
let create_rendered_rows = move |from, num| move |_| set_items(create_row_list(from, num));
let append_1_000_rows =
move |_| set_items(create_row_list(items.len(), 1000).union(items.clone()));
let update_every_10th_row = move |_| {
let mut new_items = items.clone();
let mut small_rng = SmallRng::from_entropy();
new_items
.iter_mut()
.step_by(10)
.for_each(|(_, val)| *val = create_new_row_label(&mut small_rng));
set_items(new_items);
};
let clear_rows = move |_| set_items(RowList::default());
let swap_rows = move |_| {
// this looks a bit ugly because we're using a hashmap instead of a vec
if items.len() > 998 {
let mut new_items = items.clone();
let a = new_items.get(&0).unwrap().clone();
*new_items.get_mut(&0).unwrap() = new_items.get(&998).unwrap().clone();
*new_items.get_mut(&998).unwrap() = a;
set_items(new_items);
}
};
let rows = items.iter().map(|(key, value)| {
rsx!(Row {
key: "{key}",
row_id: *key as usize,
label: value.clone(),
})
});
cx.render(rsx! {
div { class: "container"
div { class: "jumbotron"
div { class: "row"
div { class: "col-md-6", h1 { "Dioxus" } }
div { class: "col-md-6"
div { class: "row"
ActionButton { name: "Create 1,000 rows", id: "run", action: create_rendered_rows(0, 1_000) }
ActionButton { name: "Create 10,000 rows", id: "runlots", action: create_rendered_rows(0, 10_000) }
ActionButton { name: "Append 1,000 rows", id: "add", action: append_1_000_rows }
ActionButton { name: "Update every 10th row", id: "update", action: update_every_10th_row, }
ActionButton { name: "Clear", id: "clear", action: clear_rows }
ActionButton { name: "Swap rows", id: "swaprows", action: swap_rows }
}
}
}
}
table {
tbody {
{rows}
}
}
span {}
}
})
};
#[derive(Props)]
struct ActionButtonProps<F: Fn(Rc<dyn MouseEvent>)> {
name: &'static str,
id: &'static str,
action: F,
}
fn ActionButton<F: Fn(Rc<dyn MouseEvent>)>(cx: Context<ActionButtonProps<F>>) -> VNode {
cx.render(rsx! {
div { class: "col-sm-6 smallpad"
button {class:"btn btn-primary btn-block", type: "button", id: "{cx.id}", onclick: {&cx.action},
"{cx.name}"
}
}
})
}
#[derive(PartialEq, Props)]
struct RowProps {
row_id: usize,
label: Rc<str>,
}
fn Row<'a>(cx: Context<'a, RowProps>) -> VNode {
cx.render(rsx! {
tr {
td { class:"col-md-1", "{cx.row_id}" }
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
a { class: "lbl", "{cx.label}" }
}
td { class: "col-md-1"
a { class: "remove", onclick: move |_| {/* remove */}
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
}
}
td { class: "col-md-6" }
}
})
}
use rand::prelude::*;
fn create_new_row_label(rng: &mut SmallRng) -> Rc<str> {
let mut label = String::new();
label.push_str(ADJECTIVES.choose(rng).unwrap());
label.push(' ');
label.push_str(COLOURS.choose(rng).unwrap());
label.push(' ');
label.push_str(NOUNS.choose(rng).unwrap());
Rc::from(label)
}
fn create_row_list(from: usize, num: usize) -> RowList {
let mut small_rng = SmallRng::from_entropy();
(from..num + from)
.map(|f| (f, create_new_row_label(&mut small_rng)))
.collect::<RowList>()
}
static ADJECTIVES: &[&str] = &[
"pretty",
"large",
"big",
"small",
"tall",
"short",
"long",
"handsome",
"plain",
"quaint",
"clean",
"elegant",
"easy",
"angry",
"crazy",
"helpful",
"mushy",
"odd",
"unsightly",
"adorable",
"important",
"inexpensive",
"cheap",
"expensive",
"fancy",
];
static COLOURS: &[&str] = &[
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
"orange",
];
static NOUNS: &[&str] = &[
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
"pizza", "mouse", "keyboard",
];

View file

@ -1,3 +1,4 @@
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_web::WebsysRenderer;
@ -10,12 +11,20 @@ fn main() {
}
static Example: FC<()> = |ctx| {
let nodes = (0..15).map(|f| rsx! (li { key: "{f}", "{f}"}));
ctx.render(rsx! {
div {
span {
class: "px-2 py-1 flex w-36 mt-4 items-center text-xs rounded-md font-semibold text-yellow-500 bg-yellow-100"
"DUE DATE : 18 JUN"
"DUE DATE : 189 JUN"
}
p {
"these"
"are"
"text"
"nodes"
}
{nodes}
}
})
};

View file

@ -1,3 +1,4 @@
use dioxus_core as dioxus;
use dioxus_web::prelude::*;
fn main() {

View file

@ -1,3 +1,4 @@
use dioxus_core as dioxus;
use dioxus_core::{events::on::MouseEvent, prelude::*};
use dioxus_web::WebsysRenderer;
@ -14,7 +15,7 @@ fn main() {
static Example: FC<()> = |ctx| {
let (event, set_event) = use_state(&ctx, || None);
let handler = move |evt: MouseEvent| {
let handler = move |evt| {
set_event(Some(evt));
};
@ -42,17 +43,15 @@ static Example: FC<()> = |ctx| {
})
};
#[derive(Debug, PartialEq, Props)]
struct ExampleProps {
name: String
name: String,
}
static Example2: FC<ExampleProps> = |ctx| {
ctx.render(rsx!{
ctx.render(rsx! {
div {
h1 {"hello {ctx.name}"}
}
})
};

View file

@ -26,25 +26,22 @@ static App: FC<()> = |ctx| {
id: "username"
type: "text"
value: "{val}"
oninput: move |evet| {
log::debug!("Value is {:#?}", evet);
set_val(evet.value);
}
oninput: move |evet| set_val(evet.value())
}
p { "Val is: {val}" }
}
}
}
})
})
};
static Example: FC<()> = |ctx| {
ctx.render(rsx! {
div { class: "max-w-lg max-w-xs bg-blue-800 shadow-2xl rounded-lg mx-auto text-center py-12 mt-4 rounded-xl"
div { class: "container py-5 max-w-md mx-auto"
h1 { class: "text-gray-200 text-center font-extrabold -mt-3 text-3xl",
h1 { class: "text-gray-200 text-center font-extrabold -mt-3 text-3xl",
"Text Input Example"
}
}
UserInput {}
}
}
@ -54,19 +51,15 @@ static Example: FC<()> = |ctx| {
static UserInput: FC<()> = |ctx| {
let (val, set_val) = use_state(&ctx, || "asd".to_string());
rsx!{ in ctx,
rsx! { in ctx,
div { class: "mb-4"
input { class: "shadow appearance-none rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder: "Username"
id: "username"
id: "username"
type: "text"
oninput: move |evet| {
log::debug!("Value is {:#?}", evet);
set_val(evet.value);
}
oninput: move |evet| set_val(evet.value())
}
p { "Val is: {val}" }
}
}
};

View file

@ -33,12 +33,11 @@ static Example: FC<()> = |ctx| {
<button
class="inline-block py-4 px-8 mr-6 leading-none text-white bg-indigo-600 hover:bg-indigo-900 font-semibold rounded shadow"
onclick={move |_| set_name("jack")}>
"Jack!"
"Jack!"
</button>
<button
class="inline-block py-4 px-8 mr-6 leading-none text-white bg-indigo-600 hover:bg-indigo-900 font-semibold rounded shadow"
onclick={move |_| set_name("jill")}
onclick={move |_| set_name("jill")}>
"Jill!"
</button>

View file

@ -6,7 +6,7 @@ use dioxus_core::prelude::*;
use dioxus_web::*;
fn main() {
// Setup logging
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
// wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
// Run the app
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App));

View file

@ -53,7 +53,7 @@ static App: FC<()> = |ctx| {
input {
class: "new-todo"
placeholder: "What needs to be done?"
oninput: move |evt| set_draft(evt.value)
oninput: move |evt| set_draft(evt.value())
}
}

View file

@ -0,0 +1,28 @@
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_web::WebsysRenderer;
fn main() {
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
log::info!("hello world");
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(JonsFavoriteCustomApp));
}
fn JonsFavoriteCustomApp(cx: Context<()>) -> VNode {
let items = (0..20).map(|f| {
rsx! {
li {"{f}"}
}
});
cx.render(rsx! {
div {
"list"
ul {
{items}
}
}
})
}

View file

@ -1,3 +1,4 @@
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_web::WebsysRenderer;

View file

@ -0,0 +1,13 @@
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
#[derive(Props)]
struct MyProps<'a> {
blah: u128,
b: &'a (),
}
fn main() {
// let p = unsafe { MyProps {}.memoize(&MyProps {}) };
// dbg!(p);
}

View file

@ -1,14 +1,18 @@
#![allow(non_snake_case)]
use dioxus_core as dioxus;
use std::rc::Rc;
use dioxus::{events::on::MouseEvent, prelude::*};
use dioxus_core as dioxus;
use dioxus_web::WebsysRenderer;
fn main() {
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
console_error_panic_hook::set_once();
wasm_bindgen_futures::spawn_local(async {
let props = ExampleProps { initial_name: "..?"};
let props = ExampleProps {
initial_name: "..?",
};
WebsysRenderer::new_with_props(Example, props)
.run()
.await
@ -25,17 +29,17 @@ static Example: FC<ExampleProps> = |ctx| {
let name = use_state_new(&ctx, move || ctx.initial_name);
ctx.render(rsx! {
div {
div {
class: "py-12 px-4 text-center w-full max-w-2xl mx-auto"
span {
span {
class: "text-sm font-semibold"
"Dioxus Example: Jack and Jill"
}
h2 {
class: "text-5xl mt-2 mb-6 leading-tight font-semibold font-heading"
h2 {
class: "text-5xl mt-2 mb-6 leading-tight font-semibold font-heading"
"Hello, {name}"
}
CustomButton { name: "Jack!", handler: move |_| name.set("Jack") }
CustomButton { name: "Jill!", handler: move |_| name.set("Jill") }
CustomButton { name: "Bob!", handler: move |_| name.set("Bob")}
@ -45,12 +49,10 @@ static Example: FC<ExampleProps> = |ctx| {
})
};
#[derive(Props)]
struct ButtonProps<'src, F: Fn(MouseEvent)> {
struct ButtonProps<'src, F: Fn(Rc<dyn MouseEvent>)> {
name: &'src str,
handler: F
handler: F,
}
fn CustomButton<'a, F: Fn(MouseEvent)>(ctx: Context<'a, ButtonProps<'a, F>>) -> VNode {
@ -69,13 +71,12 @@ impl<F: Fn(MouseEvent)> PartialEq for ButtonProps<'_, F> {
}
}
#[derive(Props, PartialEq)]
struct PlaceholderProps {
val: &'static str
val: &'static str,
}
fn Placeholder(ctx: Context<PlaceholderProps>) -> VNode {
ctx.render(rsx!{
ctx.render(rsx! {
div {
"child: {ctx.val}"
}

View file

@ -1,10 +1,11 @@
use dioxus_core as dioxus;
use dioxus_web::{prelude::*, WebsysRenderer};
mod filtertoggles;
mod recoil;
mod state;
mod todoitem;
mod todolist;
// mod filtertoggles;
// mod recoil;
// mod state;
// mod todoitem;
// mod todolist;
static APP_STYLE: &'static str = include_str!("./style.css");
@ -13,10 +14,10 @@ fn main() {
ctx.render(rsx! {
div {
id: "app"
style { "{APP_STYLE}" }
// style { "{APP_STYLE}" }
// list
todolist::TodoList {}
// todolist::TodoList {}
// footer
footer {

View file

@ -1,5 +1,6 @@
use std::{collections::HashMap, rc::Rc};
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_web::WebsysRenderer;
@ -65,7 +66,7 @@ pub fn TodoList(ctx: Context<()>) -> VNode {
class: "new-todo"
placeholder: "What needs to be done?"
value: "{draft}"
oninput: move |evt| set_draft(evt.value)
oninput: move |evt| set_draft(evt.value())
}
}
@ -78,7 +79,7 @@ pub fn TodoList(ctx: Context<()>) -> VNode {
FilterState::Completed => item.checked,
})
.map(|(id, item)| {
TodoEntry!();
// TodoEntry!();
todo!()
// rsx!(TodoEntry {
// key: "{order}",
@ -100,17 +101,14 @@ pub struct TodoEntryProps {
item: Rc<TodoItem>,
}
pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> VNode {
// #[inline_props]
pub fn TodoEntry(
ctx: Context,
baller: &impl Fn() -> (),
caller: &impl Fn() -> (),
todo: &Rc<TodoItem>,
) -> VNode {
// pub fn TodoEntry(ctx: Context, todo: &Rc<TodoItem>) -> VNode {
pub fn TodoEntry(ctx: Context<TodoEntryProps>) -> VNode {
let (is_editing, set_is_editing) = use_state(&ctx, || false);
// let todo = &ctx.item;
let contents = "";
let todo = TodoItem {
checked: false,
contents: "asd".to_string(),
id: uuid::Uuid::new_v4(),
};
ctx.render(rsx! (
li {

View file

@ -9,7 +9,7 @@
//! Here, we show to use Dioxus' Recoil state management solution to simplify app logic
#![allow(non_snake_case)]
use dioxus_web::dioxus::prelude::*;
use recoil::*;
use std::collections::HashMap;
use uuid::Uuid;

View file

@ -12,7 +12,8 @@ pub use dioxus_core as dioxus;
use dioxus_core::{events::EventTrigger, prelude::FC};
pub use dioxus_core::prelude;
pub mod interpreter;
// pub mod interpreter;
pub mod new;
/// The `WebsysRenderer` provides a way of rendering a Dioxus Virtual DOM to the browser's DOM.
/// Under the hood, we leverage WebSys and interact directly with the DOM
@ -33,7 +34,7 @@ impl WebsysRenderer {
///
/// Run the app to completion, panicing if any error occurs while rendering.
/// Pairs well with the wasm_bindgen async handler
pub async fn start(root: impl for<'a> Fn(Context<'a, ()>) -> VNode + 'static) {
pub async fn start(root: FC<()>) {
Self::new(root).run().await.expect("Virtual DOM failed :(");
}
@ -41,7 +42,7 @@ impl WebsysRenderer {
///
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
/// The root component can access things like routing in its context.
pub fn new(root: impl for<'a> Fn(Context<'a, ()>) -> VNode + 'static) -> Self {
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root, ())
}
@ -49,10 +50,7 @@ impl WebsysRenderer {
/// 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_with_props<T: Properties + 'static>(
root: impl for<'a> Fn(Context<'a, T>) -> VNode + 'static,
root_props: T,
) -> Self {
pub fn new_with_props<T: Properties + 'static>(root: FC<T>, root_props: T) -> Self {
Self::from_vdom(VirtualDom::new_with_props(root, root_props))
}
@ -62,62 +60,65 @@ impl WebsysRenderer {
}
pub async fn run(&mut self) -> dioxus_core::error::Result<()> {
let (sender, mut receiver) = async_channel::unbounded::<EventTrigger>();
let body_element = prepare_websys_dom();
let mut patch_machine = interpreter::PatchMachine::new(body_element.clone(), move |ev| {
log::debug!("Event trigger! {:#?}", ev);
let mut c = sender.clone();
wasm_bindgen_futures::spawn_local(async move {
c.send(ev).await.unwrap();
});
});
let root_node = body_element.first_child().unwrap();
patch_machine.stack.push(root_node.clone());
// todo: initialize the event registry properly on the root
let mut websys_dom = crate::new::WebsysDom::new(body_element.clone());
let edits = self.internal_dom.rebuild()?;
log::debug!("Received edits: {:#?}", edits);
edits.iter().for_each(|edit| {
log::debug!("patching with {:?}", edit);
patch_machine.handle_edit(edit);
});
websys_dom.stack.push(root_node);
patch_machine.reset();
let root_node = body_element.first_child().unwrap();
patch_machine.stack.push(root_node.clone());
self.internal_dom.rebuild(&mut websys_dom)?;
while let Some(trigger) = websys_dom.wait_for_event().await {
let root_node = body_element.first_child().unwrap();
websys_dom.stack.push(root_node.clone());
self.internal_dom
.progress_with_event(&mut websys_dom, trigger)?;
}
// let edits = self.internal_dom.rebuild()?;
// log::debug!("Received edits: {:#?}", edits);
// edits.iter().for_each(|edit| {
// log::debug!("patching with {:?}", edit);
// patch_machine.handle_edit(edit);
// });
// patch_machine.reset();
// let root_node = body_element.first_child().unwrap();
// patch_machine.stack.push(root_node.clone());
// log::debug!("patch stack size {:?}", patch_machine.stack);
// Event loop waits for the receiver to finish up
// TODO! Connect the sender to the virtual dom's suspense system
// Suspense is basically an external event that can force renders to specific nodes
while let Ok(event) = receiver.recv().await {
log::debug!("Stack before entrance {:#?}", patch_machine.stack.top());
// log::debug!("patch stack size before {:#?}", patch_machine.stack);
// patch_machine.reset();
// patch_machine.stack.push(root_node.clone());
let edits = self.internal_dom.progress_with_event(event)?;
log::debug!("Received edits: {:#?}", edits);
// while let Ok(event) = receiver.recv().await {
// log::debug!("Stack before entrance {:#?}", patch_machine.stack.top());
// log::debug!("patch stack size before {:#?}", patch_machine.stack);
// patch_machine.reset();
// patch_machine.stack.push(root_node.clone());
// self.internal_dom
// .progress_with_event(&mut websys_dom, event)?;
// let edits = self.internal_dom.progress_with_event(event)?;
// log::debug!("Received edits: {:#?}", edits);
for edit in &edits {
// log::debug!("edit stream {:?}", edit);
// log::debug!("Stream stack {:#?}", patch_machine.stack.top());
patch_machine.handle_edit(edit);
}
// for edit in &edits {
// // log::debug!("edit stream {:?}", edit);
// // log::debug!("Stream stack {:#?}", patch_machine.stack.top());
// patch_machine.handle_edit(edit);
// }
// log::debug!("patch stack size after {:#?}", patch_machine.stack);
patch_machine.reset();
// our root node reference gets invalidated
// not sure why
// for now, just select the first child again.
// eventually, we'll just make our own root element instead of using body
// or just use body directly IDEK
let root_node = body_element.first_child().unwrap();
patch_machine.stack.push(root_node.clone());
}
// log::debug!("patch stack size after {:#?}", patch_machine.stack);
// patch_machine.reset();
// our root node reference gets invalidated
// not sure why
// for now, just select the first child again.
// eventually, we'll just make our own root element instead of using body
// or just use body directly IDEK
// let root_node = body_element.first_child().unwrap();
// patch_machine.stack.push(root_node.clone());
// }
Ok(()) // should actually never return from this, should be an error, rustc just cant see it
}

604
packages/web/src/new.rs Normal file
View file

@ -0,0 +1,604 @@
use std::{collections::HashMap, rc::Rc, sync::Arc};
use dioxus_core::{
events::{EventTrigger, VirtualEvent},
prelude::ScopeIdx,
virtual_dom::RealDomNode,
};
use fxhash::FxHashMap;
use nohash_hasher::IntMap;
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node,
};
pub struct WebsysDom {
pub stack: Stack,
nodes: IntMap<u32, Node>,
document: Document,
root: Element,
event_receiver: async_channel::Receiver<EventTrigger>,
trigger: Arc<dyn Fn(EventTrigger)>,
// every callback gets a monotomically increasing callback ID
callback_id: usize,
// map of listener types to number of those listeners
listeners: FxHashMap<String, (usize, Closure<dyn FnMut(&Event)>)>,
// Map of callback_id to component index and listener id
callback_map: FxHashMap<usize, (usize, usize)>,
// We need to make sure to add comments between text nodes
// We ensure that the text siblings are patched by preventing the browser from merging
// neighboring text nodes. Originally inspired by some of React's work from 2016.
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
// -> https://github.com/facebook/react/pull/5753
//
// `ptns` = Percy text node separator
// TODO
last_node_was_text: bool,
node_counter: Counter,
}
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");
let (sender, mut receiver) = async_channel::unbounded::<EventTrigger>();
let sender_callback = Arc::new(move |ev| {
let mut c = sender.clone();
wasm_bindgen_futures::spawn_local(async move {
c.send(ev).await.unwrap();
});
});
let mut nodes =
HashMap::with_capacity_and_hasher(1000, nohash_hasher::BuildNoHashHasher::default());
nodes.insert(0_u32, root.clone().dyn_into::<Node>().unwrap());
Self {
stack: Stack::with_capacity(10),
nodes,
callback_id: 0,
listeners: FxHashMap::default(),
callback_map: FxHashMap::default(),
document,
event_receiver: receiver,
trigger: sender_callback,
root,
last_node_was_text: false,
node_counter: Counter(0),
}
}
pub async fn wait_for_event(&mut self) -> Option<EventTrigger> {
let v = self.event_receiver.recv().await.unwrap();
Some(v)
}
}
struct Counter(u32);
impl Counter {
fn next(&mut self) -> u32 {
self.0 += 1;
self.0
}
}
impl dioxus_core::diff::RealDom for WebsysDom {
fn push_root(&mut self, root: dioxus_core::virtual_dom::RealDomNode) {
log::debug!("Called `[`push_root] {:?}", root);
let domnode = self.nodes.get(&root.0).expect("Failed to pop know root");
self.stack.push(domnode.clone());
}
fn append_child(&mut self) {
log::debug!("Called [`append_child`]");
let child = self.stack.pop();
if child.dyn_ref::<web_sys::Text>().is_some() {
if self.last_node_was_text {
let comment_node = self
.document
.create_comment("dioxus")
.dyn_into::<Node>()
.unwrap();
self.stack.top().append_child(&comment_node).unwrap();
}
self.last_node_was_text = true;
} else {
self.last_node_was_text = false;
}
self.stack.top().append_child(&child).unwrap();
}
fn replace_with(&mut self) {
log::debug!("Called [`replace_with`]");
let new_node = self.stack.pop();
let old_node = self.stack.pop();
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);
}
// // poc to see if this is a valid solution
// if let Some(id) = self.current_known {
// // update mapping
// self.known_roots.insert(id, new_node.clone());
// self.current_known = None;
// }
self.stack.push(new_node);
}
fn remove(&mut self) {
log::debug!("Called [`remove`]");
todo!()
}
fn remove_all_children(&mut self) {
log::debug!("Called [`remove_all_children`]");
todo!()
}
fn create_text_node(&mut self, text: &str) -> dioxus_core::virtual_dom::RealDomNode {
let nid = self.node_counter.next();
let textnode = self
.document
.create_text_node(text)
.dyn_into::<Node>()
.unwrap();
self.stack.push(textnode.clone());
self.nodes.insert(nid, textnode);
log::debug!("Called [`create_text_node`]: {}, {}", text, nid);
RealDomNode::new(nid)
}
fn create_element(&mut self, tag: &str) -> dioxus_core::virtual_dom::RealDomNode {
let el = self
.document
.create_element(tag)
.unwrap()
.dyn_into::<Node>()
.unwrap();
self.stack.push(el.clone());
let nid = self.node_counter.next();
self.nodes.insert(nid, el);
log::debug!("Called [`create_element`]: {}, {:?}", tag, nid);
RealDomNode::new(nid)
}
fn create_element_ns(
&mut self,
tag: &str,
namespace: &str,
) -> dioxus_core::virtual_dom::RealDomNode {
let el = self
.document
.create_element_ns(Some(namespace), tag)
.unwrap()
.dyn_into::<Node>()
.unwrap();
self.stack.push(el.clone());
let nid = self.node_counter.next();
self.nodes.insert(nid, el);
log::debug!("Called [`create_element_ns`]: {:}", nid);
RealDomNode::new(nid)
}
fn new_event_listener(
&mut self,
event: &str,
scope: dioxus_core::prelude::ScopeIdx,
el_id: usize,
real_id: RealDomNode,
) {
log::debug!(
"Called [`new_event_listener`]: {}, {:?}, {}, {:?}",
event,
scope,
el_id,
real_id
);
// attach the correct attributes to the element
// these will be used by accessing the event's target
// This ensures we only ever have one handler attached to the root, but decide
// dynamically when we want to call a listener.
let el = self.stack.top();
let el = el
.dyn_ref::<Element>()
.expect(&format!("not an element: {:?}", el));
let (gi_id, gi_gen) = (&scope).into_raw_parts();
el.set_attribute(
&format!("dioxus-event-{}", event),
&format!("{}.{}.{}.{}", gi_id, gi_gen, el_id, real_id.0),
)
.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 handler = Closure::wrap(Box::new(move |event: &web_sys::Event| {
// "Result" cannot be received from JS
// Instead, we just build and immediately execute a closure that returns result
let res = || -> anyhow::Result<EventTrigger> {
log::debug!("Handling event!");
let target = event
.target()
.expect("missing target")
.dyn_into::<Element>()
.expect("not a valid element");
let typ = event.type_();
use anyhow::Context;
let val: String = target
.get_attribute(&format!("dioxus-event-{}", typ))
.context("")?;
let mut fields = val.splitn(4, ".");
let gi_id = fields
.next()
.and_then(|f| f.parse::<usize>().ok())
.context("")?;
let gi_gen = fields
.next()
.and_then(|f| f.parse::<u64>().ok())
.context("")?;
let el_id = fields
.next()
.and_then(|f| f.parse::<usize>().ok())
.context("")?;
let real_id = fields
.next()
.and_then(|f| f.parse::<u32>().ok().map(RealDomNode::new))
.context("")?;
// Call the trigger
log::debug!(
"decoded gi_id: {}, gi_gen: {}, li_idx: {}",
gi_id,
gi_gen,
el_id
);
let triggered_scope = ScopeIdx::from_raw_parts(gi_id, gi_gen);
Ok(EventTrigger::new(
virtual_event_from_websys_event(event),
triggered_scope,
real_id,
))
};
match res() {
Ok(synthetic_event) => trigger.as_ref()(synthetic_event),
Err(_) => log::error!("Error decoding Dioxus event attribute."),
};
}) as Box<dyn FnMut(&Event)>);
self.root
.add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref())
.unwrap();
// Increment the listeners
self.listeners.insert(event.into(), (1, handler));
}
}
fn remove_event_listener(&mut self, event: &str) {
log::debug!("Called [`remove_event_listener`]: {}", event);
todo!()
}
fn set_text(&mut self, text: &str) {
log::debug!("Called [`set_text`]: {}", text);
self.stack.top().set_text_content(Some(text))
}
fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) {
log::debug!("Called [`set_attribute`]: {}, {}", name, value);
if name == "class" {
if let Some(el) = self.stack.top().dyn_ref::<Element>() {
el.set_class_name(value);
}
} else {
if let Some(el) = self.stack.top().dyn_ref::<Element>() {
el.set_attribute(name, value).unwrap();
}
}
}
fn remove_attribute(&mut self, name: &str) {
log::debug!("Called [`remove_attribute`]: {}", name);
let node = self.stack.top();
if let Some(node) = node.dyn_ref::<web_sys::Element>() {
node.remove_attribute(name).unwrap();
}
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
// Some attributes are "volatile" and don't work through `removeAttribute`.
if name == "value" {
node.set_value("");
}
if name == "checked" {
node.set_checked(false);
}
}
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
if name == "selected" {
node.set_selected(true);
}
}
}
fn raw_node_as_any_mut(&self) -> &mut dyn std::any::Any {
log::debug!("Called [`raw_node_as_any_mut`]");
todo!()
}
}
#[derive(Debug, Default)]
pub struct Stack {
list: Vec<Node>,
}
impl Stack {
pub fn with_capacity(cap: usize) -> Self {
Stack {
list: Vec::with_capacity(cap),
}
}
pub fn push(&mut self, node: Node) {
// debug!("stack-push: {:?}", node);
self.list.push(node);
}
pub fn pop(&mut self) -> Node {
let res = self.list.pop().unwrap();
res
}
pub fn clear(&mut self) {
self.list.clear();
}
pub fn top(&self) -> &Node {
match self.list.last() {
Some(a) => a,
None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
}
}
}
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!()
}
"compositionend" | "compositionstart" | "compositionupdate" => {
let evt: web_sys::CompositionEvent = event.clone().dyn_into().unwrap();
todo!()
}
"keydown" | "keypress" | "keyup" => {
let evt: web_sys::KeyboardEvent = event.clone().dyn_into().unwrap();
todo!()
}
"focus" | "blur" => {
let evt: web_sys::FocusEvent = event.clone().dyn_into().unwrap();
todo!()
}
"change" => {
let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ");
todo!()
// VirtualEvent::FormEvent(FormEvent {value:})
}
"input" | "invalid" | "reset" | "submit" => {
// is a special react events
let evt: web_sys::InputEvent = event.clone().dyn_into().expect("wrong event type");
let this: web_sys::EventTarget = evt.target().unwrap();
let value = (&this)
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| input.value())
.or_else(|| {
(&this)
.dyn_ref()
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
})
.or_else(|| {
(&this)
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.text_content()
})
.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 })
}
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
let evt: web_sys::MouseEvent = event.clone().dyn_into().unwrap();
#[derive(Debug)]
pub struct CustomMouseEvent(web_sys::MouseEvent);
impl dioxus_core::events::on::MouseEvent for CustomMouseEvent {
fn alt_key(&self) -> bool {
self.0.alt_key()
}
fn button(&self) -> i16 {
self.0.button()
}
fn buttons(&self) -> u16 {
self.0.buttons()
}
fn client_x(&self) -> i32 {
self.0.client_x()
}
fn client_y(&self) -> i32 {
self.0.client_y()
}
fn ctrl_key(&self) -> bool {
self.0.ctrl_key()
}
fn meta_key(&self) -> bool {
self.0.meta_key()
}
fn page_x(&self) -> i32 {
self.0.page_x()
}
fn page_y(&self) -> i32 {
self.0.page_y()
}
fn screen_x(&self) -> i32 {
self.0.screen_x()
}
fn screen_y(&self) -> i32 {
self.0.screen_y()
}
fn shift_key(&self) -> bool {
self.0.shift_key()
}
// yikes
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
fn get_modifier_state(&self, key_code: &str) -> bool {
self.0.get_modifier_state(key_code)
}
}
VirtualEvent::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"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
let evt: web_sys::PointerEvent = event.clone().dyn_into().unwrap();
todo!()
}
"select" => {
// let evt: web_sys::SelectionEvent = event.clone().dyn_into().unwrap();
// not required to construct anything special beyond standard event stuff
todo!()
}
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
let evt: web_sys::TouchEvent = event.clone().dyn_into().unwrap();
todo!()
}
"scroll" => {
// let evt: web_sys::UIEvent = event.clone().dyn_into().unwrap();
todo!()
}
"wheel" => {
let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap();
todo!()
}
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => {
// not required to construct anything special beyond standard event stuff
// let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap();
// let evt: web_sys::MediaEvent = 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!()
}
"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,
}
}

View file

@ -2,7 +2,6 @@ use std::{borrow::Borrow, convert::TryInto, default, fmt::Debug, sync::Arc};
use dioxus_core::{
events::{EventTrigger, VirtualEvent},
patch::Edit,
prelude::ScopeIdx,
};
use fxhash::FxHashMap;

View file

@ -182,12 +182,14 @@ pub mod prelude {
pub use dioxus_core::prelude::*;
pub use dioxus_core_macro::fc;
}
pub mod builder {
// pub use dioxus_core::builder::*;
}
pub mod events {
// pub use dioxus_core::events::*;
}
// pub mod builder {
// // pub use dioxus_core::builder::*;
// }
pub use dioxus_core::builder;
pub use dioxus_core::events;
// pub mod events {
// // pub use dioxus_core::events::*;
// }
// Just a heads-up, the core functionality of dioxus rests in Dioxus-Core. This crate just wraps a bunch of utilities
// together and exports their namespaces to something predicatble.
#[cfg(feature = "core")]
@ -229,6 +231,6 @@ pub mod atoms {}
// #[cfg(feature = "desktop")]
pub mod webview {
//! A webview based renderer for building desktop applications with Dioxus
use dioxus_core::prelude::FC;
pub fn launch<P>(f: FC<P>) {}
use dioxus_core::prelude::{Properties, FC};
pub fn launch<P: Properties>(f: FC<P>) {}
}