Merge branch 'upstream' into make-widgets-cross-framework
This commit is contained in:
commit
8b5f2cb02f
22 files changed
+380
-122
No files matched your search
@@ -92,6 +92,8 @@ With Coroutines, we can centralize our async logic. The `rx` parameter is an Cha
|
||||
|
||||
|
||||
```rust
|
||||
use futures_util::stream::StreamExt;
|
||||
|
||||
enum ProfileUpdate {
|
||||
SetUsername(String),
|
||||
SetAge(i32)
|
||||
@@ -117,6 +119,10 @@ cx.render(rsx!{
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
> Note: In order to use/run the `rx.next().await` statement you will need to extend the [`Stream`] trait (used by [`UnboundedReceiver`]) by adding 'futures_util' as a dependency to your project and adding the `use futures_util::stream::StreamExt;`.
|
||||
|
||||
|
||||
For sufficiently complex apps, we could build a bunch of different useful "services" that loop on channels to update the app.
|
||||
|
||||
```rust
|
||||
@@ -164,6 +170,8 @@ fn Banner(cx: Scope) -> Element {
|
||||
Now, in our sync service, we can structure our state however we want. We only need to update the view values when ready.
|
||||
|
||||
```rust
|
||||
use futures_util::stream::StreamExt;
|
||||
|
||||
enum SyncAction {
|
||||
SetUsername(String),
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@ You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.
|
||||
|
||||
Inside the initialization closure, you will typically make calls to other `cx` methods. For example:
|
||||
|
||||
- The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.schedule_update) to make Dioxus re-render the component whenever it changes.
|
||||
- The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope
|
||||
- The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.schedule_update) to make Dioxus re-render the component whenever it changes.
|
||||
- The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope
|
||||
+86
-39
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
] }
|
||||
|
||||
|
||||
@@ -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]),
|
||||
|
||||
+25
-12
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
|
||||
@@ -18,6 +18,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,
|
||||
@@ -44,6 +45,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 +58,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;
|
||||
|
||||
@@ -54,6 +54,10 @@ pub fn build(
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
// These are commented out because wry is currently broken in wry
|
||||
// let mut web_context = WebContext::new(cfg.data_dir.clone());
|
||||
// .with_web_context(&mut web_context);
|
||||
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ pub struct DioxusState {
|
||||
|
||||
impl DioxusState {
|
||||
/// Initialize the DioxusState in the RealDom
|
||||
pub fn create(rdom: &mut RealDom) -> Self {
|
||||
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)));
|
||||
@@ -48,7 +48,11 @@ impl DioxusState {
|
||||
self.node_id_mapping.get(element_id.0).copied().flatten()
|
||||
}
|
||||
|
||||
fn set_element_id(&mut self, mut node: NodeMut, element_id: ElementId) {
|
||||
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 {
|
||||
@@ -57,7 +61,7 @@ impl DioxusState {
|
||||
self.node_id_mapping[element_id.0] = Some(node_id);
|
||||
}
|
||||
|
||||
fn load_child(&self, rdom: &RealDom, path: &[u8]) -> NodeId {
|
||||
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];
|
||||
@@ -67,7 +71,11 @@ impl DioxusState {
|
||||
}
|
||||
|
||||
/// 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(&mut self, rdom: &mut RealDom, mutations: Mutations) {
|
||||
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 {
|
||||
@@ -222,7 +230,10 @@ impl DioxusState {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_template_node(rdom: &mut RealDom, node: &TemplateNode) -> NodeId {
|
||||
fn create_template_node<V: FromAnyValue + Send + Sync>(
|
||||
rdom: &mut RealDom<V>,
|
||||
node: &TemplateNode,
|
||||
) -> NodeId {
|
||||
match node {
|
||||
TemplateNode::Element {
|
||||
tag,
|
||||
|
||||
@@ -16,37 +16,27 @@ use crate::{NodeId, NodeMask};
|
||||
|
||||
#[derive(Default)]
|
||||
struct DirtyNodes {
|
||||
passes_dirty: Vec<u64>,
|
||||
nodes_dirty: FxHashSet<NodeId>,
|
||||
}
|
||||
|
||||
impl DirtyNodes {
|
||||
pub fn add_node(&mut self, node_id: NodeId) {
|
||||
let node_id = node_id.uindex();
|
||||
let index = node_id / 64;
|
||||
let bit = node_id % 64;
|
||||
let encoded = 1 << bit;
|
||||
if let Some(passes) = self.passes_dirty.get_mut(index) {
|
||||
*passes |= encoded;
|
||||
} else {
|
||||
self.passes_dirty.resize(index + 1, 0);
|
||||
self.passes_dirty[index] |= encoded;
|
||||
}
|
||||
self.nodes_dirty.insert(node_id);
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.passes_dirty.iter().all(|dirty| *dirty == 0)
|
||||
self.nodes_dirty.is_empty()
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<usize> {
|
||||
let index = self.passes_dirty.iter().position(|dirty| *dirty != 0)?;
|
||||
let passes = self.passes_dirty[index];
|
||||
let node_id = passes.trailing_zeros();
|
||||
let encoded = 1 << node_id;
|
||||
self.passes_dirty[index] &= !encoded;
|
||||
Some((index * 64) + node_id as usize)
|
||||
pub fn pop(&mut self) -> Option<NodeId> {
|
||||
self.nodes_dirty.iter().next().copied().map(|id| {
|
||||
self.nodes_dirty.remove(&id);
|
||||
id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the dirty nodes sorted by height for each pass. We resolve passes based on the height of the node in order to avoid resolving any node twice in a pass.
|
||||
#[derive(Clone, Unique)]
|
||||
pub struct DirtyNodeStates {
|
||||
dirty: Arc<FxHashMap<TypeId, RwLock<BTreeMap<u16, DirtyNodes>>>>,
|
||||
@@ -76,7 +66,7 @@ impl DirtyNodeStates {
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_front(&self, pass_id: TypeId) -> Option<(u16, usize)> {
|
||||
fn pop_front(&self, pass_id: TypeId) -> Option<(u16, NodeId)> {
|
||||
let mut values = self.dirty.get(&pass_id)?.write();
|
||||
let mut value = values.first_entry()?;
|
||||
let height = *value.key();
|
||||
@@ -89,7 +79,7 @@ impl DirtyNodeStates {
|
||||
Some((height, id))
|
||||
}
|
||||
|
||||
fn pop_back(&self, pass_id: TypeId) -> Option<(u16, usize)> {
|
||||
fn pop_back(&self, pass_id: TypeId) -> Option<(u16, NodeId)> {
|
||||
let mut values = self.dirty.get(&pass_id)?.write();
|
||||
let mut value = values.last_entry()?;
|
||||
let height = *value.key();
|
||||
@@ -214,7 +204,6 @@ pub fn run_pass<V: FromAnyValue + Send + Sync>(
|
||||
match pass_direction {
|
||||
PassDirection::ParentToChild => {
|
||||
while let Some((height, id)) = dirty.pop_front(type_id) {
|
||||
let id = tree.id_at(id).unwrap();
|
||||
if (update_node)(id, ctx) {
|
||||
nodes_updated.insert(id);
|
||||
for id in tree.children_ids(id) {
|
||||
@@ -227,7 +216,6 @@ pub fn run_pass<V: FromAnyValue + Send + Sync>(
|
||||
}
|
||||
PassDirection::ChildToParent => {
|
||||
while let Some((height, id)) = dirty.pop_back(type_id) {
|
||||
let id = tree.id_at(id).unwrap();
|
||||
if (update_node)(id, ctx) {
|
||||
nodes_updated.insert(id);
|
||||
if let Some(id) = tree.parent_id(id) {
|
||||
@@ -240,7 +228,6 @@ pub fn run_pass<V: FromAnyValue + Send + Sync>(
|
||||
}
|
||||
PassDirection::AnyOrder => {
|
||||
while let Some((height, id)) = dirty.pop_back(type_id) {
|
||||
let id = tree.id_at(id).unwrap();
|
||||
if (update_node)(id, ctx) {
|
||||
nodes_updated.insert(id);
|
||||
for dependant in &dependants {
|
||||
|
||||
@@ -49,7 +49,7 @@ pub trait TreeMut: TreeRef {
|
||||
|
||||
impl<'a> TreeRef for TreeRefView<'a> {
|
||||
fn parent_id(&self, id: NodeId) -> Option<NodeId> {
|
||||
self.get(id).unwrap().parent
|
||||
self.get(id).ok()?.parent
|
||||
}
|
||||
|
||||
fn children_ids(&self, id: NodeId) -> Vec<NodeId> {
|
||||
@@ -59,7 +59,7 @@ impl<'a> TreeRef for TreeRefView<'a> {
|
||||
}
|
||||
|
||||
fn height(&self, id: NodeId) -> Option<u16> {
|
||||
Some(self.get(id).unwrap().height)
|
||||
Some(self.get(id).ok()?.height)
|
||||
}
|
||||
|
||||
fn contains(&self, id: NodeId) -> bool {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,11 @@ impl FromStr for IfmtInput {
|
||||
let mut current_captured = String::new();
|
||||
while let Some(c) = chars.next() {
|
||||
if c == ':' {
|
||||
// two :s in a row is a path, not a format arg
|
||||
if chars.next_if(|c| *c == ':').is_some() {
|
||||
current_captured.push_str("::");
|
||||
continue;
|
||||
}
|
||||
let mut current_format_args = String::new();
|
||||
for c in chars.by_ref() {
|
||||
if c == '}' {
|
||||
|
||||
@@ -17,6 +17,12 @@ pub enum Segment {
|
||||
Attr(usize),
|
||||
Node(usize),
|
||||
PreRendered(String),
|
||||
/// A marker for where to insert a dynamic styles
|
||||
StyleMarker {
|
||||
// If the marker is inside a style tag or not
|
||||
// This will be true if there are static styles
|
||||
inside_style_tag: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Write for StringChain {
|
||||
@@ -61,16 +67,45 @@ impl StringCache {
|
||||
} => {
|
||||
cur_path.push(root_idx);
|
||||
write!(chain, "<{tag}")?;
|
||||
// we need to collect the styles and write them at the end
|
||||
let mut styles = Vec::new();
|
||||
let mut has_dynamic_attrs = false;
|
||||
for attr in *attrs {
|
||||
match attr {
|
||||
TemplateAttribute::Static { name, value, .. } => {
|
||||
write!(chain, " {name}=\"{value}\"")?;
|
||||
TemplateAttribute::Static {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
} => {
|
||||
if let Some("style") = namespace {
|
||||
styles.push((name, value));
|
||||
} else {
|
||||
write!(chain, " {name}=\"{value}\"")?;
|
||||
}
|
||||
}
|
||||
TemplateAttribute::Dynamic { id: index } => {
|
||||
chain.segments.push(Segment::Attr(*index))
|
||||
chain.segments.push(Segment::Attr(*index));
|
||||
has_dynamic_attrs = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write the styles
|
||||
if !styles.is_empty() {
|
||||
write!(chain, " style=\"")?;
|
||||
for (name, value) in styles {
|
||||
write!(chain, "{name}:{value};")?;
|
||||
}
|
||||
chain.segments.push(Segment::StyleMarker {
|
||||
inside_style_tag: true,
|
||||
});
|
||||
write!(chain, "\"")?;
|
||||
} else if has_dynamic_attrs {
|
||||
chain.segments.push(Segment::StyleMarker {
|
||||
inside_style_tag: false,
|
||||
});
|
||||
}
|
||||
|
||||
if children.is_empty() && tag_is_self_closing(tag) {
|
||||
write!(chain, "/>")?;
|
||||
} else {
|
||||
|
||||
@@ -70,15 +70,24 @@ impl Renderer {
|
||||
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
|
||||
.clone();
|
||||
|
||||
// We need to keep track of the dynamic styles so we can insert them into the right place
|
||||
let mut accumulated_dynamic_styles = Vec::new();
|
||||
|
||||
for segment in entry.segments.iter() {
|
||||
match segment {
|
||||
Segment::Attr(idx) => {
|
||||
let attr = &template.dynamic_attrs[*idx];
|
||||
match attr.value {
|
||||
AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?,
|
||||
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
|
||||
_ => {}
|
||||
};
|
||||
if attr.namespace == Some("style") {
|
||||
accumulated_dynamic_styles.push(attr);
|
||||
} else {
|
||||
match attr.value {
|
||||
AttributeValue::Text(value) => {
|
||||
write!(buf, " {}=\"{}\"", attr.name, value)?
|
||||
}
|
||||
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
|
||||
DynamicNode::Component(node) => {
|
||||
@@ -128,6 +137,34 @@ impl Renderer {
|
||||
},
|
||||
|
||||
Segment::PreRendered(contents) => write!(buf, "{contents}")?,
|
||||
|
||||
Segment::StyleMarker { inside_style_tag } => {
|
||||
if !accumulated_dynamic_styles.is_empty() {
|
||||
// if we are inside a style tag, we don't need to write the style attribute
|
||||
if !*inside_style_tag {
|
||||
write!(buf, " style=\"")?;
|
||||
}
|
||||
for attr in &accumulated_dynamic_styles {
|
||||
match attr.value {
|
||||
AttributeValue::Text(value) => {
|
||||
write!(buf, "{}:{};", attr.name, value)?
|
||||
}
|
||||
AttributeValue::Bool(value) => {
|
||||
write!(buf, "{}:{};", attr.name, value)?
|
||||
}
|
||||
AttributeValue::Float(f) => write!(buf, "{}:{};", attr.name, f)?,
|
||||
AttributeValue::Int(i) => write!(buf, "{}:{};", attr.name, i)?,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
if !*inside_style_tag {
|
||||
write!(buf, "\"")?;
|
||||
}
|
||||
|
||||
// clear the accumulated styles
|
||||
accumulated_dynamic_styles.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +205,9 @@ fn to_string_works() {
|
||||
vec![
|
||||
PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
|
||||
Attr(0,),
|
||||
StyleMarker {
|
||||
inside_style_tag: false,
|
||||
},
|
||||
PreRendered(">Hello world 1 -->".into(),),
|
||||
Node(0,),
|
||||
PreRendered(
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn static_styles() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
render! { div { width: "100px" } }
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
_ = dom.rebuild();
|
||||
|
||||
assert_eq!(
|
||||
dioxus_ssr::render(&dom),
|
||||
r#"<div style="width:100px;"></div>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partially_dynamic_styles() {
|
||||
let dynamic = 123;
|
||||
|
||||
assert_eq!(
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
div { width: "100px", height: "{dynamic}px" }
|
||||
}),
|
||||
r#"<div style="width:100px;height:123px;"></div>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_styles() {
|
||||
let dynamic = 123;
|
||||
|
||||
assert_eq!(
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
div { width: "{dynamic}px" }
|
||||
}),
|
||||
r#"<div style="width:123px;"></div>"#
|
||||
);
|
||||
}
|
||||
@@ -128,10 +128,11 @@ impl WebsysDom {
|
||||
mounted_id = Some(id);
|
||||
let name = attribute.name;
|
||||
if let AttributeValue::Listener(_) = value {
|
||||
let event_name = &name[2..];
|
||||
self.interpreter.new_event_listener(
|
||||
&name[2..],
|
||||
event_name,
|
||||
id.0 as u32,
|
||||
event_bubbles(name) as u8,
|
||||
event_bubbles(event_name) as u8,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user