Merge branch 'upstream' into fix-ssr-raw-attributes

This commit is contained in:
Evan Almloff 2023-04-12 09:11:25 -05:00
commit 6afd764aa4
109 changed files with 6361 additions and 4894 deletions

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",

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).

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,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

@ -2,7 +2,13 @@
name = "dioxus-autofmt"
version = "0.3.0"
edition = "2021"
authors = ["Jonathan Kelley"]
description = "Autofomatter for Dioxus RSX"
license = "MIT/Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
@ -11,7 +17,7 @@ proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
quote = "1.0"
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
serde = { version = "1.0.136", features = ["derive"] }
prettyplease = { git = "https://github.com/DioxusLabs/prettyplease-macro-fmt.git", features = [
prettyplease = { package = "prettier-please", version = "0.1.16", features = [
"verbatim",
] }

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",

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,7 @@ struct WebviewHandler {
dom: VirtualDom,
webview: Rc<wry::webview::WebView>,
waker: Waker,
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,8 @@ pub fn build(
.as_ref()
.map(|handler| handler(window, evet))
.unwrap_or_default()
});
})
.with_web_context(&mut web_context);
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
@ -78,5 +81,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

@ -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: dioxus_native_core::exports::FxHashSet<std::any::TypeId>, 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,34 @@ 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 }
shipyard_hierarchy = "0.6.0"
[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,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, 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

39
packages/rink/Cargo.toml Normal file
View file

@ -0,0 +1,39 @@
[package]
name = "rink"
version = "0.2.2"
authors = ["Jonathan Kelley, @dementhos"]
edition = "2021"
description = "TUI-based renderer for Dioxus"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "terminal"]
license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-html = { path = "../html", version = "^0.3.0" }
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.3.0" }
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"
shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false }
once_cell = "1.17.1"
[dev-dependencies]
tokio = { version = "1" }
criterion = "0.3.5"
[features]
default = []
parallel = ["shipyard/parallel"]

74
packages/rink/README.md Normal file
View file

@ -0,0 +1,74 @@
<div align="center">
<h1>Rink</h1>
<p>
<strong>A beautiful terminal user interfaces library in Rust.</strong>
</p>
</div>
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/rink">
<img src="https://img.shields.io/crates/v/rink.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/rink">
<img src="https://img.shields.io/crates/d/rink.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs -->
<a href="https://docs.rs/rink">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
<!-- CI -->
<a href="https://github.com/jkelleyrtp/rink/actions">
<img src="https://github.com/dioxuslabs/rink/actions/workflows/main.yml/badge.svg"
alt="CI status" />
</a>
<!-- Discord -->
<a href="https://discord.gg/XgGxMSkvUM">
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
</a>
</div>
<br/>
Leverage CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces. Rink is the cross-framework library that powers [`Dioxus-TUI`](https://github.com/DioxusLabs/dioxus/tree/master/packages/dioxus-tui)
![demo app](examples/example.png)
## Background
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/). Rink 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.
- **Particular frontend design**
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!**
Rendering a Dom works fine, but the ecosystem of widgets is not ready yet. Additionally, some bugs in the flexbox implementation might be quirky at times.
## Features
Rink features:
- [x] Flexbox-based layout system
- [ ] CSS selectors
- [x] inline CSS support
- [x] Built-in focusing system
* [ ] Widgets
* [ ] Support for events, hooks, and callbacks<sup>1</sup>
* [ ] Html tags<sup>2</sup>
<sup>1</sup> Basic keyboard, mouse, and focus events are implemented.
<sup>2</sup> Currently, most HTML tags don't translate into any meaning inside of Rink. So an `input` _element_ won't mean anything nor does it have any additional functionality.

View file

@ -0,0 +1,104 @@
use dioxus_html::EventData;
use dioxus_native_core::{
node::TextNode,
prelude::*,
real_dom::{NodeImmutable, NodeTypeMut},
NodeId,
};
use rink::{render, Config, Driver};
use std::rc::Rc;
use std::sync::{Arc, RwLock};
#[derive(Default)]
struct Counter {
count: usize,
counter_id: NodeId,
button_id: NodeId,
}
impl Counter {
fn create(mut root: NodeMut) -> Self {
let mut myself = Self::default();
let root_id = root.id();
let rdom = root.real_dom_mut();
// create the counter
let count = myself.count;
myself.counter_id = rdom
.create_node(NodeType::Text(TextNode::new(count.to_string())))
.id();
let mut button = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [
("display".to_string().into(), "flex".to_string().into()),
(
("background-color", "style").into(),
format!("rgb({}, {}, {})", count * 10, 0, 0,).into(),
),
(("width", "style").into(), "100%".to_string().into()),
(("height", "style").into(), "100%".to_string().into()),
(("flex-direction", "style").into(), "row".to_string().into()),
(
("justify-content", "style").into(),
"center".to_string().into(),
),
(("align-items", "style").into(), "center".to_string().into()),
]
.into_iter()
.collect(),
..Default::default()
}));
button.add_event_listener("click");
button.add_event_listener("wheel");
button.add_child(myself.counter_id);
myself.button_id = button.id();
rdom.get_mut(root_id).unwrap().add_child(myself.button_id);
myself
}
}
impl Driver for Counter {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
// update the counter
let mut rdom = rdom.write().unwrap();
let mut node = rdom.get_mut(self.button_id).unwrap();
if let NodeTypeMut::Element(mut el) = node.node_type_mut() {
el.set_attribute(
("background-color", "style"),
format!("rgb({}, {}, {})", self.count * 10, 0, 0,),
);
}
let mut text = rdom.get_mut(self.counter_id).unwrap();
let type_mut = text.node_type_mut();
if let NodeTypeMut::Text(mut text) = type_mut {
*text = self.count.to_string();
}
}
fn handle_event(
&mut self,
_: &Arc<RwLock<RealDom>>,
_: NodeId,
_: &str,
_: Rc<EventData>,
_: bool,
) {
// when a click or wheel event is fired, increment the counter
self.count += 1;
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
Box::pin(async move { tokio::time::sleep(std::time::Duration::from_millis(1000)).await })
}
}
fn main() {
render(Config::new(), |rdom, _, _| {
let mut rdom = rdom.write().unwrap();
let root = rdom.root_id();
Counter::create(rdom.get_mut(root).unwrap())
})
.unwrap();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View file

@ -0,0 +1,173 @@
use dioxus_html::EventData;
use dioxus_native_core::{
node::TextNode,
prelude::*,
real_dom::{NodeImmutable, NodeTypeMut},
NodeId,
};
use rink::{render, Config, Driver};
use rustc_hash::FxHashSet;
use std::rc::Rc;
use std::sync::{Arc, RwLock};
const SIZE: usize = 20;
#[derive(Default, Clone, Copy)]
struct Node {
container_id: Option<NodeId>,
text_id: Option<NodeId>,
count: usize,
}
struct Test {
node_states: [[Node; SIZE]; SIZE],
dirty: FxHashSet<(usize, usize)>,
}
impl Default for Test {
fn default() -> Self {
Self {
node_states: [[Node {
container_id: None,
text_id: None,
count: 0,
}; SIZE]; SIZE],
dirty: FxHashSet::default(),
}
}
}
impl Test {
fn create(mut root: NodeMut) -> Self {
let mut myself = Self::default();
// Set the root node to be a flexbox with a column direction.
if let NodeTypeMut::Element(mut el) = root.node_type_mut() {
el.set_attribute("display".to_string(), "flex".to_string());
el.set_attribute(("flex-direction", "style"), "column".to_string());
el.set_attribute(("width", "style"), "100%".to_string());
el.set_attribute(("height", "style"), "100%".to_string());
}
let root_id = root.id();
let rdom = root.real_dom_mut();
// create the grid
for (x, row) in myself.node_states.iter_mut().enumerate() {
let row_node = rdom
.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [
("display".to_string().into(), "flex".to_string().into()),
(("flex-direction", "style").into(), "row".to_string().into()),
(("width", "style").into(), "100%".to_string().into()),
(("height", "style").into(), "100%".to_string().into()),
]
.into_iter()
.collect(),
..Default::default()
}))
.id();
for (y, node) in row.iter_mut().enumerate() {
let count = node.count;
let id = rdom
.create_node(NodeType::Text(TextNode::new(count.to_string())))
.id();
let mut button = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [
("display".to_string().into(), "flex".to_string().into()),
(
("background-color", "style").into(),
format!("rgb({}, {}, {})", count * 10, 0, (x + y),).into(),
),
(("width", "style").into(), "100%".to_string().into()),
(("height", "style").into(), "100%".to_string().into()),
(("flex-direction", "style").into(), "row".to_string().into()),
(
("justify-content", "style").into(),
"center".to_string().into(),
),
(("align-items", "style").into(), "center".to_string().into()),
]
.into_iter()
.collect(),
..Default::default()
}));
button.add_event_listener("click");
button.add_event_listener("wheel");
button.add_child(id);
let button_id = button.id();
rdom.get_mut(row_node).unwrap().add_child(button_id);
node.container_id = Some(button_id);
node.text_id = Some(id);
}
rdom.get_mut(root_id).unwrap().add_child(row_node);
}
myself
}
}
impl Driver for Test {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
let mut rdom = rdom.write().unwrap();
for (x, y) in self.dirty.drain() {
let node = self.node_states[x][y];
let node_id = node.container_id.unwrap();
let mut container = rdom.get_mut(node_id).unwrap();
if let NodeTypeMut::Element(mut el) = container.node_type_mut() {
el.set_attribute(
("background-color", "style"),
format!("rgb({}, {}, {})", node.count * 10, 0, (x + y),),
);
}
let text_id = node.text_id.unwrap();
let mut text = rdom.get_mut(text_id).unwrap();
let type_mut = text.node_type_mut();
if let NodeTypeMut::Text(mut text) = type_mut {
*text = node.count.to_string();
}
}
}
fn handle_event(
&mut self,
rdom: &Arc<RwLock<RealDom>>,
id: NodeId,
_: &str,
_: Rc<EventData>,
_: bool,
) {
let rdom = rdom.read().unwrap();
let node = rdom.get(id).unwrap();
if let Some(parent) = node.parent() {
let child_number = parent
.child_ids()
.iter()
.position(|id| *id == node.id())
.unwrap();
if let Some(parents_parent) = parent.parent() {
let parents_child_number = parents_parent
.child_ids()
.iter()
.position(|id| *id == parent.id())
.unwrap();
self.node_states[parents_child_number][child_number].count += 1;
self.dirty.insert((parents_child_number, child_number));
}
}
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
Box::pin(async move { tokio::time::sleep(std::time::Duration::from_millis(1000)).await })
}
}
fn main() {
render(Config::new(), |rdom, _, _| {
let mut rdom = rdom.write().unwrap();
let root = rdom.root_id();
Test::create(rdom.get_mut(root).unwrap())
})
.unwrap();
}

View file

@ -1,18 +1,23 @@
use crate::{node::PreventDefault, TuiDom};
use crate::prevent_default::PreventDefault;
use dioxus_native_core::{
tree::TreeView,
utils::{ElementProduced, PersistantElementIter},
RealNodeId,
node_ref::{AttributeMaskBuilder, NodeMaskBuilder},
prelude::*,
real_dom::NodeImmutable,
utils::{IteratorMovement, PersistantElementIter},
};
use dioxus_native_core_macro::sorted_str_slice;
use dioxus_native_core_macro::partial_derive_state;
use once_cell::sync::Lazy;
use rustc_hash::FxHashSet;
use shipyard::Component;
use shipyard::{Get, ViewMut};
use std::{cmp::Ordering, num::NonZeroU16};
use dioxus_native_core::{
node_ref::{AttributeMask, NodeMask, NodeView},
state::NodeDepState,
};
use dioxus_native_core::node_ref::NodeView;
#[derive(Component)]
pub struct Focused(pub bool);
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub(crate) enum FocusLevel {
@ -54,20 +59,31 @@ impl Ord for FocusLevel {
}
}
#[derive(Clone, PartialEq, Debug, Default)]
#[derive(Clone, PartialEq, Debug, Default, Component)]
pub(crate) struct Focus {
pub level: FocusLevel,
}
impl NodeDepState for Focus {
type DepState = ();
type Ctx = ();
const NODE_MASK: NodeMask =
NodeMask::new_with_attrs(AttributeMask::Static(FOCUS_ATTRIBUTES)).with_listeners();
#[partial_derive_state]
impl State for Focus {
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(FOCUS_ATTRIBUTES))
.with_listeners();
fn reduce(&mut self, node: NodeView<'_>, _sibling: (), _: &Self::Ctx) -> bool {
type ParentDependencies = ();
type ChildDependencies = ();
type NodeDependencies = ();
fn update<'a>(
&mut self,
node_view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
let new = Focus {
level: if let Some(a) = node
level: if let Some(a) = node_view
.attributes()
.and_then(|mut a| a.find(|a| a.attribute.name == "tabindex"))
{
@ -86,12 +102,10 @@ impl NodeDepState for Focus {
} else {
FocusLevel::Unfocusable
}
} else if node
} else if node_view
.listeners()
.and_then(|mut listeners| {
listeners
.any(|l| FOCUS_EVENTS.binary_search(&l).is_ok())
.then_some(())
listeners.any(|l| FOCUS_EVENTS.contains(&l)).then_some(())
})
.is_some()
{
@ -107,24 +121,48 @@ impl NodeDepState for Focus {
false
}
}
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
}
}
const FOCUS_EVENTS: &[&str] = &sorted_str_slice!(["keydown", "keypress", "keyup"]);
const FOCUS_ATTRIBUTES: &[&str] = &sorted_str_slice!(["tabindex"]);
static FOCUS_EVENTS: Lazy<FxHashSet<&str>> =
Lazy::new(|| ["keydown", "keypress", "keyup"].into_iter().collect());
const FOCUS_ATTRIBUTES: &[&str] = &["tabindex"];
#[derive(Default)]
pub(crate) struct FocusState {
pub(crate) focus_iter: PersistantElementIter,
pub(crate) last_focused_id: Option<RealNodeId>,
pub(crate) last_focused_id: Option<NodeId>,
pub(crate) focus_level: FocusLevel,
pub(crate) dirty: bool,
}
impl FocusState {
pub fn create(rdom: &mut RealDom) -> Self {
let focus_iter = PersistantElementIter::create(rdom);
Self {
focus_iter,
last_focused_id: Default::default(),
focus_level: Default::default(),
dirty: Default::default(),
}
}
/// Returns true if the focus has changed.
pub fn progress(&mut self, rdom: &mut TuiDom, forward: bool) -> bool {
pub fn progress(&mut self, rdom: &mut RealDom, forward: bool) -> bool {
if let Some(last) = self.last_focused_id {
if rdom[last].state.prevent_default == PreventDefault::KeyDown {
if rdom.get(last).unwrap().get::<PreventDefault>().map(|p| *p)
== Some(PreventDefault::KeyDown)
{
return false;
}
}
@ -140,13 +178,13 @@ impl FocusState {
self.focus_iter.prev(rdom)
};
let new_id = new.id();
if let ElementProduced::Looped(_) = new {
if let IteratorMovement::Looped = new.movement() {
let mut closest_level = None;
if forward {
// find the closest focusable element after the current level
rdom.traverse_depth_first(|n| {
let node_level = n.state.focus.level;
let node_level = n.get::<Focus>().unwrap().level;
if node_level != *focus_level
&& node_level.focusable()
&& node_level > *focus_level
@ -163,7 +201,7 @@ impl FocusState {
} else {
// find the closest focusable element before the current level
rdom.traverse_depth_first(|n| {
let node_level = n.state.focus.level;
let node_level = n.get::<Focus>().unwrap().level;
if node_level != *focus_level
&& node_level.focusable()
&& node_level < *focus_level
@ -200,7 +238,7 @@ impl FocusState {
loop_marker_id = Some(new_id);
}
let current_level = rdom[new_id].state.focus.level;
let current_level = rdom.get(new_id).unwrap().get::<Focus>().unwrap().level;
let after_previous_focused = if forward {
current_level >= *focus_level
} else {
@ -214,12 +252,15 @@ impl FocusState {
}
if let Some(id) = next_focus {
if !rdom[id].state.focus.level.focusable() {
let mut node = rdom.get_mut(id).unwrap();
if !node.get::<Focus>().unwrap().level.focusable() {
panic!()
}
rdom[id].state.focused = true;
node.insert(Focused(true));
if let Some(old) = self.last_focused_id.replace(id) {
rdom[old].state.focused = false;
let mut focused_borrow: ViewMut<Focused> = rdom.raw_world().borrow().unwrap();
let focused = (&mut focused_borrow).get(old).unwrap();
focused.0 = false;
}
// reset the position to the currently focused element
while self.focus_iter.next(rdom).id() != id {}
@ -230,52 +271,14 @@ impl FocusState {
false
}
pub(crate) fn prune(&mut self, mutations: &dioxus_core::Mutations, rdom: &TuiDom) {
fn remove_children(
to_prune: &mut [&mut Option<RealNodeId>],
rdom: &TuiDom,
removed: RealNodeId,
) {
for opt in to_prune.iter_mut() {
if let Some(id) = opt {
if *id == removed {
**opt = None;
}
}
}
if let Some(children) = &rdom.children_ids(removed) {
for child in *children {
remove_children(to_prune, rdom, *child);
}
}
}
if self.focus_iter.prune(mutations, rdom) {
self.dirty = true;
}
for m in &mutations.edits {
match m {
dioxus_core::Mutation::ReplaceWith { id, .. } => remove_children(
&mut [&mut self.last_focused_id],
rdom,
rdom.element_to_node_id(*id),
),
dioxus_core::Mutation::Remove { id } => remove_children(
&mut [&mut self.last_focused_id],
rdom,
rdom.element_to_node_id(*id),
),
_ => (),
}
}
}
pub(crate) fn set_focus(&mut self, rdom: &mut TuiDom, id: RealNodeId) {
pub(crate) fn set_focus(&mut self, rdom: &mut RealDom, id: NodeId) {
if let Some(old) = self.last_focused_id.replace(id) {
rdom[old].state.focused = false;
let mut node = rdom.get_mut(old).unwrap();
node.insert(Focused(false));
}
let state = &mut rdom[id].state;
state.focused = true;
self.focus_level = state.focus.level;
let mut node = rdom.get_mut(id).unwrap();
node.insert(Focused(true));
self.focus_level = node.get::<Focus>().unwrap().level;
// reset the position to the currently focused element
while self.focus_iter.next(rdom).id() != id {}
self.dirty = true;

View file

@ -1,9 +1,8 @@
use crossterm::event::{
Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, MouseButton, MouseEventKind,
};
use dioxus_core::*;
use dioxus_native_core::tree::TreeView;
use dioxus_native_core::NodeId;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeImmutable;
use rustc_hash::{FxHashMap, FxHashSet};
use dioxus_html::geometry::euclid::{Point2D, Rect, Size2D};
@ -13,9 +12,8 @@ use dioxus_html::geometry::{
use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers};
use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons;
use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet};
use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, WheelData};
use dioxus_html::{event_bubbles, EventData, FocusData, KeyboardData, MouseData, WheelData};
use std::{
any::Any,
cell::{RefCell, RefMut},
rc::Rc,
time::{Duration, Instant},
@ -23,91 +21,37 @@ use std::{
use taffy::geometry::{Point, Size};
use taffy::{prelude::Layout, Taffy};
use crate::focus::{Focus, Focused};
use crate::layout::TaffyLayout;
use crate::{layout_to_screen_space, FocusState};
use crate::{TuiDom, TuiNode};
pub(crate) struct Event {
pub id: ElementId,
pub id: NodeId,
pub name: &'static str,
pub data: Rc<dyn Any>,
pub data: Rc<EventData>,
pub bubbles: bool,
}
// a wrapper around the input state for easier access
// todo: fix loop
// pub struct InputState(Rc<Rc<RefCell<InnerInputState>>>);
// impl InputState {
// pub fn get(cx: &ScopeState) -> InputState {
// let inner = cx
// .consume_context::<Rc<RefCell<InnerInputState>>>()
// .expect("Rink InputState can only be used in Rink apps!");
// (**inner).borrow_mut().subscribe(cx.schedule_update());
// InputState(inner)
// }
// pub fn mouse(&self) -> Option<MouseData> {
// let data = (**self.0).borrow();
// mouse.as_ref().map(|m| m.clone())
// }
// pub fn wheel(&self) -> Option<WheelData> {
// let data = (**self.0).borrow();
// wheel.as_ref().map(|w| w.clone())
// }
// pub fn screen(&self) -> Option<(u16, u16)> {
// let data = (**self.0).borrow();
// screen.as_ref().map(|m| m.clone())
// }
// pub fn last_key_pressed(&self) -> Option<KeyboardData> {
// let data = (**self.0).borrow();
// last_key_pressed
// .as_ref()
// .map(|k| &k.0.clone())
// }
// }
type EventCore = (&'static str, EventData);
#[derive(Debug)]
enum EventData {
Mouse(MouseData),
Wheel(WheelData),
Screen((u16, u16)),
Keyboard(KeyboardData),
}
impl EventData {
fn into_any(self) -> Rc<dyn Any + Send + Sync> {
match self {
Self::Mouse(m) => Rc::new(m),
Self::Wheel(w) => Rc::new(w),
Self::Screen(s) => Rc::new(s),
Self::Keyboard(k) => Rc::new(k),
}
}
}
const MAX_REPEAT_TIME: Duration = Duration::from_millis(100);
pub struct InnerInputState {
mouse: Option<MouseData>,
wheel: Option<WheelData>,
last_key_pressed: Option<(KeyboardData, Instant)>,
screen: Option<(u16, u16)>,
pub(crate) focus_state: FocusState,
// subscribers: Vec<Rc<dyn Fn() + 'static>>,
}
impl InnerInputState {
fn new() -> Self {
fn create(rdom: &mut RealDom) -> Self {
Self {
mouse: None,
wheel: None,
last_key_pressed: None,
screen: None,
// subscribers: Vec::new(),
focus_state: FocusState::default(),
focus_state: FocusState::create(rdom),
}
}
@ -148,7 +92,6 @@ impl InnerInputState {
*m = new_mouse_data;
}
EventData::Wheel(ref w) => self.wheel = Some(w.clone()),
EventData::Screen(ref s) => self.screen = Some(*s),
EventData::Keyboard(ref mut k) => {
let is_repeating = self
.last_key_pressed
@ -165,6 +108,7 @@ impl InnerInputState {
self.last_key_pressed = Some((k.clone(), Instant::now()));
}
_ => {}
}
}
@ -173,7 +117,7 @@ impl InnerInputState {
evts: &mut Vec<EventCore>,
resolved_events: &mut Vec<Event>,
layout: &Taffy,
dom: &mut TuiDom,
dom: &mut RealDom,
) {
let previous_mouse = self.mouse.clone();
@ -200,32 +144,26 @@ impl InnerInputState {
if old_focus != self.focus_state.last_focused_id {
// elements with listeners will always have a element id
if let Some(id) = self.focus_state.last_focused_id {
let element = dom.tree.get(id).unwrap();
if let Some(id) = element.node_data.element_id {
resolved_events.push(Event {
name: "focus",
id,
data: Rc::new(FocusData {}),
bubbles: event_bubbles("focus"),
});
resolved_events.push(Event {
name: "focusin",
id,
data: Rc::new(FocusData {}),
bubbles: event_bubbles("focusin"),
});
}
resolved_events.push(Event {
name: "focus",
id,
data: Rc::new(EventData::Focus(FocusData {})),
bubbles: event_bubbles("focus"),
});
resolved_events.push(Event {
name: "focusin",
id,
data: Rc::new(EventData::Focus(FocusData {})),
bubbles: event_bubbles("focusin"),
});
}
if let Some(id) = old_focus {
let element = dom.tree.get(id).unwrap();
if let Some(id) = element.node_data.element_id {
resolved_events.push(Event {
name: "focusout",
id,
data: Rc::new(FocusData {}),
bubbles: event_bubbles("focusout"),
});
}
resolved_events.push(Event {
name: "focusout",
id,
data: Rc::new(EventData::Focus(FocusData {})),
bubbles: event_bubbles("focusout"),
});
}
}
@ -239,7 +177,7 @@ impl InnerInputState {
previous_mouse: Option<MouseData>,
resolved_events: &mut Vec<Event>,
layout: &Taffy,
dom: &mut TuiDom,
dom: &mut RealDom,
) {
fn layout_contains_point(layout: &Layout, point: ScreenPoint) -> bool {
let Point { x, y } = layout.location;
@ -259,29 +197,27 @@ impl InnerInputState {
fn try_create_event(
name: &'static str,
data: Rc<dyn Any>,
data: Rc<EventData>,
will_bubble: &mut FxHashSet<NodeId>,
resolved_events: &mut Vec<Event>,
node: &TuiNode,
dom: &TuiDom,
node: NodeRef,
dom: &RealDom,
) {
// only trigger event if the event was not triggered already by a child
let id = node.node_data.node_id;
let id = node.id();
if will_bubble.insert(id) {
let mut parent = dom.parent(id);
let mut parent = Some(node);
while let Some(current_parent) = parent {
let parent_id = current_parent.node_data.node_id;
let parent_id = current_parent.id();
will_bubble.insert(parent_id);
parent = dom.parent(parent_id);
}
if let Some(id) = node.mounted_id() {
resolved_events.push(Event {
name,
id,
data,
bubbles: event_bubbles(name),
})
parent = current_parent.parent_id().and_then(|id| dom.get(id));
}
resolved_events.push(Event {
name,
id,
data,
bubbles: event_bubbles(name),
})
}
}
@ -345,7 +281,10 @@ impl InnerInputState {
if currently_contains && previously_contained {
try_create_event(
"mousemove",
Rc::new(prepare_mouse_data(mouse_data, &node_layout)),
Rc::new(EventData::Mouse(prepare_mouse_data(
mouse_data,
&node_layout,
))),
&mut will_bubble,
resolved_events,
node,
@ -369,7 +308,7 @@ impl InnerInputState {
if currently_contains && !previously_contained {
try_create_event(
"mouseenter",
Rc::new(mouse_data.clone()),
Rc::new(dioxus_html::EventData::Mouse(mouse_data.clone())),
&mut will_bubble,
resolved_events,
node,
@ -392,7 +331,10 @@ impl InnerInputState {
if currently_contains && !previously_contained {
try_create_event(
"mouseover",
Rc::new(prepare_mouse_data(mouse_data, &node_layout)),
Rc::new(EventData::Mouse(prepare_mouse_data(
mouse_data,
&node_layout,
))),
&mut will_bubble,
resolved_events,
node,
@ -412,7 +354,10 @@ impl InnerInputState {
if currently_contains {
try_create_event(
"mousedown",
Rc::new(prepare_mouse_data(mouse_data, &node_layout)),
Rc::new(EventData::Mouse(prepare_mouse_data(
mouse_data,
&node_layout,
))),
&mut will_bubble,
resolved_events,
node,
@ -433,7 +378,10 @@ impl InnerInputState {
if currently_contains {
try_create_event(
"mouseup",
Rc::new(prepare_mouse_data(mouse_data, &node_layout)),
Rc::new(EventData::Mouse(prepare_mouse_data(
mouse_data,
&node_layout,
))),
&mut will_bubble,
resolved_events,
node,
@ -455,7 +403,10 @@ impl InnerInputState {
if currently_contains {
try_create_event(
"click",
Rc::new(prepare_mouse_data(mouse_data, &node_layout)),
Rc::new(EventData::Mouse(prepare_mouse_data(
mouse_data,
&node_layout,
))),
&mut will_bubble,
resolved_events,
node,
@ -478,7 +429,10 @@ impl InnerInputState {
if currently_contains {
try_create_event(
"contextmenu",
Rc::new(prepare_mouse_data(mouse_data, &node_layout)),
Rc::new(EventData::Mouse(prepare_mouse_data(
mouse_data,
&node_layout,
))),
&mut will_bubble,
resolved_events,
node,
@ -502,7 +456,7 @@ impl InnerInputState {
if currently_contains {
try_create_event(
"wheel",
Rc::new(w.clone()),
Rc::new(EventData::Wheel(w.clone())),
&mut will_bubble,
resolved_events,
node,
@ -527,7 +481,10 @@ impl InnerInputState {
if !currently_contains && previously_contained {
try_create_event(
"mouseleave",
Rc::new(prepare_mouse_data(mouse_data, &node_layout)),
Rc::new(EventData::Mouse(prepare_mouse_data(
mouse_data,
&node_layout,
))),
&mut will_bubble,
resolved_events,
node,
@ -550,7 +507,10 @@ impl InnerInputState {
if !currently_contains && previously_contained {
try_create_event(
"mouseout",
Rc::new(prepare_mouse_data(mouse_data, &node_layout)),
Rc::new(EventData::Mouse(prepare_mouse_data(
mouse_data,
&node_layout,
))),
&mut will_bubble,
resolved_events,
node,
@ -564,11 +524,13 @@ impl InnerInputState {
if was_released {
let mut focus_id = None;
dom.traverse_depth_first(|node| {
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
let node_layout = layout
.layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
.unwrap();
let currently_contains = layout_contains_point(node_layout, new_pos);
if currently_contains && node.state.focus.level.focusable() {
focus_id = Some(node.node_data.node_id);
if currently_contains && node.get::<Focus>().unwrap().level.focusable() {
focus_id = Some(node.id());
}
});
if let Some(id) = focus_id {
@ -583,13 +545,18 @@ impl InnerInputState {
// }
}
fn get_abs_layout(node: &TuiNode, dom: &TuiDom, taffy: &Taffy) -> Layout {
let mut node_layout = *taffy.layout(node.state.layout.node.unwrap()).unwrap();
fn get_abs_layout(node: NodeRef, dom: &RealDom, taffy: &Taffy) -> Layout {
let mut node_layout = *taffy
.layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
.unwrap();
let mut current = node;
while let Some(parent) = dom.parent(current.node_data.node_id) {
while let Some(parent) = current.parent_id() {
let parent = dom.get(parent).unwrap();
current = parent;
let parent_layout = taffy.layout(parent.state.layout.node.unwrap()).unwrap();
let parent_layout = taffy
.layout(parent.get::<TaffyLayout>().unwrap().node.unwrap())
.unwrap();
node_layout.location.x += parent_layout.location.x;
node_layout.location.y += parent_layout.location.y;
}
@ -604,11 +571,7 @@ pub struct RinkInputHandler {
impl RinkInputHandler {
/// global context that handles events
/// limitations: GUI key modifier is never detected, key up events are not detected, and only two mouse buttons may be pressed at once
pub fn new() -> (
Self,
Rc<RefCell<InnerInputState>>,
impl FnMut(crossterm::event::Event),
) {
pub fn create(rdom: &mut RealDom) -> (Self, impl FnMut(crossterm::event::Event)) {
let queued_events = Rc::new(RefCell::new(Vec::new()));
let queued_events2 = Rc::downgrade(&queued_events);
@ -620,23 +583,18 @@ impl RinkInputHandler {
}
};
let state = Rc::new(RefCell::new(InnerInputState::new()));
let state = Rc::new(RefCell::new(InnerInputState::create(rdom)));
(
Self {
state: state.clone(),
state,
queued_events,
},
state,
regester_event,
)
}
pub(crate) fn prune(&self, mutations: &dioxus_core::Mutations, rdom: &TuiDom) {
self.state.borrow_mut().focus_state.prune(mutations, rdom);
}
pub(crate) fn get_events(&self, layout: &Taffy, dom: &mut TuiDom) -> Vec<Event> {
pub(crate) fn get_events(&self, layout: &Taffy, dom: &mut RealDom) -> Vec<Event> {
let mut resolved_events = Vec::new();
(*self.state).borrow_mut().update(
@ -667,28 +625,27 @@ impl RinkInputHandler {
]
.contains(&e.0)
})
.map(|evt| (evt.0, evt.1.into_any()));
.map(|evt| (evt.0, evt.1));
let mut hm: FxHashMap<&'static str, Vec<Rc<dyn Any + Send + Sync>>> = FxHashMap::default();
let mut hm: FxHashMap<&'static str, Vec<Rc<EventData>>> = FxHashMap::default();
for (event, data) in events {
if let Some(v) = hm.get_mut(event) {
v.push(data);
v.push(Rc::new(data));
} else {
hm.insert(event, vec![data]);
hm.insert(event, vec![Rc::new(data)]);
}
}
for (event, datas) in hm {
for node in dom.get_listening_sorted(event) {
for data in &datas {
if node.state.focused {
if let Some(id) = node.mounted_id() {
resolved_events.push(Event {
name: event,
id,
data: data.clone(),
bubbles: event_bubbles(event),
});
}
let focused = node.get::<Focused>();
if focused.is_some() && focused.unwrap().0 {
resolved_events.push(Event {
name: event,
id: node.id(),
data: data.clone(),
bubbles: event_bubbles(event),
});
}
}
}
@ -770,7 +727,7 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> {
MouseEventKind::ScrollUp => ("wheel", get_wheel_data(true)),
}
}
TermEvent::Resize(x, y) => ("resize", EventData::Screen((x, y))),
_ => return None,
};
Some((name, data))

View file

@ -1,12 +1,13 @@
use std::sync::{Arc, Mutex};
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::layout_attributes::{
apply_layout_attributes_cfg, BorderWidths, LayoutConfigeration,
};
use dioxus_native_core::node::OwnedAttributeView;
use dioxus_native_core::node_ref::{AttributeMask, NodeMask, NodeView};
use dioxus_native_core::state::ChildDepState;
use dioxus_native_core_macro::sorted_str_slice;
use dioxus_native_core::node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView};
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use taffy::prelude::*;
use crate::{screen_to_layout_space, unit_to_layout_space};
@ -16,6 +17,7 @@ pub(crate) enum PossiblyUninitalized<T> {
Uninitalized,
Initialized(T),
}
impl<T> PossiblyUninitalized<T> {
pub fn unwrap(self) -> T {
match self {
@ -36,35 +38,35 @@ impl<T> Default for PossiblyUninitalized<T> {
}
}
#[derive(Clone, PartialEq, Default, Debug)]
#[derive(Clone, PartialEq, Default, Debug, Component)]
pub(crate) struct TaffyLayout {
pub style: Style,
pub node: PossiblyUninitalized<Node>,
}
impl ChildDepState for TaffyLayout {
type Ctx = Arc<Mutex<Taffy>>;
type DepState = (Self,);
// use tag to force this to be called when a node is built
const NODE_MASK: NodeMask =
NodeMask::new_with_attrs(AttributeMask::Static(SORTED_LAYOUT_ATTRS))
.with_text()
.with_tag();
#[partial_derive_state]
impl State for TaffyLayout {
type ChildDependencies = (Self,);
type ParentDependencies = ();
type NodeDependencies = ();
/// Setup the layout
fn reduce<'a>(
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(SORTED_LAYOUT_ATTRS))
.with_text();
fn update<'a>(
&mut self,
node: NodeView,
children: impl Iterator<Item = (&'a Self,)>,
ctx: &Self::Ctx,
) -> bool
where
Self::DepState: 'a,
{
node_view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
ctx: &SendAnyMap,
) -> bool {
let mut changed = false;
let mut taffy = ctx.lock().expect("poisoned taffy");
let taffy: &Arc<Mutex<Taffy>> = ctx.get().unwrap();
let mut taffy = taffy.lock().expect("poisoned taffy");
let mut style = Style::default();
if let Some(text) = node.text() {
if let Some(text) = node_view.text() {
let char_len = text.chars().count();
style = Style {
@ -87,14 +89,11 @@ impl ChildDepState for TaffyLayout {
}
} else {
// gather up all the styles from the attribute list
if let Some(attributes) = node.attributes() {
if let Some(attributes) = node_view.attributes() {
for OwnedAttributeView {
attribute, value, ..
} in attributes
{
assert!(SORTED_LAYOUT_ATTRS
.binary_search(&attribute.name.as_ref())
.is_ok());
if let Some(text) = value.as_text() {
apply_layout_attributes_cfg(
&attribute.name,
@ -191,10 +190,22 @@ impl ChildDepState for TaffyLayout {
}
changed
}
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
}
}
// these are the attributes in layout_attiributes in native-core
const SORTED_LAYOUT_ATTRS: &[&str] = &sorted_str_slice!([
const SORTED_LAYOUT_ATTRS: &[&str] = &[
"align-content",
"align-items",
"align-self",
@ -346,5 +357,5 @@ const SORTED_LAYOUT_ATTRS: &[&str] = &sorted_str_slice!([
"word-break",
"word-spacing",
"word-wrap",
"z-index"
]);
"z-index",
];

View file

@ -1,3 +1,4 @@
use crate::focus::Focus;
use anyhow::Result;
use crossterm::{
cursor::{MoveTo, RestorePosition, SavePosition, Show},
@ -5,21 +6,21 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use dioxus_core::*;
use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, NodeMask, SendAnyMap};
use dioxus_html::EventData;
use dioxus_native_core::prelude::*;
use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, SendAnyMap};
use focus::FocusState;
use futures::{
channel::mpsc::{UnboundedReceiver, UnboundedSender},
pin_mut, StreamExt,
};
use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt};
use futures_channel::mpsc::unbounded;
use query::Query;
use std::rc::Rc;
use layout::TaffyLayout;
use prevent_default::PreventDefault;
use std::{io, time::Duration};
use std::{
cell::RefCell,
pin::Pin,
sync::{Arc, Mutex},
};
use std::{io, time::Duration};
use std::{rc::Rc, sync::RwLock};
use style_attributes::StyleModifier;
use taffy::Taffy;
pub use taffy::{geometry::Point, prelude::*};
use tokio::select;
@ -29,18 +30,17 @@ mod config;
mod focus;
mod hooks;
mod layout;
mod node;
pub mod prelude;
mod prevent_default;
pub mod query;
mod render;
mod style;
mod style_attributes;
mod widget;
mod widgets;
pub use config::*;
pub use hooks::*;
pub(crate) use node::*;
pub use query::Query;
// the layout space has a multiplier of 10 to minimize rounding errors
pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
@ -59,7 +59,12 @@ pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
pub struct TuiContext {
tx: UnboundedSender<InputEvent>,
}
impl TuiContext {
pub fn new(tx: UnboundedSender<InputEvent>) -> Self {
Self { tx }
}
pub fn quit(&self) {
self.tx.unbounded_send(InputEvent::Close).unwrap();
}
@ -71,21 +76,25 @@ impl TuiContext {
}
}
pub fn launch(app: Component<()>) {
launch_cfg(app, Config::default())
}
pub fn render<R: Driver>(
cfg: Config,
create_renderer: impl FnOnce(
&Arc<RwLock<RealDom>>,
&Arc<Mutex<Taffy>>,
UnboundedSender<InputEvent>,
) -> R,
) -> Result<()> {
let mut rdom = RealDom::new([
TaffyLayout::to_type_erased(),
Focus::to_type_erased(),
StyleModifier::to_type_erased(),
PreventDefault::to_type_erased(),
]);
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) {
let mut dom = VirtualDom::new_with_props(app, props);
let (handler, state, register_event) = RinkInputHandler::new();
let (handler, mut register_event) = RinkInputHandler::create(&mut rdom);
// Setup input handling
let (event_tx, event_rx) = unbounded();
let (event_tx, mut event_reciever) = unbounded();
let event_tx_clone = event_tx.clone();
if !cfg.headless {
std::thread::spawn(move || {
@ -101,59 +110,22 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
});
}
let cx = dom.base_scope();
let rdom = Rc::new(RefCell::new(RealDom::new()));
let rdom = Arc::new(RwLock::new(rdom));
let taffy = Arc::new(Mutex::new(Taffy::new()));
cx.provide_context(state);
cx.provide_context(TuiContext { tx: event_tx_clone });
cx.provide_context(Query {
rdom: rdom.clone(),
stretch: taffy.clone(),
});
let mut renderer = create_renderer(&rdom, &taffy, event_tx_clone);
{
let mut rdom = rdom.borrow_mut();
let mutations = dom.rebuild();
let (to_update, _) = rdom.apply_mutations(mutations);
renderer.update(&rdom);
let mut any_map = SendAnyMap::new();
any_map.insert(taffy.clone());
let _to_rerender = rdom.update_state(to_update, any_map);
let mut rdom = rdom.write().unwrap();
let _ = rdom.update_state(any_map);
}
render_vdom(
&mut dom,
event_rx,
handler,
cfg,
rdom,
taffy,
register_event,
)
.unwrap();
}
fn render_vdom(
vdom: &mut VirtualDom,
mut event_reciever: UnboundedReceiver<InputEvent>,
handler: RinkInputHandler,
cfg: Config,
rdom: Rc<RefCell<TuiDom>>,
taffy: Arc<Mutex<Taffy>>,
mut register_event: impl FnMut(crossterm::event::Event),
) -> Result<()> {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
#[cfg(all(feature = "hot-reload", debug_assertions))]
let mut hot_reload_rx = {
let (hot_reload_tx, hot_reload_rx) =
tokio::sync::mpsc::unbounded_channel::<dioxus_hot_reload::HotReloadMsg>();
dioxus_hot_reload::connect(move |msg| {
let _ = hot_reload_tx.send(msg);
});
hot_reload_rx
};
let mut terminal = (!cfg.headless).then(|| {
enable_raw_mode().unwrap();
let mut stdout = std::io::stdout();
@ -172,7 +144,7 @@ fn render_vdom(
}
let mut to_rerender = FxDashSet::default();
to_rerender.insert(NodeId(0));
to_rerender.insert(rdom.read().unwrap().root_id());
let mut updated = true;
loop {
@ -187,19 +159,27 @@ fn render_vdom(
if !to_rerender.is_empty() || updated {
updated = false;
fn resize(dims: Rect, taffy: &mut Taffy, rdom: &TuiDom) {
fn resize(dims: Rect, taffy: &mut Taffy, rdom: &RealDom) {
let width = screen_to_layout_space(dims.width);
let height = screen_to_layout_space(dims.height);
let root_node = rdom[NodeId(0)].state.layout.node.unwrap();
let root_node = rdom
.get(rdom.root_id())
.unwrap()
.get::<TaffyLayout>()
.unwrap()
.node
.unwrap();
// the root node fills the entire area
let mut style = *taffy.style(root_node).unwrap();
style.size = Size {
let new_size = Size {
width: Dimension::Points(width),
height: Dimension::Points(height),
};
taffy.set_style(root_node, style).unwrap();
if style.size != new_size {
style.size = new_size;
taffy.set_style(root_node, style).unwrap();
}
let size = Size {
width: AvailableSpace::Definite(width),
@ -210,16 +190,16 @@ fn render_vdom(
if let Some(terminal) = &mut terminal {
execute!(terminal.backend_mut(), SavePosition).unwrap();
terminal.draw(|frame| {
let rdom = rdom.borrow();
let rdom = rdom.write().unwrap();
let mut taffy = taffy.lock().expect("taffy lock poisoned");
// size is guaranteed to not change when rendering
resize(frame.size(), &mut taffy, &rdom);
let root = &rdom[NodeId(0)];
render::render_vnode(frame, &taffy, &rdom, root, cfg, Point::ZERO);
let root = rdom.get(rdom.root_id()).unwrap();
render::render_vnode(frame, &taffy, root, cfg, Point::ZERO);
})?;
execute!(terminal.backend_mut(), RestorePosition, Show).unwrap();
} else {
let rdom = rdom.borrow();
let rdom = rdom.read().unwrap();
resize(
Rect {
x: 0,
@ -233,14 +213,8 @@ fn render_vdom(
}
}
#[cfg(all(feature = "hot-reload", debug_assertions))]
let mut hot_reload_msg = None;
{
let wait = vdom.wait_for_work();
#[cfg(all(feature = "hot-reload", debug_assertions))]
let hot_reload_wait = hot_reload_rx.recv();
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
let hot_reload_wait: std::future::Pending<Option<()>> = std::future::pending();
let wait = renderer.poll_async();
pin_mut!(wait);
@ -253,8 +227,8 @@ fn render_vdom(
InputEvent::UserInput(event) => match event {
TermEvent::Key(key) => {
if matches!(key.code, KeyCode::Char('C' | 'c'))
&& key.modifiers.contains(KeyModifiers::CONTROL)
&& cfg.ctrl_c_quit
&& key.modifiers.contains(KeyModifiers::CONTROL)
&& cfg.ctrl_c_quit
{
break;
}
@ -269,52 +243,34 @@ fn render_vdom(
register_event(evt);
}
},
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;
}
}
}
// if we have a new template, replace the old one
#[cfg(all(feature = "hot-reload", debug_assertions))]
if let Some(msg) = hot_reload_msg {
match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
vdom.replace_template(template);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
break;
}
}
}
{
let evts = {
let mut rdom = rdom.borrow_mut();
handler.get_events(&taffy.lock().expect("taffy lock poisoned"), &mut rdom)
};
{
let evts = {
handler.get_events(
&taffy.lock().expect("taffy lock poisoned"),
&mut rdom.write().unwrap(),
)
};
updated |= handler.state().focus_state.clean();
for e in evts {
renderer.handle_event(&rdom, e.id, e.name, e.data, e.bubbles);
}
}
for e in evts {
vdom.handle_event(e.name, e.data, e.id, e.bubbles)
}
let mut rdom = rdom.borrow_mut();
let mutations = vdom.render_immediate();
handler.prune(&mutations, &rdom);
// updates the dom's nodes
let (to_update, dirty) = rdom.apply_mutations(mutations);
renderer.update(&rdom);
// update the style and layout
let mut rdom = rdom.write().unwrap();
let mut any_map = SendAnyMap::new();
any_map.insert(taffy.clone());
to_rerender = rdom.update_state(to_update, any_map);
let (new_to_rerender, dirty) = rdom.update_state(any_map);
to_rerender = new_to_rerender;
let text_mask = NodeMaskBuilder::new().with_text().build();
for (id, mask) in dirty {
if mask.overlaps(&NodeMask::new().with_text()) {
if mask.overlaps(&text_mask) {
to_rerender.insert(id);
}
}
@ -336,7 +292,20 @@ fn render_vdom(
}
#[derive(Debug)]
enum InputEvent {
pub enum InputEvent {
UserInput(TermEvent),
Close,
}
pub trait Driver {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>);
fn handle_event(
&mut self,
rdom: &Arc<RwLock<RealDom>>,
id: NodeId,
event: &str,
value: Rc<EventData>,
bubbles: bool,
);
fn poll_async(&mut self) -> Pin<Box<dyn Future<Output = ()> + '_>>;
}

View file

@ -0,0 +1,2 @@
#[cfg(feature = "dioxus-bindings")]
pub use crate::widgets::*;

View file

@ -0,0 +1,86 @@
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
#[derive(PartialEq, Debug, Clone, Copy, Component, Default)]
pub(crate) enum PreventDefault {
Focus,
KeyPress,
KeyRelease,
KeyDown,
KeyUp,
MouseDown,
Click,
MouseEnter,
MouseLeave,
MouseOut,
#[default]
Unknown,
MouseOver,
ContextMenu,
Wheel,
MouseUp,
}
#[partial_derive_state]
impl State for PreventDefault {
type ParentDependencies = ();
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: dioxus_native_core::node_ref::NodeMaskBuilder<'static> =
dioxus_native_core::node_ref::NodeMaskBuilder::new()
.with_attrs(dioxus_native_core::node_ref::AttributeMaskBuilder::Some(&[
"dioxus-prevent-default",
]))
.with_listeners();
fn update<'a>(
&mut self,
node_view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
let new = match node_view.attributes().and_then(|mut attrs| {
attrs
.find(|a| a.attribute.name == "dioxus-prevent-default")
.and_then(|a| a.value.as_text())
}) {
Some("onfocus") => PreventDefault::Focus,
Some("onkeypress") => PreventDefault::KeyPress,
Some("onkeyrelease") => PreventDefault::KeyRelease,
Some("onkeydown") => PreventDefault::KeyDown,
Some("onkeyup") => PreventDefault::KeyUp,
Some("onclick") => PreventDefault::Click,
Some("onmousedown") => PreventDefault::MouseDown,
Some("onmouseup") => PreventDefault::MouseUp,
Some("onmouseenter") => PreventDefault::MouseEnter,
Some("onmouseover") => PreventDefault::MouseOver,
Some("onmouseleave") => PreventDefault::MouseLeave,
Some("onmouseout") => PreventDefault::MouseOut,
Some("onwheel") => PreventDefault::Wheel,
Some("oncontextmenu") => PreventDefault::ContextMenu,
_ => return false,
};
if new == *self {
false
} else {
*self = new;
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
}
}

View file

@ -1,17 +1,13 @@
use std::{
cell::{Ref, RefCell},
rc::Rc,
sync::{Arc, Mutex, MutexGuard},
};
use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard};
use dioxus_core::ElementId;
use dioxus_native_core::prelude::*;
use taffy::{
geometry::Point,
prelude::{Layout, Size},
Taffy,
};
use crate::{layout_to_screen_space, TuiDom};
use crate::{layout::TaffyLayout, layout_to_screen_space};
/// Allows querying the layout of nodes after rendering. It will only provide a correct value after a node is rendered.
/// Provided as a root context for all tui applictions.
@ -46,28 +42,38 @@ use crate::{layout_to_screen_space, TuiDom};
/// ```
#[derive(Clone)]
pub struct Query {
pub(crate) rdom: Rc<RefCell<TuiDom>>,
pub(crate) rdom: Arc<RwLock<RealDom>>,
pub(crate) stretch: Arc<Mutex<Taffy>>,
}
impl Query {
pub fn get(&self, id: ElementId) -> ElementRef {
pub fn new(rdom: Arc<RwLock<RealDom>>, stretch: Arc<Mutex<Taffy>>) -> Self {
Self { rdom, stretch }
}
pub fn get(&self, id: NodeId) -> ElementRef {
let rdom = self.rdom.read();
let stretch = self.stretch.lock();
ElementRef::new(
self.rdom.borrow(),
self.stretch.lock().expect("taffy lock poisoned"),
rdom.expect("rdom lock poisoned"),
stretch.expect("taffy lock poisoned"),
id,
)
}
}
pub struct ElementRef<'a> {
inner: Ref<'a, TuiDom>,
inner: RwLockReadGuard<'a, RealDom>,
stretch: MutexGuard<'a, Taffy>,
id: ElementId,
id: NodeId,
}
impl<'a> ElementRef<'a> {
fn new(inner: Ref<'a, TuiDom>, stretch: MutexGuard<'a, Taffy>, id: ElementId) -> Self {
fn new(
inner: RwLockReadGuard<'a, RealDom>,
stretch: MutexGuard<'a, Taffy>,
id: NodeId,
) -> Self {
Self { inner, stretch, id }
}
@ -85,7 +91,15 @@ impl<'a> ElementRef<'a> {
pub fn layout(&self) -> Option<Layout> {
let layout = self
.stretch
.layout(self.inner[self.id].state.layout.node.ok()?)
.layout(
self.inner
.get(self.id)
.unwrap()
.get::<TaffyLayout>()
.unwrap()
.node
.ok()?,
)
.ok();
layout.map(|layout| Layout {
order: layout.order,

View file

@ -1,4 +1,4 @@
use dioxus_native_core::tree::TreeView;
use dioxus_native_core::prelude::*;
use std::io::Stdout;
use taffy::{
geometry::Point,
@ -8,11 +8,13 @@ use taffy::{
use tui::{backend::CrosstermBackend, layout::Rect, style::Color};
use crate::{
focus::Focused,
layout::TaffyLayout,
layout_to_screen_space,
style::{RinkColor, RinkStyle},
style_attributes::{BorderEdge, BorderStyle},
style_attributes::{BorderEdge, BorderStyle, StyleModifier},
widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext},
Config, TuiDom, TuiNode,
Config,
};
const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
@ -20,20 +22,19 @@ const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
pub(crate) fn render_vnode(
frame: &mut tui::Frame<CrosstermBackend<Stdout>>,
layout: &Taffy,
rdom: &TuiDom,
node: &TuiNode,
node: NodeRef,
cfg: Config,
parent_location: Point<f32>,
) {
use dioxus_native_core::node::NodeType;
if let NodeType::Placeholder = &node.node_data.node_type {
if let NodeType::Placeholder = &*node.node_type() {
return;
}
let Layout {
mut location, size, ..
} = layout.layout(node.state.layout.node.unwrap()).unwrap();
} = layout
.layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
.unwrap();
location.x += parent_location.x;
location.y += parent_location.y;
@ -44,8 +45,8 @@ pub(crate) fn render_vnode(
let width = layout_to_screen_space(fx + width).round() as u16 - x;
let height = layout_to_screen_space(fy + height).round() as u16 - y;
match &node.node_data.node_type {
NodeType::Text { text } => {
match &*node.node_type() {
NodeType::Text(text) => {
#[derive(Default)]
struct Label<'a> {
text: &'a str,
@ -64,8 +65,8 @@ pub(crate) fn render_vnode(
}
let label = Label {
text,
style: node.state.style.core,
text: &text.text,
style: node.get::<StyleModifier>().unwrap().core,
};
let area = Rect::new(x, y, width, height);
@ -82,15 +83,15 @@ pub(crate) fn render_vnode(
frame.render_widget(WidgetWithContext::new(node, cfg), area);
}
for c in rdom.children_ids(node.node_data.node_id).unwrap() {
render_vnode(frame, layout, rdom, &rdom[*c], cfg, location);
for c in node.children() {
render_vnode(frame, layout, c, cfg, location);
}
}
NodeType::Placeholder => unreachable!(),
}
}
impl RinkWidget for &TuiNode {
impl RinkWidget for NodeRef<'_> {
fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
use tui::symbols::line::*;
@ -266,18 +267,22 @@ impl RinkWidget for &TuiNode {
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
let mut new_cell = RinkCell::default();
if let Some(c) = self.state.style.core.bg {
if let Some(c) = self.get::<StyleModifier>().unwrap().core.bg {
new_cell.bg = c;
}
if self.state.focused {
new_cell.bg.alpha = 100;
new_cell.bg.color = new_cell.bg.blend(Color::White);
if let Some(focused) = self.get::<Focused>() {
if focused.0 {
new_cell.bg.alpha = 100;
new_cell.bg.color = new_cell.bg.blend(Color::White);
}
}
buf.set(x, y, new_cell);
}
}
let borders = &self.state.style.modifier.borders;
let style = self.get::<StyleModifier>().unwrap();
let borders = &style.modifier.borders;
let last_edge = &borders.left;
let current_edge = &borders.top;
@ -294,7 +299,7 @@ impl RinkWidget for &TuiNode {
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(self.state.style.core.fg);
let color = current_edge.color.or(style.core.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
@ -329,7 +334,7 @@ impl RinkWidget for &TuiNode {
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(self.state.style.core.fg);
let color = current_edge.color.or(style.core.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
@ -364,7 +369,7 @@ impl RinkWidget for &TuiNode {
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(self.state.style.core.fg);
let color = current_edge.color.or(style.core.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
@ -399,7 +404,7 @@ impl RinkWidget for &TuiNode {
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(self.state.style.core.fg);
let color = current_edge.color.or(style.core.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;

View file

@ -32,36 +32,48 @@
use dioxus_native_core::{
layout_attributes::parse_value,
node::OwnedAttributeView,
node_ref::{AttributeMask, NodeMask, NodeView},
state::ParentDepState,
node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView},
prelude::*,
};
use dioxus_native_core_macro::sorted_str_slice;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
use taffy::prelude::*;
use crate::style::{RinkColor, RinkStyle};
#[derive(Default, Clone, PartialEq, Debug)]
#[derive(Default, Clone, PartialEq, Debug, Component)]
pub struct StyleModifier {
pub core: RinkStyle,
pub modifier: TuiModifier,
}
impl ParentDepState for StyleModifier {
type Ctx = ();
type DepState = (Self,);
// todo: seperate each attribute into it's own class
const NODE_MASK: NodeMask =
NodeMask::new_with_attrs(AttributeMask::Static(SORTED_STYLE_ATTRS)).with_element();
#[partial_derive_state]
impl State for StyleModifier {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
fn reduce(&mut self, node: NodeView, parent: Option<(&Self,)>, _: &Self::Ctx) -> bool {
// todo: seperate each attribute into it's own class
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(SORTED_STYLE_ATTRS))
.with_element();
fn update<'a>(
&mut self,
node_view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
let mut new = StyleModifier::default();
if parent.is_some() {
new.core.fg = None;
}
// handle text modifier elements
if node.namespace().is_none() {
if let Some(tag) = node.tag() {
if node_view.namespace().is_none() {
if let Some(tag) = node_view.tag() {
match tag {
"b" => apply_style_attributes("font-weight", "bold", &mut new),
"strong" => apply_style_attributes("font-weight", "bold", &mut new),
@ -79,7 +91,7 @@ impl ParentDepState for StyleModifier {
}
// gather up all the styles from the attribute list
if let Some(attrs) = node.attributes() {
if let Some(attrs) = node_view.attributes() {
for OwnedAttributeView {
attribute, value, ..
} in attrs
@ -103,6 +115,18 @@ impl ParentDepState for StyleModifier {
false
}
}
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(Default, Clone, PartialEq, Debug)]
@ -598,7 +622,7 @@ fn apply_transition(_name: &str, _value: &str, _style: &mut StyleModifier) {
todo!()
}
const SORTED_STYLE_ATTRS: &[&str] = &sorted_str_slice!([
const SORTED_STYLE_ATTRS: &[&str] = &[
"animation",
"animation-delay",
"animation-direction",
@ -799,5 +823,5 @@ const SORTED_STYLE_ATTRS: &[&str] = &sorted_str_slice!([
"text-justify",
"text-overflow",
"text-shadow",
"text-transform"
]);
"text-transform",
];

73
packages/rink/test.html Normal file
View file

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style>
html,
body {
height: 100%;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: black;
/* justify-content: center;
align-items: center; */
/* margin: auto; */
}
.smaller {
height: 70%;
width: 70%;
background-color: green;
/* justify-content: center; */
/* align-items: center; */
}
.superinner {
height: 100%;
width: 100%;
/* display: flex; */
/* */
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
margin-right: 20px;
/* */
background-color: red;
justify-content: center;
align-items: center;
flex-direction: column;
/* margin: 20px; */
/* margin: 20px; */
}
</style>
</head>
<body>
<div class="container">
<div class="smaller">
<div class="superinner">
<h1>Hello World</h1>
<p>This is a test</p>
</div>
</div>
</div>
<!-- <div class="container">
<div class="smaller">
hello world
<div style="color: green; margin: 40px;">
goodbye
<div style="color:red;">
asdasdasd
</div>
</div>
</div>
</div> -->
</body>
</html>

View file

@ -36,7 +36,6 @@ thiserror = "1.0.30"
futures-util = "0.3.21"
serde = { version = "1", optional = true }
serde_urlencoded = { version = "0.7.1", optional = true }
simple_logger = "4.0.0"
[features]
default = ["query"]
@ -46,7 +45,6 @@ wasm_test = []
[dev-dependencies]
console_error_panic_hook = "0.1.7"
log = "0.4.14"
wasm-logger = "0.2.0"
wasm-bindgen-test = "0.3"
gloo-utils = "0.1.2"

View file

@ -1,12 +1,19 @@
[package]
name = "rsx-rosetta"
version = "0.0.0"
edition = "2018"
version = "0.3.0"
edition = "2021"
authors = ["Jonathan Kelley"]
description = "Autofomatter for Dioxus RSX"
license = "MIT/Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-autofmt = { path = "../autofmt" }
dioxus-autofmt = { path = "../autofmt", version = "0.3.0" }
dioxus-rsx = { path = "../rsx" , version = "^0.0.3" }
html_parser = "0.6.3"
proc-macro2 = "1.0.49"

View file

@ -29,7 +29,7 @@ pub struct FileMapBuildResult<Ctx: HotReloadingContext> {
pub struct FileMap<Ctx: HotReloadingContext> {
pub map: HashMap<PathBuf, (String, Option<Template<'static>>)>,
in_workspace: HashMap<PathBuf, bool>,
in_workspace: HashMap<PathBuf, Option<PathBuf>>,
phantom: std::marker::PhantomData<Ctx>,
}
@ -112,11 +112,8 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
) {
// if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root
// we need to check if the file is in a workspace or not and strip the prefix accordingly
let prefix = if in_workspace {
crate_dir.parent().ok_or(io::Error::new(
io::ErrorKind::Other,
"Could not load workspace",
))?
let prefix = if let Some(workspace) = &in_workspace {
workspace
} else {
crate_dir
};
@ -173,9 +170,9 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
Ok(UpdateResult::NeedsRebuild)
}
fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result<bool> {
fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result<Option<PathBuf>> {
if let Some(in_workspace) = self.in_workspace.get(crate_dir) {
Ok(*in_workspace)
Ok(in_workspace.clone())
} else {
let mut cmd = Cmd::new();
let manafest_path = crate_dir.join("Cargo.toml");
@ -186,9 +183,10 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let in_workspace = metadata.workspace_root != crate_dir;
let workspace_path = in_workspace.then(|| metadata.workspace_root.into());
self.in_workspace
.insert(crate_dir.to_path_buf(), in_workspace);
Ok(in_workspace)
.insert(crate_dir.to_path_buf(), workspace_path.clone());
Ok(workspace_path)
}
}
}

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