Merge branch 'upstream' into make-widgets-cross-framework

This commit is contained in:
Evan Almloff committed 2023-04-06 14:07:01 -05:00
commit 8b5f2cb02f
22 files changed
+380 -122

No files matched your search

+8
View File
@@ -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
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 {
+8 -2
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",
] }
+1
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]),
+25 -12
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,
});
}
}
}
+5 -5
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,
},
+10
View File
@@ -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;
+4
View File
@@ -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)
}
+20
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
+33 -6
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,
}
+16 -5
View File
@@ -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,
+11 -24
View File
@@ -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 {
+2 -2
View File
@@ -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 {
-2
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"
+10 -3
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"
@@ -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)
}
}
}
+5
View File
@@ -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 == '}' {
+38 -3
View File
@@ -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 {
+45 -5
View File
@@ -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 --&gt;".into(),),
Node(0,),
PreRendered(
+40
View File
@@ -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>"#
);
}
+3 -2
View File
@@ -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,
);
}
}