Merge branch 'master' into full-tailwind-css-example

This commit is contained in:
ealmloff 2023-04-24 17:15:05 -05:00 committed by GitHub
commit 4b5bea171a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
129 changed files with 7048 additions and 4924 deletions

View file

@ -39,7 +39,7 @@ jobs:
override: true
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
- uses: actions/checkout@v3
- uses: actions-rs/cargo@v1
with:
@ -58,7 +58,7 @@ jobs:
override: true
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
- uses: davidB/rust-cargo-make@v1
- uses: browser-actions/setup-firefox@latest
- uses: jetli/wasm-pack-action@v0.4.0
@ -98,7 +98,7 @@ jobs:
override: true
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
- run: rustup component add clippy
- uses: actions/checkout@v3
- uses: actions-rs/cargo@v1

View file

@ -15,7 +15,8 @@ members = [
"packages/liveview",
"packages/autofmt",
"packages/rsx",
"packages/tui",
"packages/dioxus-tui",
"packages/rink",
"packages/native-core",
"packages/native-core-macro",
"packages/rsx-rosetta",
@ -24,6 +25,7 @@ members = [
"docs/guide",
# Full project examples
"examples/tailwind",
"examples/PWA-example",
]
# This is a "virtual package"

View file

@ -11,10 +11,13 @@ dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop" }
dioxus-web = { path = "../../packages/web" }
dioxus-ssr = { path = "../../packages/ssr" }
dioxus-native-core = { path = "../../packages/native-core" }
dioxus-native-core-macro = { path = "../../packages/native-core-macro" }
dioxus-router = { path = "../../packages/router" }
dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] }
dioxus-tui = { path = "../../packages/tui" }
dioxus-tui = { path = "../../packages/dioxus-tui" }
fermi = { path = "../../packages/fermi" }
shipyard = "0.6.2"
# dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] }

View file

@ -0,0 +1,311 @@
use dioxus::html::input_data::keyboard_types::{Code, Key, Modifiers};
use dioxus::prelude::*;
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use dioxus_native_core_macro::partial_derive_state;
// ANCHOR: state_impl
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
// ANCHOR_END: state_impl
// ANCHOR: rendering
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn app(cx: Scope) -> Element {
let count = use_state(cx, || 0);
use_future(cx, (count,), |(count,)| async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
count.set(*count + 1);
}
});
cx.render(rsx! {
div{
color: "red",
"{count}"
}
})
}
// create the vdom, the real_dom, and the binding layer between them
let mut vdom = VirtualDom::new(app);
let mut rdom: RealDom = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut dioxus_intigration_state = DioxusState::create(&mut rdom);
let mutations = vdom.rebuild();
// update the structure of the real_dom tree
dioxus_intigration_state.apply_mutations(&mut rdom, mutations);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// wait for the vdom to update
vdom.wait_for_work().await;
// get the mutations from the vdom
let mutations = vdom.render_immediate();
// update the structure of the real_dom tree
dioxus_intigration_state.apply_mutations(&mut rdom, mutations);
// update the state of the real_dom tree
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first(|node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
let node = node.node_type();
let node_type = &*node;
println!("{indent}{id:?} {color:?} {size:?} {border:?} {node_type:?}");
});
}
})
}
// ANCHOR_END: rendering
// ANCHOR: derive_state
// All states must derive Component (https://docs.rs/shipyard/latest/shipyard/derive.Component.html)
// They also must implement Default or provide a custom implementation of create in the State trait
#[derive(Default, Component)]
struct MyState;
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for MyState {
// The states of the parent nodes this state depends on
type ParentDependencies = ();
// The states of the child nodes this state depends on
type ChildDependencies = (Self,);
// The states of the current node this state depends on
type NodeDependencies = ();
// The parts of the current text, element, or placeholder node in the tree that this state depends on
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
// How to update the state of the current node based on the state of the parent nodes, child nodes, and the current node
// Returns true if the node was updated and false if the node was not updated
fn update<'a>(
&mut self,
// The view of the current node limited to the parts this state depends on
_node_view: NodeView<()>,
// The state of the current node that this state depends on
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
// The state of the parent nodes that this state depends on
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
// The state of the child nodes that this state depends on
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
// The context of the current node used to pass global state into the tree
_context: &SendAnyMap,
) -> bool {
todo!()
}
// partial_derive_state will generate a default implementation of all the other methods
}
// ANCHOR_END: derive_state
#[allow(unused)]
// ANCHOR: cursor
fn text_editing() {
let mut cursor = Cursor::default();
let mut text = String::new();
// handle keyboard input with a max text length of 10
cursor.handle_input(
&Code::ArrowRight,
&Key::ArrowRight,
&Modifiers::empty(),
&mut text,
10,
);
// mannually select text between characters 0-5 on the first line (this could be from dragging with a mouse)
cursor.start = Pos::new(0, 0);
cursor.end = Some(Pos::new(5, 0));
// delete the selected text and move the cursor to the start of the selection
cursor.delete_selection(&mut text);
}
// ANCHOR_END: cursor

View file

@ -99,13 +99,14 @@ Template {
attr_paths: &'a [&'a [u8]],
}
```
> For more detailed docs about the struture of templates see the [Template api docs](https://docs.rs/dioxus-core/latest/dioxus_core/prelude/struct.Template.html)
This template will be sent to the renderer in the [list of templates](https://docs.rs/dioxus-core/latest/dioxus_core/struct.Mutations.html#structfield.templates) supplied with the mutations the first time it is used. Any time the renderer encounters a [LoadTemplate](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.LoadTemplate) mutation after this, it should clone the template and store it in the given id.
For dynamic nodes and dynamic text nodes, a placeholder node should be created and inserted into the UI so that the node can be navigated to later.
For dynamic nodes and dynamic text nodes, a placeholder node should be created and inserted into the UI so that the node can be modified later.
In HTML renderers, this template could look like:
In HTML renderers, this template could look like this:
```html
<h1>""</h1>
@ -206,7 +207,9 @@ nodes: [
"count: 0",
]
```
Over time, our stack looked like this:
```rust
[Root]
[Root, <h1>""</h1>]
@ -218,15 +221,15 @@ Conveniently, this approach completely separates the Virtual DOM and the Real DO
Dioxus is also really fast. Because Dioxus splits the diff and patch phase, it's able to make all the edits to the RealDOM in a very short amount of time (less than a single frame) making rendering very snappy. It also allows Dioxus to cancel large diffing operations if higher priority work comes in while it's diffing.
This little demo serves to show exactly how a Renderer would need to process an edit stream to build UIs.
This little demo serves to show exactly how a Renderer would need to process a mutation stream to build UIs.
## 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.
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 for 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
```rust, ignore
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());
@ -254,9 +257,9 @@ pub async fn run(&mut self) -> dioxus_core::error::Result<()> {
}
```
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 `UserEvent` type. Right now, the VirtualEvent system is modeled almost entirely around the HTML spec, but we are interested in slimming it down.
It's important to decode what the real events are for your event system into Dioxus' synthetic event system (synthetic meaning abstracted). This simply means matching your event type and creating a Dioxus `UserEvent` type. Right now, the virtual event system is modeled almost entirely around the HTML spec, but we are interested in slimming it down.
```rust
```rust, ignore
fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
match event.type_().as_str() {
"keydown" => {
@ -294,16 +297,17 @@ For more examples and information on how to create custom namespaces, see the [`
# Native Core
If you are creating a renderer in rust, the [native-core](https://github.com/DioxusLabs/dioxus/tree/master/packages/native-core) crate provides some utilities to implement a renderer. It provides an abstraction over Mutations and Templates and contains helpers that can handle the layout, and text editing for you.
If you are creating a renderer in rust, the [native-core](https://github.com/DioxusLabs/dioxus/tree/master/packages/native-core) crate provides some utilities to implement a renderer. It provides an abstraction over Mutations and Templates and contains helpers that can handle the layout and text editing for you.
## The RealDom
The `RealDom` is a higher-level abstraction over updating the Dom. It updates with `Mutations` and provides a way to incrementally update the state of nodes based on attributes or other states that change.
The `RealDom` is a higher-level abstraction over updating the Dom. It uses an entity component system to manage the state of nodes. This system allows you to modify insert and modify arbitrary components on nodes. On top of this, the RealDom provides a way to manage a tree of nodes, and the State trait provides a way to automatically add and update these components when the tree is modified. It also provides a way to apply `Mutations` to the RealDom.
### Example
Let's build a toy renderer with borders, size, and text color.
Before we start let's take a look at an example element we can render:
```rust
cx.render(rsx!{
div{
@ -372,188 +376,24 @@ In the following diagram arrows represent dataflow:
[//]: # " end"
[//]: # " end"
To help in building a Dom, native-core provides four traits: State, ChildDepState, ParentDepState, NodeDepState, and a RealDom struct. The ChildDepState, ParentDepState, and NodeDepState provide a way to describe how some information in a node relates to that of its relatives. By providing how to build a single node from its relations, native-core will derive a way to update the state of all nodes for you with ```#[derive(State)]```. Once you have a state you can provide it as a generic to RealDom. RealDom provides all of the methods to interact and update your new dom.
To help in building a Dom, native-core provides the State trait and a RealDom struct. The State trait provides a way to describe how states in a node depend on other states in its relatives. By describing how to update a single node from its relations, native-core will derive a way to update the states of all nodes for you. Once you have a state you can provide it as a generic to RealDom. RealDom provides all of the methods to interact and update your new dom.
```rust
Native Core cannot create all of the required methods for the State trait, but it can derive some of them. To implement the State trait, you must implement the following methods and let the `#[partial_derive_state]` macro handle the rest:
use dioxus_native_core::node_ref::*;
use dioxus_native_core::state::{ChildDepState, NodeDepState, ParentDepState, State};
use dioxus_native_core_macro::{sorted_str_slice, State};
#[derive(Default, Copy, Clone)]
struct Size(f64, f64);
// Size only depends on the current node and its children, so it implements ChildDepState
impl ChildDepState for Size {
// Size accepts a font size context
type Ctx = f64;
// Size depends on the Size part of each child
type DepState = (Self,);
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMask =
NodeMask::new_with_attrs(AttributeMask::Static(&sorted_str_slice!([
"width", "height"
])))
.with_text();
fn reduce<'a>(
&mut self,
node: NodeView,
children: impl Iterator<Item = (&'a Self,)>,
ctx: &Self::Ctx,
) -> bool
where
Self::DepState: 'a,
{
let mut width;
let mut height;
if let Some(text) = node.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * ctx;
height = *ctx;
} else {
// otherwise, the size is the maximum size of the children
width = children
.by_ref()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
// TextColor only depends on the current node and its parent, so it implements ParentDepState
impl ParentDepState for TextColor {
type Ctx = ();
// TextColor depends on the TextColor part of the parent
type DepState = (Self,);
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMask = NodeMask::new_with_attrs(AttributeMask::Static(&["color"]));
fn reduce(&mut self, node: NodeView, parent: Option<(&Self,)>, _ctx: &Self::Ctx) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node
.attributes()
.and_then(|attrs| attrs.next())
.map(|attr| attr.attribute.name.as_str())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(_) => panic!("unknown color"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, PartialEq, Default)]
struct Border(bool);
// TextColor only depends on the current node, so it implements NodeDepState
impl NodeDepState for Border {
type Ctx = ();
type DepState = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMask = NodeMask::new_with_attrs(AttributeMask::Static(&["border"]));
fn reduce(&mut self, node: NodeView, _sibling: (), _ctx: &Self::Ctx) -> bool {
// check if the node contians a border attribute
let new = Self(
node.attributes()
.and_then(|attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
// State provides a derive macro, but anotations on the members are needed in the form #[dep_type(dep_member, CtxType)]
#[derive(State, Default, Clone)]
struct ToyState {
// the color member of it's parent and no context
#[parent_dep_state(color)]
color: TextColor,
// depends on the node, and no context
#[node_dep_state()]
border: Border,
// depends on the layout_width member of children and f32 context (for text size)
#[child_dep_state(size, f32)]
size: Size,
}
```rust, ignore
{{#include ../../../examples/custom_renderer.rs:derive_state}}
```
Now that we have our state, we can put it to use in our dom. We can update the dom with update_state to update the structure of the dom (adding, removing, and changing properties of nodes) and then apply_mutations to update the ToyState for each of the nodes that changed.
Lets take a look at how to implement the State trait for a simple renderer.
```rust
fn main(){
fn app(cx: Scope) -> Element {
cx.render(rsx!{
div{
color: "red",
"hello world"
}
})
}
let vdom = VirtualDom::new(app);
let rdom: RealDom<ToyState> = RealDom::new();
{{#include ../../../examples/custom_renderer.rs:state_impl}}
```
let mutations = dom.rebuild();
// update the structure of the real_dom tree
let to_update = rdom.apply_mutations(vec![mutations]);
let mut ctx = AnyMap::new();
// set the font size to 3.3
ctx.insert(3.3f64);
// update the ToyState for nodes in the real_dom tree
let _to_rerender = rdom.update_state(&dom, to_update, ctx).unwrap();
Now that we have our state, we can put it to use in our RealDom. We can update the RealDom with apply_mutations to update the structure of the dom (adding, removing, and changing properties of nodes) and then update_state to update the States for each of the nodes that changed.
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop{
let wait = vdom.wait_for_work();
let mutations = vdom.work_with_deadline(|| false);
let to_update = rdom.apply_mutations(mutations);
let mut ctx = AnyMap::new();
ctx.insert(3.3f64);
let _to_rerender = rdom.update_state(vdom, to_update, ctx).unwrap();
// render...
}
})
}
```rust
{{#include ../../../examples/custom_renderer.rs:rendering}}
```
## Layout
@ -565,26 +405,9 @@ For most platforms, the layout of the Elements will stay the same. The [layout_a
To make it easier to implement text editing in rust renderers, `native-core` also contains a renderer-agnostic cursor system. The cursor can handle text editing, selection, and movement with common keyboard shortcuts integrated.
```rust
let mut cursor = Cursor::default();
let mut text = String::new();
let keyboard_data = dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::ArrowRight,
dioxus_html::input_data::keyboard_types::Code::ArrowRight,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
);
// handle keyboard input with a max text length of 10
cursor.handle_input(&keyboard_data, &mut text, 10);
// mannually select text between characters 0-5 on the first line (this could be from dragging with a mouse)
cursor.start = Pos::new(0, 0);
cursor.end = Some(Pos::new(5, 0));
// delete the selected text and move the cursor to the start of the selection
cursor.delete_selection(&mut text);
{{#include ../../../examples/custom_renderer.rs:cursor}}
```
## Conclusion
That should be it! You should have nearly all the knowledge required on how to implement your renderer. We're super interested in seeing Dioxus apps brought to custom desktop renderers, mobile renderers, video game UI, and even augmented reality! If you're interested in contributing to any of these projects, don't be afraid to reach out or join the [community](https://discord.gg/XgGxMSkvUM).

View file

@ -5,6 +5,7 @@ Build a standalone native desktop app that looks and feels the same across opera
Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
Examples:
- [File Explorer](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer)
- [WiFi Scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner)
@ -12,21 +13,22 @@ Examples:
## Support
The desktop is a powerful target for Dioxus but is currently limited in capability when compared to the Web platform. Currently, desktop apps are rendered with the platform's WebView library, but your Rust code is running natively on a native thread. This means that browser APIs are *not* available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs *are* accessible, so streaming, WebSockets, filesystem, etc are all viable APIs. In the future, we plan to move to a custom web renderer-based DOM renderer with WGPU integrations.
The desktop is a powerful target for Dioxus but is currently limited in capability when compared to the Web platform. Currently, desktop apps are rendered with the platform's WebView library, but your Rust code is running natively on a native thread. This means that browser APIs are _not_ available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs _are_ accessible, so streaming, WebSockets, filesystem, etc are all viable APIs. In the future, we plan to move to a custom web renderer-based DOM renderer with WGPU integrations.
Dioxus Desktop is built off [Tauri](https://tauri.app/). Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly.
# Getting started
## Platform-Specific Dependencies
Dioxus desktop renders through a web view. Depending on your platform, you might need to install some dependancies.
### Windows
Windows Desktop apps depend on WebView2 a library that should be installed in all modern Windows distributions. If you have Edge installed, then Dioxus will work fine. If you *don't* have Webview2, [then you can install it through Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). MS provides 3 options:
Windows Desktop apps depend on WebView2 a library that should be installed in all modern Windows distributions. If you have Edge installed, then Dioxus will work fine. If you _don't_ have Webview2, [then you can install it through Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). MS provides 3 options:
1. A tiny "evergreen" *bootstrapper* that fetches an installer from Microsoft's CDN
2. A tiny *installer* that fetches Webview2 from Microsoft's CDN
1. A tiny "evergreen" _bootstrapper_ that fetches an installer from Microsoft's CDN
2. A tiny _installer_ that fetches Webview2 from Microsoft's CDN
3. A statically linked version of Webview2 in your final binary for offline users
For development purposes, use Option 1.
@ -36,19 +38,18 @@ For development purposes, use Option 1.
Webview Linux apps require WebkitGtk. When distributing, this can be part of your dependency tree in your `.rpm` or `.deb`. However, likely, your users will already have WebkitGtk.
```bash
sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
```
When using Debian/bullseye `libappindicator3-dev` is no longer available but replaced by `libayatana-appindicator3-dev`.
```bash
# on Debian/bullseye use:
sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
```
If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/v1/guides/getting-started/prerequisites#setting-up-linux).
### MacOS
Currently everything for macOS is built right in! However, you might run into an issue if you're using nightly Rust due to some permissions issues in our Tao dependency (which have been resolved but not published).

View file

@ -0,0 +1,17 @@
[package]
name = "dioxus-pwa-example"
version = "0.1.0"
authors = ["Antonio Curavalea <one.kyonblack@gmail.com>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { path = "../../packages/dioxus", version = "^0.3.0"}
dioxus-web = { path = "../../packages/web", version = "^0.3.0"}
log = "0.4.6"
# WebAssembly Debug
wasm-logger = "0.2.0"
console_error_panic_hook = "0.1.7"

View file

@ -0,0 +1,42 @@
[application]
# App (Project) Name
name = "dioxus-pwa-example"
# Dioxus App Default Platform
# desktop, web, mobile, ssr
default_platform = "web"
# `build` & `serve` dist path
out_dir = "dist"
# resource (public) file folder
asset_dir = "public"
[web.app]
# HTML title tag content
title = "dioxus | ⛺"
[web.watcher]
# when watcher trigger, regenerate the `index.html`
reload_html = true
# which files or dirs will be watcher monitoring
watch_path = ["src", "public"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Dioxus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,44 @@
# Dioxus PWA example
This is a basic example of a progressive web app (PWA) using Dioxus and Dioxus CLI.
Currently PWA functionality requires the use of a service worker and manifest file, so this isn't 100% Rust yet.
It is also very much usable as a template for your projects, if you're aiming to create a PWA.
## Try the example
Make sure you have Dioxus CLI installed (if you're unsure, run `cargo install dioxus-cli`).
You can run `dioxus serve` in this directory to start the web server locally, or run
`dioxus build --release` to build the project so you can deploy it on a separate web-server.
## Project Structure
```
├── Cargo.toml
├── Dioxus.toml
├── index.html // Custom HTML is needed for this, to load the SW and manifest.
├── LICENSE
├── public
│ ├── favicon.ico
│ ├── logo_192.png
│ ├── logo_512.png
│ ├── manifest.json // The manifest file - edit this as you need to.
│ └── sw.js // The service worker - you must edit this for actual projects.
├── README.md
└── src
└── main.rs
```
## Resources
If you're just getting started with PWAs, here are some useful resources:
* [PWABuilder docs](https://docs.pwabuilder.com/#/)
* [MDN article on PWAs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
For service worker scripting (in JavaScript):
* [Service worker guide from PWABuilder](https://docs.pwabuilder.com/#/home/sw-intro)
* [Service worker examples, also from PWABuilder](https://github.com/pwa-builder/pwabuilder-serviceworkers)
If you want to stay as close to 100% Rust as possible, you can try using [wasi-worker](https://github.com/dunnock/wasi-worker) to replace the JS service worker file. The JSON manifest will still be required though.

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>{app_title}</title>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(
'/sw.js'
);
}
</script>
<link rel="manifest" href="manifest.json">
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8" />
{style_include}
</head>
<body>
<div id="main"></div>
<script type="module">
import init from "/{base_path}/assets/dioxus/{app_name}.js";
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
});
</script>
{script_include}
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -0,0 +1,34 @@
{
"name": "Dioxus",
"icons": [
{
"src": "logo_192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo_512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "any"
},
{
"src": "logo_512.png",
"type": "image/png",
"sizes": "any",
"purpose": "any"
}
],
"start_url": "/",
"id": "/",
"display": "standalone",
"display_override": ["window-control-overlay", "standalone"],
"scope": "/",
"theme_color": "#000000",
"background_color": "#ffffff",
"short_name": "Dioxus",
"description": "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust.",
"dir": "ltr",
"lang": "en",
"orientation": "portrait"
}

View file

@ -0,0 +1,198 @@
"use strict";
//console.log('WORKER: executing.');
/* A version number is useful when updating the worker logic,
allowing you to remove outdated cache entries during the update.
*/
var version = 'v1.0.0::';
/* These resources will be downloaded and cached by the service worker
during the installation process. If any resource fails to be downloaded,
then the service worker won't be installed either.
*/
var offlineFundamentals = [
// add here the files you want to cache
'favicon.ico'
];
/* The install event fires when the service worker is first installed.
You can use this event to prepare the service worker to be able to serve
files while visitors are offline.
*/
self.addEventListener("install", function (event) {
//console.log('WORKER: install event in progress.');
/* Using event.waitUntil(p) blocks the installation process on the provided
promise. If the promise is rejected, the service worker won't be installed.
*/
event.waitUntil(
/* The caches built-in is a promise-based API that helps you cache responses,
as well as finding and deleting them.
*/
caches
/* You can open a cache by name, and this method returns a promise. We use
a versioned cache name here so that we can remove old cache entries in
one fell swoop later, when phasing out an older service worker.
*/
.open(version + 'fundamentals')
.then(function (cache) {
/* After the cache is opened, we can fill it with the offline fundamentals.
The method below will add all resources in `offlineFundamentals` to the
cache, after making requests for them.
*/
return cache.addAll(offlineFundamentals);
})
.then(function () {
//console.log('WORKER: install completed');
})
);
});
/* The fetch event fires whenever a page controlled by this service worker requests
a resource. This isn't limited to `fetch` or even XMLHttpRequest. Instead, it
comprehends even the request for the HTML page on first load, as well as JS and
CSS resources, fonts, any images, etc.
*/
self.addEventListener("fetch", function (event) {
//console.log('WORKER: fetch event in progress.');
/* We should only cache GET requests, and deal with the rest of method in the
client-side, by handling failed POST,PUT,PATCH,etc. requests.
*/
if (event.request.method !== 'GET') {
/* If we don't block the event as shown below, then the request will go to
the network as usual.
*/
//console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
return;
}
/* Similar to event.waitUntil in that it blocks the fetch event on a promise.
Fulfillment result will be used as the response, and rejection will end in a
HTTP response indicating failure.
*/
event.respondWith(
caches
/* This method returns a promise that resolves to a cache entry matching
the request. Once the promise is settled, we can then provide a response
to the fetch request.
*/
.match(event.request)
.then(function (cached) {
/* Even if the response is in our cache, we go to the network as well.
This pattern is known for producing "eventually fresh" responses,
where we return cached responses immediately, and meanwhile pull
a network response and store that in the cache.
Read more:
https://ponyfoo.com/articles/progressive-networking-serviceworker
*/
var networked = fetch(event.request)
// We handle the network request with success and failure scenarios.
.then(fetchedFromNetwork, unableToResolve)
// We should catch errors on the fetchedFromNetwork handler as well.
.catch(unableToResolve);
/* We return the cached response immediately if there is one, and fall
back to waiting on the network as usual.
*/
//console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
return cached || networked;
function fetchedFromNetwork(response) {
/* We copy the response before replying to the network request.
This is the response that will be stored on the ServiceWorker cache.
*/
var cacheCopy = response.clone();
//console.log('WORKER: fetch response from network.', event.request.url);
caches
// We open a cache to store the response for this request.
.open(version + 'pages')
.then(function add(cache) {
/* We store the response for this request. It'll later become
available to caches.match(event.request) calls, when looking
for cached responses.
*/
cache.put(event.request, cacheCopy);
})
.then(function () {
//console.log('WORKER: fetch response stored in cache.', event.request.url);
});
// Return the response so that the promise is settled in fulfillment.
return response;
}
/* When this method is called, it means we were unable to produce a response
from either the cache or the network. This is our opportunity to produce
a meaningful response even when all else fails. It's the last chance, so
you probably want to display a "Service Unavailable" view or a generic
error response.
*/
function unableToResolve() {
/* There's a couple of things we can do here.
- Test the Accept header and then return one of the `offlineFundamentals`
e.g: `return caches.match('/some/cached/image.png')`
- You should also consider the origin. It's easier to decide what
"unavailable" means for requests against your origins than for requests
against a third party, such as an ad provider.
- Generate a Response programmaticaly, as shown below, and return that.
*/
//console.log('WORKER: fetch request failed in both cache and network.');
/* Here we're creating a response programmatically. The first parameter is the
response body, and the second one defines the options for the response.
*/
return new Response('<h1>Service Unavailable</h1>', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/html'
})
});
}
})
);
});
/* The activate event fires after a service worker has been successfully installed.
It is most useful when phasing out an older version of a service worker, as at
this point you know that the new worker was installed correctly. In this example,
we delete old caches that don't match the version in the worker we just finished
installing.
*/
self.addEventListener("activate", function (event) {
/* Just like with the install event, event.waitUntil blocks activate on a promise.
Activation will fail unless the promise is fulfilled.
*/
//console.log('WORKER: activate event in progress.');
event.waitUntil(
caches
/* This method returns a promise which will resolve to an array of available
cache keys.
*/
.keys()
.then(function (keys) {
// We return a promise that settles when all outdated caches are deleted.
return Promise.all(
keys
.filter(function (key) {
// Filter by keys that don't start with the latest version prefix.
return !key.startsWith(version);
})
.map(function (key) {
/* Return a promise that's fulfilled
when each outdated cache is deleted.
*/
return caches.delete(key);
})
);
})
.then(function () {
//console.log('WORKER: activate completed.');
})
);
});

View file

@ -0,0 +1,20 @@
use dioxus::prelude::*;
fn main() {
// init debug tool for WebAssembly
wasm_logger::init(wasm_logger::Config::default());
console_error_panic_hook::set_once();
dioxus_web::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! (
div {
style: "text-align: center;",
h1 { "🌗 Dioxus 🚀" }
h3 { "Frontend that scales." }
p { "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust." }
}
))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 14 KiB

36
examples/counter.rs Normal file
View file

@ -0,0 +1,36 @@
//! Comparison example with leptos' counter example
//! https://github.com/leptos-rs/leptos/blob/main/examples/counters/src/lib.rs
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let counters = use_state(cx, || vec![0, 0, 0]);
let sum: usize = counters.iter().copied().sum();
render! {
div {
button { onclick: move |_| counters.make_mut().push(0), "Add counter" }
button { onclick: move |_| { counters.make_mut().pop(); }, "Remove counter" }
p { "Total: {sum}" }
for (i, counter) in counters.iter().enumerate() {
li {
button { onclick: move |_| counters.make_mut()[i] -= 1, "-1" }
input {
value: "{counter}",
oninput: move |e| {
if let Ok(value) = e.value.parse::<usize>() {
counters.make_mut()[i] = value;
}
}
}
button { onclick: move |_| counters.make_mut()[i] += 1, "+1" }
button { onclick: move |_| { counters.make_mut().remove(i); }, "x" }
}
}
}
}
}

View file

@ -7,9 +7,10 @@ fn main() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
"This should show an image:"
p {
"This should show an image:"
}
img { src: "examples/assets/logo.png" }
img { src: "/Users/jonkelley/Desktop/blitz.png" }
}
})
}

View file

@ -1,3 +1,5 @@
use std::fmt::Display;
use dioxus::prelude::*;
fn main() {
@ -5,9 +7,20 @@ fn main() {
}
fn app(cx: Scope) -> Element {
cx.render(rsx! { generic_child::<i32>{} })
cx.render(rsx! { generic_child {
data: 0i32
} })
}
fn generic_child<T>(cx: Scope) -> Element {
cx.render(rsx! { div {} })
#[derive(PartialEq, Props)]
struct GenericChildProps<T: Display + PartialEq> {
data: T,
}
fn generic_child<T: Display + PartialEq>(cx: Scope<GenericChildProps<T>>) -> Element {
let data = &cx.props.data;
cx.render(rsx! { div {
"{data}"
} })
}

View file

@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element {
onclick: move |_| {
use rand::Rng;
let mut rng = rand::thread_rng();
val.set(rng.gen_range(1..6));
val.set(rng.gen_range(1..=6));
}
}
}

View file

@ -7,7 +7,7 @@ fn main() {
dioxus_desktop::launch(app);
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum FilterState {
All,
Active,
@ -39,56 +39,99 @@ pub fn app(cx: Scope<()>) -> Element {
.collect::<Vec<_>>();
filtered_todos.sort_unstable();
let show_clear_completed = todos.values().any(|todo| todo.checked);
let items_left = filtered_todos.len();
let item_text = match items_left {
let active_todo_count = todos.values().filter(|item| !item.checked).count();
let active_todo_text = match active_todo_count {
1 => "item",
_ => "items",
};
cx.render(rsx!{
let show_clear_completed = todos.values().any(|todo| todo.checked);
let selected = |state| {
if *filter == state {
"selected"
} else {
"false"
}
};
cx.render(rsx! {
section { class: "todoapp",
style { include_str!("./assets/todomvc.css") }
div {
header { class: "header",
h1 {"todos"}
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| {
draft.set(evt.value.clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
todos.make_mut().insert(
**todo_id,
TodoItem {
id: **todo_id,
checked: false,
contents: draft.to_string(),
},
);
*todo_id.make_mut() += 1;
draft.set("".to_string());
}
header { class: "header",
h1 {"todos"}
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| {
draft.set(evt.value.clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
todos.make_mut().insert(
**todo_id,
TodoItem {
id: **todo_id,
checked: false,
contents: draft.to_string(),
},
);
*todo_id.make_mut() += 1;
draft.set("".to_string());
}
}
}
}
section {
class: "main",
if !todos.is_empty() {
rsx! {
input {
id: "toggle-all",
class: "toggle-all",
r#type: "checkbox",
onchange: move |_| {
let check = active_todo_count != 0;
for (_, item) in todos.make_mut().iter_mut() {
item.checked = check;
}
},
checked: if active_todo_count == 0 { "true" } else { "false" },
}
label { r#for: "toggle-all" }
}
}
ul { class: "todo-list",
filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id, todos: todos }))
filtered_todos.iter().map(|id| rsx!(TodoEntry {
key: "{id}",
id: *id,
todos: todos,
}))
}
(!todos.is_empty()).then(|| rsx!(
footer { class: "footer",
span { class: "todo-count",
strong {"{items_left} "}
span {"{item_text} left"}
strong {"{active_todo_count} "}
span {"{active_todo_text} left"}
}
ul { class: "filters",
li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }}
li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }}
li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }}
for (state, state_text, url) in [
(FilterState::All, "All", "#/"),
(FilterState::Active, "Active", "#/active"),
(FilterState::Completed, "Completed", "#/completed"),
] {
li {
a {
href: url,
class: selected(state),
onclick: move |_| filter.set(state),
prevent_default: "onclick",
state_text
}
}
}
}
show_clear_completed.then(|| rsx!(
button {
@ -102,8 +145,8 @@ pub fn app(cx: Scope<()>) -> Element {
}
}
footer { class: "info",
p {"Double-click to edit a todo"}
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
p { "Double-click to edit a todo" }
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
}
})
@ -136,13 +179,17 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap();
}
}
label {
r#for: "cbg-{todo.id}",
onclick: move |_| is_editing.set(true),
ondblclick: move |_| is_editing.set(true),
prevent_default: "onclick",
"{todo.contents}"
}
button {
class: "destroy",
onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
prevent_default: "onclick",
}
}
is_editing.then(|| rsx!{
input {

View file

@ -35,6 +35,7 @@ fn sort_bfs(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
}
#[test]
#[cfg(debug_assertions)]
fn sorting() {
let r: [(usize, &[u8]); 5] = [
(0, &[0, 1]),

View file

@ -148,6 +148,13 @@ impl<'b> VirtualDom {
// Make sure the roots get transferred over while we're here
right_template.root_ids.transfer(&left_template.root_ids);
// Update the node refs
for i in 0..right_template.root_ids.len() {
if let Some(root_id) = right_template.root_ids.get(i) {
self.update_template(root_id, right_template);
}
}
}
fn diff_dynamic_node(
@ -628,10 +635,12 @@ impl<'b> VirtualDom {
}
let id = self.find_last_element(&new[last]);
self.mutations.push(Mutation::InsertAfter {
id,
m: nodes_created,
});
if nodes_created > 0 {
self.mutations.push(Mutation::InsertAfter {
id,
m: nodes_created,
})
}
nodes_created = 0;
}
@ -652,10 +661,12 @@ impl<'b> VirtualDom {
}
let id = self.find_first_element(&new[last]);
self.mutations.push(Mutation::InsertBefore {
id,
m: nodes_created,
});
if nodes_created > 0 {
self.mutations.push(Mutation::InsertBefore {
id,
m: nodes_created,
});
}
nodes_created = 0;
}
@ -676,10 +687,12 @@ impl<'b> VirtualDom {
}
let id = self.find_first_element(&new[first_lis]);
self.mutations.push(Mutation::InsertBefore {
id,
m: nodes_created,
});
if nodes_created > 0 {
self.mutations.push(Mutation::InsertBefore {
id,
m: nodes_created,
});
}
}
}

View file

@ -68,7 +68,7 @@ pub enum Mutation<'a> {
/// The ID of the element being mounted to
id: ElementId,
/// The number of nodes on the stack
/// The number of nodes on the stack to append to the target element
m: usize,
},
@ -155,7 +155,7 @@ pub enum Mutation<'a> {
/// The ID of the node we're going to replace with
id: ElementId,
/// The number of nodes on the stack to use to replace
/// The number of nodes on the stack to replace the target element with
m: usize,
},
@ -167,7 +167,7 @@ pub enum Mutation<'a> {
/// `[0,1,2]` represents 1st child's 2nd child's 3rd child.
path: &'static [u8],
/// The number of nodes on the stack to use to replace
/// The number of nodes on the stack to replace the target element with
m: usize,
},
@ -176,7 +176,7 @@ pub enum Mutation<'a> {
/// The ID of the node to insert after.
id: ElementId,
/// The ids of the nodes to insert after the target node.
/// The number of nodes on the stack to insert after the target node.
m: usize,
},
@ -185,7 +185,7 @@ pub enum Mutation<'a> {
/// The ID of the node to insert before.
id: ElementId,
/// The ids of the nodes to insert before the target node.
/// The number of nodes on the stack to insert before the target node.
m: usize,
},

View file

@ -21,7 +21,7 @@ serde = "1.0.136"
serde_json = "1.0.79"
thiserror = "1.0.30"
log = "0.4.14"
wry = { version = "0.23.4" }
wry = { version = "0.27.2" }
futures-channel = "0.3.21"
tokio = { version = "1.16.1", features = [
"sync",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::path::PathBuf;
use wry::application::window::Icon;
@ -18,6 +19,7 @@ pub struct Config {
pub(crate) pre_rendered: Option<String>,
pub(crate) disable_context_menu: bool,
pub(crate) resource_dir: Option<PathBuf>,
pub(crate) data_dir: Option<PathBuf>,
pub(crate) custom_head: Option<String>,
pub(crate) custom_index: Option<String>,
pub(crate) root_name: String,
@ -27,7 +29,7 @@ type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
pub(crate) type WryProtocol = (
String,
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Vec<u8>>> + 'static>,
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static>,
);
impl Config {
@ -44,6 +46,7 @@ impl Config {
pre_rendered: None,
disable_context_menu: !cfg!(debug_assertions),
resource_dir: None,
data_dir: None,
custom_head: None,
custom_index: None,
root_name: "main".to_string(),
@ -56,6 +59,14 @@ impl Config {
self
}
/// set the directory where data will be stored in release mode.
///
/// > Note: This **must** be set when bundling on Windows.
pub fn with_data_directory(mut self, path: impl Into<PathBuf>) -> Self {
self.data_dir = Some(path.into());
self
}
/// Set whether or not the right-click context menu should be disabled.
pub fn with_disable_context_menu(mut self, disable: bool) -> Self {
self.disable_context_menu = disable;
@ -88,7 +99,7 @@ impl Config {
/// Set a custom protocol
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
where
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Vec<u8>>> + 'static,
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static,
{
self.protocols.push((name, Box::new(handler)));
self

View file

@ -36,8 +36,8 @@ use tao::{
};
pub use wry;
pub use wry::application as tao;
use wry::application::window::WindowId;
use wry::webview::WebView;
use wry::{application::window::WindowId, webview::WebContext};
/// Launch the WebView and run the event loop.
///
@ -281,7 +281,7 @@ fn create_new_window(
event_handlers: &WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
) -> WebviewHandler {
let webview = webview::build(&mut cfg, event_loop, proxy.clone());
let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
dom.base_scope().provide_context(DesktopContext::new(
webview.clone(),
@ -299,6 +299,7 @@ fn create_new_window(
webview,
dom,
waker: waker::tao_waker(proxy, id),
web_context,
}
}
@ -306,6 +307,9 @@ struct WebviewHandler {
dom: VirtualDom,
webview: Rc<wry::webview::WebView>,
waker: Waker,
// This is nessisary because of a bug in wry. Wry assumes the webcontext is alive for the lifetime of the webview. We need to keep the webcontext alive, otherwise the webview will crash
#[allow(dead_code)]
web_context: WebContext,
}
/// Poll the virtualdom until it's pending

View file

@ -1,5 +1,8 @@
use dioxus_interpreter_js::INTERPRETER_JS;
use std::path::{Path, PathBuf};
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use wry::{
http::{status::StatusCode, Request, Response},
Result,
@ -27,7 +30,7 @@ pub(super) fn desktop_handler(
custom_head: Option<String>,
custom_index: Option<String>,
root_name: &str,
) -> Result<Response<Vec<u8>>> {
) -> Result<Response<Cow<'static, [u8]>>> {
// If the request is for the root, we'll serve the index.html file.
if request.uri().path() == "/" {
// If a custom index is provided, just defer to that, expecting the user to know what they're doing.
@ -53,7 +56,7 @@ pub(super) fn desktop_handler(
return Response::builder()
.header("Content-Type", "text/html")
.body(body)
.body(Cow::from(body))
.map_err(From::from);
}
@ -72,13 +75,13 @@ pub(super) fn desktop_handler(
if asset.exists() {
return Response::builder()
.header("Content-Type", get_mime_from_path(&asset)?)
.body(std::fs::read(asset)?)
.body(Cow::from(std::fs::read(asset)?))
.map_err(From::from);
}
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(String::from("Not Found").into_bytes())
.body(Cow::from(String::from("Not Found").into_bytes()))
.map_err(From::from)
}

View file

@ -7,13 +7,13 @@ use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
pub use wry;
pub use wry::application as tao;
use wry::application::window::Window;
use wry::webview::{WebView, WebViewBuilder};
use wry::webview::{WebContext, WebView, WebViewBuilder};
pub fn build(
cfg: &mut Config,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
proxy: EventLoopProxy<UserWindowEvent>,
) -> Rc<WebView> {
) -> (Rc<WebView>, WebContext) {
let builder = cfg.window.clone();
let window = builder.build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take();
@ -33,6 +33,8 @@ pub fn build(
));
}
let mut web_context = WebContext::new(cfg.data_dir.clone());
let mut webview = WebViewBuilder::new(window)
.unwrap()
.with_transparent(cfg.window.window.transparent)
@ -52,7 +54,19 @@ pub fn build(
.as_ref()
.map(|handler| handler(window, evet))
.unwrap_or_default()
});
})
.with_web_context(&mut web_context);
#[cfg(windows)]
{
// Windows has a platform specific settings to disable the browser shortcut keys
use wry::webview::WebViewBuilderExtWindows;
webview = webview.with_browser_accelerator_keys(false);
}
// These are commented out because wry is currently broken in wry
// let mut web_context = WebContext::new(cfg.data_dir.clone());
// .with_web_context(&mut web_context);
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
@ -78,5 +92,5 @@ pub fn build(
webview = webview.with_devtools(true);
}
Rc::new(webview.build().unwrap())
(Rc::new(webview.build().unwrap()), web_context)
}

View file

@ -16,20 +16,15 @@ license = "MIT/Apache-2.0"
dioxus = { path = "../dioxus", version = "^0.3.0" }
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
dioxus-html = { path = "../html", version = "^0.3.0" }
dioxus-native-core = { path = "../native-core", version = "^0.3.0" }
dioxus-native-core = { path = "../native-core", version = "^0.2.0", features = ["dioxus"] }
dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
rink = { path = "../rink" }
tui = "0.17.0"
crossterm = "0.23.0"
anyhow = "1.0.42"
tokio = { version = "1.15.0", features = ["full"] }
futures = "0.3.19"
taffy = "0.2.1"
smallvec = "1.6"
rustc-hash = "1.1.0"
anymap = "1.0.0-beta.2"
futures-channel = "0.3.25"
[dev-dependencies]
dioxus = { path = "../dioxus" }

View file

@ -1,5 +1,5 @@
<div align="center">
<h1>Rink</h1>
<h1>Dioxus TUI</h1>
<p>
<strong>Beautiful terminal user interfaces in Rust with <a href="https://dioxuslabs.com/">Dioxus </a>.</strong>
</p>
@ -37,7 +37,6 @@
</a>
</div>
<br/>
Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus.
@ -63,33 +62,34 @@ fn app(cx: Scope) -> Element {
You can use Html-like semantics with inline styles, tree hierarchy, components, and more in your [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application.
Rink is essentially a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful.
Dioxus TUI is essentially a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Dioxus TUI doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful.
## Limitations
- **Subset of Html**
Terminals can only render a subset of HTML. We support as much as we can.
Terminals can only render a subset of HTML. We support as much as we can.
- **Particular frontend design**
Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
## Status
**WARNING: Rink is currently under construction!**
**WARNING: Dioxus TUI is currently under construction!**
Rendering a VirtualDom works fine, but the ecosystem of hooks is not yet ready. Additionally, some bugs in the flexbox implementation might be quirky at times.
## Features
Rink features:
Dioxus TUI features:
- [x] Flexbox-based layout system
- [ ] CSS selectors
- [x] inline CSS support
- [x] Built-in focusing system
* [x] Widgets<sup>1</sup>
* [ ] Support for events, hooks, and callbacks<sup>2</sup>
* [ ] Html tags<sup>3</sup>
<sup>1</sup> Currently only a subset of the input element is implemented as a component (not an element). The `Input` component supports sliders, text, numbers, passwords, buttons, and checkboxes.
<sup>2</sup> Basic keyboard, mouse, and focus events are implemented.
<sup>3</sup> Currently, most HTML tags don't translate into any meaning inside of Dioxus TUI. So an `input` *element* won't mean anything nor does it have any additional functionality.
<sup>3</sup> Currently, most HTML tags don't translate into any meaning inside of Dioxus TUI. So an `input` _element_ won't mean anything nor does it have any additional functionality.

View file

@ -0,0 +1,165 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use dioxus::prelude::*;
use dioxus_tui::{Config, TuiContext};
criterion_group!(mbenches, tui_update);
criterion_main!(mbenches);
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
fn tui_update(c: &mut Criterion) {
{
let mut group = c.benchmark_group("Update boxes");
for size in 1..=20usize {
let parameter_string = format!("{}", (size).pow(2));
group.bench_with_input(
BenchmarkId::new("size", parameter_string),
&size,
|b, size| {
b.iter(|| {
dioxus_tui::launch_cfg_with_props(
app,
GridProps {
size: *size,
update_count: 1,
},
Config::default().with_headless(),
)
})
},
);
}
}
{
let mut group = c.benchmark_group("Update many boxes");
for update_count in 1..=20usize {
let update_count = update_count * 20;
let parameter_string = update_count.to_string();
group.bench_with_input(
BenchmarkId::new("update count", parameter_string),
&update_count,
|b, update_count| {
b.iter(|| {
dioxus_tui::launch_cfg_with_props(
app,
GridProps {
size: 20,
update_count: *update_count,
},
Config::default().with_headless(),
)
})
},
);
}
}
}
#[derive(Props, PartialEq)]
struct BoxProps {
x: usize,
y: usize,
hue: f32,
alpha: f32,
}
#[allow(non_snake_case)]
fn Box(cx: Scope<BoxProps>) -> Element {
let count = use_state(cx, || 0);
let x = cx.props.x * 2;
let y = cx.props.y * 2;
let hue = cx.props.hue;
let display_hue = cx.props.hue as u32 / 10;
let count = count.get();
let alpha = cx.props.alpha + (count % 100) as f32;
cx.render(rsx! {
div {
left: "{x}%",
top: "{y}%",
width: "100%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
align_items: "center",
p{"{display_hue:03}"}
}
})
}
#[derive(Props, PartialEq)]
struct GridProps {
size: usize,
update_count: usize,
}
#[allow(non_snake_case)]
fn Grid(cx: Scope<GridProps>) -> Element {
let size = cx.props.size;
let count = use_state(cx, || 0);
let counts = use_ref(cx, || vec![0; size * size]);
let ctx: TuiContext = cx.consume_context().unwrap();
if *count.get() + cx.props.update_count >= (size * size) {
ctx.quit();
} else {
for _ in 0..cx.props.update_count {
counts.with_mut(|c| {
let i = *count.current();
c[i] += 1;
c[i] %= 360;
});
count.with_mut(|i| {
*i += 1;
*i %= size * size;
});
}
}
render! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
(0..size).map(|x|
{
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "row",
(0..size).map(|y|
{
let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32;
let key = format!("{}-{}", x, y);
rsx! {
Box{
x: x,
y: y,
alpha: 100.0,
hue: alpha,
key: "{key}",
}
}
}
)
}
}
}
)
}
}
}
fn app(cx: Scope<GridProps>) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: cx.props.size,
update_count: cx.props.update_count,
}
}
})
}

View file

@ -1,6 +1,7 @@
use dioxus::core::RenderReturn;
use dioxus::prelude::*;
use dioxus_tui::query::Query;
use dioxus_tui::DioxusElementToNodeId;
use dioxus_tui::Query;
use dioxus_tui::Size;
fn main() {
@ -11,6 +12,7 @@ fn app(cx: Scope) -> Element {
let hue = use_state(cx, || 0.0);
let brightness = use_state(cx, || 0.0);
let tui_query: Query = cx.consume_context().unwrap();
let mapping: DioxusElementToNodeId = cx.consume_context().unwrap();
// disable templates so that every node has an id and can be queried
cx.render(rsx! {
div{
@ -19,7 +21,7 @@ fn app(cx: Scope) -> Element {
onmousemove: move |evt| {
if let RenderReturn::Ready(node) = cx.root_node() {
if let Some(id) = node.root_ids.get(0){
let node = tui_query.get(id);
let node = tui_query.get(mapping.get_node_id(id).unwrap());
let Size{width, height} = node.size().unwrap();
let pos = evt.inner().element_coordinates();
hue.set((pos.x as f32/width as f32)*255.0);

View file

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View file

@ -1,26 +1,12 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use dioxus::prelude::*;
use dioxus_tui::{Config, TuiContext};
criterion_group!(mbenches, tui_update);
criterion_main!(mbenches);
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
fn tui_update(c: &mut Criterion) {
let mut group = c.benchmark_group("Update boxes");
// We can also use loops to define multiple benchmarks, even over multiple dimensions.
for size in 1..=8usize {
let parameter_string = format!("{}", (3 * size).pow(2));
group.bench_with_input(
BenchmarkId::new("size", parameter_string),
&size,
|b, size| {
b.iter(|| {
dioxus_tui::launch_cfg_with_props(app, *size, Config::default().with_headless())
})
},
);
fn main() {
for size in 1..=20usize {
for _ in 0..10 {
dioxus_tui::launch_cfg_with_props(app, size, Config::default().with_headless())
}
}
}

View file

@ -1,7 +1,6 @@
use dioxus::prelude::*;
use dioxus_html::FormData;
use dioxus_tui::prelude::*;
use dioxus_tui::Config;
fn main() {
dioxus_tui::launch_cfg(app, Config::new());

View file

@ -0,0 +1,143 @@
pub mod prelude;
pub mod widgets;
use std::{
ops::Deref,
rc::Rc,
sync::{Arc, RwLock},
};
use dioxus_core::{Component, ElementId, VirtualDom};
use dioxus_html::EventData;
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
use dioxus_native_core::prelude::*;
pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext};
use rink::{render, Driver};
pub fn launch(app: Component<()>) {
launch_cfg(app, Config::default())
}
pub fn launch_cfg(app: Component<()>, cfg: Config) {
launch_cfg_with_props(app, (), cfg);
}
pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props, cfg: Config) {
render(cfg, |rdom, taffy, event_tx| {
let dioxus_state = {
let mut rdom = rdom.write().unwrap();
DioxusState::create(&mut rdom)
};
let dioxus_state = Rc::new(RwLock::new(dioxus_state));
let mut vdom = VirtualDom::new_with_props(app, props)
.with_root_context(TuiContext::new(event_tx))
.with_root_context(Query::new(rdom.clone(), taffy.clone()))
.with_root_context(DioxusElementToNodeId {
mapping: dioxus_state.clone(),
});
let muts = vdom.rebuild();
let mut rdom = rdom.write().unwrap();
dioxus_state
.write()
.unwrap()
.apply_mutations(&mut rdom, muts);
DioxusRenderer {
vdom,
dioxus_state,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: {
let (hot_reload_tx, hot_reload_rx) =
tokio::sync::mpsc::unbounded_channel::<dioxus_hot_reload::HotReloadMsg>();
dioxus_hot_reload::connect(move |msg| {
let _ = hot_reload_tx.send(msg);
});
hot_reload_rx
},
}
})
.unwrap();
}
struct DioxusRenderer {
vdom: VirtualDom,
dioxus_state: Rc<RwLock<DioxusState>>,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
}
impl Driver for DioxusRenderer {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
let muts = self.vdom.render_immediate();
{
let mut rdom = rdom.write().unwrap();
self.dioxus_state
.write()
.unwrap()
.apply_mutations(&mut rdom, muts);
}
}
fn handle_event(
&mut self,
rdom: &Arc<RwLock<RealDom>>,
id: NodeId,
event: &str,
value: Rc<EventData>,
bubbles: bool,
) {
let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() };
if let Some(id) = id {
self.vdom
.handle_event(event, value.deref().clone().into_any(), id, bubbles);
}
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
#[cfg(all(feature = "hot-reload", debug_assertions))]
return Box::pin(async {
let hot_reload_wait = self.hot_reload_rx.recv();
let mut hot_reload_msg = None;
let wait_for_work = self.vdom.wait_for_work();
tokio::select! {
Some(msg) = hot_reload_wait => {
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
hot_reload_msg = Some(msg);
}
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
let () = msg;
}
_ = wait_for_work => {}
}
// if we have a new template, replace the old one
if let Some(msg) = hot_reload_msg {
match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
self.vdom.replace_template(template);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
}
}
}
});
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
Box::pin(self.vdom.wait_for_work())
}
}
#[derive(Clone)]
pub struct DioxusElementToNodeId {
mapping: Rc<RwLock<DioxusState>>,
}
impl DioxusElementToNodeId {
pub fn get_node_id(&self, element_id: ElementId) -> Option<NodeId> {
self.mapping
.read()
.unwrap()
.try_element_to_node_id(element_id)
}
}

View file

@ -1 +1,2 @@
pub use crate::widgets::*;
pub use rink::Config;

View file

@ -37,7 +37,7 @@ pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
}
state.set(new_state);
};
cx.render(rsx! {
render! {
div{
width: "{width}",
height: "{height}",
@ -55,5 +55,5 @@ pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
},
"{text}"
}
})
}
}

View file

@ -61,7 +61,7 @@ pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> {
}
state.set(new_state);
};
cx.render(rsx! {
render! {
div {
width: "{width}",
height: "{height}",
@ -78,5 +78,5 @@ pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> {
},
"{text}"
}
})
}
}

View file

@ -0,0 +1,22 @@
mod button;
mod checkbox;
mod input;
mod number;
mod password;
mod slider;
mod textbox;
use dioxus_core::{RenderReturn, Scope};
use dioxus_native_core::NodeId;
pub use input::*;
use crate::DioxusElementToNodeId;
pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<NodeId> {
if let RenderReturn::Ready(sync) = cx.root_node() {
let mapping: DioxusElementToNodeId = cx.consume_context()?;
mapping.get_node_id(sync.root_ids.get(0)?)
} else {
None
}
}

View file

@ -1,11 +1,11 @@
use crate::widgets::get_root_id;
use crate::Query;
use crossterm::{cursor::MoveTo, execute};
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use rink::Query;
use std::{collections::HashMap, io::stdout};
use taffy::geometry::Point;
@ -99,7 +99,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
update(text.clone());
};
cx.render(rsx! {
render! {
div{
width: "{width}",
height: "{height}",
@ -113,7 +113,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
};
if is_text{
let mut text = text_ref.write();
cursor.write().handle_input(&k, &mut *text, max_len);
cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len);
update(text.clone());
let node = tui_query.get(get_root_id(cx).unwrap());
@ -205,5 +205,5 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
"{text_after_second_cursor}"
}
})
}
}

View file

@ -1,11 +1,11 @@
use crate::widgets::get_root_id;
use crate::Query;
use crossterm::{cursor::*, execute};
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use rink::Query;
use std::{collections::HashMap, io::stdout};
use taffy::geometry::Point;
@ -88,7 +88,9 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
return;
}
let mut text = text_ref.write();
cursor.write().handle_input(&k, &mut *text, max_len);
cursor
.write()
.handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len);
if let Some(input_handler) = &cx.props.raw_oninput {
input_handler.call(FormData {
value: text.clone(),
@ -114,7 +116,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
}
};
cx.render(rsx! {
render! {
div {
width: "{width}",
height: "{height}",
@ -187,5 +189,5 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
"{text_after_second_cursor}"
}
})
}
}

View file

@ -1,11 +1,11 @@
use std::collections::HashMap;
use crate::widgets::get_root_id;
use crate::Query;
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use rink::Query;
#[derive(Props)]
pub(crate) struct SliderProps<'a> {
@ -62,7 +62,7 @@ pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> {
}
};
cx.render(rsx! {
render! {
div{
width: "{width}",
height: "{height}",
@ -103,5 +103,5 @@ pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> {
background_color: "rgba(10,10,10,0.5)",
}
}
})
}
}

View file

@ -1,11 +1,11 @@
use crate::widgets::get_root_id;
use crate::Query;
use crossterm::{cursor::*, execute};
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use rink::Query;
use std::{collections::HashMap, io::stdout};
use taffy::geometry::Point;
@ -79,7 +79,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
"solid"
};
cx.render(rsx! {
render! {
div{
width: "{width}",
height: "{height}",
@ -90,7 +90,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
return;
}
let mut text = text_ref.write();
cursor.write().handle_input(&k, &mut *text, max_len);
cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len);
if let Some(input_handler) = &cx.props.raw_oninput{
input_handler.call(FormData{
value: text.clone(),
@ -178,5 +178,5 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
"{text_after_second_cursor}"
}
})
}
}

View file

@ -63,6 +63,26 @@ fn NameCard(cx: Scope) -> Element {
}
```
If needed, we can update the atom's value, based on itself:
```rust, ignore
static COUNT: Atom<i32> = |_| 0;
fn Counter(cx: Scope) -> Element {
let mut count = use_atom_state(cx, COUNT);
cx.render(rsx!{
p {
"{count}"
}
button {
onclick: move |_| count += 1,
"Increment counter"
}
})
}
```
It's that simple!
## Installation

View file

@ -5,6 +5,16 @@ use std::convert::TryInto;
use std::fmt::{Debug, Formatter};
use std::str::FromStr;
#[cfg(feature = "serialize")]
fn resilient_deserialize_code<'de, D>(deserializer: D) -> Result<Code, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
// If we fail to deserialize the code for any reason, just return Unidentified instead of failing.
Ok(Code::deserialize(deserializer).unwrap_or(Code::Unidentified))
}
pub type KeyboardEvent = Event<KeyboardData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, PartialEq, Eq)]
@ -27,6 +37,10 @@ pub struct KeyboardData {
pub key_code: KeyCode,
/// the physical key on the keyboard
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "resilient_deserialize_code")
)]
code: Code,
/// Indicate if the `alt` modifier key was pressed during this keyboard event
@ -102,7 +116,7 @@ impl KeyboardData {
/// The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout.
pub fn key(&self) -> Key {
#[allow(deprecated)]
FromStr::from_str(&self.key).expect("could not parse")
FromStr::from_str(&self.key).unwrap_or(Key::Unidentified)
}
/// A physical key on the keyboard (as opposed to the character generated by pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the state of the modifier keys.
@ -158,10 +172,24 @@ impl Debug for KeyboardData {
}
}
#[cfg_attr(
feature = "serialize",
derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr)
)]
#[cfg(feature = "serialize")]
impl<'de> serde::Deserialize<'de> for KeyCode {
fn deserialize<D>(deserializer: D) -> Result<KeyCode, D::Error>
where
D: serde::Deserializer<'de>,
{
// We could be deserializing a unicode character, so we need to use u64 even if the output only takes u8
let value = u64::deserialize(deserializer)?;
if let Ok(smaller_uint) = value.try_into() {
Ok(KeyCode::from_raw_code(smaller_uint))
} else {
Ok(KeyCode::Unknown)
}
}
}
#[cfg_attr(feature = "serialize", derive(serde_repr::Serialize_repr))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum KeyCode {
@ -525,7 +553,6 @@ pub enum KeyCode {
// kanji, = 244
// unlock trackpad (Chrome/Edge), = 251
// toggle touchpad, = 255
#[cfg_attr(feature = "serialize", serde(other))]
Unknown,
}

View file

@ -10,15 +10,14 @@ homepage = "https://dioxuslabs.com"
documentation = "https://docs.rs/dioxus"
keywords = ["dom", "ui", "gui", "react", "wasm"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wasm-bindgen = { version = "0.2.79", optional = true }
js-sys = { version = "0.3.56", optional = true }
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
sledgehammer_bindgen = { version = "0.1.3", optional = true }
sledgehammer_utils = { version = "0.1.0", optional = true }
sledgehammer_bindgen = { version = "0.2.1", optional = true }
sledgehammer_utils = { version = "0.1.1", optional = true }
[features]
default = []

View file

@ -108,6 +108,7 @@ mod js {
const listeners = new ListenerMap();
let nodes = [];
let stack = [];
let root;
const templates = {};
let node, els, end, ptr_end, k;
export function save_template(nodes, tmpl_id) {

View file

@ -1,563 +1,386 @@
extern crate proc_macro;
mod sorted_slice;
use std::collections::HashSet;
use proc_macro::TokenStream;
use quote::{quote, ToTokens, __private::Span};
use sorted_slice::StrSlice;
use syn::parenthesized;
use syn::parse::ParseBuffer;
use syn::punctuated::Punctuated;
use syn::{
self,
parse::{Parse, ParseStream, Result},
parse_macro_input, parse_quote, Error, Field, Ident, Token, Type,
};
use quote::{format_ident, quote};
use syn::{parse_macro_input, ItemImpl, Type, TypePath, TypeTuple};
/// Sorts a slice of string literals at compile time.
#[proc_macro]
pub fn sorted_str_slice(input: TokenStream) -> TokenStream {
let slice: StrSlice = parse_macro_input!(input as StrSlice);
let strings = slice.map.values();
quote!([#(#strings, )*]).into()
}
/// A helper attribute for deriving `State` for a struct.
#[proc_macro_attribute]
pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream {
let impl_block: syn::ItemImpl = parse_macro_input!(input as syn::ItemImpl);
#[derive(PartialEq, Debug, Clone)]
enum DependencyKind {
Node,
Child,
Parent,
}
/// Derive's the state from any elements that have a node_dep_state, child_dep_state, parent_dep_state, or state attribute.
///
/// # Declaring elements
/// Each of the attributes require specifying the members of the struct it depends on to allow the macro to find the optimal resultion order.
/// These dependencies should match the types declared in the trait the member implements.
///
/// # The node_dep_state attribute
/// The node_dep_state attribute declares a member that implements the NodeDepState trait.
/// ```rust, ignore
/// #[derive(State)]
/// struct MyStruct {
/// // MyDependency implements ChildDepState<()>
/// #[node_dep_state()]
/// my_dependency_1: MyDependency,
/// // MyDependency2 implements ChildDepState<(MyDependency,)>
/// #[node_dep_state(my_dependency_1)]
/// my_dependency_2: MyDependency2,
/// }
/// // or
/// #[derive(State)]
/// struct MyStruct {
/// // MyDependnancy implements NodeDepState<()>
/// #[node_dep_state()]
/// my_dependency_1: MyDependency,
/// // MyDependency2 implements NodeDepState<()>
/// #[node_dep_state()]
/// my_dependency_2: MyDependency2,
/// // MyDependency3 implements NodeDepState<(MyDependency, MyDependency2)> with Ctx = f32
/// #[node_dep_state((my_dependency_1, my_dependency_2), f32)]
/// my_dependency_3: MyDependency2,
/// }
/// ```
/// # The child_dep_state attribute
/// The child_dep_state attribute declares a member that implements the ChildDepState trait.
/// ```rust, ignore
/// #[derive(State)]
/// struct MyStruct {
/// // MyDependnacy implements ChildDepState with DepState = Self
/// #[child_dep_state(my_dependency_1)]
/// my_dependency_1: MyDependency,
/// }
/// // or
/// #[derive(State)]
/// struct MyStruct {
/// // MyDependnacy implements ChildDepState with DepState = Self
/// #[child_dep_state(my_dependency_1)]
/// my_dependency_1: MyDependency,
/// // MyDependnacy2 implements ChildDepState with DepState = MyDependency and Ctx = f32
/// #[child_dep_state(my_dependency_1, f32)]
/// my_dependency_2: MyDependency2,
/// }
/// ```
/// # The parent_dep_state attribute
/// The parent_dep_state attribute declares a member that implements the ParentDepState trait.
/// The parent_dep_state attribute can be called in the forms:
/// ```rust, ignore
/// #[derive(State)]
/// struct MyStruct {
/// // MyDependnacy implements ParentDepState with DepState = Self
/// #[parent_dep_state(my_dependency_1)]
/// my_dependency_1: MyDependency,
/// }
/// // or
/// #[derive(State)]
/// struct MyStruct {
/// // MyDependnacy implements ParentDepState with DepState = Self
/// #[parent_dep_state(my_dependency_1)]
/// my_dependency_1: MyDependency,
/// // MyDependnacy2 implements ParentDepState with DepState = MyDependency and Ctx = f32
/// #[parent_dep_state(my_dependency_1, f32)]
/// my_dependency_2: MyDependency2,
/// }
/// ```
///
/// # Combining dependancies
/// The node_dep_state, parent_dep_state, and child_dep_state attributes can be combined to allow for more complex dependancies.
/// For example if we wanted to combine the font that is passed from the parent to the child and the layout of the size children to find the size of the current node we could do:
/// ```rust, ignore
/// #[derive(State)]
/// struct MyStruct {
/// // ChildrenSize implements ChildDepState with DepState = Size
/// #[child_dep_state(size)]
/// children_size: ChildrenSize,
/// // FontSize implements ParentDepState with DepState = Self
/// #[parent_dep_state(font_size)]
/// font_size: FontSize,
/// // Size implements NodeDepState<(ChildrenSize, FontSize)>
/// #[parent_dep_state((children_size, font_size))]
/// size: Size,
/// }
/// ```
///
/// # The state attribute
/// The state macro declares a member that implements the State trait. This allows you to organize your state into multiple isolated components.
/// Unlike the other attributes, the state attribute does not accept any arguments, because a nested state cannot depend on any other part of the state.
///
/// # Custom values
///
/// If your state has a custom value type you can specify it with the state attribute.
///
/// ```rust, ignore
/// #[derive(State)]
/// #[state(custom_value = MyCustomType)]
/// struct MyStruct {
/// // ...
/// }
#[proc_macro_derive(
State,
attributes(node_dep_state, child_dep_state, parent_dep_state, state)
)]
pub fn state_macro_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_derive_macro(&ast)
}
fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream {
let custom_type = ast
.attrs
let has_create_fn = impl_block
.items
.iter()
.find(|a| a.path.is_ident("state"))
.and_then(|attr| {
// parse custom_type = "MyType"
let assignment = attr.parse_args::<syn::Expr>().unwrap();
if let syn::Expr::Assign(assign) = assignment {
let (left, right) = (&*assign.left, &*assign.right);
if let syn::Expr::Path(e) = left {
let path = &e.path;
if let Some(ident) = path.get_ident() {
if ident == "custom_value" {
return match right {
syn::Expr::Path(e) => {
let path = &e.path;
Some(quote! {#path})
}
_ => None,
};
}
}
}
}
None
})
.unwrap_or(quote! {()});
let type_name = &ast.ident;
let fields: Vec<_> = match &ast.data {
syn::Data::Struct(data) => match &data.fields {
syn::Fields::Named(e) => &e.named,
syn::Fields::Unnamed(_) => todo!("unnamed fields"),
syn::Fields::Unit => todo!("unit structs"),
}
.any(|item| matches!(item, syn::ImplItem::Method(method) if method.sig.ident == "create"));
let parent_dependencies = impl_block
.items
.iter()
.collect(),
_ => unimplemented!(),
};
let strct = Struct::new(type_name.clone(), &fields);
match StateStruct::parse(&fields, &strct) {
Ok(state_strct) => {
let passes = state_strct.state_members.iter().map(|m| {
let unit = &m.mem.unit_type;
match m.dep_kind {
DependencyKind::Node => quote! {dioxus_native_core::AnyPass::Node(&#unit)},
DependencyKind::Child => quote! {dioxus_native_core::AnyPass::Upward(&#unit)},
DependencyKind::Parent => {
quote! {dioxus_native_core::AnyPass::Downward(&#unit)}
}
}
});
let member_types = state_strct.state_members.iter().map(|m| &m.mem.ty);
let impl_members = state_strct
.state_members
.iter()
.map(|m| m.impl_pass(state_strct.ty, &custom_type));
let gen = quote! {
#(#impl_members)*
impl State<#custom_type> for #type_name {
const PASSES: &'static [dioxus_native_core::AnyPass<dioxus_native_core::node::Node<Self, #custom_type>>] = &[
#(#passes),*
];
const MASKS: &'static [dioxus_native_core::NodeMask] = &[#(#member_types::NODE_MASK),*];
}
};
gen.into()
}
Err(e) => e.into_compile_error().into(),
}
}
struct Struct {
name: Ident,
members: Vec<Member>,
}
impl Struct {
fn new(name: Ident, fields: &[&Field]) -> Self {
let members = fields
.iter()
.enumerate()
.filter_map(|(i, f)| Member::parse(&name, f, i as u64))
.collect();
Self { name, members }
}
}
struct StateStruct<'a> {
state_members: Vec<StateMember<'a>>,
#[allow(unused)]
child_states: Vec<&'a Member>,
ty: &'a Ident,
}
impl<'a> StateStruct<'a> {
/// Parse the state structure, and find a resolution order that will allow us to update the state for each node in after the state(s) it depends on have been resolved.
fn parse(fields: &[&'a Field], strct: &'a Struct) -> Result<Self> {
let mut parse_err = Ok(());
let mut state_members: Vec<_> = strct
.members
.iter()
.zip(fields.iter())
.filter_map(|(m, f)| match StateMember::parse(f, m, strct) {
Ok(m) => m,
Err(err) => {
parse_err = Err(err);
None
}
})
.collect();
parse_err?;
for i in 0..state_members.len() {
let deps: Vec<_> = state_members[i].dep_mems.iter().map(|m| m.id).collect();
for dep in deps {
state_members[dep as usize].dependant_mems.push(i as u64);
}
}
let child_states = strct
.members
.iter()
.zip(fields.iter())
.filter(|(_, f)| {
f.attrs.iter().any(|a| {
a.path
.get_ident()
.filter(|i| i.to_string().as_str() == "state")
.is_some()
})
})
.map(|(m, _)| m);
// members need to be sorted so that members are updated after the members they depend on
Ok(Self {
ty: &strct.name,
state_members,
child_states: child_states.collect(),
})
}
}
fn try_parenthesized(input: ParseStream) -> Result<ParseBuffer> {
let inside;
parenthesized!(inside in input);
Ok(inside)
}
struct Dependency {
ctx_ty: Option<Type>,
deps: Vec<Ident>,
}
impl Parse for Dependency {
fn parse(input: ParseStream) -> Result<Self> {
let deps: Option<Punctuated<Ident, Token![,]>> = {
try_parenthesized(input)
.ok()
.and_then(|inside| inside.parse_terminated(Ident::parse).ok())
};
let deps: Vec<_> = deps
.map(|deps| deps.into_iter().collect())
.or_else(|| {
input
.parse::<Ident>()
.ok()
.filter(|i: &Ident| i != "NONE")
.map(|i| vec![i])
})
.unwrap_or_default();
let comma: Option<Token![,]> = input.parse().ok();
let ctx_ty = input.parse().ok();
Ok(Self {
ctx_ty: comma.and(ctx_ty),
deps,
})
}
}
/// The type of the member and the ident of the member
#[derive(PartialEq, Debug)]
struct Member {
id: u64,
ty: Type,
unit_type: Ident,
ident: Ident,
}
impl Member {
fn parse(parent: &Ident, field: &Field, id: u64) -> Option<Self> {
Some(Self {
id,
ty: field.ty.clone(),
unit_type: Ident::new(
("_Unit".to_string()
+ parent.to_token_stream().to_string().as_str()
+ field.ty.to_token_stream().to_string().as_str())
.as_str(),
Span::call_site(),
),
ident: field.ident.as_ref()?.clone(),
})
}
}
#[derive(Debug, Clone)]
struct StateMember<'a> {
mem: &'a Member,
// the kind of dependncies this state has
dep_kind: DependencyKind,
// the depenancy and if it is satified
dep_mems: Vec<&'a Member>,
// any members that depend on this member
dependant_mems: Vec<u64>,
// the context this state requires
ctx_ty: Option<Type>,
}
impl<'a> StateMember<'a> {
fn parse(
field: &Field,
mem: &'a Member,
parent: &'a Struct,
) -> Result<Option<StateMember<'a>>> {
let mut err = Ok(());
let member = field.attrs.iter().find_map(|a| {
let dep_kind = a
.path
.get_ident()
.and_then(|i| match i.to_string().as_str() {
"node_dep_state" => Some(DependencyKind::Node),
"child_dep_state" => Some(DependencyKind::Child),
"parent_dep_state" => Some(DependencyKind::Parent),
_ => None,
})?;
match a.parse_args::<Dependency>() {
Ok(dependency) => {
let dep_mems = dependency
.deps
.iter()
.filter_map(|name| {
if let Some(found) = parent.members.iter().find(|m| &m.ident == name) {
Some(found)
} else {
err = Err(Error::new(
name.span(),
format!("{} not found in {}", name, parent.name),
));
None
}
})
.collect();
Some(Self {
mem,
dep_kind,
dep_mems,
dependant_mems: Vec::new(),
ctx_ty: dependency.ctx_ty,
})
}
Err(e) => {
err = Err(e);
None
}
}
});
err?;
Ok(member)
}
/// generate code to call the resolve function for the state. This does not handle checking if resolving the state is necessary, or marking the states that depend on this state as dirty.
fn impl_pass(
&self,
parent_type: &Ident,
custom_type: impl ToTokens,
) -> quote::__private::TokenStream {
let ident = &self.mem.ident;
let get_ctx = if let Some(ctx_ty) = &self.ctx_ty {
if ctx_ty == &parse_quote!(()) {
quote! {&()}
.find_map(|item| {
if let syn::ImplItem::Type(syn::ImplItemType { ident, ty, .. }) = item {
(ident == "ParentDependencies").then_some(ty)
} else {
let msg = ctx_ty.to_token_stream().to_string() + " not found in context";
quote! {ctx.get().expect(#msg)}
None
}
})
.expect("ParentDependencies must be defined");
let child_dependencies = impl_block
.items
.iter()
.find_map(|item| {
if let syn::ImplItem::Type(syn::ImplItemType { ident, ty, .. }) = item {
(ident == "ChildDependencies").then_some(ty)
} else {
None
}
})
.expect("ChildDependencies must be defined");
let node_dependencies = impl_block
.items
.iter()
.find_map(|item| {
if let syn::ImplItem::Type(syn::ImplItemType { ident, ty, .. }) = item {
(ident == "NodeDependencies").then_some(ty)
} else {
None
}
})
.expect("NodeDependencies must be defined");
let this_type = &impl_block.self_ty;
let this_type = extract_type_path(this_type)
.unwrap_or_else(|| panic!("Self must be a type path, found {}", quote!(#this_type)));
let mut combined_dependencies = HashSet::new();
let self_path: TypePath = syn::parse_quote!(Self);
let parent_dependencies = match extract_tuple(parent_dependencies) {
Some(tuple) => {
let mut parent_dependencies = Vec::new();
for type_ in &tuple.elems {
let mut type_ = extract_type_path(type_).unwrap_or_else(|| {
panic!(
"ParentDependencies must be a tuple of type paths, found {}",
quote!(#type_)
)
});
if type_ == self_path {
type_ = this_type.clone();
}
combined_dependencies.insert(type_.clone());
parent_dependencies.push(type_);
}
parent_dependencies
}
_ => panic!(
"ParentDependencies must be a tuple, found {}",
quote!(#parent_dependencies)
),
};
let child_dependencies = match extract_tuple(child_dependencies) {
Some(tuple) => {
let mut child_dependencies = Vec::new();
for type_ in &tuple.elems {
let mut type_ = extract_type_path(type_).unwrap_or_else(|| {
panic!(
"ChildDependencies must be a tuple of type paths, found {}",
quote!(#type_)
)
});
if type_ == self_path {
type_ = this_type.clone();
}
combined_dependencies.insert(type_.clone());
child_dependencies.push(type_);
}
child_dependencies
}
_ => panic!(
"ChildDependencies must be a tuple, found {}",
quote!(#child_dependencies)
),
};
let node_dependencies = match extract_tuple(node_dependencies) {
Some(tuple) => {
let mut node_dependencies = Vec::new();
for type_ in &tuple.elems {
let mut type_ = extract_type_path(type_).unwrap_or_else(|| {
panic!(
"NodeDependencies must be a tuple of type paths, found {}",
quote!(#type_)
)
});
if type_ == self_path {
type_ = this_type.clone();
}
combined_dependencies.insert(type_.clone());
node_dependencies.push(type_);
}
node_dependencies
}
_ => panic!(
"NodeDependencies must be a tuple, found {}",
quote!(#node_dependencies)
),
};
combined_dependencies.insert(this_type.clone());
let combined_dependencies: Vec<_> = combined_dependencies.into_iter().collect();
let parent_dependancies_idxes: Vec<_> = parent_dependencies
.iter()
.filter_map(|ident| combined_dependencies.iter().position(|i| i == ident))
.collect();
let child_dependencies_idxes: Vec<_> = child_dependencies
.iter()
.filter_map(|ident| combined_dependencies.iter().position(|i| i == ident))
.collect();
let node_dependencies_idxes: Vec<_> = node_dependencies
.iter()
.filter_map(|ident| combined_dependencies.iter().position(|i| i == ident))
.collect();
let this_type_idx = combined_dependencies
.iter()
.enumerate()
.find_map(|(i, ident)| (this_type == *ident).then_some(i))
.unwrap();
let this_view = format_ident!("__data{}", this_type_idx);
let combined_dependencies_quote = combined_dependencies.iter().map(|ident| {
if ident == &this_type {
quote! {shipyard::ViewMut<#ident>}
} else {
quote! {shipyard::View<#ident>}
}
});
let combined_dependencies_quote = quote!((#(#combined_dependencies_quote,)*));
let ItemImpl {
attrs,
defaultness,
unsafety,
impl_token,
generics,
trait_,
self_ty,
items,
..
} = impl_block;
let for_ = trait_.as_ref().map(|t| t.2);
let trait_ = trait_.map(|t| t.1);
let split_views: Vec<_> = (0..combined_dependencies.len())
.map(|i| {
let ident = format_ident!("__data{}", i);
if i == this_type_idx {
quote! {mut #ident}
} else {
quote! {#ident}
}
})
.collect();
let node_view = node_dependencies_idxes
.iter()
.map(|i| format_ident!("__data{}", i))
.collect::<Vec<_>>();
let get_node_view = {
if node_dependencies.is_empty() {
quote! {
let raw_node = ();
}
} else {
quote! {&()}
};
let ty = &self.mem.ty;
let unit_type = &self.mem.unit_type;
let node_view =
quote!(dioxus_native_core::node_ref::NodeView::new(&node.node_data, #ty::NODE_MASK));
let dep_idents = self.dep_mems.iter().map(|m| &m.ident);
let impl_specific = match self.dep_kind {
DependencyKind::Node => {
quote! {
impl dioxus_native_core::NodePass<dioxus_native_core::node::Node<#parent_type, #custom_type>> for #unit_type {
fn pass(&self, node: &mut dioxus_native_core::node::Node<#parent_type, #custom_type>, ctx: &dioxus_native_core::SendAnyMap) -> bool {
node.state.#ident.reduce(#node_view, (#(&node.state.#dep_idents,)*), #get_ctx)
}
}
}
}
DependencyKind::Child => {
let update = if self.dep_mems.iter().any(|m| m.id == self.mem.id) {
quote! {
if update {
dioxus_native_core::PassReturn{
progress: true,
mark_dirty: true,
}
} else {
dioxus_native_core::PassReturn{
progress: false,
mark_dirty: false,
}
}
}
} else {
quote! {
if update {
dioxus_native_core::PassReturn{
progress: false,
mark_dirty: true,
}
} else {
dioxus_native_core::PassReturn{
progress: false,
mark_dirty: false,
}
}
}
let temps = (0..node_dependencies.len())
.map(|i| format_ident!("__temp{}", i))
.collect::<Vec<_>>();
quote! {
let raw_node: (#(*const #node_dependencies,)*) = {
let (#(#temps,)*) = (#(&#node_view,)*).get(id).unwrap_or_else(|err| panic!("Failed to get node view {:?}", err));
(#(#temps as *const _,)*)
};
quote!(
impl dioxus_native_core::UpwardPass<dioxus_native_core::node::Node<#parent_type, #custom_type>> for #unit_type{
fn pass<'a>(
&self,
node: &mut dioxus_native_core::node::Node<#parent_type, #custom_type>,
children: &mut dyn Iterator<Item = &'a mut dioxus_native_core::node::Node<#parent_type, #custom_type>>,
ctx: &dioxus_native_core::SendAnyMap,
) -> dioxus_native_core::PassReturn {
let update = node.state.#ident.reduce(#node_view, children.map(|c| (#(&c.state.#dep_idents,)*)), #get_ctx);
#update
}
}
)
}
DependencyKind::Parent => {
let update = if self.dep_mems.iter().any(|m| m.id == self.mem.id) {
quote! {
if update {
dioxus_native_core::PassReturn{
progress: true,
mark_dirty: true,
}
} else {
dioxus_native_core::PassReturn{
progress: false,
mark_dirty: false,
}
}
}
} else {
quote! {
if update {
dioxus_native_core::PassReturn{
progress: false,
mark_dirty: true,
}
} else {
dioxus_native_core::PassReturn{
progress: false,
mark_dirty: false,
}
}
}
};
quote!(
impl dioxus_native_core::DownwardPass<dioxus_native_core::node::Node<#parent_type, #custom_type>> for #unit_type {
fn pass(&self, node: &mut dioxus_native_core::node::Node<#parent_type, #custom_type>, parent: Option<&mut dioxus_native_core::node::Node<#parent_type, #custom_type>>, ctx: &dioxus_native_core::SendAnyMap) -> dioxus_native_core::PassReturn{
let update = node.state.#ident.reduce(#node_view, parent.as_ref().map(|p| (#(&p.state.#dep_idents,)*)), #get_ctx);
#update
}
}
)
}
};
let pass_id = self.mem.id;
let depenancies = self.dep_mems.iter().map(|m| m.id);
let dependants = &self.dependant_mems;
let mask = self
.dep_mems
.iter()
.map(|m| 1u64 << m.id)
.fold(1 << self.mem.id, |a, b| a | b);
quote! {
#[derive(Clone, Copy)]
struct #unit_type;
#impl_specific
impl dioxus_native_core::Pass for #unit_type {
fn pass_id(&self) -> dioxus_native_core::PassId {
dioxus_native_core::PassId(#pass_id)
}
fn dependancies(&self) -> &'static [dioxus_native_core::PassId] {
&[#(dioxus_native_core::PassId(#depenancies)),*]
}
fn dependants(&self) -> &'static [dioxus_native_core::PassId] {
&[#(dioxus_native_core::PassId(#dependants)),*]
}
fn mask(&self) -> dioxus_native_core::MemberMask {
dioxus_native_core::MemberMask(#mask)
}
}
}
};
let deref_node_view = {
if node_dependencies.is_empty() {
quote! {
let node = raw_node;
}
} else {
let indexes = (0..node_dependencies.len()).map(syn::Index::from);
quote! {
let node = unsafe { (#(dioxus_native_core::prelude::DependancyView::new(&*raw_node.#indexes),)*) };
}
}
};
let parent_view = parent_dependancies_idxes
.iter()
.map(|i| format_ident!("__data{}", i))
.collect::<Vec<_>>();
let get_parent_view = {
if parent_dependencies.is_empty() {
quote! {
let raw_parent = tree.parent_id(id).map(|_| ());
}
} else {
let temps = (0..parent_dependencies.len())
.map(|i| format_ident!("__temp{}", i))
.collect::<Vec<_>>();
quote! {
let raw_parent = tree.parent_id(id).and_then(|parent_id| {
let raw_parent: Option<(#(*const #parent_dependencies,)*)> = (#(&#parent_view,)*).get(parent_id).ok().map(|c| {
let (#(#temps,)*) = c;
(#(#temps as *const _,)*)
});
raw_parent
});
}
}
};
let deref_parent_view = {
if parent_dependencies.is_empty() {
quote! {
let parent = raw_parent;
}
} else {
let indexes = (0..parent_dependencies.len()).map(syn::Index::from);
quote! {
let parent = unsafe { raw_parent.map(|raw_parent| (#(dioxus_native_core::prelude::DependancyView::new(&*raw_parent.#indexes),)*)) };
}
}
};
let child_view = child_dependencies_idxes
.iter()
.map(|i| format_ident!("__data{}", i))
.collect::<Vec<_>>();
let get_child_view = {
if child_dependencies.is_empty() {
quote! {
let raw_children: Vec<_> = tree.children_ids(id).into_iter().map(|_| ()).collect();
}
} else {
let temps = (0..child_dependencies.len())
.map(|i| format_ident!("__temp{}", i))
.collect::<Vec<_>>();
quote! {
let raw_children: Vec<_> = tree.children_ids(id).into_iter().filter_map(|id| {
let raw_children: Option<(#(*const #child_dependencies,)*)> = (#(&#child_view,)*).get(id).ok().map(|c| {
let (#(#temps,)*) = c;
(#(#temps as *const _,)*)
});
raw_children
}).collect();
}
}
};
let deref_child_view = {
if child_dependencies.is_empty() {
quote! {
let children = raw_children;
}
} else {
let indexes = (0..child_dependencies.len()).map(syn::Index::from);
quote! {
let children = unsafe { raw_children.iter().map(|raw_children| (#(dioxus_native_core::prelude::DependancyView::new(&*raw_children.#indexes),)*)).collect::<Vec<_>>() };
}
}
};
let trait_generics = trait_
.as_ref()
.unwrap()
.segments
.last()
.unwrap()
.arguments
.clone();
// if a create function is defined, we don't generate one
// otherwise we generate a default one that uses the update function and the default constructor
let create_fn = (!has_create_fn).then(|| {
quote! {
fn create<'a>(
node_view: dioxus_native_core::prelude::NodeView # trait_generics,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &dioxus_native_core::prelude::SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
});
quote!(
#(#attrs)*
#defaultness #unsafety #impl_token #generics #trait_ #for_ #self_ty {
#create_fn
#(#items)*
fn workload_system(type_id: std::any::TypeId, dependants: std::sync::Arc<dioxus_native_core::prelude::Dependants>, pass_direction: dioxus_native_core::prelude::PassDirection) -> dioxus_native_core::exports::shipyard::WorkloadSystem {
use dioxus_native_core::exports::shipyard::{IntoWorkloadSystem, Get, AddComponent};
use dioxus_native_core::tree::TreeRef;
use dioxus_native_core::prelude::{NodeType, NodeView};
let node_mask = Self::NODE_MASK.build();
(move |data: #combined_dependencies_quote, run_view: dioxus_native_core::prelude::RunPassView #trait_generics| {
let (#(#split_views,)*) = data;
let tree = run_view.tree.clone();
let node_types = run_view.node_type.clone();
dioxus_native_core::prelude::run_pass(type_id, dependants.clone(), pass_direction, run_view, |id, context| {
let node_data: &NodeType<_> = node_types.get(id).unwrap_or_else(|err| panic!("Failed to get node type {:?}", err));
// get all of the states from the tree view
// Safety: No node has itself as a parent or child.
let raw_myself: Option<*mut Self> = (&mut #this_view).get(id).ok().map(|c| c as *mut _);
#get_node_view
#get_parent_view
#get_child_view
let myself: Option<&mut Self> = unsafe { raw_myself.map(|val| &mut *val) };
#deref_node_view
#deref_parent_view
#deref_child_view
let view = NodeView::new(id, node_data, &node_mask);
if let Some(myself) = myself {
myself
.update(view, node, parent, children, context)
}
else {
(&mut #this_view).add_component_unchecked(
id,
Self::create(view, node, parent, children, context));
true
}
})
}).into_workload_system().unwrap()
}
}
)
.into()
}
fn extract_tuple(ty: &Type) -> Option<TypeTuple> {
match ty {
Type::Tuple(tuple) => Some(tuple.clone()),
Type::Group(group) => extract_tuple(&group.elem),
_ => None,
}
}
fn extract_type_path(ty: &Type) -> Option<TypePath> {
match ty {
Type::Path(path) => Some(path.clone()),
Type::Group(group) => extract_type_path(&group.elem),
_ => None,
}
}

View file

@ -1,29 +0,0 @@
extern crate proc_macro;
use std::collections::BTreeMap;
use syn::{
self, bracketed,
parse::{Parse, ParseStream, Result},
LitStr, Token,
};
pub struct StrSlice {
pub map: BTreeMap<String, LitStr>,
}
impl Parse for StrSlice {
fn parse(input: ParseStream) -> Result<Self> {
let content;
bracketed!(content in input);
let mut map = BTreeMap::new();
while let Ok(s) = content.parse::<LitStr>() {
map.insert(s.value(), s);
#[allow(unused_must_use)]
{
content.parse::<Token![,]>();
}
}
Ok(StrSlice { map })
}
}

View file

@ -1,244 +0,0 @@
use dioxus::prelude::*;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::real_dom::*;
use dioxus_native_core::state::{
ChildDepState, ElementBorrowable, NodeDepState, ParentDepState, State,
};
use dioxus_native_core::tree::*;
use dioxus_native_core::SendAnyMap;
use dioxus_native_core_macro::State;
macro_rules! dep {
( child( $name:ty, $dep:ty ) ) => {
impl ChildDepState for $name {
type Ctx = ();
type DepState = $dep;
const NODE_MASK: NodeMask = NodeMask::ALL;
fn reduce<'a>(
&mut self,
_: NodeView,
_: impl Iterator<Item = <Self::DepState as ElementBorrowable>::ElementBorrowed<'a>>,
_: &Self::Ctx,
) -> bool
where
Self::DepState: 'a,
{
self.0 += 1;
true
}
}
};
( parent( $name:ty, $dep:ty ) ) => {
impl ParentDepState for $name {
type Ctx = ();
type DepState = $dep;
const NODE_MASK: NodeMask = NodeMask::ALL;
fn reduce(
&mut self,
_: NodeView,
_: Option<<Self::DepState as ElementBorrowable>::ElementBorrowed<'_>>,
_: &Self::Ctx,
) -> bool {
self.0 += 1;
true
}
}
};
( node( $name:ty, $dep:ty ) ) => {
impl NodeDepState for $name {
type Ctx = ();
type DepState = $dep;
const NODE_MASK: NodeMask = NodeMask::ALL;
fn reduce(
&mut self,
_: NodeView,
_: <Self::DepState as ElementBorrowable>::ElementBorrowed<'_>,
_: &Self::Ctx,
) -> bool {
self.0 += 1;
true
}
}
};
}
macro_rules! test_state{
( $s:ty, child: ( $( $child:ident ),* ), node: ( $( $node:ident ),* ), parent: ( $( $parent:ident ),* ) ) => {
#[test]
fn state_reduce_initally_called_minimally() {
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
render!{
div {
div{
div{
p{}
}
p{
"hello"
}
div{
h1{}
}
p{
"world"
}
}
}
}
}
let mut vdom = VirtualDom::new(Base);
let mutations = vdom.rebuild();
let mut dom: RealDom<$s> = RealDom::new();
let (nodes_updated, _) = dom.apply_mutations(mutations);
let _to_rerender = dom.update_state(nodes_updated, SendAnyMap::new());
dom.traverse_depth_first(|n| {
$(
assert_eq!(n.state.$child.0, 1);
)*
$(
assert_eq!(n.state.$node.0, 1);
)*
$(
assert_eq!(n.state.$parent.0, 1);
)*
});
}
}
}
mod node_depends_on_child_and_parent {
use super::*;
#[derive(Debug, Clone, Default, PartialEq)]
struct Node(i32);
dep!(node(Node, (Child, Parent)));
#[derive(Debug, Clone, Default, PartialEq)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
#[derive(Debug, Clone, Default, State)]
struct StateTester {
#[node_dep_state((child, parent))]
node: Node,
#[child_dep_state(child)]
child: Child,
#[parent_dep_state(parent)]
parent: Parent,
}
test_state!(StateTester, child: (child), node: (node), parent: (parent));
}
mod child_depends_on_node_that_depends_on_parent {
use super::*;
#[derive(Debug, Clone, Default, PartialEq)]
struct Node(i32);
dep!(node(Node, (Parent,)));
#[derive(Debug, Clone, Default, PartialEq)]
struct Child(i32);
dep!(child(Child, (Node,)));
#[derive(Debug, Clone, Default, PartialEq)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
#[derive(Debug, Clone, Default, State)]
struct StateTester {
#[node_dep_state(parent)]
node: Node,
#[child_dep_state(node)]
child: Child,
#[parent_dep_state(parent)]
parent: Parent,
}
test_state!(StateTester, child: (child), node: (node), parent: (parent));
}
mod parent_depends_on_node_that_depends_on_child {
use super::*;
#[derive(Debug, Clone, Default, PartialEq)]
struct Node(i32);
dep!(node(Node, (Child,)));
#[derive(Debug, Clone, Default, PartialEq)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq)]
struct Parent(i32);
dep!(parent(Parent, (Node,)));
#[derive(Debug, Clone, Default, State)]
struct StateTester {
#[node_dep_state(child)]
node: Node,
#[child_dep_state(child)]
child: Child,
#[parent_dep_state(node)]
parent: Parent,
}
test_state!(StateTester, child: (child), node: (node), parent: (parent));
}
mod node_depends_on_other_node_state {
use super::*;
#[derive(Debug, Clone, Default, PartialEq)]
struct Node1(i32);
dep!(node(Node1, (Node2,)));
#[derive(Debug, Clone, Default, PartialEq)]
struct Node2(i32);
dep!(node(Node2, ()));
#[derive(Debug, Clone, Default, State)]
struct StateTester {
#[node_dep_state((node2))]
node1: Node1,
#[node_dep_state()]
node2: Node2,
}
test_state!(StateTester, child: (), node: (node1, node2), parent: ());
}
mod node_child_and_parent_state_depends_on_self {
use super::*;
#[derive(Debug, Clone, Default, PartialEq)]
struct Node(i32);
dep!(node(Node, ()));
#[derive(Debug, Clone, Default, PartialEq)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
#[derive(Debug, Clone, Default, State)]
struct StateTester {
#[node_dep_state()]
node: Node,
#[child_dep_state(child)]
child: Child,
#[parent_dep_state(parent)]
parent: Parent,
}
test_state!(StateTester, child: (child), node: (node), parent: (parent));
}

View file

@ -1,421 +0,0 @@
use dioxus::prelude::*;
use dioxus_native_core::real_dom::*;
use dioxus_native_core::state::{ChildDepState, NodeDepState, ParentDepState, State};
use dioxus_native_core::tree::TreeView;
use dioxus_native_core::{node_ref::*, NodeId, SendAnyMap};
use dioxus_native_core_macro::State;
#[derive(Debug, Clone, Default, State)]
struct CallCounterState {
#[child_dep_state(child_counter)]
child_counter: ChildDepCallCounter,
#[parent_dep_state(parent_counter)]
parent_counter: ParentDepCallCounter,
#[node_dep_state()]
node_counter: NodeDepCallCounter,
}
#[derive(Debug, Clone, Default)]
struct ChildDepCallCounter(u32);
impl ChildDepState for ChildDepCallCounter {
type Ctx = ();
type DepState = (Self,);
const NODE_MASK: NodeMask = NodeMask::ALL;
fn reduce<'a>(
&mut self,
_: NodeView,
_: impl Iterator<Item = (&'a Self,)>,
_: &Self::Ctx,
) -> bool
where
Self::DepState: 'a,
{
self.0 += 1;
true
}
}
#[derive(Debug, Clone, Default)]
struct ParentDepCallCounter(u32);
impl ParentDepState for ParentDepCallCounter {
type Ctx = ();
type DepState = (Self,);
const NODE_MASK: NodeMask = NodeMask::ALL;
fn reduce(&mut self, _node: NodeView, _parent: Option<(&Self,)>, _ctx: &Self::Ctx) -> bool {
self.0 += 1;
true
}
}
#[derive(Debug, Clone, Default)]
struct NodeDepCallCounter(u32);
impl NodeDepState for NodeDepCallCounter {
type Ctx = ();
type DepState = ();
const NODE_MASK: NodeMask = NodeMask::ALL;
fn reduce(&mut self, _node: NodeView, _sibling: (), _ctx: &Self::Ctx) -> bool {
self.0 += 1;
true
}
}
#[allow(clippy::vec_box)]
#[derive(Debug, Clone, PartialEq, Default)]
struct BubbledUpStateTester(Option<String>, Vec<Box<BubbledUpStateTester>>);
impl ChildDepState for BubbledUpStateTester {
type Ctx = u32;
type DepState = (Self,);
const NODE_MASK: NodeMask = NodeMask::new().with_tag();
fn reduce<'a>(
&mut self,
node: NodeView,
children: impl Iterator<Item = (&'a Self,)>,
ctx: &Self::Ctx,
) -> bool
where
Self::DepState: 'a,
{
assert_eq!(*ctx, 42);
*self = BubbledUpStateTester(
node.tag().map(|s| s.to_string()),
children
.into_iter()
.map(|(c,)| Box::new(c.clone()))
.collect(),
);
true
}
}
#[derive(Debug, Clone, PartialEq, Default)]
struct PushedDownStateTester(Option<String>, Option<Box<PushedDownStateTester>>);
impl ParentDepState for PushedDownStateTester {
type Ctx = u32;
type DepState = (Self,);
const NODE_MASK: NodeMask = NodeMask::new().with_tag();
fn reduce(&mut self, node: NodeView, parent: Option<(&Self,)>, ctx: &Self::Ctx) -> bool {
assert_eq!(*ctx, 42);
*self = PushedDownStateTester(
node.tag().map(|s| s.to_string()),
parent.map(|(c,)| Box::new(c.clone())),
);
true
}
}
#[derive(Debug, Clone, PartialEq, Default)]
struct NodeStateTester(Option<String>, Vec<(String, String)>);
impl NodeDepState for NodeStateTester {
type Ctx = u32;
type DepState = ();
const NODE_MASK: NodeMask = NodeMask::new_with_attrs(AttributeMask::All).with_tag();
fn reduce(&mut self, node: NodeView, _sibling: (), ctx: &Self::Ctx) -> bool {
assert_eq!(*ctx, 42);
*self = NodeStateTester(
node.tag().map(|s| s.to_string()),
node.attributes()
.map(|iter| {
iter.map(|a| {
(
a.attribute.name.to_string(),
a.value.as_text().unwrap().to_string(),
)
})
.collect()
})
.unwrap_or_default(),
);
true
}
}
#[derive(State, Clone, Default, Debug)]
struct StateTester {
#[child_dep_state(bubbled, u32)]
bubbled: BubbledUpStateTester,
#[parent_dep_state(pushed, u32)]
pushed: PushedDownStateTester,
#[node_dep_state(NONE, u32)]
node: NodeStateTester,
}
#[test]
fn state_initial() {
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
render! {
div {
p{
color: "red"
}
h1{}
}
}
}
let mut vdom = VirtualDom::new(Base);
let mutations = vdom.rebuild();
let mut dom: RealDom<StateTester> = RealDom::new();
let (nodes_updated, _) = dom.apply_mutations(mutations);
let mut ctx = SendAnyMap::new();
ctx.insert(42u32);
let _to_rerender = dom.update_state(nodes_updated, ctx);
let root_div_id = dom.children_ids(NodeId(0)).unwrap()[0];
let root_div = &dom.get(root_div_id).unwrap();
assert_eq!(root_div.state.bubbled.0, Some("div".to_string()));
assert_eq!(
root_div.state.bubbled.1,
vec![
Box::new(BubbledUpStateTester(Some("p".to_string()), Vec::new())),
Box::new(BubbledUpStateTester(Some("h1".to_string()), Vec::new()))
]
);
assert_eq!(root_div.state.pushed.0, Some("div".to_string()));
assert_eq!(
root_div.state.pushed.1,
Some(Box::new(PushedDownStateTester(
Some("Root".to_string()),
None
)))
);
assert_eq!(root_div.state.node.0, Some("div".to_string()));
assert_eq!(root_div.state.node.1, vec![]);
let child_p_id = dom.children_ids(root_div_id).unwrap()[0];
let child_p = &dom[child_p_id];
assert_eq!(child_p.state.bubbled.0, Some("p".to_string()));
assert_eq!(child_p.state.bubbled.1, Vec::new());
assert_eq!(child_p.state.pushed.0, Some("p".to_string()));
assert_eq!(
child_p.state.pushed.1,
Some(Box::new(PushedDownStateTester(
Some("div".to_string()),
Some(Box::new(PushedDownStateTester(
Some("Root".to_string()),
None
)))
)))
);
assert_eq!(child_p.state.node.0, Some("p".to_string()));
assert_eq!(
child_p.state.node.1,
vec![("color".to_string(), "red".to_string())]
);
let child_h1_id = dom.children_ids(root_div_id).unwrap()[1];
let child_h1 = &dom[child_h1_id];
assert_eq!(child_h1.state.bubbled.0, Some("h1".to_string()));
assert_eq!(child_h1.state.bubbled.1, Vec::new());
assert_eq!(child_h1.state.pushed.0, Some("h1".to_string()));
assert_eq!(
child_h1.state.pushed.1,
Some(Box::new(PushedDownStateTester(
Some("div".to_string()),
Some(Box::new(PushedDownStateTester(
Some("Root".to_string()),
None
)))
)))
);
assert_eq!(child_h1.state.node.0, Some("h1".to_string()));
assert_eq!(child_h1.state.node.1, vec![]);
}
#[test]
fn state_reduce_parent_called_minimally_on_update() {
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
let width = if cx.generation() == 0 { "100%" } else { "99%" };
cx.render(rsx! {
div {
width: "{width}",
div{
div{
p{}
}
p{
"hello"
}
div{
h1{}
}
p{
"world"
}
}
}
})
}
let mut vdom = VirtualDom::new(Base);
let mut dom: RealDom<CallCounterState> = RealDom::new();
let (nodes_updated, _) = dom.apply_mutations(vdom.rebuild());
let _to_rerender = dom.update_state(nodes_updated, SendAnyMap::new());
vdom.mark_dirty(ScopeId(0));
let (nodes_updated, _) = dom.apply_mutations(vdom.render_immediate());
let _to_rerender = dom.update_state(nodes_updated, SendAnyMap::new());
let mut is_root = true;
dom.traverse_depth_first(|n| {
if is_root {
is_root = false;
assert_eq!(n.state.parent_counter.0, 1);
} else {
assert_eq!(n.state.parent_counter.0, 2);
}
});
}
#[test]
fn state_reduce_child_called_minimally_on_update() {
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
let width = if cx.generation() == 0 { "100%" } else { "99%" };
cx.render(rsx! {
// updated: 2
div {
// updated: 2
div{
// updated: 2
div{
// updated: 2
p{
width: "{width}",
}
}
// updated: 1
p{
// updated: 1
"hello"
}
// updated: 1
div{
// updated: 1
h1{}
}
// updated: 1
p{
// updated: 1
"world"
}
}
}
})
}
let mut vdom = VirtualDom::new(Base);
let mut dom: RealDom<CallCounterState> = RealDom::new();
let (nodes_updated, _) = dom.apply_mutations(vdom.rebuild());
let _to_rerender = dom.update_state(nodes_updated, SendAnyMap::new());
vdom.mark_dirty(ScopeId(0));
let (nodes_updated, _) = dom.apply_mutations(vdom.render_immediate());
let _to_rerender = dom.update_state(nodes_updated, SendAnyMap::new());
let mut traverse_count = 0;
dom.traverse_depth_first(|n| {
assert_eq!(n.state.child_counter.0, {
if traverse_count > 4 {
1
} else {
2
}
});
traverse_count += 1;
});
}
#[derive(Debug, Clone, Default, State)]
struct UnorderedDependanciesState {
#[node_dep_state(c)]
b: BDepCallCounter,
#[node_dep_state()]
c: CDepCallCounter,
#[node_dep_state(b)]
a: ADepCallCounter,
}
#[derive(Debug, Clone, Default, PartialEq)]
struct ADepCallCounter(usize, BDepCallCounter);
impl NodeDepState for ADepCallCounter {
type Ctx = ();
type DepState = (BDepCallCounter,);
const NODE_MASK: NodeMask = NodeMask::NONE;
fn reduce(
&mut self,
_node: NodeView,
(sibling,): (&BDepCallCounter,),
_ctx: &Self::Ctx,
) -> bool {
self.0 += 1;
self.1 = sibling.clone();
true
}
}
#[derive(Debug, Clone, Default, PartialEq)]
struct BDepCallCounter(usize, CDepCallCounter);
impl NodeDepState for BDepCallCounter {
type Ctx = ();
type DepState = (CDepCallCounter,);
const NODE_MASK: NodeMask = NodeMask::NONE;
fn reduce(
&mut self,
_node: NodeView,
(sibling,): (&CDepCallCounter,),
_ctx: &Self::Ctx,
) -> bool {
self.0 += 1;
self.1 = sibling.clone();
true
}
}
#[derive(Debug, Clone, Default, PartialEq)]
struct CDepCallCounter(usize);
impl NodeDepState for CDepCallCounter {
type Ctx = ();
type DepState = ();
const NODE_MASK: NodeMask = NodeMask::ALL;
fn reduce(&mut self, _node: NodeView, _sibling: (), _ctx: &Self::Ctx) -> bool {
self.0 += 1;
true
}
}
#[test]
fn dependancies_order_independant() {
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
render!(div {
width: "100%",
p{
"hello"
}
})
}
let mut vdom = VirtualDom::new(Base);
let mut dom: RealDom<UnorderedDependanciesState> = RealDom::new();
let mutations = vdom.rebuild();
let (nodes_updated, _) = dom.apply_mutations(mutations);
let _to_rerender = dom.update_state(nodes_updated, SendAnyMap::new());
let c = CDepCallCounter(1);
let b = BDepCallCounter(1, c.clone());
let a = ADepCallCounter(1, b.clone());
dom.traverse_depth_first(|n| {
assert_eq!(&n.state.a, &a);
assert_eq!(&n.state.b, &b);
assert_eq!(&n.state.c, &c);
});
}

View file

@ -1,6 +1,6 @@
[package]
name = "dioxus-native-core"
version = "0.3.0"
version = "0.2.0"
edition = "2021"
license = "MIT/Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
@ -11,24 +11,33 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-core = { path = "../core", version = "^0.3.0" }
dioxus-html = { path = "../html", version = "^0.3.0" }
dioxus-core-macro = { path = "../core-macro", version = "^0.3.0" }
dioxus-core = { path = "../core", version = "^0.3.0", optional = true }
keyboard-types = "0.6.2"
taffy = "0.2.1"
smallvec = "1.6"
rustc-hash = "1.1.0"
anymap = "1.0.0-beta.2"
slab = "0.4"
parking_lot = "0.12.1"
parking_lot = { version = "0.12.1", features = ["send_guard"] }
crossbeam-deque = "0.8.2"
dashmap = "5.4.0"
hashbrown = { version = "0.13.2", features = ["raw"] }
# for parsing attributes
lightningcss = "1.0.0-alpha.39"
rayon = "1.6.1"
shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false }
[dev-dependencies]
rand = "0.8.5"
dioxus = { path = "../dioxus", version = "^0.3.0" }
tokio = { version = "*", features = ["full"] }
dioxus-native-core = { path = ".", features = ["dioxus"] }
dioxus-native-core-macro = { path = "../native-core-macro" }
tokio = { version = "1.0", features = ["full"] }
[features]
default = []
dioxus = ["dioxus-core"]
parallel = ["shipyard/parallel"]

View file

@ -18,7 +18,7 @@
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/guide/) |
[API Docs](https://docs.rs/dioxus-native-core/latest/dioxus_native_core) |
[Chat](https://discord.gg/XgGxMSkvUM)

View file

@ -0,0 +1,231 @@
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeTypeMut;
use dioxus_native_core_macro::partial_derive_state;
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
#[derive(Default, Debug, Copy, Clone, Component)]
struct X;
impl FromAnyValue for X {
fn from_any_value(_: &dyn std::any::Any) -> Self {
X
}
}
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State<X> for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<X>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State<X> for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<X>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State<X> for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<X>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rdom: RealDom<X> = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut count = 0;
// intial render
let text_id = rdom.create_node(format!("Count: {count}")).id();
let mut root = rdom.get_mut(rdom.root_id()).unwrap();
// set the color to red
if let NodeTypeMut::Element(mut element) = root.node_type_mut() {
element.set_attribute(("color", "style"), "red".to_string());
}
root.add_child(text_id);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// update the count
count += 1;
let mut text = rdom.get_mut(text_id).unwrap();
if let NodeTypeMut::Text(mut text) = text.node_type_mut() {
*text = format!("Count: {count}");
}
let mut ctx = SendAnyMap::new();
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first(|node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
println!("{indent}{id:?} {color:?} {size:?} {border:?}");
});
// wait 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}

View file

@ -0,0 +1,175 @@
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeTypeMut;
use dioxus_native_core_macro::partial_derive_state;
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = (FontSize,);
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
(font_size,): <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
let font_size = font_size.size;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Component)]
struct FontSize {
size: f64,
}
impl Default for FontSize {
fn default() -> Self {
Self { size: 16.0 }
}
}
#[partial_derive_state]
impl State for FontSize {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["font-size"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
let mut new = None;
for attr in node_view.attributes().into_iter().flatten() {
if attr.attribute.name == "font-size" {
new = Some(FontSize {
size: attr.value.as_float().unwrap(),
});
}
}
let new = new.unwrap_or(parent.map(|(p,)| *p).unwrap_or_default());
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rdom: RealDom = RealDom::new([FontSize::to_type_erased(), Size::to_type_erased()]);
let mut count = 0;
// intial render
let text_id = rdom.create_node(format!("Count: {count}")).id();
let mut root = rdom.get_mut(rdom.root_id()).unwrap();
// set the color to red
if let NodeTypeMut::Element(mut element) = root.node_type_mut() {
element.set_attribute(("color", "style"), "red".to_string());
element.set_attribute(("font-size", "style"), 1.);
}
root.add_child(text_id);
let ctx = SendAnyMap::new();
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// update the count and font size
count += 1;
let mut text = rdom.get_mut(text_id).unwrap();
if let NodeTypeMut::Text(mut text) = text.node_type_mut() {
*text = format!("Count: {count}");
}
if let NodeTypeMut::Element(mut element) =
rdom.get_mut(rdom.root_id()).unwrap().node_type_mut()
{
element.set_attribute(("font-size", "style"), count as f64);
}
let ctx = SendAnyMap::new();
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first(|node| {
let indent = " ".repeat(node.height() as usize);
let font_size = *node.get::<FontSize>().unwrap();
let size = *node.get::<Size>().unwrap();
let id = node.id();
println!("{indent}{id:?} {font_size:?} {size:?}");
});
// wait 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}

View file

@ -0,0 +1,222 @@
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeTypeMut;
use dioxus_native_core_macro::partial_derive_state;
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rdom: RealDom = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut count = 0;
// intial render
let text_id = rdom.create_node(format!("Count: {count}")).id();
let mut root = rdom.get_mut(rdom.root_id()).unwrap();
// set the color to red
if let NodeTypeMut::Element(mut element) = root.node_type_mut() {
element.set_attribute(("color", "style"), "red".to_string());
}
root.add_child(text_id);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// update the count
count += 1;
let mut text = rdom.get_mut(text_id).unwrap();
if let NodeTypeMut::Text(mut text) = text.node_type_mut() {
*text = format!("Count: {count}");
}
let mut ctx = SendAnyMap::new();
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first(|node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
println!("{indent}{id:?} {color:?} {size:?} {border:?}");
});
// wait 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}

View file

@ -0,0 +1,249 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn app(cx: Scope) -> Element {
let count = use_state(cx, || 0);
use_future(cx, (count,), |(count,)| async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
count.set(*count + 1);
}
});
cx.render(rsx! {
div{
color: "red",
"{count}",
Comp {}
}
})
}
fn Comp(cx: Scope) -> Element {
cx.render(rsx! {
div{
border: "",
"hello world"
}
})
}
// create the vdom, the real_dom, and the binding layer between them
let mut vdom = VirtualDom::new(app);
let mut rdom: RealDom = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut dioxus_intigration_state = DioxusState::create(&mut rdom);
let mutations = vdom.rebuild();
// update the structure of the real_dom tree
dioxus_intigration_state.apply_mutations(&mut rdom, mutations);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// wait for the vdom to update
vdom.wait_for_work().await;
// get the mutations from the vdom
let mutations = vdom.render_immediate();
// update the structure of the real_dom tree
dioxus_intigration_state.apply_mutations(&mut rdom, mutations);
// update the state of the real_dom tree
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first(|node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
let node = node.node_type();
let node_type = &*node;
println!("{indent}{id:?} {color:?} {size:?} {border:?} {node_type:?}");
});
}
})
}

View file

@ -0,0 +1,296 @@
//! Integration between Dioxus and the RealDom
use crate::tree::TreeMut;
use dioxus_core::{BorrowedAttributeValue, ElementId, Mutations, TemplateNode};
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::Component;
use crate::{
node::{
ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue,
TextNode,
},
prelude::*,
real_dom::NodeTypeMut,
NodeId,
};
#[derive(Component)]
struct ElementIdComponent(ElementId);
/// The state of the Dioxus integration with the RealDom
pub struct DioxusState {
templates: FxHashMap<String, Vec<NodeId>>,
stack: Vec<NodeId>,
node_id_mapping: Vec<Option<NodeId>>,
}
impl DioxusState {
/// Initialize the DioxusState in the RealDom
pub fn create<V: FromAnyValue + Send + Sync>(rdom: &mut RealDom<V>) -> Self {
let root_id = rdom.root_id();
let mut root = rdom.get_mut(root_id).unwrap();
root.insert(ElementIdComponent(ElementId(0)));
Self {
templates: FxHashMap::default(),
stack: vec![root_id],
node_id_mapping: vec![Some(root_id)],
}
}
/// Convert an ElementId to a NodeId
pub fn element_to_node_id(&self, element_id: ElementId) -> NodeId {
self.try_element_to_node_id(element_id).unwrap()
}
/// Attempt to convert an ElementId to a NodeId. This will return None if the ElementId is not in the RealDom.
pub fn try_element_to_node_id(&self, element_id: ElementId) -> Option<NodeId> {
self.node_id_mapping.get(element_id.0).copied().flatten()
}
fn set_element_id<V: FromAnyValue + Send + Sync>(
&mut self,
mut node: NodeMut<V>,
element_id: ElementId,
) {
let node_id = node.id();
node.insert(ElementIdComponent(element_id));
if self.node_id_mapping.len() <= element_id.0 {
self.node_id_mapping.resize(element_id.0 + 1, None);
}
self.node_id_mapping[element_id.0] = Some(node_id);
}
fn load_child<V: FromAnyValue + Send + Sync>(&self, rdom: &RealDom<V>, path: &[u8]) -> NodeId {
let mut current = rdom.get(*self.stack.last().unwrap()).unwrap();
for i in path {
let new_id = current.child_ids()[*i as usize];
current = rdom.get(new_id).unwrap();
}
current.id()
}
/// Updates the dom with some mutations and return a set of nodes that were updated. Pass the dirty nodes to update_state.
pub fn apply_mutations<V: FromAnyValue + Send + Sync>(
&mut self,
rdom: &mut RealDom<V>,
mutations: Mutations,
) {
for template in mutations.templates {
let mut template_root_ids = Vec::new();
for root in template.roots {
let id = create_template_node(rdom, root);
template_root_ids.push(id);
}
self.templates
.insert(template.name.to_string(), template_root_ids);
}
for e in mutations.edits {
use dioxus_core::Mutation::*;
match e {
AppendChildren { id, m } => {
let children = self.stack.split_off(self.stack.len() - m);
let parent = self.element_to_node_id(id);
for child in children {
rdom.get_mut(parent).unwrap().add_child(child);
}
}
AssignId { path, id } => {
let node_id = self.load_child(rdom, path);
self.set_element_id(rdom.get_mut(node_id).unwrap(), id);
}
CreatePlaceholder { id } => {
let node = NodeType::Placeholder;
let node = rdom.create_node(node);
let node_id = node.id();
self.set_element_id(node, id);
self.stack.push(node_id);
}
CreateTextNode { value, id } => {
let node_data = NodeType::Text(TextNode {
listeners: FxHashSet::default(),
text: value.to_string(),
});
let node = rdom.create_node(node_data);
let node_id = node.id();
self.set_element_id(node, id);
self.stack.push(node_id);
}
HydrateText { path, value, id } => {
let node_id = self.load_child(rdom, path);
let node = rdom.get_mut(node_id).unwrap();
self.set_element_id(node, id);
let mut node = rdom.get_mut(node_id).unwrap();
let node_type_mut = node.node_type_mut();
if let NodeTypeMut::Text(mut text) = node_type_mut {
*text.text_mut() = value.to_string();
} else {
drop(node_type_mut);
node.set_type(NodeType::Text(TextNode {
text: value.to_string(),
listeners: FxHashSet::default(),
}));
}
}
LoadTemplate { name, index, id } => {
let template_id = self.templates[name][index];
let clone_id = rdom.get_mut(template_id).unwrap().clone_node();
let clone = rdom.get_mut(clone_id).unwrap();
self.set_element_id(clone, id);
self.stack.push(clone_id);
}
ReplaceWith { id, m } => {
let new_nodes = self.stack.split_off(self.stack.len() - m);
let old_node_id = self.element_to_node_id(id);
for new in new_nodes {
let mut node = rdom.get_mut(new).unwrap();
node.insert_before(old_node_id);
}
rdom.get_mut(old_node_id).unwrap().remove();
}
ReplacePlaceholder { path, m } => {
let new_nodes = self.stack.split_off(self.stack.len() - m);
let old_node_id = self.load_child(rdom, path);
for new in new_nodes {
let mut node = rdom.get_mut(new).unwrap();
node.insert_before(old_node_id);
}
rdom.get_mut(old_node_id).unwrap().remove();
}
InsertAfter { id, m } => {
let new_nodes = self.stack.split_off(self.stack.len() - m);
let old_node_id = self.element_to_node_id(id);
for new in new_nodes.into_iter().rev() {
let mut node = rdom.get_mut(new).unwrap();
node.insert_after(old_node_id);
}
}
InsertBefore { id, m } => {
let new_nodes = self.stack.split_off(self.stack.len() - m);
let old_node_id = self.element_to_node_id(id);
for new in new_nodes {
rdom.tree_mut().insert_before(old_node_id, new);
}
}
SetAttribute {
name,
value,
id,
ns,
} => {
let node_id = self.element_to_node_id(id);
let mut node = rdom.get_mut(node_id).unwrap();
let mut node_type_mut = node.node_type_mut();
if let NodeTypeMut::Element(element) = &mut node_type_mut {
if let BorrowedAttributeValue::None = &value {
element.remove_attribute(&OwnedAttributeDiscription {
name: name.to_string(),
namespace: ns.map(|s| s.to_string()),
});
} else {
element.set_attribute(
OwnedAttributeDiscription {
name: name.to_string(),
namespace: ns.map(|s| s.to_string()),
},
OwnedAttributeValue::from(value),
);
}
}
}
SetText { value, id } => {
let node_id = self.element_to_node_id(id);
let mut node = rdom.get_mut(node_id).unwrap();
let node_type_mut = node.node_type_mut();
if let NodeTypeMut::Text(mut text) = node_type_mut {
*text.text_mut() = value.to_string();
}
}
NewEventListener { name, id } => {
let node_id = self.element_to_node_id(id);
let mut node = rdom.get_mut(node_id).unwrap();
node.add_event_listener(name);
}
RemoveEventListener { id, name } => {
let node_id = self.element_to_node_id(id);
let mut node = rdom.get_mut(node_id).unwrap();
node.remove_event_listener(name);
}
Remove { id } => {
let node_id = self.element_to_node_id(id);
rdom.get_mut(node_id).unwrap().remove();
}
PushRoot { id } => {
let node_id = self.element_to_node_id(id);
self.stack.push(node_id);
}
}
}
}
}
fn create_template_node<V: FromAnyValue + Send + Sync>(
rdom: &mut RealDom<V>,
node: &TemplateNode,
) -> NodeId {
match node {
TemplateNode::Element {
tag,
namespace,
attrs,
children,
} => {
let node = NodeType::Element(ElementNode {
tag: tag.to_string(),
namespace: namespace.map(|s| s.to_string()),
attributes: attrs
.iter()
.filter_map(|attr| match attr {
dioxus_core::TemplateAttribute::Static {
name,
value,
namespace,
} => Some((
OwnedAttributeDiscription {
namespace: namespace.map(|s| s.to_string()),
name: name.to_string(),
},
OwnedAttributeValue::Text(value.to_string()),
)),
dioxus_core::TemplateAttribute::Dynamic { .. } => None,
})
.collect(),
listeners: FxHashSet::default(),
});
let node_id = rdom.create_node(node).id();
for child in *children {
let child_id = create_template_node(rdom, child);
rdom.get_mut(node_id).unwrap().add_child(child_id);
}
node_id
}
TemplateNode::Text { text } => rdom
.create_node(NodeType::Text(TextNode {
text: text.to_string(),
..Default::default()
}))
.id(),
TemplateNode::Dynamic { .. } => rdom.create_node(NodeType::Placeholder).id(),
TemplateNode::DynamicText { .. } => {
rdom.create_node(NodeType::Text(TextNode::default())).id()
}
}
}
/// A trait that extends the `NodeImmutable` trait with methods that are useful for dioxus.
pub trait NodeImmutableDioxusExt<V: FromAnyValue + Send + Sync>: NodeImmutable<V> {
/// Returns the id of the element that this node is mounted to.
/// Not all nodes are mounted to an element, only nodes with dynamic content that have been renderered will have an id.
fn mounted_id(&self) -> Option<ElementId> {
let id = self.get::<ElementIdComponent>();
id.map(|id| id.0)
}
}
impl<T: NodeImmutable<V>, V: FromAnyValue + Send + Sync> NodeImmutableDioxusExt<V> for T {}

View file

@ -1,3 +1,5 @@
//! Utility functions for applying layout attributes to taffy layout
/*
- [ ] pub display: Display, ----> taffy doesnt support all display types
- [x] pub position_type: PositionType, --> taffy doesnt support everything
@ -46,12 +48,14 @@ use taffy::{
style::{FlexDirection, PositionType},
};
/// Default values for layout attributes
#[derive(Default)]
pub struct LayoutConfigeration {
/// the default border widths to use
pub border_widths: BorderWidths,
}
/// Default border widths
pub struct BorderWidths {
/// the default border width to use for thin borders
pub thin: f32,

View file

@ -1,51 +1,49 @@
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
use std::any::Any;
use std::hash::BuildHasherDefault;
pub use node_ref::NodeMask;
pub use passes::{
AnyPass, DownwardPass, MemberMask, NodePass, Pass, PassId, PassReturn, UpwardPass,
};
use node_ref::NodeMask;
use rustc_hash::FxHasher;
pub use tree::NodeId;
#[cfg(feature = "dioxus")]
pub mod dioxus;
pub mod layout_attributes;
pub mod node;
pub mod node_ref;
pub mod passes;
pub mod node_watcher;
mod passes;
pub mod real_dom;
pub mod state;
pub mod tree;
pub mod utils;
pub use shipyard::EntityId as NodeId;
/// A id for a node that lives in the real dom.
pub type RealNodeId = NodeId;
pub mod exports {
//! Important dependencies that are used by the rest of the library
//! Feel free to just add the dependencies in your own Crates.toml
// exported for the macro
#[doc(hidden)]
pub use rustc_hash::FxHashSet;
pub use shipyard;
}
/// A prelude of commonly used items
pub mod prelude {
#[cfg(feature = "dioxus")]
pub use crate::dioxus::*;
pub use crate::node::{ElementNode, FromAnyValue, NodeType, OwnedAttributeView, TextNode};
pub use crate::node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView};
pub use crate::passes::{run_pass, PassDirection, RunPassView, TypeErasedState};
pub use crate::passes::{Dependancy, DependancyView, Dependants, State};
pub use crate::real_dom::{NodeImmutable, NodeMut, NodeRef, RealDom};
pub use crate::NodeId;
pub use crate::SendAnyMap;
}
/// A map that can be sent between threads
pub type FxDashMap<K, V> = dashmap::DashMap<K, V, BuildHasherDefault<FxHasher>>;
/// A set that can be sent between threads
pub type FxDashSet<K> = dashmap::DashSet<K, BuildHasherDefault<FxHasher>>;
/// A map of types that can be sent between threads
pub type SendAnyMap = anymap::Map<dyn Any + Send + Sync + 'static>;
/// Used in derived state macros
#[derive(Eq, PartialEq)]
#[doc(hidden)]
pub struct HeightOrdering {
pub height: u16,
pub id: RealNodeId,
}
impl HeightOrdering {
pub fn new(height: u16, id: RealNodeId) -> Self {
HeightOrdering { height, id }
}
}
// not the ordering after height is just for deduplication it can be any ordering as long as it is consistent
impl Ord for HeightOrdering {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.height.cmp(&other.height).then(self.id.cmp(&other.id))
}
}
impl PartialOrd for HeightOrdering {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

View file

@ -1,65 +1,113 @@
use crate::{state::State, tree::NodeId};
use dioxus_core::{AnyValue, BorrowedAttributeValue, ElementId};
//! Items related to Nodes in the RealDom
use rustc_hash::{FxHashMap, FxHashSet};
use std::fmt::Debug;
use shipyard::Component;
use std::{
any::Any,
fmt::{Debug, Display},
};
/// The node is stored client side and stores only basic data about the node.
#[derive(Debug, Clone)]
pub struct Node<S: State<V>, V: FromAnyValue + 'static = ()> {
/// The transformed state of the node.
pub state: S,
/// The raw data for the node
pub node_data: NodeData<V>,
/// A element node in the RealDom
#[derive(Debug, Clone, Default)]
pub struct ElementNode<V: FromAnyValue = ()> {
/// The [tag](https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName) of the element
pub tag: String,
/// The [namespace](https://developer.mozilla.org/en-US/docs/Web/API/Element/namespaceURI) of the element
pub namespace: Option<String>,
/// The attributes of the element
pub attributes: FxHashMap<OwnedAttributeDiscription, OwnedAttributeValue<V>>,
/// The events the element is listening for
pub listeners: FxHashSet<String>,
}
#[derive(Debug, Clone)]
pub struct NodeData<V: FromAnyValue = ()> {
/// The id of the node
pub node_id: NodeId,
/// The id of the node in the vdom.
pub element_id: Option<ElementId>,
/// Additional inforation specific to the node type
pub node_type: NodeType<V>,
impl ElementNode {
/// Create a new element node
pub fn new(tag: impl Into<String>, namespace: impl Into<Option<String>>) -> Self {
Self {
tag: tag.into(),
namespace: namespace.into(),
attributes: Default::default(),
listeners: Default::default(),
}
}
}
/// A type of node with data specific to the node type. The types are a subset of the [VNode] types.
#[derive(Debug, Clone)]
/// A text node in the RealDom
#[derive(Debug, Clone, Default)]
pub struct TextNode {
/// The text of the node
pub text: String,
/// The events the node is listening for
pub listeners: FxHashSet<String>,
}
impl TextNode {
/// Create a new text node
pub fn new(text: String) -> Self {
Self {
text,
listeners: Default::default(),
}
}
}
/// A type of node with data specific to the node type.
#[derive(Debug, Clone, Component)]
pub enum NodeType<V: FromAnyValue = ()> {
Text {
text: String,
},
Element {
tag: String,
namespace: Option<String>,
attributes: FxHashMap<OwnedAttributeDiscription, OwnedAttributeValue<V>>,
listeners: FxHashSet<String>,
},
/// A text node
Text(TextNode),
/// An element node
Element(ElementNode<V>),
/// A placeholder node. This can be used as a cheaper placeholder for a node that will be created later
Placeholder,
}
impl<S: State<V>, V: FromAnyValue> Node<S, V> {
pub(crate) fn new(node_type: NodeType<V>) -> Self {
Node {
state: S::default(),
node_data: NodeData {
element_id: None,
node_type,
node_id: NodeId(0),
},
}
}
/// get the mounted id of the node
pub fn mounted_id(&self) -> Option<ElementId> {
self.node_data.element_id
impl<V: FromAnyValue, S: Into<String>> From<S> for NodeType<V> {
fn from(text: S) -> Self {
Self::Text(TextNode::new(text.into()))
}
}
impl<V: FromAnyValue> From<TextNode> for NodeType<V> {
fn from(text: TextNode) -> Self {
Self::Text(text)
}
}
impl<V: FromAnyValue> From<ElementNode<V>> for NodeType<V> {
fn from(element: ElementNode<V>) -> Self {
Self::Element(element)
}
}
/// A discription of an attribute on a DOM node, such as `id` or `href`.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct OwnedAttributeDiscription {
/// The name of the attribute.
pub name: String,
/// The namespace of the attribute used to identify what kind of attribute it is.
///
/// For renderers that use HTML, this can be used to identify if the attribute is a style attribute.
/// Instead of parsing the style attribute every time a style is changed, you can set an attribute with the `style` namespace.
pub namespace: Option<String>,
pub volatile: bool,
}
impl From<String> for OwnedAttributeDiscription {
fn from(name: String) -> Self {
Self {
name,
namespace: None,
}
}
}
impl<S: Into<String>, N: Into<String>> From<(S, N)> for OwnedAttributeDiscription {
fn from(name: (S, N)) -> Self {
Self {
name: name.0.into(),
namespace: Some(name.1.into()),
}
}
}
/// An attribute on a DOM node, such as `id="my-thing"` or
@ -73,21 +121,59 @@ pub struct OwnedAttributeView<'a, V: FromAnyValue = ()> {
pub value: &'a OwnedAttributeValue<V>,
}
/// The value of an attribute on a DOM node. This contains non-text values to allow users to skip parsing attribute values in some cases.
#[derive(Clone)]
pub enum OwnedAttributeValue<V: FromAnyValue = ()> {
/// A string value. This is the most common type of attribute.
Text(String),
/// A floating point value.
Float(f64),
/// An integer value.
Int(i64),
/// A boolean value.
Bool(bool),
/// A custom value specific to the renderer
Custom(V),
}
pub trait FromAnyValue: Clone {
fn from_any_value(value: &dyn AnyValue) -> Self;
impl<V: FromAnyValue> From<String> for OwnedAttributeValue<V> {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl<V: FromAnyValue> From<f64> for OwnedAttributeValue<V> {
fn from(value: f64) -> Self {
Self::Float(value)
}
}
impl<V: FromAnyValue> From<i64> for OwnedAttributeValue<V> {
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl<V: FromAnyValue> From<bool> for OwnedAttributeValue<V> {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl<V: FromAnyValue> From<V> for OwnedAttributeValue<V> {
fn from(value: V) -> Self {
Self::Custom(value)
}
}
/// Something that can be converted from a borrowed [Any] value.
pub trait FromAnyValue: Clone + 'static {
/// Convert from an [Any] value.
fn from_any_value(value: &dyn Any) -> Self;
}
impl FromAnyValue for () {
fn from_any_value(_: &dyn AnyValue) -> Self {}
fn from_any_value(_: &dyn Any) -> Self {}
}
impl<V: FromAnyValue> Debug for OwnedAttributeValue<V> {
@ -102,20 +188,34 @@ impl<V: FromAnyValue> Debug for OwnedAttributeValue<V> {
}
}
impl<V: FromAnyValue> From<BorrowedAttributeValue<'_>> for OwnedAttributeValue<V> {
fn from(value: BorrowedAttributeValue<'_>) -> Self {
impl<V: FromAnyValue> Display for OwnedAttributeValue<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text(arg0) => f.write_str(arg0),
Self::Float(arg0) => f.write_str(&arg0.to_string()),
Self::Int(arg0) => f.write_str(&arg0.to_string()),
Self::Bool(arg0) => f.write_str(&arg0.to_string()),
Self::Custom(_) => f.write_str("custom"),
}
}
}
#[cfg(feature = "dioxus")]
impl<V: FromAnyValue> From<dioxus_core::BorrowedAttributeValue<'_>> for OwnedAttributeValue<V> {
fn from(value: dioxus_core::BorrowedAttributeValue<'_>) -> Self {
match value {
BorrowedAttributeValue::Text(text) => Self::Text(text.to_string()),
BorrowedAttributeValue::Float(float) => Self::Float(float),
BorrowedAttributeValue::Int(int) => Self::Int(int),
BorrowedAttributeValue::Bool(bool) => Self::Bool(bool),
BorrowedAttributeValue::Any(any) => Self::Custom(V::from_any_value(&*any)),
BorrowedAttributeValue::None => panic!("None attribute values result in removing the attribute, not converting it to a None value.")
dioxus_core::BorrowedAttributeValue::Text(text) => Self::Text(text.to_string()),
dioxus_core::BorrowedAttributeValue::Float(float) => Self::Float(float),
dioxus_core::BorrowedAttributeValue::Int(int) => Self::Int(int),
dioxus_core::BorrowedAttributeValue::Bool(bool) => Self::Bool(bool),
dioxus_core::BorrowedAttributeValue::Any(any) => Self::Custom(V::from_any_value(any.as_any())),
dioxus_core::BorrowedAttributeValue::None => panic!("None attribute values result in removing the attribute, not converting it to a None value.")
}
}
}
impl<V: FromAnyValue> OwnedAttributeValue<V> {
/// Attempt to convert the attribute value to a string.
pub fn as_text(&self) -> Option<&str> {
match self {
OwnedAttributeValue::Text(text) => Some(text),
@ -123,20 +223,25 @@ impl<V: FromAnyValue> OwnedAttributeValue<V> {
}
}
/// Attempt to convert the attribute value to a float.
pub fn as_float(&self) -> Option<f64> {
match self {
OwnedAttributeValue::Float(float) => Some(*float),
OwnedAttributeValue::Int(int) => Some(*int as f64),
_ => None,
}
}
/// Attempt to convert the attribute value to an integer.
pub fn as_int(&self) -> Option<i64> {
match self {
OwnedAttributeValue::Float(float) => Some(*float as i64),
OwnedAttributeValue::Int(int) => Some(*int),
_ => None,
}
}
/// Attempt to convert the attribute value to a boolean.
pub fn as_bool(&self) -> Option<bool> {
match self {
OwnedAttributeValue::Bool(bool) => Some(*bool),
@ -144,6 +249,7 @@ impl<V: FromAnyValue> OwnedAttributeValue<V> {
}
}
/// Attempt to convert the attribute value to a custom value.
pub fn as_custom(&self) -> Option<&V> {
match self {
OwnedAttributeValue::Custom(custom) => Some(custom),

View file

@ -1,43 +1,41 @@
use dioxus_core::ElementId;
//! Utilities that provide limited access to nodes
use rustc_hash::FxHashSet;
use crate::{
node::{FromAnyValue, NodeData, NodeType, OwnedAttributeView},
state::union_ordered_iter,
RealNodeId,
node::{ElementNode, FromAnyValue, NodeType, OwnedAttributeView},
NodeId,
};
/// A view into a [VNode] with limited access.
/// A view into a [NodeType] with a mask that determines what is visible.
#[derive(Debug)]
pub struct NodeView<'a, V: FromAnyValue = ()> {
inner: &'a NodeData<V>,
mask: NodeMask,
id: NodeId,
inner: &'a NodeType<V>,
mask: &'a NodeMask,
}
impl<'a, V: FromAnyValue> NodeView<'a, V> {
/// Create a new NodeView from a VNode, and mask.
pub fn new(node: &'a NodeData<V>, view: NodeMask) -> Self {
pub fn new(id: NodeId, node: &'a NodeType<V>, view: &'a NodeMask) -> Self {
Self {
inner: node,
mask: view,
id,
}
}
/// Get the id of the node
pub fn id(&self) -> Option<ElementId> {
self.inner.element_id
}
/// Get the node id of the node
pub fn node_id(&self) -> RealNodeId {
self.inner.node_id
pub fn node_id(&self) -> NodeId {
self.id
}
/// Get the tag of the node if the tag is enabled in the mask
pub fn tag(&self) -> Option<&'a str> {
self.mask
.tag
.then_some(match &self.inner.node_type {
NodeType::Element { tag, .. } => Some(&**tag),
.then_some(match &self.inner {
NodeType::Element(ElementNode { tag, .. }) => Some(&**tag),
_ => None,
})
.flatten()
@ -47,8 +45,8 @@ impl<'a, V: FromAnyValue> NodeView<'a, V> {
pub fn namespace(&self) -> Option<&'a str> {
self.mask
.namespace
.then_some(match &self.inner.node_type {
NodeType::Element { namespace, .. } => namespace.as_deref(),
.then_some(match &self.inner {
NodeType::Element(ElementNode { namespace, .. }) => namespace.as_deref(),
_ => None,
})
.flatten()
@ -58,8 +56,8 @@ impl<'a, V: FromAnyValue> NodeView<'a, V> {
pub fn attributes<'b>(
&'b self,
) -> Option<impl Iterator<Item = OwnedAttributeView<'a, V>> + 'b> {
match &self.inner.node_type {
NodeType::Element { attributes, .. } => Some(
match &self.inner {
NodeType::Element(ElementNode { attributes, .. }) => Some(
attributes
.iter()
.filter(move |(attr, _)| self.mask.attritutes.contains_attribute(&attr.name))
@ -76,18 +74,21 @@ impl<'a, V: FromAnyValue> NodeView<'a, V> {
pub fn text(&self) -> Option<&str> {
self.mask
.text
.then_some(match &self.inner.node_type {
NodeType::Text { text } => Some(&**text),
.then_some(match &self.inner {
NodeType::Text(text) => Some(&text.text),
_ => None,
})
.flatten()
.map(|x| &**x)
}
/// Get the listeners if it is enabled in the mask
pub fn listeners(&self) -> Option<impl Iterator<Item = &'a str> + '_> {
if self.mask.listeners {
match &self.inner.node_type {
NodeType::Element { listeners, .. } => Some(listeners.iter().map(|l| &**l)),
match &self.inner {
NodeType::Element(ElementNode { listeners, .. }) => {
Some(listeners.iter().map(|l| &**l))
}
_ => None,
}
} else {
@ -99,125 +100,54 @@ impl<'a, V: FromAnyValue> NodeView<'a, V> {
/// A mask that contains a list of attributes that are visible.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum AttributeMask {
/// All attributes are visible
All,
/// A list of attribute names that are visible, this list must be sorted
Dynamic(Vec<String>),
/// A list of attribute names that are visible, this list must be sorted
Static(&'static [&'static str]),
/// Only the given attributes are visible
Some(FxHashSet<Box<str>>),
}
impl AttributeMask {
/// A empty attribute mask
pub const NONE: Self = Self::Static(&[]);
/// Check if the given attribute is visible
fn contains_attribute(&self, attr: &str) -> bool {
match self {
AttributeMask::All => true,
AttributeMask::Dynamic(l) => l.binary_search_by_key(&attr, |s| s.as_str()).is_ok(),
AttributeMask::Static(l) => l.binary_search(&attr).is_ok(),
AttributeMask::Some(attrs) => attrs.contains(attr),
}
}
/// Create a new dynamic attribute mask with a single attribute
pub fn single(new: &str) -> Self {
Self::Dynamic(vec![new.to_string()])
}
/// Ensure the attribute list is sorted.
pub fn verify(&self) {
match &self {
AttributeMask::Static(attrs) => debug_assert!(
attrs.windows(2).all(|w| w[0] < w[1]),
"attritutes must be increasing"
),
AttributeMask::Dynamic(attrs) => debug_assert!(
attrs.windows(2).all(|w| w[0] < w[1]),
"attritutes must be increasing"
),
_ => (),
}
let mut set = FxHashSet::default();
set.insert(new.into());
Self::Some(set)
}
/// Combine two attribute masks
pub fn union(&self, other: &Self) -> Self {
let new = match (self, other) {
(AttributeMask::Dynamic(s), AttributeMask::Dynamic(o)) => {
AttributeMask::Dynamic(union_ordered_iter(
s.iter().map(|s| s.as_str()),
o.iter().map(|s| s.as_str()),
s.len() + o.len(),
))
match (self, other) {
(AttributeMask::Some(s), AttributeMask::Some(o)) => {
AttributeMask::Some(s.union(o).cloned().collect())
}
(AttributeMask::Static(s), AttributeMask::Dynamic(o)) => {
AttributeMask::Dynamic(union_ordered_iter(
s.iter().copied(),
o.iter().map(|s| s.as_str()),
s.len() + o.len(),
))
}
(AttributeMask::Dynamic(s), AttributeMask::Static(o)) => {
AttributeMask::Dynamic(union_ordered_iter(
s.iter().map(|s| s.as_str()),
o.iter().copied(),
s.len() + o.len(),
))
}
(AttributeMask::Static(s), AttributeMask::Static(o)) => AttributeMask::Dynamic(
union_ordered_iter(s.iter().copied(), o.iter().copied(), s.len() + o.len()),
),
_ => AttributeMask::All,
};
new.verify();
new
}
}
/// Check if two attribute masks overlap
fn overlaps(&self, other: &Self) -> bool {
fn overlaps_iter<'a>(
self_iter: impl Iterator<Item = &'a str>,
mut other_iter: impl Iterator<Item = &'a str>,
) -> bool {
if let Some(mut other_attr) = other_iter.next() {
for self_attr in self_iter {
while other_attr < self_attr {
if let Some(attr) = other_iter.next() {
other_attr = attr;
} else {
return false;
}
}
if other_attr == self_attr {
return true;
}
}
}
false
}
match (self, other) {
(AttributeMask::All, AttributeMask::All) => true,
(AttributeMask::All, AttributeMask::Dynamic(v)) => !v.is_empty(),
(AttributeMask::All, AttributeMask::Static(s)) => !s.is_empty(),
(AttributeMask::Dynamic(v), AttributeMask::All) => !v.is_empty(),
(AttributeMask::Static(s), AttributeMask::All) => !s.is_empty(),
(AttributeMask::Dynamic(v1), AttributeMask::Dynamic(v2)) => {
overlaps_iter(v1.iter().map(|s| s.as_str()), v2.iter().map(|s| s.as_str()))
}
(AttributeMask::Dynamic(v), AttributeMask::Static(s)) => {
overlaps_iter(v.iter().map(|s| s.as_str()), s.iter().copied())
}
(AttributeMask::Static(s), AttributeMask::Dynamic(v)) => {
overlaps_iter(v.iter().map(|s| s.as_str()), s.iter().copied())
}
(AttributeMask::Static(s1), AttributeMask::Static(s2)) => {
overlaps_iter(s1.iter().copied(), s2.iter().copied())
(AttributeMask::All, AttributeMask::Some(attrs)) => !attrs.is_empty(),
(AttributeMask::Some(attrs), AttributeMask::All) => !attrs.is_empty(),
(AttributeMask::Some(attrs1), AttributeMask::Some(attrs2)) => {
!attrs1.is_disjoint(attrs2)
}
_ => true,
}
}
}
impl Default for AttributeMask {
fn default() -> Self {
AttributeMask::Static(&[])
AttributeMask::Some(FxHashSet::default())
}
}
@ -232,14 +162,6 @@ pub struct NodeMask {
}
impl NodeMask {
/// A node mask with no parts visible.
pub const NONE: Self = Self::new();
/// A node mask with every part visible.
pub const ALL: Self = Self::new_with_attrs(AttributeMask::All)
.with_text()
.with_element()
.with_listeners();
/// Check if two masks overlap
pub fn overlaps(&self, other: &Self) -> bool {
(self.tag && other.tag)
@ -260,10 +182,71 @@ impl NodeMask {
}
}
/// Create a new node mask with the given attributes
pub const fn new_with_attrs(attritutes: AttributeMask) -> Self {
/// Allow the mask to view the given attributes
pub fn add_attributes(&mut self, attributes: AttributeMask) {
self.attritutes = self.attritutes.union(&attributes);
}
/// Set the mask to view the tag
pub fn set_tag(&mut self) {
self.tag = true;
}
/// Set the mask to view the namespace
pub fn set_namespace(&mut self) {
self.namespace = true;
}
/// Set the mask to view the text
pub fn set_text(&mut self) {
self.text = true;
}
/// Set the mask to view the listeners
pub fn set_listeners(&mut self) {
self.listeners = true;
}
}
/// A builder for a mask that controls what attributes are visible.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum AttributeMaskBuilder<'a> {
/// All attributes are visible
All,
/// Only the given attributes are visible
Some(&'a [&'a str]),
}
impl Default for AttributeMaskBuilder<'_> {
fn default() -> Self {
AttributeMaskBuilder::Some(&[])
}
}
/// A mask that limits what parts of a node a dependency can see.
#[derive(Default, PartialEq, Eq, Clone, Debug)]
pub struct NodeMaskBuilder<'a> {
attritutes: AttributeMaskBuilder<'a>,
tag: bool,
namespace: bool,
text: bool,
listeners: bool,
}
impl<'a> NodeMaskBuilder<'a> {
/// A node mask with no parts visible.
pub const NONE: Self = Self::new();
/// A node mask with every part visible.
pub const ALL: Self = Self::new()
.with_attrs(AttributeMaskBuilder::All)
.with_text()
.with_element()
.with_listeners();
/// Create a empty node mask
pub const fn new() -> Self {
Self {
attritutes,
attritutes: AttributeMaskBuilder::Some(&[]),
tag: false,
namespace: false,
text: false,
@ -271,9 +254,10 @@ impl NodeMask {
}
}
/// Create a empty node mask
pub const fn new() -> Self {
Self::new_with_attrs(AttributeMask::NONE)
/// Allow the mask to view the given attributes
pub const fn with_attrs(mut self, attritutes: AttributeMaskBuilder<'a>) -> Self {
self.attritutes = attritutes;
self
}
/// Allow the mask to view the tag
@ -304,4 +288,20 @@ impl NodeMask {
self.listeners = true;
self
}
/// Build the mask
pub fn build(self) -> NodeMask {
NodeMask {
attritutes: match self.attritutes {
AttributeMaskBuilder::All => AttributeMask::All,
AttributeMaskBuilder::Some(attrs) => {
AttributeMask::Some(attrs.iter().map(|s| (*s).into()).collect())
}
},
tag: self.tag,
namespace: self.namespace,
text: self.text,
listeners: self.listeners,
}
}
}

View file

@ -0,0 +1,17 @@
//! Helpers for watching for changes in the DOM tree.
use crate::{node::FromAnyValue, prelude::*};
/// A trait for watching for changes in the DOM tree.
pub trait NodeWatcher<V: FromAnyValue + Send + Sync> {
/// Called after a node is added to the tree.
fn on_node_added(&self, _node: NodeMut<V>) {}
/// Called before a node is removed from the tree.
fn on_node_removed(&self, _node: NodeMut<V>) {}
/// Called after a node is moved to a new parent.
fn on_node_moved(&self, _node: NodeMut<V>) {}
// /// Called after the text content of a node is changed.
// fn on_text_changed(&self, _node: NodeMut<V>) {}
// /// Called after an attribute of an element is changed.
// fn on_attribute_changed(&self, _node: NodeMut<V>, attribute: &str) {}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,305 +0,0 @@
use std::cmp::Ordering;
use crate::node::{FromAnyValue, Node};
use crate::node_ref::{NodeMask, NodeView};
use crate::passes::{resolve_passes, resolve_passes_single_threaded, AnyPass, DirtyNodeStates};
use crate::tree::TreeView;
use crate::{FxDashSet, RealNodeId, SendAnyMap};
/// Join two sorted iterators
pub(crate) fn union_ordered_iter<'a>(
s_iter: impl Iterator<Item = &'a str>,
o_iter: impl Iterator<Item = &'a str>,
new_len_guess: usize,
) -> Vec<String> {
let mut s_peekable = s_iter.peekable();
let mut o_peekable = o_iter.peekable();
let mut v = Vec::with_capacity(new_len_guess);
while let Some(s_i) = s_peekable.peek() {
while let Some(o_i) = o_peekable.peek() {
match o_i.cmp(s_i) {
Ordering::Greater => {
break;
}
Ordering::Less => {
v.push(o_peekable.next().unwrap().to_string());
}
Ordering::Equal => {
o_peekable.next();
break;
}
}
}
v.push(s_peekable.next().unwrap().to_string());
}
for o_i in o_peekable {
v.push(o_i.to_string());
}
for w in v.windows(2) {
debug_assert!(w[1] > w[0]);
}
v
}
/// This state is derived from children. For example a node's size could be derived from the size of children.
/// Called when the current node's node properties are modified, a child's [ChildDepState] is modified or a child is removed.
/// Called at most once per update.
/// ```rust
/// # use dioxus_native_core::node_ref::NodeView;
/// # use dioxus_native_core::state::ChildDepState;
/// #[derive(Clone, Copy, PartialEq, Default)]
/// struct Layout {
/// width: u32,
/// height: u32,
/// }
///
/// impl ChildDepState for Layout {
/// type Ctx = ();
/// // The layout depends on the layout of the children.
/// type DepState = (Layout,);
/// fn reduce<'a>(
/// &mut self,
/// _node: NodeView,
/// children: impl Iterator<Item = (&'a Self,)>,
/// _ctx: &Self::Ctx,
/// ) -> bool
/// where
/// Self::DepState: 'a{
/// /// Children are layed out form left to right. The width of the parent is the sum of the widths and the max of the heights.
/// let new = children.map(|(&c,)|c).reduce(|c1, c2| Layout{
/// width: c1.width + c2.width,
/// height: c1.height.max(c2.height)
/// }).unwrap_or_default();
/// let changed = new != *self;
/// *self = new;
/// changed
/// }
/// }
/// ```
pub trait ChildDepState<V: FromAnyValue = ()> {
/// The context is passed to the [ChildDepState::reduce] when it is resolved.
type Ctx;
/// A state from each child node that this node depends on. Typically this is Self, but it could be any state that is within the state tree.
/// Depstate must be a tuple containing any number of borrowed elements that are either [ChildDepState] or [NodeDepState].
type DepState: ElementBorrowable;
/// The part of a node that this state cares about. This is used to determine if the state should be updated when a node is updated.
const NODE_MASK: NodeMask = NodeMask::NONE;
/// Resolve the state current node's state from the state of the children, the state of the node, and some external context.
fn reduce<'a>(
&mut self,
node: NodeView<'a, V>,
children: impl Iterator<Item = <Self::DepState as ElementBorrowable>::ElementBorrowed<'a>>,
ctx: &Self::Ctx,
) -> bool
where
Self::DepState: 'a;
}
/// This state that is passed down to children. For example text properties (`<b>` `<i>` `<u>`) would be passed to children.
/// Called when the current node's node properties are modified or a parrent's [ParentDepState] is modified.
/// Called at most once per update.
/// ```rust
/// use dioxus_native_core::node_ref::{NodeMask, AttributeMask, NodeView};
/// use dioxus_native_core::state::*;
///
/// #[derive(Clone, Copy, PartialEq)]
/// struct FontSize(usize);
///
/// impl ParentDepState for FontSize {
/// type Ctx = ();
/// // The font size depends on the font size of the parent element.
/// type DepState = (Self,);
/// const NODE_MASK: NodeMask =
/// NodeMask::new_with_attrs(AttributeMask::Static(&[
/// "font-size"
/// ]));
/// fn reduce<'a>(
/// &mut self,
/// node: NodeView,
/// parent: Option<(&'a Self,)>,
/// ctx: &Self::Ctx,
/// ) -> bool{
/// let old = *self;
/// // If the font size was set on the parent, it is passed down to the current element
/// if let Some((parent,)) = parent {
/// *self = *parent;
/// }
/// // If the current node overrides the font size, use that size insead.
/// for attr in node.attributes().unwrap() {
/// match attr.attribute.name.as_str() {
/// "font-size" => {
/// self.0 = attr.value.as_text().unwrap().parse().unwrap();
/// }
/// // font-size is the only attribute we specified in the mask, so it is the only one we can see
/// _ => unreachable!(),
/// }
/// }
/// old != *self
/// }
/// }
/// ```
pub trait ParentDepState<V: FromAnyValue = ()> {
/// The context is passed to the [ParentDepState::reduce] when it is resolved.
type Ctx;
/// A state from from the parent node that this node depends on. Typically this is Self, but it could be any state that is within the state tree.
/// Depstate must be a tuple containing any number of borrowed elements that are either [ParentDepState] or [NodeDepState].
type DepState: ElementBorrowable;
/// The part of a node that this state cares about. This is used to determine if the state should be updated when a node is updated.
const NODE_MASK: NodeMask = NodeMask::NONE;
/// Resolve the state current node's state from the state of the parent node, the state of the node, and some external context.
fn reduce<'a>(
&mut self,
node: NodeView<'a, V>,
parent: Option<<Self::DepState as ElementBorrowable>::ElementBorrowed<'a>>,
ctx: &Self::Ctx,
) -> bool;
}
/// This state that is upadated lazily. For example any propertys that do not effect other parts of the dom like bg-color.
/// Called when the current node's node properties are modified or a one of its dependanices are modified.
/// Called at most once per update.
/// NodeDepState is the only state that can accept multiple dependancies, but only from the current node.
/// ```rust
/// use dioxus_native_core::node_ref::{NodeMask, AttributeMask, NodeView};
/// use dioxus_native_core::state::*;
///
/// #[derive(Clone, Copy, PartialEq)]
/// struct TabIndex(usize);
///
/// impl NodeDepState for TabIndex {
/// type Ctx = ();
/// type DepState = ();
/// const NODE_MASK: NodeMask =
/// NodeMask::new_with_attrs(AttributeMask::Static(&[
/// "tabindex"
/// ]));
/// fn reduce(
/// &mut self,
/// node: NodeView,
/// siblings: (),
/// ctx: &(),
/// ) -> bool {
/// let old = self.clone();
/// for attr in node.attributes().unwrap() {
/// match attr.attribute.name.as_str() {
/// "tabindex" => {
/// self.0 = attr.value.as_text().unwrap().parse().unwrap();
/// }
/// // tabindex is the only attribute we specified in the mask, so it is the only one we can see
/// _ => unreachable!(),
/// }
/// }
/// old != *self
/// }
/// }
/// ```
pub trait NodeDepState<V: FromAnyValue = ()> {
/// Depstate must be a tuple containing any number of borrowed elements that are either [ChildDepState], [ParentDepState] or [NodeDepState].
type DepState: ElementBorrowable;
/// The state passed to [NodeDepState::reduce] when it is resolved.
type Ctx;
/// The part of a node that this state cares about. This is used to determine if the state should be updated when a node is updated.
const NODE_MASK: NodeMask = NodeMask::NONE;
/// Resolve the state current node's state from the state of the sibling states, the state of the node, and some external context.
fn reduce<'a>(
&mut self,
node: NodeView<'a, V>,
node_state: <Self::DepState as ElementBorrowable>::ElementBorrowed<'a>,
ctx: &Self::Ctx,
) -> bool;
}
/// Do not implement this trait. It is only meant to be derived and used through [crate::real_dom::RealDom].
pub trait State<V: FromAnyValue + 'static>: Default + Clone + 'static {
#[doc(hidden)]
const PASSES: &'static [AnyPass<Node<Self, V>>];
#[doc(hidden)]
const MASKS: &'static [NodeMask];
#[doc(hidden)]
fn update<T: TreeView<Node<Self, V>> + Sync + Send>(
dirty: DirtyNodeStates,
tree: &mut T,
ctx: SendAnyMap,
) -> FxDashSet<RealNodeId> {
let passes = Self::PASSES.iter().collect();
resolve_passes(tree, dirty, passes, ctx)
}
#[doc(hidden)]
fn update_single_threaded<T: TreeView<Node<Self, V>>>(
dirty: DirtyNodeStates,
tree: &mut T,
ctx: SendAnyMap,
) -> FxDashSet<RealNodeId> {
let passes = Self::PASSES.iter().collect();
resolve_passes_single_threaded(tree, dirty, passes, ctx)
}
}
impl ChildDepState for () {
type Ctx = ();
type DepState = ();
fn reduce<'a>(&mut self, _: NodeView<'a>, _: impl Iterator<Item = ()>, _: &Self::Ctx) -> bool
where
Self::DepState: 'a,
{
false
}
}
impl ParentDepState for () {
type Ctx = ();
type DepState = ();
fn reduce(&mut self, _: NodeView, _: Option<()>, _: &Self::Ctx) -> bool {
false
}
}
impl NodeDepState for () {
type DepState = ();
type Ctx = ();
fn reduce(&mut self, _: NodeView, _sibling: (), _: &Self::Ctx) -> bool {
false
}
}
pub trait ElementBorrowable {
type ElementBorrowed<'a>
where
Self: 'a;
fn borrow_elements(&self) -> Self::ElementBorrowed<'_>;
}
macro_rules! impl_element_borrowable {
($($t:ident),*) => {
impl< $($t),* > ElementBorrowable for ($($t,)*) {
type ElementBorrowed<'a> = ($(&'a $t,)*) where Self: 'a;
#[allow(clippy::unused_unit, non_snake_case)]
fn borrow_elements<'a>(&'a self) -> Self::ElementBorrowed<'a> {
let ($($t,)*) = self;
($(&$t,)*)
}
}
};
}
impl_element_borrowable!();
impl_element_borrowable!(A);
impl_element_borrowable!(A, B);
impl_element_borrowable!(A, B, C);
impl_element_borrowable!(A, B, C, D);
impl_element_borrowable!(A, B, C, D, E);
impl_element_borrowable!(A, B, C, D, E, F);
impl_element_borrowable!(A, B, C, D, E, F, G);
impl_element_borrowable!(A, B, C, D, E, F, G, H);
impl_element_borrowable!(A, B, C, D, E, F, G, H, I);
impl_element_borrowable!(A, B, C, D, E, F, G, H, I, J);
impl_element_borrowable!(A, B, C, D, E, F, G, H, I, J, K);
impl_element_borrowable!(A, B, C, D, E, F, G, H, I, J, K, L);
impl_element_borrowable!(A, B, C, D, E, F, G, H, I, J, K, L, M);
impl_element_borrowable!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
impl_element_borrowable!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
impl_element_borrowable!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);

View file

@ -1,669 +1,323 @@
use std::collections::VecDeque;
use std::marker::PhantomData;
//! A tree of nodes intigated with shipyard
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug, PartialOrd, Ord)]
pub struct NodeId(pub usize);
use crate::NodeId;
use shipyard::{Component, EntitiesViewMut, Get, View, ViewMut};
use std::fmt::Debug;
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Node<T> {
value: T,
/// A node in a tree.
#[derive(PartialEq, Eq, Clone, Debug, Component)]
pub struct Node {
parent: Option<NodeId>,
children: Vec<NodeId>,
height: u16,
}
#[derive(Debug)]
pub struct Tree<T> {
nodes: Slab<Node<T>>,
root: NodeId,
}
impl<T> Tree<T> {
fn try_remove(&mut self, id: NodeId) -> Option<Node<T>> {
self.nodes.try_remove(id.0).map(|node| {
if let Some(parent) = node.parent {
self.nodes
.get_mut(parent.0)
.unwrap()
.children
.retain(|child| child != &id);
}
for child in &node.children {
self.remove_recursive(*child);
}
node
})
}
fn remove_recursive(&mut self, node: NodeId) {
let node = self.nodes.remove(node.0);
for child in node.children {
self.remove_recursive(child);
}
}
fn set_height(&mut self, node: NodeId, height: u16) {
let self_mut = self as *mut Self;
let node = self.nodes.get_mut(node.0).unwrap();
node.height = height;
unsafe {
// Safety: No node has itself as a child
for child in &node.children {
(*self_mut).set_height(*child, height + 1);
}
}
}
}
pub trait TreeView<T>: Sized {
type Iterator<'a>: Iterator<Item = &'a T>
where
T: 'a,
Self: 'a;
type IteratorMut<'a>: Iterator<Item = &'a mut T>
where
T: 'a,
Self: 'a;
fn root(&self) -> NodeId;
fn contains(&self, id: NodeId) -> bool {
self.get(id).is_some()
}
fn get(&self, id: NodeId) -> Option<&T>;
fn get_unchecked(&self, id: NodeId) -> &T {
unsafe { self.get(id).unwrap_unchecked() }
}
fn get_mut(&mut self, id: NodeId) -> Option<&mut T>;
fn get_mut_unchecked(&mut self, id: NodeId) -> &mut T {
unsafe { self.get_mut(id).unwrap_unchecked() }
}
fn children(&self, id: NodeId) -> Option<Self::Iterator<'_>>;
fn children_mut(&mut self, id: NodeId) -> Option<Self::IteratorMut<'_>>;
fn parent_child_mut(&mut self, id: NodeId) -> Option<(&mut T, Self::IteratorMut<'_>)>;
fn children_ids(&self, id: NodeId) -> Option<&[NodeId]>;
fn parent(&self, id: NodeId) -> Option<&T>;
fn parent_mut(&mut self, id: NodeId) -> Option<&mut T>;
fn node_parent_mut(&mut self, id: NodeId) -> Option<(&mut T, Option<&mut T>)>;
/// A view of a tree.
pub type TreeRefView<'a> = View<'a, Node>;
/// A mutable view of a tree.
pub type TreeMutView<'a> = (EntitiesViewMut<'a>, ViewMut<'a, Node>);
/// A immutable view of a tree.
pub trait TreeRef {
/// The parent id of the node.
fn parent_id(&self, id: NodeId) -> Option<NodeId>;
/// The children ids of the node.
fn children_ids(&self, id: NodeId) -> Vec<NodeId>;
/// The height of the node.
fn height(&self, id: NodeId) -> Option<u16>;
fn size(&self) -> usize;
fn traverse_depth_first(&self, mut f: impl FnMut(&T)) {
let mut stack = vec![self.root()];
while let Some(id) = stack.pop() {
if let Some(node) = self.get(id) {
f(node);
if let Some(children) = self.children_ids(id) {
stack.extend(children.iter().copied().rev());
}
}
}
}
fn traverse_depth_first_mut(&mut self, mut f: impl FnMut(&mut T)) {
let mut stack = vec![self.root()];
while let Some(id) = stack.pop() {
if let Some(node) = self.get_mut(id) {
f(node);
if let Some(children) = self.children_ids(id) {
stack.extend(children.iter().copied().rev());
}
}
}
}
fn traverse_breadth_first(&self, mut f: impl FnMut(&T)) {
let mut queue = VecDeque::new();
queue.push_back(self.root());
while let Some(id) = queue.pop_front() {
if let Some(node) = self.get(id) {
f(node);
if let Some(children) = self.children_ids(id) {
for id in children {
queue.push_back(*id);
}
}
}
}
}
fn traverse_breadth_first_mut(&mut self, mut f: impl FnMut(&mut T)) {
let mut queue = VecDeque::new();
queue.push_back(self.root());
while let Some(id) = queue.pop_front() {
if let Some(node) = self.get_mut(id) {
f(node);
if let Some(children) = self.children_ids(id) {
for id in children {
queue.push_back(*id);
}
}
}
}
}
/// Returns true if the node exists.
fn contains(&self, id: NodeId) -> bool;
}
pub trait TreeLike<T>: TreeView<T> {
fn new(root: T) -> Self;
fn create_node(&mut self, value: T) -> NodeId;
fn add_child(&mut self, parent: NodeId, child: NodeId);
fn remove(&mut self, id: NodeId) -> Option<T>;
fn remove_all_children(&mut self, id: NodeId) -> Vec<T>;
fn replace(&mut self, old: NodeId, new: NodeId);
fn insert_before(&mut self, id: NodeId, new: NodeId);
fn insert_after(&mut self, id: NodeId, new: NodeId);
/// A mutable view of a tree.
pub trait TreeMut: TreeRef {
/// Removes the node and all of its children.
fn remove(&mut self, id: NodeId);
/// Removes the node and all of its children.
fn remove_single(&mut self, id: NodeId);
/// Adds a new node to the tree.
fn create_node(&mut self, id: NodeId);
/// Adds a child to the node.
fn add_child(&mut self, parent: NodeId, new: NodeId);
/// Replaces the node with a new node.
fn replace(&mut self, old_id: NodeId, new_id: NodeId);
/// Inserts a node before another node.
fn insert_before(&mut self, old_id: NodeId, new_id: NodeId);
/// Inserts a node after another node.
fn insert_after(&mut self, old_id: NodeId, new_id: NodeId);
}
pub struct ChildNodeIterator<'a, T> {
nodes: &'a Slab<Node<T>>,
children_ids: Vec<NodeId>,
index: usize,
node_type: PhantomData<T>,
}
impl<'a, T: 'a> Iterator for ChildNodeIterator<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.children_ids.get(self.index).map(|id| {
self.index += 1;
&self.nodes.get(id.0).unwrap().value
})
}
}
pub struct ChildNodeIteratorMut<'a, T> {
nodes: Vec<&'a mut Node<T>>,
}
impl<'a, T: 'a> Iterator for ChildNodeIteratorMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
self.nodes.pop().map(|node| &mut node.value)
}
}
impl<T> TreeView<T> for Tree<T> {
type Iterator<'a> = ChildNodeIterator<'a, T> where T: 'a;
type IteratorMut<'a> = ChildNodeIteratorMut<'a, T> where T: 'a;
fn root(&self) -> NodeId {
self.root
}
fn get(&self, id: NodeId) -> Option<&T> {
self.nodes.get(id.0).map(|node| &node.value)
}
fn get_mut(&mut self, id: NodeId) -> Option<&mut T> {
self.nodes.get_mut(id.0).map(|node| &mut node.value)
}
fn children(&self, id: NodeId) -> Option<Self::Iterator<'_>> {
self.children_ids(id).map(|children_ids| ChildNodeIterator {
nodes: &self.nodes,
children_ids: children_ids.to_vec(),
index: 0,
node_type: PhantomData,
})
}
fn children_mut(&mut self, id: NodeId) -> Option<Self::IteratorMut<'_>> {
// Safety: No node has itself as a parent.
if let Some(children_ids) = self.children_ids(id) {
let children_ids = children_ids.to_vec();
Some(ChildNodeIteratorMut {
nodes: unsafe {
self.nodes
.get_many_mut_unchecked(children_ids.into_iter().rev().map(|id| id.0))
.unwrap()
},
})
} else {
None
}
}
fn children_ids(&self, id: NodeId) -> Option<&[NodeId]> {
self.nodes.get(id.0).map(|node| node.children.as_slice())
}
fn parent(&self, id: NodeId) -> Option<&T> {
self.nodes
.get(id.0)
.and_then(|node| node.parent.map(|id| self.nodes.get(id.0).unwrap()))
.map(|node| &node.value)
}
fn parent_mut(&mut self, id: NodeId) -> Option<&mut T> {
let self_ptr = self as *mut Self;
unsafe {
// Safety: No node has itself as a parent.
self.nodes
.get_mut(id.0)
.and_then(move |node| {
node.parent
.map(move |id| (*self_ptr).nodes.get_mut(id.0).unwrap())
})
.map(|node| &mut node.value)
}
}
impl<'a> TreeRef for TreeRefView<'a> {
fn parent_id(&self, id: NodeId) -> Option<NodeId> {
self.nodes.get(id.0).and_then(|node| node.parent)
self.get(id).ok()?.parent
}
fn children_ids(&self, id: NodeId) -> Vec<NodeId> {
self.get(id)
.map(|node| node.children.clone())
.unwrap_or_default()
}
fn height(&self, id: NodeId) -> Option<u16> {
self.nodes.get(id.0).map(|n| n.height)
Some(self.get(id).ok()?.height)
}
fn get_unchecked(&self, id: NodeId) -> &T {
unsafe { &self.nodes.get_unchecked(id.0).value }
}
fn get_mut_unchecked(&mut self, id: NodeId) -> &mut T {
unsafe { &mut self.nodes.get_unchecked_mut(id.0).value }
}
fn size(&self) -> usize {
self.nodes.len()
}
fn node_parent_mut(&mut self, id: NodeId) -> Option<(&mut T, Option<&mut T>)> {
if let Some(parent_id) = self.parent_id(id) {
self.nodes
.get2_mut(id.0, parent_id.0)
.map(|(node, parent)| (&mut node.value, Some(&mut parent.value)))
} else {
self.nodes.get_mut(id.0).map(|node| (&mut node.value, None))
}
}
fn parent_child_mut(&mut self, id: NodeId) -> Option<(&mut T, Self::IteratorMut<'_>)> {
// Safety: No node will appear as a child twice
if let Some(children_ids) = self.children_ids(id) {
debug_assert!(!children_ids.iter().any(|child_id| *child_id == id));
let mut borrowed = unsafe {
let as_vec = children_ids.to_vec();
self.nodes
.get_many_mut_unchecked(
as_vec
.into_iter()
.rev()
.map(|id| id.0)
.chain(std::iter::once(id.0)),
)
.unwrap()
};
let node = &mut borrowed.pop().unwrap().value;
Some((node, ChildNodeIteratorMut { nodes: borrowed }))
} else {
None
}
fn contains(&self, id: NodeId) -> bool {
self.get(id).is_ok()
}
}
impl<T> TreeLike<T> for Tree<T> {
fn new(root: T) -> Self {
let mut nodes = Slab::default();
let root = NodeId(nodes.insert(Node {
value: root,
parent: None,
children: Vec::new(),
height: 0,
}));
Self { nodes, root }
impl<'a> TreeMut for TreeMutView<'a> {
fn remove(&mut self, id: NodeId) {
fn recurse(tree: &mut TreeMutView<'_>, id: NodeId) {
let children = tree.children_ids(id);
for child in children {
recurse(tree, child);
}
}
{
let mut node_data_mut = &mut self.1;
if let Some(parent) = node_data_mut.get(id).unwrap().parent {
let parent = (&mut node_data_mut).get(parent).unwrap();
parent.children.retain(|&child| child != id);
}
}
recurse(self, id);
}
fn create_node(&mut self, value: T) -> NodeId {
NodeId(self.nodes.insert(Node {
value,
parent: None,
children: Vec::new(),
height: 0,
}))
fn remove_single(&mut self, id: NodeId) {
{
let mut node_data_mut = &mut self.1;
if let Some(parent) = node_data_mut.get(id).unwrap().parent {
let parent = (&mut node_data_mut).get(parent).unwrap();
parent.children.retain(|&child| child != id);
}
}
}
fn create_node(&mut self, id: NodeId) {
let (entities, node_data_mut) = self;
entities.add_component(
id,
node_data_mut,
Node {
parent: None,
children: Vec::new(),
height: 0,
},
);
}
fn add_child(&mut self, parent: NodeId, new: NodeId) {
self.nodes.get_mut(new.0).unwrap().parent = Some(parent);
let parent = self.nodes.get_mut(parent.0).unwrap();
parent.children.push(new);
let height = parent.height + 1;
self.set_height(new, height);
}
fn remove(&mut self, id: NodeId) -> Option<T> {
self.try_remove(id).map(|node| node.value)
}
fn remove_all_children(&mut self, id: NodeId) -> Vec<T> {
let mut children = Vec::new();
let self_mut = self as *mut Self;
for child in self.children_ids(id).unwrap() {
unsafe {
// Safety: No node has itself as a child
children.push((*self_mut).remove(*child).unwrap());
}
let height;
{
let mut node_state = &mut self.1;
(&mut node_state).get(new).unwrap().parent = Some(parent);
let parent = (&mut node_state).get(parent).unwrap();
parent.children.push(new);
height = parent.height + 1;
}
children
set_height(self, new, height);
}
fn replace(&mut self, old_id: NodeId, new_id: NodeId) {
// remove the old node
let old = self
.try_remove(old_id)
.expect("tried to replace a node that doesn't exist");
// update the parent's link to the child
if let Some(parent_id) = old.parent {
let parent = self.nodes.get_mut(parent_id.0).unwrap();
for id in &mut parent.children {
if *id == old_id {
*id = new_id;
{
let mut node_state = &mut self.1;
// update the parent's link to the child
if let Some(parent_id) = node_state.get(old_id).unwrap().parent {
let parent = (&mut node_state).get(parent_id).unwrap();
for id in &mut parent.children {
if *id == old_id {
*id = new_id;
break;
}
}
let height = parent.height + 1;
set_height(self, new_id, height);
}
let height = parent.height + 1;
self.set_height(new_id, height);
}
// remove the old node
self.remove(old_id);
}
fn insert_before(&mut self, id: NodeId, new: NodeId) {
let node = self.nodes.get(id.0).unwrap();
let parent_id = node.parent.expect("tried to insert before root");
self.nodes.get_mut(new.0).unwrap().parent = Some(parent_id);
let parent = self.nodes.get_mut(parent_id.0).unwrap();
fn insert_before(&mut self, old_id: NodeId, new_id: NodeId) {
let mut node_state = &mut self.1;
let old_node = node_state.get(old_id).unwrap();
let parent_id = old_node.parent.expect("tried to insert before root");
(&mut node_state).get(new_id).unwrap().parent = Some(parent_id);
let parent = (&mut node_state).get(parent_id).unwrap();
let index = parent
.children
.iter()
.position(|child| child == &id)
.position(|child| *child == old_id)
.unwrap();
parent.children.insert(index, new);
parent.children.insert(index, new_id);
let height = parent.height + 1;
self.set_height(new, height);
set_height(self, new_id, height);
}
fn insert_after(&mut self, id: NodeId, new: NodeId) {
let node = self.nodes.get(id.0).unwrap();
let parent_id = node.parent.expect("tried to insert before root");
self.nodes.get_mut(new.0).unwrap().parent = Some(parent_id);
let parent = self.nodes.get_mut(parent_id.0).unwrap();
fn insert_after(&mut self, old_id: NodeId, new_id: NodeId) {
let mut node_state = &mut self.1;
let old_node = node_state.get(old_id).unwrap();
let parent_id = old_node.parent.expect("tried to insert before root");
(&mut node_state).get(new_id).unwrap().parent = Some(parent_id);
let parent = (&mut node_state).get(parent_id).unwrap();
let index = parent
.children
.iter()
.position(|child| child == &id)
.position(|child| *child == old_id)
.unwrap();
parent.children.insert(index + 1, new);
parent.children.insert(index + 1, new_id);
let height = parent.height + 1;
self.set_height(new, height);
set_height(self, new_id, height);
}
}
/// Sets the height of a node and updates the height of all its children
fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) {
let children = {
let mut node_data_mut = &mut tree.1;
let mut node = (&mut node_data_mut).get(node).unwrap();
node.height = height;
node.children.clone()
};
for child in children {
set_height(tree, child, height + 1);
}
}
impl<'a> TreeRef for TreeMutView<'a> {
fn parent_id(&self, id: NodeId) -> Option<NodeId> {
let node_data = &self.1;
node_data.get(id).unwrap().parent
}
fn children_ids(&self, id: NodeId) -> Vec<NodeId> {
let node_data = &self.1;
node_data
.get(id)
.map(|node| node.children.clone())
.unwrap_or_default()
}
fn height(&self, id: NodeId) -> Option<u16> {
let node_data = &self.1;
node_data.get(id).map(|node| node.height).ok()
}
fn contains(&self, id: NodeId) -> bool {
self.1.get(id).is_ok()
}
}
#[test]
fn creation() {
let mut tree = Tree::new(1);
let parent = tree.root();
let child = tree.create_node(0);
tree.add_child(parent, child);
use shipyard::World;
#[derive(Component)]
struct Num(i32);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 2);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(child), Some(1));
assert_eq!(*tree.get(parent).unwrap(), 1);
assert_eq!(*tree.get(child).unwrap(), 0);
assert_eq!(tree.parent_id(parent), None);
assert_eq!(tree.parent_id(child).unwrap(), parent);
assert_eq!(tree.children_ids(parent).unwrap(), &[child]);
let mut world = World::new();
let parent_id = world.add_entity(Num(1i32));
let child_id = world.add_entity(Num(0i32));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent_id);
tree.create_node(child_id);
tree.add_child(parent_id, child_id);
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(child_id), Some(1));
assert_eq!(tree.parent_id(parent_id), None);
assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
assert_eq!(tree.children_ids(parent_id), &[child_id]);
}
#[test]
fn insertion() {
let mut tree = Tree::new(0);
let parent = tree.root();
let child = tree.create_node(2);
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
let parent = world.add_entity(Num(0));
let child = world.add_entity(Num(2));
let before = world.add_entity(Num(1));
let after = world.add_entity(Num(3));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent);
tree.create_node(child);
tree.create_node(before);
tree.create_node(after);
tree.add_child(parent, child);
let before = tree.create_node(1);
tree.insert_before(child, before);
let after = tree.create_node(3);
tree.insert_after(child, after);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 4);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(child), Some(1));
assert_eq!(tree.height(before), Some(1));
assert_eq!(tree.height(after), Some(1));
assert_eq!(*tree.get(parent).unwrap(), 0);
assert_eq!(*tree.get(before).unwrap(), 1);
assert_eq!(*tree.get(child).unwrap(), 2);
assert_eq!(*tree.get(after).unwrap(), 3);
assert_eq!(tree.parent_id(before).unwrap(), parent);
assert_eq!(tree.parent_id(child).unwrap(), parent);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent).unwrap(), &[before, child, after]);
assert_eq!(tree.children_ids(parent), &[before, child, after]);
}
#[test]
fn deletion() {
let mut tree = Tree::new(0);
let parent = tree.root();
let child = tree.create_node(2);
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
let parent = world.add_entity(Num(0));
let child = world.add_entity(Num(2));
let before = world.add_entity(Num(1));
let after = world.add_entity(Num(3));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent);
tree.create_node(child);
tree.create_node(before);
tree.create_node(after);
tree.add_child(parent, child);
let before = tree.create_node(1);
tree.insert_before(child, before);
let after = tree.create_node(3);
tree.insert_after(child, after);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 4);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(child), Some(1));
assert_eq!(tree.height(before), Some(1));
assert_eq!(tree.height(after), Some(1));
assert_eq!(*tree.get(parent).unwrap(), 0);
assert_eq!(*tree.get(before).unwrap(), 1);
assert_eq!(*tree.get(child).unwrap(), 2);
assert_eq!(*tree.get(after).unwrap(), 3);
assert_eq!(tree.parent_id(before).unwrap(), parent);
assert_eq!(tree.parent_id(child).unwrap(), parent);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent).unwrap(), &[before, child, after]);
assert_eq!(tree.children_ids(parent), &[before, child, after]);
tree.remove(child);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 3);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(before), Some(1));
assert_eq!(tree.height(after), Some(1));
assert_eq!(*tree.get(parent).unwrap(), 0);
assert_eq!(*tree.get(before).unwrap(), 1);
assert_eq!(tree.get(child), None);
assert_eq!(*tree.get(after).unwrap(), 3);
assert_eq!(tree.parent_id(before).unwrap(), parent);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent).unwrap(), &[before, after]);
assert_eq!(tree.children_ids(parent), &[before, after]);
tree.remove(before);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 2);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(after), Some(1));
assert_eq!(*tree.get(parent).unwrap(), 0);
assert_eq!(tree.get(before), None);
assert_eq!(*tree.get(after).unwrap(), 3);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent).unwrap(), &[after]);
assert_eq!(tree.children_ids(parent), &[after]);
tree.remove(after);
println!("Tree: {tree:#?}");
assert_eq!(tree.size(), 1);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(*tree.get(parent).unwrap(), 0);
assert_eq!(tree.get(after), None);
assert_eq!(tree.children_ids(parent).unwrap(), &[]);
}
#[test]
fn traverse_depth_first() {
let mut tree = Tree::new(0);
let parent = tree.root();
let child1 = tree.create_node(1);
tree.add_child(parent, child1);
let grandchild1 = tree.create_node(2);
tree.add_child(child1, grandchild1);
let child2 = tree.create_node(3);
tree.add_child(parent, child2);
let grandchild2 = tree.create_node(4);
tree.add_child(child2, grandchild2);
let mut node_count = 0;
tree.traverse_depth_first(move |node| {
assert_eq!(*node, node_count);
node_count += 1;
});
}
#[test]
fn get_node_children_mut() {
let mut tree = Tree::new(0);
let parent = tree.root();
let child1 = tree.create_node(1);
tree.add_child(parent, child1);
let child2 = tree.create_node(2);
tree.add_child(parent, child2);
let child3 = tree.create_node(3);
tree.add_child(parent, child3);
let (parent, children) = tree.parent_child_mut(parent).unwrap();
for (i, child) in children.enumerate() {
assert_eq!(*child, i + 1);
}
println!("Parent: {parent:#?}");
}
#[test]
fn get_many_mut_unchecked() {
let mut slab = Slab::new();
let parent = slab.insert(0);
let child = slab.insert(1);
let grandchild = slab.insert(2);
let all =
unsafe { slab.get_many_mut_unchecked([parent, child, grandchild].into_iter()) }.unwrap();
println!("All: {all:#?}");
}
#[derive(Debug)]
struct Slab<T> {
data: Vec<Option<T>>,
free: VecDeque<usize>,
}
impl<T> Default for Slab<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Slab<T> {
fn new() -> Self {
Self {
data: Vec::new(),
free: VecDeque::new(),
}
}
fn get(&self, id: usize) -> Option<&T> {
self.data.get(id).and_then(|x| x.as_ref())
}
unsafe fn get_unchecked(&self, id: usize) -> &T {
self.data.get_unchecked(id).as_ref().unwrap()
}
fn get_mut(&mut self, id: usize) -> Option<&mut T> {
self.data.get_mut(id).and_then(|x| x.as_mut())
}
unsafe fn get_unchecked_mut(&mut self, id: usize) -> &mut T {
self.data.get_unchecked_mut(id).as_mut().unwrap()
}
fn get2_mut(&mut self, id1: usize, id2: usize) -> Option<(&mut T, &mut T)> {
assert!(id1 != id2);
let ptr = self.data.as_mut_ptr();
let first = unsafe { &mut *ptr.add(id1) };
let second = unsafe { &mut *ptr.add(id2) };
if let (Some(first), Some(second)) = (first, second) {
Some((first, second))
} else {
None
}
}
unsafe fn get_many_mut_unchecked(
&mut self,
ids: impl Iterator<Item = usize>,
) -> Option<Vec<&mut T>> {
let ptr = self.data.as_mut_ptr();
let mut result = Vec::new();
for id in ids {
let item = unsafe { &mut *ptr.add(id) };
if let Some(item) = item {
result.push(item);
} else {
return None;
}
}
Some(result)
}
fn insert(&mut self, value: T) -> usize {
if let Some(id) = self.free.pop_front() {
self.data[id] = Some(value);
id
} else {
self.data.push(Some(value));
self.data.len() - 1
}
}
fn try_remove(&mut self, id: usize) -> Option<T> {
self.data.get_mut(id).and_then(|x| {
self.free.push_back(id);
x.take()
})
}
fn remove(&mut self, id: usize) -> T {
self.try_remove(id).unwrap()
}
fn len(&self) -> usize {
self.data.len() - self.free.len()
}
assert_eq!(tree.children_ids(parent), &[]);
}

View file

@ -1,6 +1,8 @@
//! A cursor implementation that can be used to navigate and edit text.
use std::{cmp::Ordering, ops::Range};
use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers};
use keyboard_types::{Code, Key, Modifiers};
/// This contains the information about the text that is used by the cursor to handle navigation.
pub trait Text {
@ -221,26 +223,28 @@ impl Cursor {
/// Handle moving the cursor with the given key.
pub fn handle_input<T: Text + ?Sized>(
&mut self,
data: &dioxus_html::KeyboardData,
code: &Code,
key: &Key,
modifiers: &Modifiers,
text: &mut impl TextEditable<T>,
max_text_length: usize,
) {
use Code::*;
match data.code() {
match code {
ArrowUp => {
self.move_cursor(
|c| c.up(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT),
modifiers.contains(Modifiers::SHIFT),
);
}
ArrowDown => {
self.move_cursor(
|c| c.down(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT),
modifiers.contains(Modifiers::SHIFT),
);
}
ArrowRight => {
if data.modifiers().contains(Modifiers::CONTROL) {
if modifiers.contains(Modifiers::CONTROL) {
self.move_cursor(
|c| {
let mut change = 1;
@ -255,17 +259,17 @@ impl Cursor {
}
c.move_col(change as i32, text.as_ref());
},
data.modifiers().contains(Modifiers::SHIFT),
modifiers.contains(Modifiers::SHIFT),
);
} else {
self.move_cursor(
|c| c.right(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT),
modifiers.contains(Modifiers::SHIFT),
);
}
}
ArrowLeft => {
if data.modifiers().contains(Modifiers::CONTROL) {
if modifiers.contains(Modifiers::CONTROL) {
self.move_cursor(
|c| {
let mut change = -1;
@ -279,23 +283,23 @@ impl Cursor {
}
c.move_col(change, text.as_ref());
},
data.modifiers().contains(Modifiers::SHIFT),
modifiers.contains(Modifiers::SHIFT),
);
} else {
self.move_cursor(
|c| c.left(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT),
modifiers.contains(Modifiers::SHIFT),
);
}
}
End => {
self.move_cursor(
|c| c.col = c.len_line(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT),
modifiers.contains(Modifiers::SHIFT),
);
}
Home => {
self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT));
self.move_cursor(|c| c.col = 0, modifiers.contains(Modifiers::SHIFT));
}
Backspace => {
self.start.realize_col(text.as_ref());
@ -305,7 +309,7 @@ impl Cursor {
} else if start_idx > 0 {
self.start.left(text.as_ref());
text.delete_range(start_idx - 1..start_idx);
if data.modifiers().contains(Modifiers::CONTROL) {
if modifiers.contains(Modifiers::CONTROL) {
start_idx = self.start.idx(text.as_ref());
while start_idx > 0
&& text
@ -340,7 +344,7 @@ impl Cursor {
}
_ => {
self.start.realize_col(text.as_ref());
if let Key::Character(character) = data.key() {
if let Key::Character(character) = key {
if text.as_ref().length() + 1 - self.selection_len(text.as_ref())
<= max_text_length
{
@ -460,13 +464,9 @@ fn cursor_input() {
for _ in 0..5 {
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::ArrowRight,
dioxus_html::input_data::keyboard_types::Code::ArrowRight,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&keyboard_types::Code::ArrowRight,
&keyboard_types::Key::ArrowRight,
&Modifiers::empty(),
&mut text,
10,
);
@ -474,13 +474,9 @@ fn cursor_input() {
for _ in 0..5 {
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Backspace,
dioxus_html::input_data::keyboard_types::Code::Backspace,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&keyboard_types::Code::Backspace,
&keyboard_types::Key::Backspace,
&Modifiers::empty(),
&mut text,
10,
);
@ -491,61 +487,41 @@ fn cursor_input() {
let goal_text = "hello world\nhi";
let max_width = goal_text.len();
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("h".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyH,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&keyboard_types::Code::KeyH,
&keyboard_types::Key::Character("h".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("e".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyE,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&keyboard_types::Code::KeyE,
&keyboard_types::Key::Character("e".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyL,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&keyboard_types::Code::KeyL,
&keyboard_types::Key::Character("l".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyL,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&keyboard_types::Code::KeyL,
&keyboard_types::Key::Character("l".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyO,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&keyboard_types::Code::KeyO,
&keyboard_types::Key::Character("o".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
@ -553,13 +529,9 @@ fn cursor_input() {
// these should be ignored
for _ in 0..10 {
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyO,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&keyboard_types::Code::KeyO,
&keyboard_types::Key::Character("o".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);

View file

@ -1,3 +1,7 @@
//! # Utilities for renders using the RealDOM
//!
//! This includes an iterator that can be used to iterate over the children of a node that persists changes in the struture of the DOM, and a cursor for text editing.
mod persistant_iterator;
pub use persistant_iterator::*;
pub mod cursor;

View file

@ -1,252 +1,204 @@
use smallvec::SmallVec;
use crate::{
node::{FromAnyValue, NodeType},
real_dom::RealDom,
state::State,
tree::TreeView,
NodeId, RealNodeId,
node::FromAnyValue,
node_watcher::NodeWatcher,
prelude::{NodeMut, NodeRef},
real_dom::{NodeImmutable, RealDom},
NodeId,
};
use std::{
fmt::Debug,
sync::{Arc, Mutex},
};
use dioxus_core::{Mutation, Mutations};
use std::fmt::Debug;
#[derive(Debug)]
pub enum ElementProduced {
/// The iterator produced an element by progressing to the next node in a depth first order.
Progressed(RealNodeId),
/// The element produced by the iterator
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ElementProduced {
id: NodeId,
movement: IteratorMovement,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
/// The method by which the iterator produced an element
pub enum IteratorMovement {
/// The iterator produced an element by progressing to the next node
Progressed,
/// The iterator reached the end of the tree and looped back to the root
Looped(RealNodeId),
Looped,
}
impl ElementProduced {
pub fn id(&self) -> RealNodeId {
match self {
ElementProduced::Progressed(id) => *id,
ElementProduced::Looped(id) => *id,
/// Get the id of the element produced
pub fn id(&self) -> NodeId {
self.id
}
/// The movement the iterator made to produce the element
pub fn movement(&self) -> &IteratorMovement {
&self.movement
}
fn looped(id: NodeId) -> Self {
Self {
id,
movement: IteratorMovement::Looped,
}
}
fn progressed(id: NodeId) -> Self {
Self {
id,
movement: IteratorMovement::Progressed,
}
}
}
#[derive(Debug)]
enum NodePosition {
AtNode,
InChild(usize),
struct PersistantElementIterUpdater<V> {
stack: Arc<Mutex<smallvec::SmallVec<[NodeId; 5]>>>,
phantom: std::marker::PhantomData<V>,
}
impl NodePosition {
fn map(&self, mut f: impl FnMut(usize) -> usize) -> Self {
match self {
Self::AtNode => Self::AtNode,
Self::InChild(i) => Self::InChild(f(*i)),
}
}
fn get_or_insert(&mut self, child_idx: usize) -> usize {
match self {
Self::AtNode => {
*self = Self::InChild(child_idx);
child_idx
impl<V: FromAnyValue + Sync + Send> NodeWatcher<V> for PersistantElementIterUpdater<V> {
fn on_node_moved(&self, node: NodeMut<V>) {
// if any element is moved, update its parents in the stack
let mut stack = self.stack.lock().unwrap();
let moved = node.id();
let rdom = node.real_dom();
if let Some(r) = stack.iter().position(|el_id| *el_id == moved) {
let back = &stack[r..];
let mut new = SmallVec::new();
let mut parent = node.parent_id();
while let Some(p) = parent.and_then(|id| rdom.get(id)) {
new.push(p.id());
parent = p.parent_id();
}
Self::InChild(i) => *i,
new.extend(back.iter().copied());
*stack = new;
}
}
fn on_node_removed(&self, node: NodeMut<V>) {
// if any element is removed in the chain, remove it and its children from the stack
let mut stack = self.stack.lock().unwrap();
let removed = node.id();
if let Some(r) = stack.iter().position(|el_id| *el_id == removed) {
stack.truncate(r);
}
}
}
/// Focus systems need a iterator that can persist through changes in the [dioxus_core::VirtualDom].
/// Focus systems need a iterator that can persist through changes in the [crate::prelude::RealDom]
/// This iterator traverses the tree depth first.
/// Iterate through it with [PersistantElementIter::next] [PersistantElementIter::prev], and update it with [PersistantElementIter::prune] (with data from [`dioxus_core::prelude::VirtualDom::work_with_deadline`]).
/// You can iterate through it with [PersistantElementIter::next] and [PersistantElementIter::prev].
/// The iterator loops around when it reaches the end or the beginning.
pub struct PersistantElementIter {
// stack of elements and fragments
stack: smallvec::SmallVec<[(RealNodeId, NodePosition); 5]>,
}
impl Default for PersistantElementIter {
fn default() -> Self {
PersistantElementIter {
stack: smallvec::smallvec![(NodeId(0), NodePosition::AtNode)],
}
}
// stack of elements and fragments, the last element is the last element that was yielded
stack: Arc<Mutex<smallvec::SmallVec<[NodeId; 5]>>>,
}
impl PersistantElementIter {
pub fn new() -> Self {
Self::default()
}
/// Create a new iterator in the RealDom
pub fn create<V: FromAnyValue + Send + Sync>(rdom: &mut RealDom<V>) -> Self {
let inner = Arc::new(Mutex::new(smallvec::smallvec![rdom.root_id()]));
/// remove stale element refreneces
/// returns true if the focused element is removed
pub fn prune<S: State<V>, V: FromAnyValue>(
&mut self,
mutations: &Mutations,
rdom: &RealDom<S, V>,
) -> bool {
let mut changed = false;
let ids_removed: Vec<_> = mutations
.edits
.iter()
.filter_map(|m| {
// nodes within templates will never be removed
match m {
Mutation::Remove { id } => Some(rdom.element_to_node_id(*id)),
Mutation::ReplaceWith { id, .. } => Some(rdom.element_to_node_id(*id)),
_ => None,
}
})
.collect();
// if any element is removed in the chain, remove it and its children from the stack
if let Some(r) = self
.stack
.iter()
.position(|(el_id, _)| ids_removed.iter().any(|id| el_id == id))
{
self.stack.truncate(r);
changed = true;
}
// if a child is removed or inserted before or at the current element, update the child index
for (el_id, child_idx) in self.stack.iter_mut() {
if let NodePosition::InChild(child_idx) = child_idx {
if let Some(children) = &rdom.tree.children_ids(*el_id) {
for m in &mutations.edits {
match m {
Mutation::Remove { id } => {
let id = rdom.element_to_node_id(*id);
if children.iter().take(*child_idx + 1).any(|c| *c == id) {
*child_idx -= 1;
}
}
Mutation::InsertBefore { id, m } => {
let id = rdom.element_to_node_id(*id);
if children.iter().take(*child_idx + 1).any(|c| *c == id) {
*child_idx += *m;
}
}
Mutation::InsertAfter { id, m } => {
let id = rdom.element_to_node_id(*id);
if children.iter().take(*child_idx).any(|c| *c == id) {
*child_idx += *m;
}
}
_ => (),
}
}
}
}
}
changed
rdom.add_node_watcher(PersistantElementIterUpdater {
stack: inner.clone(),
phantom: std::marker::PhantomData,
});
PersistantElementIter { stack: inner }
}
/// get the next element
pub fn next<S: State<V>, V: FromAnyValue>(&mut self, rdom: &RealDom<S, V>) -> ElementProduced {
if self.stack.is_empty() {
let id = NodeId(0);
let new = (id, NodePosition::AtNode);
self.stack.push(new);
ElementProduced::Looped(id)
pub fn next<V: FromAnyValue + Send + Sync>(&mut self, rdom: &RealDom<V>) -> ElementProduced {
let mut stack = self.stack.lock().unwrap();
if stack.is_empty() {
let id = rdom.root_id();
let new = id;
stack.push(new);
ElementProduced::looped(id)
} else {
let (last, old_child_idx) = self.stack.last_mut().unwrap();
let node = &rdom[*last];
match &node.node_data.node_type {
NodeType::Element { .. } => {
let children = rdom.tree.children_ids(*last).unwrap();
*old_child_idx = old_child_idx.map(|i| i + 1);
// if we have children, go to the next child
let child_idx = old_child_idx.get_or_insert(0);
if child_idx >= children.len() {
self.pop();
self.next(rdom)
} else {
let id = children[child_idx];
if let NodeType::Element { .. } = &rdom[id].node_data.node_type {
self.stack.push((id, NodePosition::AtNode));
let mut look_in_children = true;
loop {
if let Some(current) = stack.last().and_then(|last| rdom.get(*last)) {
// if the current element has children, add the first child to the stack and return it
if look_in_children {
if let Some(first) = current.children().first() {
let new = first.id();
stack.push(new);
return ElementProduced::progressed(new);
}
ElementProduced::Progressed(id)
}
stack.pop();
if let Some(new) = current.next() {
// the next element exists, add it to the stack and return it
let new = new.id();
stack.push(new);
return ElementProduced::progressed(new);
}
// otherwise, continue the loop and go to the parent
} else {
// if there is no parent, loop back to the root
let new = rdom.root_id();
stack.clear();
stack.push(new);
return ElementProduced::looped(new);
}
NodeType::Text { .. } | NodeType::Placeholder { .. } => {
// we are at a leaf, so we are done
ElementProduced::Progressed(self.pop())
}
look_in_children = false;
}
}
}
/// get the previous element
pub fn prev<S: State<V>, V: FromAnyValue>(&mut self, rdom: &RealDom<S, V>) -> ElementProduced {
pub fn prev<V: FromAnyValue + Send + Sync>(&mut self, rdom: &RealDom<V>) -> ElementProduced {
// recursively add the last child element to the stack
fn push_back<S: State<V>, V: FromAnyValue>(
stack: &mut smallvec::SmallVec<[(RealNodeId, NodePosition); 5]>,
new_node: RealNodeId,
rdom: &RealDom<S, V>,
) -> RealNodeId {
match &rdom[new_node].node_data.node_type {
NodeType::Element { .. } => {
let children = rdom.tree.children_ids(new_node).unwrap();
if children.is_empty() {
new_node
} else {
stack.push((new_node, NodePosition::InChild(children.len() - 1)));
push_back(stack, *children.last().unwrap(), rdom)
}
}
_ => new_node,
fn push_back<V: FromAnyValue + Send + Sync>(
stack: &mut smallvec::SmallVec<[NodeId; 5]>,
node: NodeRef<V>,
) -> NodeId {
stack.push(node.id());
if let Some(last) = node.children().last() {
push_back(stack, *last)
} else {
node.id()
}
}
if self.stack.is_empty() {
let new_node = NodeId(0);
ElementProduced::Looped(push_back(&mut self.stack, new_node, rdom))
let mut stack = self.stack.lock().unwrap();
if stack.is_empty() {
let id = rdom.root_id();
let last = push_back(&mut stack, rdom.get(id).unwrap());
ElementProduced::looped(last)
} else if let Some(current) = stack.pop().and_then(|last| rdom.get(last)) {
if let Some(new) = current.prev() {
// the next element exists, add it to the stack and return it
let new = push_back(&mut stack, new);
ElementProduced::progressed(new)
}
// otherwise, yeild the parent
else if let Some(parent) = stack.last() {
// if there is a parent, return it
ElementProduced::progressed(*parent)
} else {
// if there is no parent, loop back to the root
let id = rdom.root_id();
let last = push_back(&mut stack, rdom.get(id).unwrap());
ElementProduced::looped(last)
}
} else {
let (last, old_child_idx) = self.stack.last_mut().unwrap();
let node = &rdom[*last];
match &node.node_data.node_type {
NodeType::Element { .. } => {
let children = rdom.tree.children_ids(*last).unwrap();
// if we have children, go to the next child
if let NodePosition::InChild(0) = old_child_idx {
ElementProduced::Progressed(self.pop())
} else {
*old_child_idx = old_child_idx.map(|i| i - 1);
if let NodePosition::InChild(child_idx) = old_child_idx {
if *child_idx >= children.len() || children.is_empty() {
self.pop();
self.prev(rdom)
} else {
let new_node = children[*child_idx];
ElementProduced::Progressed(push_back(
&mut self.stack,
new_node,
rdom,
))
}
} else {
self.pop();
self.prev(rdom)
}
}
}
NodeType::Text { .. } | NodeType::Placeholder { .. } => {
// we are at a leaf, so we are done
ElementProduced::Progressed(self.pop())
}
}
// if there is no parent, loop back to the root
let id = rdom.root_id();
let last = push_back(&mut stack, rdom.get(id).unwrap());
ElementProduced::looped(last)
}
}
fn pop(&mut self) -> RealNodeId {
self.stack.pop().unwrap().0
}
}
#[derive(Default, Clone, Debug)]
struct Empty {}
impl State<()> for Empty {
const PASSES: &'static [crate::AnyPass<crate::node::Node<Self, ()>>] = &[];
const MASKS: &'static [crate::NodeMask] = &[];
}
#[test]
#[allow(unused_variables)]
fn traverse() {
use crate::dioxus::DioxusState;
use crate::prelude::*;
use dioxus::prelude::*;
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
@ -266,82 +218,85 @@ fn traverse() {
let mut vdom = VirtualDom::new(Base);
let mutations = vdom.rebuild();
let mut rdom: RealDom<Empty> = RealDom::new();
let mut rdom: RealDom = RealDom::new([]);
let _to_update = rdom.apply_mutations(mutations);
let mut iter = PersistantElementIter::create(&mut rdom);
let mut dioxus_state = DioxusState::create(&mut rdom);
dioxus_state.apply_mutations(&mut rdom, mutations);
let mut iter = PersistantElementIter::new();
let div_tag = "div".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
let text1 = "hello".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Text { text: text1, .. }
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Text(text1)
));
let p_tag = "p".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Element { tag: p_tag, .. }
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: p_tag, .. })
));
let text2 = "world".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Text { text: text2, .. }
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Text(text2)
));
let text3 = "hello world".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Text { text: text3, .. }
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Text(text3)
));
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Text { text: text3, .. }
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Text(text3)
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Text { text: text2, .. }
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Text(text2)
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Element { tag: p_tag, .. }
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: p_tag, .. })
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Text { text: text1, .. }
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Text(text1)
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Text { text: text3, .. }
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Text(text3)
));
}
#[test]
#[allow(unused_variables)]
fn persist_removes() {
use crate::dioxus::DioxusState;
use crate::prelude::*;
use dioxus::prelude::*;
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
@ -365,16 +320,17 @@ fn persist_removes() {
}
let mut vdom = VirtualDom::new(Base);
let mut rdom: RealDom<Empty> = RealDom::new();
let mut rdom: RealDom = RealDom::new([]);
let build = vdom.rebuild();
println!("{build:#?}");
let _to_update = rdom.apply_mutations(build);
// this will end on the node that is removed
let mut iter1 = PersistantElementIter::new();
let mut iter1 = PersistantElementIter::create(&mut rdom);
// this will end on the after node that is removed
let mut iter2 = PersistantElementIter::new();
let mut iter2 = PersistantElementIter::create(&mut rdom);
let mut dioxus_state = DioxusState::create(&mut rdom);
dioxus_state.apply_mutations(&mut rdom, build);
// root
iter1.next(&rdom).id();
iter2.next(&rdom).id();
@ -401,29 +357,27 @@ fn persist_removes() {
vdom.mark_dirty(ScopeId(0));
let update = vdom.render_immediate();
println!("{update:#?}");
iter1.prune(&update, &rdom);
iter2.prune(&update, &rdom);
let _to_update = rdom.apply_mutations(update);
dioxus_state.apply_mutations(&mut rdom, update);
let root_tag = "Root".to_string();
let idx = iter1.next(&rdom).id();
dbg!(&rdom[idx].node_data.node_type);
assert!(matches!(
&rdom[idx].node_data.node_type,
NodeType::Element { tag: root_tag, .. }
&*rdom.get(idx).unwrap().node_type(),
NodeType::Element(ElementNode { tag: root_tag, .. })
));
let idx = iter2.next(&rdom).id();
dbg!(&rdom[idx].node_data.node_type);
assert!(matches!(
&rdom[idx].node_data.node_type,
NodeType::Element { tag: root_tag, .. }
&*rdom.get(idx).unwrap().node_type(),
NodeType::Element(ElementNode { tag: root_tag, .. })
));
}
#[test]
#[allow(unused_variables)]
fn persist_instertions_before() {
use crate::dioxus::DioxusState;
use crate::prelude::*;
use dioxus::prelude::*;
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
@ -447,12 +401,13 @@ fn persist_instertions_before() {
}
let mut vdom = VirtualDom::new(Base);
let mut rdom: RealDom<Empty> = RealDom::new();
let mut rdom: RealDom = RealDom::new([]);
let mut dioxus_state = DioxusState::create(&mut rdom);
let build = vdom.rebuild();
let _to_update = rdom.apply_mutations(build);
dioxus_state.apply_mutations(&mut rdom, build);
let mut iter = PersistantElementIter::new();
let mut iter = PersistantElementIter::create(&mut rdom);
// div
iter.next(&rdom).id();
// p
@ -466,20 +421,21 @@ fn persist_instertions_before() {
vdom.mark_dirty(ScopeId(0));
let update = vdom.render_immediate();
iter.prune(&update, &rdom);
let _to_update = rdom.apply_mutations(update);
dioxus_state.apply_mutations(&mut rdom, update);
let p_tag = "div".to_string();
let idx = iter.next(&rdom).id();
assert!(matches!(
&rdom[idx].node_data.node_type,
NodeType::Element { tag: p_tag, .. }
&*rdom.get(idx).unwrap().node_type(),
NodeType::Element(ElementNode { tag: p_tag, .. })
));
}
#[test]
#[allow(unused_variables)]
fn persist_instertions_after() {
use crate::dioxus::DioxusState;
use crate::prelude::*;
use dioxus::prelude::*;
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
@ -503,12 +459,13 @@ fn persist_instertions_after() {
}
let mut vdom = VirtualDom::new(Base);
let mut rdom: RealDom<Empty> = RealDom::new();
let mut rdom: RealDom = RealDom::new([]);
let mut iter = PersistantElementIter::create(&mut rdom);
let mut dioxus_state = DioxusState::create(&mut rdom);
let build = vdom.rebuild();
let _to_update = rdom.apply_mutations(build);
dioxus_state.apply_mutations(&mut rdom, build);
let mut iter = PersistantElementIter::new();
// div
iter.next(&rdom).id();
// p
@ -521,19 +478,18 @@ fn persist_instertions_after() {
iter.next(&rdom).id();
let update = vdom.rebuild();
iter.prune(&update, &rdom);
let _to_update = rdom.apply_mutations(update);
dioxus_state.apply_mutations(&mut rdom, update);
let p_tag = "p".to_string();
let idx = iter.next(&rdom).id();
assert!(matches!(
&rdom[idx].node_data.node_type,
NodeType::Element { tag: p_tag, .. }
&*rdom.get(idx).unwrap().node_type(),
NodeType::Element(ElementNode { tag: p_tag, .. })
));
let text = "hello world".to_string();
let idx = iter.next(&rdom).id();
assert!(matches!(
&rdom[idx].node_data.node_type,
NodeType::Text { text, .. }
&*rdom.get(idx).unwrap().node_type(),
NodeType::Text(text)
));
}

View file

@ -0,0 +1,237 @@
use dioxus::prelude::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
macro_rules! dep {
( child( $name:ty, $dep:ty ) ) => {
#[partial_derive_state]
impl State for $name {
type ParentDependencies = ();
type ChildDependencies = $dep;
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::ALL;
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
};
( parent( $name:ty, $dep:ty ) ) => {
#[partial_derive_state]
impl State for $name {
type ParentDependencies = $dep;
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::ALL;
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
};
( node( $name:ty, $dep:ty ) ) => {
#[partial_derive_state]
impl State for $name {
type ParentDependencies = $dep;
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::ALL;
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
};
}
macro_rules! test_state{
( state: ( $( $state:ty ),* ) ) => {
#[test]
fn state_reduce_initally_called_minimally() {
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
render!{
div {
div{
div{
p{}
}
p{
"hello"
}
div{
h1{}
}
p{
"world"
}
}
}
}
}
let mut vdom = VirtualDom::new(Base);
let mutations = vdom.rebuild();
let mut dom: RealDom = RealDom::new([$( <$state>::to_type_erased() ),*]);
let mut dioxus_state = DioxusState::create(&mut dom);
dioxus_state.apply_mutations(&mut dom, mutations);
dom.update_state(SendAnyMap::new());
dom.traverse_depth_first(|n| {
$(
assert_eq!(n.get::<$state>().unwrap().0, 1);
)*
});
}
}
}
mod node_depends_on_child_and_parent {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, (Child, Parent)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
test_state!(state: (Child, Node, Parent));
}
mod child_depends_on_node_that_depends_on_parent {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, (Parent,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Node,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
test_state!(state: (Child, Node, Parent));
}
mod parent_depends_on_node_that_depends_on_child {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Node,)));
test_state!(state: (Child, Node, Parent));
}
mod node_depends_on_other_node_state {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node1(i32);
dep!(node(Node1, (Node2,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node2(i32);
dep!(node(Node2, ()));
test_state!(state: (Node1, Node2));
}
mod node_child_and_parent_state_depends_on_self {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, ()));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
test_state!(state: (Child, Node, Parent));
}

View file

@ -1,12 +1,8 @@
use dioxus::prelude::Props;
use dioxus_core::*;
use dioxus_native_core::{
node_ref::{AttributeMask, NodeView},
real_dom::RealDom,
state::{ParentDepState, State},
NodeMask, SendAnyMap,
};
use dioxus_native_core_macro::{sorted_str_slice, State};
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
use std::cell::Cell;
fn random_ns() -> Option<&'static str> {
@ -232,7 +228,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
static mut TEMPLATE_COUNT: usize = 0;
#[derive(PartialEq, Props)]
#[derive(PartialEq, Props, Component)]
struct DepthProps {
depth: usize,
root: bool,
@ -294,26 +290,48 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
node
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BlablaState {}
/// Font style are inherited by default if not specified otherwise by some of the supported attributes.
impl ParentDepState for BlablaState {
type Ctx = ();
type DepState = (Self,);
const NODE_MASK: NodeMask =
NodeMask::new_with_attrs(AttributeMask::Static(&sorted_str_slice!(["blabla",])));
fn reduce(&mut self, _node: NodeView, _parent: Option<(&Self,)>, _ctx: &Self::Ctx) -> bool {
false
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Component)]
pub struct BlablaState {
count: usize,
}
#[derive(Clone, State, Default, Debug)]
pub struct NodeState {
#[parent_dep_state(blabla)]
blabla: BlablaState,
#[partial_derive_state]
impl State for BlablaState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["blabla"]))
.with_element();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some((parent,)) = parent {
if parent.count != 0 {
self.count += 1;
}
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
// test for panics when creating random nodes and templates
@ -328,11 +346,12 @@ fn create() {
},
);
let mutations = vdom.rebuild();
let mut rdom: RealDom<NodeState> = RealDom::new();
let (to_update, _diff) = rdom.apply_mutations(mutations);
let mut rdom: RealDom = RealDom::new([BlablaState::to_type_erased()]);
let mut dioxus_state = DioxusState::create(&mut rdom);
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(to_update, ctx);
rdom.update_state(ctx);
}
}
@ -349,17 +368,18 @@ fn diff() {
},
);
let mutations = vdom.rebuild();
let mut rdom: RealDom<NodeState> = RealDom::new();
let (to_update, _diff) = rdom.apply_mutations(mutations);
let mut rdom: RealDom = RealDom::new([BlablaState::to_type_erased()]);
let mut dioxus_state = DioxusState::create(&mut rdom);
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(to_update, ctx);
rdom.update_state(ctx);
for _ in 0..10 {
let mutations = vdom.render_immediate();
let (to_update, _diff) = rdom.apply_mutations(mutations);
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(to_update, ctx);
rdom.update_state(ctx);
}
}
}

View file

@ -1,34 +1,51 @@
use dioxus::prelude::*;
use dioxus_native_core::{
node_ref::{AttributeMask, NodeView},
real_dom::RealDom,
state::{ParentDepState, State},
NodeMask, SendAnyMap,
};
use dioxus_native_core_macro::{sorted_str_slice, State};
use std::sync::{Arc, Mutex};
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
use tokio::time::sleep;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BlablaState {}
/// Font style are inherited by default if not specified otherwise by some of the supported attributes.
impl ParentDepState for BlablaState {
type Ctx = ();
type DepState = (Self,);
const NODE_MASK: NodeMask =
NodeMask::new_with_attrs(AttributeMask::Static(&sorted_str_slice!(["blabla",])));
fn reduce(&mut self, _node: NodeView, _parent: Option<(&Self,)>, _ctx: &Self::Ctx) -> bool {
false
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Component)]
pub struct BlablaState {
count: usize,
}
#[derive(Clone, State, Default, Debug)]
pub struct NodeState {
#[parent_dep_state(blabla)]
blabla: BlablaState,
#[partial_derive_state]
impl State for BlablaState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["blabla"]))
.with_element();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some((parent,)) = parent {
if parent.count != 0 {
self.count += 1;
}
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
mod dioxus_elements {
@ -76,6 +93,7 @@ mod dioxus_elements {
#[test]
fn native_core_is_okay() {
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn app(cx: Scope) -> Element {
@ -113,23 +131,24 @@ fn native_core_is_okay() {
.unwrap();
rt.block_on(async {
let rdom = Arc::new(Mutex::new(RealDom::<NodeState>::new()));
let rdom = Arc::new(Mutex::new(RealDom::new([BlablaState::to_type_erased()])));
let mut dioxus_state = DioxusState::create(&mut rdom.lock().unwrap());
let mut dom = VirtualDom::new(app);
let muts = dom.rebuild();
let (to_update, _diff) = rdom.lock().unwrap().apply_mutations(muts);
let mutations = dom.rebuild();
dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations);
let ctx = SendAnyMap::new();
rdom.lock().unwrap().update_state(to_update, ctx);
rdom.lock().unwrap().update_state(ctx);
for _ in 0..10 {
dom.wait_for_work().await;
let mutations = dom.render_immediate();
let (to_update, _diff) = rdom.lock().unwrap().apply_mutations(mutations);
dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations);
let ctx = SendAnyMap::new();
rdom.lock().unwrap().update_state(to_update, ctx);
rdom.lock().unwrap().update_state(ctx);
}
});
}

View file

@ -0,0 +1,440 @@
use dioxus_native_core::node::NodeType;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::Component;
fn create_blank_element() -> NodeType {
NodeType::Element(ElementNode {
tag: "div".to_owned(),
namespace: None,
attributes: FxHashMap::default(),
listeners: FxHashSet::default(),
})
}
#[test]
fn node_pass() {
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct Number(i32);
#[partial_derive_state]
impl State for Number {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([Number::to_type_erased()]);
tree.update_state(SendAnyMap::new());
assert_eq!(
tree.get(tree.root_id()).unwrap().get().as_deref(),
Some(&Number(1))
);
// mark the node as dirty
tree.get_mut(tree.root_id()).unwrap().get_mut::<Number>();
tree.update_state(SendAnyMap::new());
assert_eq!(
tree.get(tree.root_id()).unwrap().get().as_deref(),
Some(&Number(2))
);
}
#[test]
fn dependant_node_pass() {
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct AddNumber(i32);
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = ();
type NodeDependencies = (SubtractNumber,);
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct SubtractNumber(i32);
#[partial_derive_state]
impl State for SubtractNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 -= 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([
AddNumber::to_type_erased(),
SubtractNumber::to_type_erased(),
]);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-1)));
// mark the subtract state as dirty, it should update the add state
tree.get_mut(tree.root_id())
.unwrap()
.get_mut::<SubtractNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(2)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
// mark the add state as dirty, it should ~not~ update the subtract state
tree.get_mut(tree.root_id()).unwrap().get_mut::<AddNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(3)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
}
#[test]
fn independant_node_pass() {
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct AddNumber(i32);
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct SubtractNumber(i32);
#[partial_derive_state]
impl State for SubtractNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 -= 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([
AddNumber::to_type_erased(),
SubtractNumber::to_type_erased(),
]);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-1)));
// mark the subtract state as dirty, it should ~not~ update the add state
tree.get_mut(tree.root_id())
.unwrap()
.get_mut::<SubtractNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
// mark the add state as dirty, it should ~not~ update the subtract state
tree.get_mut(tree.root_id()).unwrap().get_mut::<AddNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(2)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
}
#[test]
fn down_pass() {
#[derive(Debug, Clone, PartialEq, Component)]
struct AddNumber(i32);
impl Default for AddNumber {
fn default() -> Self {
Self(1)
}
}
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = (AddNumber,);
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some((parent,)) = parent {
self.0 += parent.0;
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([AddNumber::to_type_erased()]);
let grandchild1 = tree.create_node(create_blank_element());
let grandchild1 = grandchild1.id();
let mut child1 = tree.create_node(create_blank_element());
child1.add_child(grandchild1);
let child1 = child1.id();
let grandchild2 = tree.create_node(create_blank_element());
let grandchild2 = grandchild2.id();
let mut child2 = tree.create_node(create_blank_element());
child2.add_child(grandchild2);
let child2 = child2.id();
let mut parent = tree.get_mut(tree.root_id()).unwrap();
parent.add_child(child1);
parent.add_child(child2);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
dbg!(root.id());
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
let child1 = tree.get(child1).unwrap();
dbg!(child1.id());
assert_eq!(child1.get().as_deref(), Some(&AddNumber(2)));
let grandchild1 = tree.get(grandchild1).unwrap();
assert_eq!(grandchild1.get().as_deref(), Some(&AddNumber(3)));
let child2 = tree.get(child2).unwrap();
assert_eq!(child2.get().as_deref(), Some(&AddNumber(2)));
let grandchild2 = tree.get(grandchild2).unwrap();
assert_eq!(grandchild2.get().as_deref(), Some(&AddNumber(3)));
}
#[test]
fn up_pass() {
// Tree before:
// 1=\
// 1=\
// 1
// 1=\
// 1
// Tree after:
// 4=\
// 2=\
// 1
// 2=\
// 1
#[derive(Debug, Clone, PartialEq, Component)]
struct AddNumber(i32);
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = (AddNumber,);
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += children.iter().map(|(i,)| i.0).sum::<i32>();
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self(1);
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([AddNumber::to_type_erased()]);
let grandchild1 = tree.create_node(create_blank_element());
let grandchild1 = grandchild1.id();
let mut child1 = tree.create_node(create_blank_element());
child1.add_child(grandchild1);
let child1 = child1.id();
let grandchild2 = tree.create_node(create_blank_element());
let grandchild2 = grandchild2.id();
let mut child2 = tree.create_node(create_blank_element());
child2.add_child(grandchild2);
let child2 = child2.id();
let mut parent = tree.get_mut(tree.root_id()).unwrap();
parent.add_child(child1);
parent.add_child(child2);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(5)));
let child1 = tree.get(child1).unwrap();
assert_eq!(child1.get().as_deref(), Some(&AddNumber(2)));
let grandchild1 = tree.get(grandchild1).unwrap();
assert_eq!(grandchild1.get().as_deref(), Some(&AddNumber(1)));
let child2 = tree.get(child2).unwrap();
assert_eq!(child2.get().as_deref(), Some(&AddNumber(2)));
let grandchild2 = tree.get(grandchild2).unwrap();
assert_eq!(grandchild2.get().as_deref(), Some(&AddNumber(1)));
}

2
packages/rink/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
Cargo.lock

2
packages/rink/.vscode/spellright.dict vendored Normal file
View file

@ -0,0 +1,2 @@
esque
Tui

Some files were not shown because too many files have changed in this diff Show more