Merge branch 'master' into maybe-sync-signal

This commit is contained in:
Evan Almloff 2023-12-14 12:29:32 -06:00
parent c1bfe9514f
commit 70c3abb8df
118 changed files with 2206 additions and 412 deletions

View file

@ -33,7 +33,7 @@ jobs:
# cd fermi && mdbook build -d ../nightly/fermi && cd .. # cd fermi && mdbook build -d ../nightly/fermi && cd ..
- name: Deploy 🚀 - name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.4.3 uses: JamesIves/github-pages-deploy-action@v4.5.0
with: with:
branch: gh-pages # The branch the action should deploy to. branch: gh-pages # The branch the action should deploy to.
folder: docs/nightly # The folder the action should deploy. folder: docs/nightly # The folder the action should deploy.

View file

@ -39,7 +39,7 @@ jobs:
# cd fermi && mdbook build -d ../nightly/fermi && cd .. # cd fermi && mdbook build -d ../nightly/fermi && cd ..
- name: Deploy 🚀 - name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.4.3 uses: JamesIves/github-pages-deploy-action@v4.5.0
with: with:
branch: gh-pages # The branch the action should deploy to. branch: gh-pages # The branch the action should deploy to.
folder: docs/nightly # The folder the action should deploy. folder: docs/nightly # The folder the action should deploy.

View file

@ -124,8 +124,6 @@ jobs:
} }
steps: steps:
- uses: actions/checkout@v4
- name: install stable - name: install stable
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
@ -141,6 +139,11 @@ jobs:
workspaces: core -> ../target workspaces: core -> ../target
save-if: ${{ matrix.features.key == 'all' }} save-if: ${{ matrix.features.key == 'all' }}
- name: Install rustfmt
run: rustup component add rustfmt
- uses: actions/checkout@v4
- name: test - name: test
run: | run: |
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }} ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}

View file

@ -86,8 +86,7 @@ jobs:
# working-directory: tokio # working-directory: tokio
env: env:
# todo: disable memory leaks ignore MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
PROPTEST_CASES: 10 PROPTEST_CASES: 10
# Cache the global cargo directory, but NOT the local `target` directory which # Cache the global cargo directory, but NOT the local `target` directory which

View file

@ -20,7 +20,7 @@ jobs:
steps: steps:
# Do our best to cache the toolchain and node install steps # Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: 16 node-version: 16
- name: Install Rust - name: Install Rust

View file

@ -50,7 +50,7 @@ members = [
exclude = ["examples/mobile_demo"] exclude = ["examples/mobile_demo"]
[workspace.package] [workspace.package]
version = "0.4.2" version = "0.4.3"
# dependencies that are shared across packages # dependencies that are shared across packages
[workspace.dependencies] [workspace.dependencies]
@ -77,7 +77,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" }
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" } dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" } rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
dioxus-signals = { path = "packages/signals" } dioxus-signals = { path = "packages/signals" }
generational-box = { path = "packages/generational-box" } generational-box = { path = "packages/generational-box", version = "0.4.3" }
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" } dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" } dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" } dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
@ -88,7 +88,7 @@ slab = "0.4.2"
futures-channel = "0.3.21" futures-channel = "0.3.21"
futures-util = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false }
rustc-hash = "1.1.0" rustc-hash = "1.1.0"
wasm-bindgen = "0.2.87" wasm-bindgen = "0.2.88"
html_parser = "0.7.0" html_parser = "0.7.0"
thiserror = "1.0.40" thiserror = "1.0.40"
prettyplease = { package = "prettier-please", version = "0.2", features = [ prettyplease = { package = "prettier-please", version = "0.2", features = [
@ -99,7 +99,7 @@ prettyplease = { package = "prettier-please", version = "0.2", features = [
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly # It is not meant to be published, but is used so "cargo run --example XYZ" works properly
[package] [package]
name = "dioxus-examples" name = "dioxus-examples"
version = "0.0.0" version = "0.4.3"
authors = ["Jonathan Kelley"] authors = ["Jonathan Kelley"]
edition = "2021" edition = "2021"
description = "Top level crate for the Dioxus repository" description = "Top level crate for the Dioxus repository"

View file

@ -24,12 +24,64 @@ script = [
] ]
script_runner = "@duckscript" script_runner = "@duckscript"
[tasks.format]
command = "cargo"
args = ["fmt", "--all"]
[tasks.check]
command = "cargo"
args = ["check", "--workspace", "--examples", "--tests"]
[tasks.clippy]
command = "cargo"
args = [
"clippy",
"--workspace",
"--examples",
"--tests",
"--",
"-D",
"warnings",
]
[tasks.tidy]
category = "Formatting"
dependencies = ["format", "check", "clippy"]
description = "Format and Check workspace"
[tasks.install-miri]
toolchain = "nightly"
install_crate = { rustup_component_name = "miri", binary = "cargo +nightly miri", test_arg = "--help" }
private = true
[tasks.miri-native]
command = "cargo"
toolchain = "nightly"
dependencies = ["install-miri"]
args = [
"miri",
"test",
"--package",
"dioxus-native-core",
"--test",
"miri_native",
]
[tasks.miri-stress]
command = "cargo"
toolchain = "nightly"
dependencies = ["install-miri"]
args = ["miri", "test", "--package", "dioxus-core", "--test", "miri_stress"]
[tasks.miri]
dependencies = ["miri-native", "miri-stress"]
[tasks.tests] [tasks.tests]
category = "Testing" category = "Testing"
dependencies = ["tests-setup"] dependencies = ["tests-setup"]
description = "Run all tests" description = "Run all tests"
env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]} env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"] }
run_task = {name = ["test-flow", "test-with-browser"], fork = true} run_task = { name = ["test-flow", "test-with-browser"], fork = true }
[tasks.build] [tasks.build]
command = "cargo" command = "cargo"
@ -42,10 +94,24 @@ private = true
[tasks.test] [tasks.test]
dependencies = ["build"] dependencies = ["build"]
command = "cargo" command = "cargo"
args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"] args = [
"test",
"--lib",
"--bins",
"--tests",
"--examples",
"--workspace",
"--exclude",
"dioxus-router",
"--exclude",
"dioxus-desktop",
]
private = true private = true
[tasks.test-with-browser] [tasks.test-with-browser]
env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] } env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = [
"**/packages/router",
"**/packages/desktop",
] }
private = true private = true
workspace = true workspace = true

View file

@ -62,6 +62,7 @@ fn app(cx: Scope) -> Element {
div { id: "wrapper", div { id: "wrapper",
div { class: "app", div { class: "app",
div { class: "calculator", div { class: "calculator",
tabindex: "0",
onkeydown: handle_key_down_event, onkeydown: handle_key_down_event,
div { class: "calculator-display", val.to_string() } div { class: "calculator-display", val.to_string() }
div { class: "calculator-keypad", div { class: "calculator-keypad",

View file

@ -2,6 +2,7 @@
name = "openid_auth_demo" name = "openid_auth_demo"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element {
a: "asd".to_string(), a: "asd".to_string(),
c: "asd".to_string(), c: "asd".to_string(),
d: Some("asd".to_string()), d: Some("asd".to_string()),
e: Some("asd".to_string()),
}
Button {
a: "asd".to_string(),
b: "asd".to_string(),
c: "asd".to_string(),
d: Some("asd".to_string()),
e: "asd".to_string(), e: "asd".to_string(),
} }
Button {
a: "asd".to_string(),
c: "asd".to_string(),
d: Some("asd".to_string()),
}
}) })
} }

View file

@ -2,6 +2,7 @@
name = "query_segments_demo" name = "query_segments_demo"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop" } dioxus-desktop = { path = "../../packages/desktop" }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
dioxus-web = { path = "../../packages/web" } dioxus-web = { path = "../../packages/web" }

View file

@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
use dioxus_rsx::IfmtInput; use dioxus_rsx::IfmtInput;
use crate::write_ifmt; use crate::{indent::IndentOptions, write_ifmt};
/// The output buffer that tracks indent and string /// The output buffer that tracks indent and string
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Buffer { pub struct Buffer {
pub buf: String, pub buf: String,
pub indent: usize, pub indent_level: usize,
pub indent: IndentOptions,
} }
impl Buffer { impl Buffer {
@ -31,16 +32,16 @@ impl Buffer {
} }
pub fn tab(&mut self) -> Result { pub fn tab(&mut self) -> Result {
self.write_tabs(self.indent) self.write_tabs(self.indent_level)
} }
pub fn indented_tab(&mut self) -> Result { pub fn indented_tab(&mut self) -> Result {
self.write_tabs(self.indent + 1) self.write_tabs(self.indent_level + 1)
} }
pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result { pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
for _ in 0..num { for _ in 0..num {
write!(self.buf, " ")? write!(self.buf, "{}", self.indent.indent_str())?
} }
Ok(()) Ok(())
} }

View file

@ -66,7 +66,7 @@ impl Writer<'_> {
// check if we have a lot of attributes // check if we have a lot of attributes
let attr_len = self.is_short_attrs(attributes); let attr_len = self.is_short_attrs(attributes);
let is_short_attr_list = (attr_len + self.out.indent * 4) < 80; let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
let children_len = self.is_short_children(children); let children_len = self.is_short_children(children);
let is_small_children = children_len.is_some(); let is_small_children = children_len.is_some();
@ -86,7 +86,7 @@ impl Writer<'_> {
// if we have few children and few attributes, make it a one-liner // if we have few children and few attributes, make it a one-liner
if is_short_attr_list && is_small_children { if is_short_attr_list && is_small_children {
if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 { if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
opt_level = ShortOptimization::Oneliner; opt_level = ShortOptimization::Oneliner;
} else { } else {
opt_level = ShortOptimization::PropsOnTop; opt_level = ShortOptimization::PropsOnTop;
@ -185,11 +185,11 @@ impl Writer<'_> {
} }
while let Some(attr) = attr_iter.next() { while let Some(attr) = attr_iter.next() {
self.out.indent += 1; self.out.indent_level += 1;
if !sameline { if !sameline {
self.write_comments(attr.attr.start())?; self.write_comments(attr.attr.start())?;
} }
self.out.indent -= 1; self.out.indent_level -= 1;
if !sameline { if !sameline {
self.out.indented_tabbed_line()?; self.out.indented_tabbed_line()?;
@ -398,14 +398,14 @@ impl Writer<'_> {
for idx in start.line..end.line { for idx in start.line..end.line {
let line = &self.src[idx]; let line = &self.src[idx];
if line.trim().starts_with("//") { if line.trim().starts_with("//") {
for _ in 0..self.out.indent + 1 { for _ in 0..self.out.indent_level + 1 {
write!(self.out, " ")? write!(self.out, " ")?
} }
writeln!(self.out, "{}", line.trim()).unwrap(); writeln!(self.out, "{}", line.trim()).unwrap();
} }
} }
for _ in 0..self.out.indent { for _ in 0..self.out.indent_level {
write!(self.out, " ")? write!(self.out, " ")?
} }

View file

@ -29,7 +29,7 @@ impl Writer<'_> {
let first_line = &self.src[start.line - 1]; let first_line = &self.src[start.line - 1];
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?; write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4; let prev_block_indent_level = self.out.indent.count_indents(first_line);
for (id, line) in self.src[start.line..end.line].iter().enumerate() { for (id, line) in self.src[start.line..end.line].iter().enumerate() {
writeln!(self.out)?; writeln!(self.out)?;
@ -43,9 +43,9 @@ impl Writer<'_> {
}; };
// trim the leading whitespace // trim the leading whitespace
let previous_indent = crate::leading_whitespaces(line) / 4; let previous_indent = self.out.indent.count_indents(line);
let offset = previous_indent.saturating_sub(prev_block_indent_level); let offset = previous_indent.saturating_sub(prev_block_indent_level);
let required_indent = self.out.indent + offset; let required_indent = self.out.indent_level + offset;
self.out.write_tabs(required_indent)?; self.out.write_tabs(required_indent)?;
let line = line.trim_start(); let line = line.trim_start();

View file

@ -0,0 +1,108 @@
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum IndentType {
Spaces,
Tabs,
}
#[derive(Debug, Clone)]
pub struct IndentOptions {
width: usize,
indent_string: String,
}
impl IndentOptions {
pub fn new(typ: IndentType, width: usize) -> Self {
assert_ne!(width, 0, "Cannot have an indent width of 0");
Self {
width,
indent_string: match typ {
IndentType::Tabs => "\t".into(),
IndentType::Spaces => " ".repeat(width),
},
}
}
/// Gets a string containing one indent worth of whitespace
pub fn indent_str(&self) -> &str {
&self.indent_string
}
/// Computes the line length in characters, counting tabs as the indent width.
pub fn line_length(&self, line: &str) -> usize {
line.chars()
.map(|ch| if ch == '\t' { self.width } else { 1 })
.sum()
}
/// Estimates how many times the line has been indented.
pub fn count_indents(&self, mut line: &str) -> usize {
let mut indent = 0;
while !line.is_empty() {
// Try to count tabs
let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
if num_tabs > 0 {
indent += num_tabs;
line = &line[num_tabs..];
continue;
}
// Try to count spaces
let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
if num_spaces >= self.width {
// Intentionally floor here to take only the amount of space that matches an indent
let num_space_indents = num_spaces / self.width;
indent += num_space_indents;
line = &line[num_space_indents * self.width..];
continue;
}
// Line starts with either non-indent characters or an unevent amount of spaces,
// so no more indent remains.
break;
}
indent
}
}
impl Default for IndentOptions {
fn default() -> Self {
Self::new(IndentType::Spaces, 4)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn count_indents() {
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
0
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
1
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t v += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 2).count_indents(" v += 2"),
2
);
}
}

View file

@ -16,8 +16,11 @@ mod collect_macros;
mod component; mod component;
mod element; mod element;
mod expr; mod expr;
mod indent;
mod writer; mod writer;
pub use indent::{IndentOptions, IndentType};
/// A modification to the original file to be applied by an IDE /// A modification to the original file to be applied by an IDE
/// ///
/// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes. /// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
@ -47,7 +50,7 @@ pub struct FormattedBlock {
/// back to the file precisely. /// back to the file precisely.
/// ///
/// Nested blocks of RSX will be handled automatically /// Nested blocks of RSX will be handled automatically
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> { pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
let mut formatted_blocks = Vec::new(); let mut formatted_blocks = Vec::new();
let parsed = syn::parse_file(contents).unwrap(); let parsed = syn::parse_file(contents).unwrap();
@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
} }
let mut writer = Writer::new(contents); let mut writer = Writer::new(contents);
writer.out.indent = indent;
// Don't parse nested macros // Don't parse nested macros
let mut end_span = LineColumn { column: 0, line: 0 }; let mut end_span = LineColumn { column: 0, line: 0 };
@ -76,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
let rsx_start = macro_path.span().start(); let rsx_start = macro_path.span().start();
writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4; writer.out.indent_level = writer
.out
.indent
.count_indents(writer.src[rsx_start.line - 1]);
write_body(&mut writer, &body); write_body(&mut writer, &body);
@ -159,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
buf.consume() buf.consume()
} }
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> { pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap(); let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
let mut buf = Writer::new(block); let mut buf = Writer::new(block);
buf.out.indent = indent_level; buf.out.indent = indent;
buf.out.indent_level = indent_level;
write_body(&mut buf, &body); write_body(&mut buf, &body);
@ -230,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
let display = DisplayIfmt(input); let display = DisplayIfmt(input);
write!(writable, "{}", display) write!(writable, "{}", display)
} }
pub fn leading_whitespaces(input: &str) -> usize {
input
.chars()
.map_while(|c| match c {
' ' => Some(1),
'\t' => Some(4),
_ => None,
})
.sum()
}

View file

@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
// Push out the indent level and write each component, line by line // Push out the indent level and write each component, line by line
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result { pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
self.out.indent += 1; self.out.indent_level += 1;
self.write_body_no_indent(children)?; self.write_body_no_indent(children)?;
self.out.indent -= 1; self.out.indent_level -= 1;
Ok(()) Ok(())
} }

View file

@ -12,7 +12,7 @@ macro_rules! twoway {
#[test] #[test]
fn $name() { fn $name() {
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx")); let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
let formatted = dioxus_autofmt::fmt_file(src); let formatted = dioxus_autofmt::fmt_file(src, Default::default());
let out = dioxus_autofmt::apply_formats(src, formatted); let out = dioxus_autofmt::apply_formats(src, formatted);
// normalize line endings // normalize line endings
let out = out.replace("\r", ""); let out = out.replace("\r", "");

View file

@ -1,10 +1,12 @@
use dioxus_autofmt::{IndentOptions, IndentType};
macro_rules! twoway { macro_rules! twoway {
($val:literal => $name:ident) => { ($val:literal => $name:ident ($indent:expr)) => {
#[test] #[test]
fn $name() { fn $name() {
let src_right = include_str!(concat!("./wrong/", $val, ".rsx")); let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx")); let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
let formatted = dioxus_autofmt::fmt_file(src_wrong); let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
let out = dioxus_autofmt::apply_formats(src_wrong, formatted); let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
// normalize line endings // normalize line endings
@ -16,8 +18,11 @@ macro_rules! twoway {
}; };
} }
twoway!("comments" => comments); twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4)));
twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4)));
twoway!("multi" => multi); twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4)));
twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4)));
twoway!("multiexpr" => multiexpr); twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4)));
twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4)));

View file

@ -0,0 +1,7 @@
rsx! {
div {
// Comments
class: "asdasd",
"hello world"
}
}

View file

@ -0,0 +1,5 @@
rsx! {
div {
// Comments
class: "asdasd", "hello world" }
}

View file

@ -0,0 +1,3 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! { div { "hello world" } })
}

View file

@ -0,0 +1,5 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {"hello world" }
})
}

View file

@ -0,0 +1,8 @@
fn ItWroks() {
cx.render(rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
left,
right
}
})
}

View file

@ -0,0 +1,5 @@
fn ItWroks() {
cx.render(rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
})
}

View file

@ -1,6 +1,6 @@
[package] [package]
name = "dioxus-cli" name = "dioxus-cli"
version = "0.4.1" version = "0.4.3"
authors = ["Jonathan Kelley"] authors = ["Jonathan Kelley"]
edition = "2021" edition = "2021"
description = "CLI tool for developing, testing, and publishing Dioxus apps" description = "CLI tool for developing, testing, and publishing Dioxus apps"

View file

@ -10,8 +10,8 @@ It handles building, bundling, development and publishing to simplify developmen
### Install the stable version (recommended) ### Install the stable version (recommended)
``` ```shell
cargo install dioxus-cli --locked cargo install dioxus-cli
``` ```
### Install the latest development build through git ### Install the latest development build through git
@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio
However, this is not fully tested. However, this is not fully tested.
That means you're probably going to have more bugs despite having the latest bug fixes. That means you're probably going to have more bugs despite having the latest bug fixes.
``` ```shell
cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
``` ```
@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
### Install from local folder ### Install from local folder
``` ```shell
cargo install --path . --debug cargo install --path . --debug
``` ```
@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
Alternatively, you can specify the template path: Alternatively, you can specify the template path:
``` ```shell
dx create hello --template gh:dioxuslabs/dioxus-template dx create hello --template gh:dioxuslabs/dioxus-template
``` ```

View file

@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
index_on_404 = true index_on_404 = true
watch_path = ["src"] watch_path = ["src", "examples"]
# include `assets` in web platform # include `assets` in web platform
[web.resource] [web.resource]

View file

@ -48,14 +48,25 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
// [1] Build the .wasm module // [1] Build the .wasm module
log::info!("🚅 Running build command..."); log::info!("🚅 Running build command...");
let wasm_check_command = std::process::Command::new("rustup")
.args(["show"])
.output()?;
let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
if !wasm_check_output.contains("wasm32-unknown-unknown") {
log::info!("wasm32-unknown-unknown target not detected, installing..");
let _ = std::process::Command::new("rustup")
.args(["target", "add", "wasm32-unknown-unknown"])
.output()?;
}
let cmd = subprocess::Exec::cmd("cargo"); let cmd = subprocess::Exec::cmd("cargo");
let cmd = cmd let cmd = cmd
.cwd(crate_dir) .cwd(crate_dir)
.arg("build") .arg("build")
.arg("--target") .arg("--target")
.arg("wasm32-unknown-unknown") .arg("wasm32-unknown-unknown")
.arg("--message-format=json") .arg("--message-format=json");
.arg("--quiet");
let cmd = if config.release { let cmd = if config.release {
cmd.arg("--release") cmd.arg("--release")
@ -65,7 +76,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
let cmd = if config.verbose { let cmd = if config.verbose {
cmd.arg("--verbose") cmd.arg("--verbose")
} else { } else {
cmd cmd.arg("--quiet")
}; };
let cmd = if config.custom_profile.is_some() { let cmd = if config.custom_profile.is_some() {
@ -261,6 +272,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
} }
if config.verbose { if config.verbose {
cmd = cmd.arg("--verbose"); cmd = cmd.arg("--verbose");
} else {
cmd = cmd.arg("--quiet");
} }
if config.custom_profile.is_some() { if config.custom_profile.is_some() {
@ -468,7 +481,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
.unwrap_or_default() .unwrap_or_default()
.contains_key("tailwindcss") .contains_key("tailwindcss")
{ {
style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n"); style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n");
} }
replace_or_insert_before("{style_include}", &style_str, "</head", &mut html); replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);

View file

@ -1,3 +1,4 @@
use dioxus_autofmt::{IndentOptions, IndentType};
use futures::{stream::FuturesUnordered, StreamExt}; use futures::{stream::FuturesUnordered, StreamExt};
use std::{fs, path::Path, process::exit}; use std::{fs, path::Path, process::exit};
@ -35,7 +36,8 @@ impl Autoformat {
} }
if let Some(raw) = self.raw { if let Some(raw) = self.raw {
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) { let indent = indentation_for(".")?;
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
println!("{}", inner); println!("{}", inner);
} else { } else {
// exit process with error // exit process with error
@ -46,17 +48,21 @@ impl Autoformat {
// Format single file // Format single file
if let Some(file) = self.file { if let Some(file) = self.file {
let file_content = if file == "-" { let file_content;
let indent;
if file == "-" {
indent = indentation_for(".")?;
let mut contents = String::new(); let mut contents = String::new();
std::io::stdin().read_to_string(&mut contents)?; std::io::stdin().read_to_string(&mut contents)?;
Ok(contents) file_content = Ok(contents);
} else { } else {
fs::read_to_string(&file) indent = indentation_for(".")?;
file_content = fs::read_to_string(&file);
}; };
match file_content { match file_content {
Ok(s) => { Ok(s) => {
let edits = dioxus_autofmt::fmt_file(&s); let edits = dioxus_autofmt::fmt_file(&s, indent);
let out = dioxus_autofmt::apply_formats(&s, edits); let out = dioxus_autofmt::apply_formats(&s, edits);
if file == "-" { if file == "-" {
print!("{}", out); print!("{}", out);
@ -93,6 +99,12 @@ async fn autoformat_project(check: bool) -> Result<()> {
let mut files_to_format = vec![]; let mut files_to_format = vec![];
collect_rs_files(&crate_config.crate_dir, &mut files_to_format); collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
if files_to_format.is_empty() {
return Ok(());
}
let indent = indentation_for(&files_to_format[0])?;
let counts = files_to_format let counts = files_to_format
.into_iter() .into_iter()
.filter(|file| { .filter(|file| {
@ -104,10 +116,11 @@ async fn autoformat_project(check: bool) -> Result<()> {
}) })
.map(|path| async { .map(|path| async {
let _path = path.clone(); let _path = path.clone();
let _indent = indent.clone();
let res = tokio::spawn(async move { let res = tokio::spawn(async move {
let contents = tokio::fs::read_to_string(&path).await?; let contents = tokio::fs::read_to_string(&path).await?;
let edits = dioxus_autofmt::fmt_file(&contents); let edits = dioxus_autofmt::fmt_file(&contents, _indent.clone());
let len = edits.len(); let len = edits.len();
if !edits.is_empty() { if !edits.is_empty() {
@ -151,6 +164,49 @@ async fn autoformat_project(check: bool) -> Result<()> {
Ok(()) Ok(())
} }
fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
let out = std::process::Command::new("cargo")
.args(["fmt", "--", "--print-config", "current"])
.arg(file_or_dir.as_ref())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.output()?;
if !out.status.success() {
return Err(Error::CargoError("cargo fmt failed".into()));
}
let config = String::from_utf8_lossy(&out.stdout);
let hard_tabs = config
.lines()
.find(|line| line.starts_with("hard_tabs "))
.and_then(|line| line.split_once('='))
.map(|(_, value)| value.trim() == "true")
.ok_or_else(|| {
Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
})?;
let tab_spaces = config
.lines()
.find(|line| line.starts_with("tab_spaces "))
.and_then(|line| line.split_once('='))
.map(|(_, value)| value.trim().parse::<usize>())
.ok_or_else(|| {
Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
})?
.map_err(|_| {
Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
})?;
Ok(IndentOptions::new(
if hard_tabs {
IndentType::Tabs
} else {
IndentType::Spaces
},
tab_spaces,
))
}
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) { fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
let Ok(folder) = folder.read_dir() else { let Ok(folder) = folder.read_dir() else {
return; return;

View file

@ -37,8 +37,8 @@ impl Build {
.platform .platform
.unwrap_or(crate_config.dioxus_config.application.default_platform); .unwrap_or(crate_config.dioxus_config.application.default_platform);
#[cfg(feature = "plugin")] // #[cfg(feature = "plugin")]
let _ = PluginManager::on_build_start(&crate_config, &platform); // let _ = PluginManager::on_build_start(&crate_config, &platform);
match platform { match platform {
Platform::Web => { Platform::Web => {
@ -66,8 +66,8 @@ impl Build {
)?; )?;
file.write_all(temp.as_bytes())?; file.write_all(temp.as_bytes())?;
#[cfg(feature = "plugin")] // #[cfg(feature = "plugin")]
let _ = PluginManager::on_build_finish(&crate_config, &platform); // let _ = PluginManager::on_build_finish(&crate_config, &platform);
Ok(()) Ok(())
} }

View file

@ -105,7 +105,7 @@ impl Default for DioxusConfig {
}, },
proxy: Some(vec![]), proxy: Some(vec![]),
watcher: WebWatcherConfig { watcher: WebWatcherConfig {
watch_path: Some(vec![PathBuf::from("src")]), watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
reload_html: Some(false), reload_html: Some(false),
index_on_404: Some(true), index_on_404: Some(true),
}, },

View file

@ -29,6 +29,9 @@ pub enum Error {
#[error("Cargo Error: {0}")] #[error("Cargo Error: {0}")]
CargoError(String), CargoError(String),
#[error("Couldn't retrieve cargo metadata")]
CargoMetadata(#[source] cargo_metadata::Error),
#[error("{0}")] #[error("{0}")]
CustomError(String), CustomError(String),

View file

@ -28,7 +28,19 @@ pub fn set_up_logging() {
message = message, message = message,
)); ));
}) })
.level(log::LevelFilter::Info) .level(match std::env::var("DIOXUS_LOG") {
Ok(level) => match level.to_lowercase().as_str() {
"error" => log::LevelFilter::Error,
"warn" => log::LevelFilter::Warn,
"info" => log::LevelFilter::Info,
"debug" => log::LevelFilter::Debug,
"trace" => log::LevelFilter::Trace,
_ => {
panic!("Invalid log level: {}", level)
}
},
Err(_) => log::LevelFilter::Info,
})
.chain(std::io::stdout()) .chain(std::io::stdout())
.apply() .apply()
.unwrap(); .unwrap();

View file

@ -9,42 +9,31 @@ use dioxus_cli::plugin::PluginManager;
use Commands::*; use Commands::*;
fn get_bin(bin: Option<String>) -> Result<Option<PathBuf>> { fn get_bin(bin: Option<String>) -> Result<PathBuf> {
const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace."; let metadata = cargo_metadata::MetadataCommand::new()
.exec()
.map_err(Error::CargoMetadata)?;
let package = if let Some(bin) = bin {
metadata
.workspace_packages()
.into_iter()
.find(|p| p.name == bin)
.ok_or(format!("no such package: {}", bin))
.map_err(Error::CargoError)?
} else {
metadata
.root_package()
.ok_or("no root package?".into())
.map_err(Error::CargoError)?
};
if let Some(ref bin) = bin { let crate_dir = package
let manifest = cargo_toml::Manifest::from_path("./Cargo.toml") .manifest_path
.map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?; .parent()
.ok_or("couldn't take parent dir".into())
.map_err(Error::CargoError)?;
if let Some(workspace) = manifest.workspace { Ok(crate_dir.into())
for item in workspace.members.iter() {
let path = PathBuf::from(item);
if !path.exists() {
continue;
}
if !path.is_dir() {
continue;
}
if path.ends_with(bin.clone()) {
return Ok(Some(path));
}
}
} else {
return Err(Error::CargoError(ERR_MESSAGE.to_string()));
}
}
// If the bin exists but we couldn't find it
if bin.is_some() {
return Err(Error::CargoError(
"The specified bin does not exist.".to_string(),
));
}
Ok(None)
} }
#[tokio::main] #[tokio::main]
@ -53,34 +42,36 @@ async fn main() -> anyhow::Result<()> {
set_up_logging(); set_up_logging();
let bin = get_bin(args.bin)?; let bin = get_bin(args.bin);
let _dioxus_config = DioxusConfig::load(bin.clone()) if let Ok(bin) = &bin {
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))? .map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
.unwrap_or_else(|| { .unwrap_or_else(|| {
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config"); log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
DioxusConfig::default() DioxusConfig::default()
}); });
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
PluginManager::init(_dioxus_config.plugin) PluginManager::init(_dioxus_config.plugin)
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?; .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
}
match args.action { match args.action {
Translate(opts) => opts Translate(opts) => opts
.translate() .translate()
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)), .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
Build(opts) => opts Build(opts) if bin.is_ok() => opts
.build(bin.clone()) .build(Some(bin.unwrap().clone()))
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)), .map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
Clean(opts) => opts Clean(opts) if bin.is_ok() => opts
.clean(bin.clone()) .clean(Some(bin.unwrap().clone()))
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)), .map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
Serve(opts) => opts Serve(opts) if bin.is_ok() => opts
.serve(bin.clone()) .serve(Some(bin.unwrap().clone()))
.await .await
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)), .map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
@ -92,8 +83,8 @@ async fn main() -> anyhow::Result<()> {
.config() .config()
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)), .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
Bundle(opts) => opts Bundle(opts) if bin.is_ok() => opts
.bundle(bin.clone()) .bundle(Some(bin.unwrap().clone()))
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)), .map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
@ -118,5 +109,6 @@ async fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
_ => Err(anyhow::anyhow!(bin.unwrap_err())),
} }
} }

View file

@ -43,8 +43,6 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
let hot_reload_tx = broadcast::channel(100).0; let hot_reload_tx = broadcast::channel(100).0;
clear_paths();
Some(HotReloadState { Some(HotReloadState {
messages: hot_reload_tx.clone(), messages: hot_reload_tx.clone(),
file_map: file_map.clone(), file_map: file_map.clone(),
@ -73,6 +71,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
move || { move || {
let mut current_child = currently_running_child.write().unwrap(); let mut current_child = currently_running_child.write().unwrap();
log::trace!("Killing old process");
current_child.kill()?; current_child.kill()?;
let (child, result) = start_desktop(&config)?; let (child, result) = start_desktop(&config)?;
*current_child = child; *current_child = child;
@ -109,7 +108,14 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
} }
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> { async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
match LocalSocketListener::bind("@dioxusin") { let metadata = cargo_metadata::MetadataCommand::new()
.no_deps()
.exec()
.unwrap();
let target_dir = metadata.target_directory.as_std_path();
let path = target_dir.join("dioxusin");
clear_paths(&path);
match LocalSocketListener::bind(path) {
Ok(local_socket_stream) => { Ok(local_socket_stream) => {
let aborted = Arc::new(Mutex::new(false)); let aborted = Arc::new(Mutex::new(false));
// States // States
@ -121,9 +127,9 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
let file_map = hot_reload_state.file_map.clone(); let file_map = hot_reload_state.file_map.clone();
let channels = channels.clone(); let channels = channels.clone();
let aborted = aborted.clone(); let aborted = aborted.clone();
let _ = local_socket_stream.set_nonblocking(true);
move || { move || {
loop { loop {
//accept() will block the thread when local_socket_stream is in blocking mode (default)
match local_socket_stream.accept() { match local_socket_stream.accept() {
Ok(mut connection) => { Ok(mut connection) => {
// send any templates than have changed before the socket connected // send any templates than have changed before the socket connected
@ -181,17 +187,14 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
Ok(()) Ok(())
} }
fn clear_paths() { fn clear_paths(file_socket_path: &std::path::Path) {
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
// On unix, if you force quit the application, it can leave the file socket open // On unix, if you force quit the application, it can leave the file socket open
// This will cause the local socket listener to fail to open // This will cause the local socket listener to fail to open
// We check if the file socket is already open from an old session and then delete it // We check if the file socket is already open from an old session and then delete it
let paths = ["./dioxusin", "./@dioxusin"];
for path in paths { if file_socket_path.exists() {
let path = std::path::PathBuf::from(path); let _ = std::fs::remove_file(file_socket_path);
if path.exists() {
let _ = std::fs::remove_file(path);
}
} }
} }
} }
@ -212,6 +215,7 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
// Run the desktop application // Run the desktop application
log::trace!("Building application");
let result = crate::builder::build_desktop(config, true)?; let result = crate::builder::build_desktop(config, true)?;
match &config.executable { match &config.executable {
@ -222,6 +226,7 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
if cfg!(windows) { if cfg!(windows) {
file.set_extension("exe"); file.set_extension("exe");
} }
log::trace!("Running application from {:?}", file);
let child = Command::new(file.to_str().unwrap()).spawn()?; let child = Command::new(file.to_str().unwrap()).spawn()?;
Ok((child, result)) Ok((child, result))

View file

@ -32,7 +32,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
.watcher .watcher
.watch_path .watch_path
.clone() .clone()
.unwrap_or_else(|| vec![PathBuf::from("src")]); .unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]);
let watcher_config = config.clone(); let watcher_config = config.clone();
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| { let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
@ -55,6 +55,16 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
break; break;
} }
// Workaround for notify and vscode-like editor:
// when edit & save a file in vscode, there will be two notifications,
// the first one is a file with empty content.
// filter the empty file notification to avoid false rebuild during hot-reload
if let Ok(metadata) = fs::metadata(path) {
if metadata.len() == 0 {
continue;
}
}
match rsx_file_map.update_rsx(path, &config.crate_dir) { match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => { Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(msgs); messages.extend(msgs);
@ -121,12 +131,12 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
.unwrap(); .unwrap();
for sub_path in allow_watch_path { for sub_path in allow_watch_path {
watcher if let Err(err) = watcher.watch(
.watch( &config.crate_dir.join(sub_path),
&config.crate_dir.join(sub_path), notify::RecursiveMode::Recursive,
notify::RecursiveMode::Recursive, ) {
) log::error!("Failed to watch path: {}", err);
.unwrap(); }
} }
Ok(watcher) Ok(watcher)
} }

View file

@ -22,17 +22,20 @@ pub fn print_console_info(
options: PrettierOptions, options: PrettierOptions,
web_info: Option<WebServerInfo>, web_info: Option<WebServerInfo>,
) { ) {
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { // Don't clear the screen if the user has set the DIOXUS_LOG environment variable to "trace" so that we can see the logs
"cls" if Some("trace") != std::env::var("DIOXUS_LOG").ok().as_deref() {
} else { if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
"clear" "cls"
}) } else {
.output() "clear"
{ })
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout)); .output()
} else { {
// Try ANSI-Escape characters print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
print!("\x1b[2J\x1b[H"); } else {
// Try ANSI-Escape characters
print!("\x1b[2J\x1b[H");
}
} }
let mut profile = if config.release { "Release" } else { "Debug" }.to_string(); let mut profile = if config.release { "Release" } else { "Debug" }.to_string();

View file

@ -31,6 +31,7 @@ fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
block: parse_quote! { block: parse_quote! {
{ {
#[warn(non_snake_case)] #[warn(non_snake_case)]
#[allow(clippy::inline_always)]
#[inline(always)] #[inline(always)]
#inner_comp_fn #inner_comp_fn
#inner_comp_ident (#cx_pat) #inner_comp_ident (#cx_pat)

View file

@ -186,13 +186,13 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
let arg_name = arg_name.into_token_stream().to_string(); let arg_name = arg_name.into_token_stream().to_string();
let arg_type = crate::utils::format_type_string(arg_type); let arg_type = crate::utils::format_type_string(arg_type);
let input_arg_doc = let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n').replace('\n', "<br/>"); .replace("\n\n", "</p><p>");
let prop_def_link = format!("{props_def_link}::{arg_name}"); let prop_def_link = format!("{props_def_link}::{arg_name}");
let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`"); let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
if let Some(deprecation) = deprecation { if let Some(deprecation) = deprecation {
arg_doc.push_str("<br/>👎 Deprecated"); arg_doc.push_str("<p>👎 Deprecated");
if let Some(since) = deprecation.since { if let Some(since) = deprecation.since {
arg_doc.push_str(&format!(" since {since}")); arg_doc.push_str(&format!(" since {since}"));
@ -205,14 +205,16 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
arg_doc.push_str(&format!(": {note}")); arg_doc.push_str(&format!(": {note}"));
} }
arg_doc.push_str("</p>");
if !input_arg_doc.is_empty() { if !input_arg_doc.is_empty() {
arg_doc.push_str("<hr/>"); arg_doc.push_str("<hr/>");
} }
} else {
arg_doc.push_str("<br/>");
} }
arg_doc.push_str(&input_arg_doc); if !input_arg_doc.is_empty() {
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
}
props_docs.push(parse_quote! { props_docs.push(parse_quote! {
#[doc = #arg_doc] #[doc = #arg_doc]

View file

@ -243,10 +243,6 @@ mod field_info {
} }
.into() .into()
} }
pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
type_from_inside_option(self.ty, check_option_name)
}
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -551,18 +547,16 @@ mod struct_info {
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| { let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into())); args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
}); });
let phantom_generics = self.generics.params.iter().map(|param| match param { let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
syn::GenericParam::Lifetime(lifetime) => { syn::GenericParam::Lifetime(lifetime) => {
let lifetime = &lifetime.lifetime; let lifetime = &lifetime.lifetime;
quote!(::core::marker::PhantomData<&#lifetime ()>) Some(quote!(::core::marker::PhantomData<&#lifetime ()>))
} }
syn::GenericParam::Type(ty) => { syn::GenericParam::Type(ty) => {
let ty = &ty.ident; let ty = &ty.ident;
quote!(::core::marker::PhantomData<#ty>) Some(quote!(::core::marker::PhantomData<#ty>))
}
syn::GenericParam::Const(_cnst) => {
quote!()
} }
syn::GenericParam::Const(_cnst) => None,
}); });
let builder_method_doc = match self.builder_attr.builder_method_doc { let builder_method_doc = match self.builder_attr.builder_method_doc {
Some(ref doc) => quote!(#doc), Some(ref doc) => quote!(#doc),
@ -633,7 +627,7 @@ Finally, call `.build()` to create the instance of `{name}`.
Ok(quote! { Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause { impl #impl_generics #name #ty_generics #where_clause {
#[doc = #builder_method_doc] #[doc = #builder_method_doc]
#[allow(dead_code)] #[allow(dead_code, clippy::type_complexity)]
#vis fn builder() -> #builder_name #generics_with_empty { #vis fn builder() -> #builder_name #generics_with_empty {
#builder_name { #builder_name {
fields: #empties_tuple, fields: #empties_tuple,
@ -785,31 +779,16 @@ Finally, call `.build()` to create the instance of `{name}`.
None => quote!(), None => quote!(),
}; };
// NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of let arg_type = field_type;
// nesting is different so we have to do this little dance. let (arg_type, arg_expr) =
let arg_type = if field.builder_attr.strip_option { if field.builder_attr.auto_into || field.builder_attr.strip_option {
field.type_from_inside_option(false).ok_or_else(|| { (
Error::new_spanned( quote!(impl ::core::convert::Into<#arg_type>),
field_type, quote!(#field_name.into()),
"can't `strip_option` - field is not `Option<...>`",
) )
})? } else {
} else { (quote!(#arg_type), quote!(#field_name))
field_type };
};
let (arg_type, arg_expr) = if field.builder_attr.auto_into {
(
quote!(impl ::core::convert::Into<#arg_type>),
quote!(#field_name.into()),
)
} else {
(quote!(#arg_type), quote!(#field_name))
};
let arg_expr = if field.builder_attr.strip_option {
quote!(Some(#arg_expr))
} else {
arg_expr
};
let repeated_fields_error_type_name = syn::Ident::new( let repeated_fields_error_type_name = syn::Ident::new(
&format!( &format!(
@ -825,6 +804,7 @@ Finally, call `.build()` to create the instance of `{name}`.
#[allow(dead_code, non_camel_case_types, missing_docs)] #[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause { impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
#doc #doc
#[allow(clippy::type_complexity)]
pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > { pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
let #field_name = (#arg_expr,); let #field_name = (#arg_expr,);
let ( #(#descructuring,)* ) = self.fields; let ( #(#descructuring,)* ) = self.fields;
@ -843,6 +823,7 @@ Finally, call `.build()` to create the instance of `{name}`.
#[deprecated( #[deprecated(
note = #repeated_fields_error_message note = #repeated_fields_error_message
)] )]
#[allow(clippy::type_complexity)]
pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > { pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
self self
} }

View file

@ -164,17 +164,11 @@ impl VirtualDom {
}); });
// Now that all the references are gone, we can safely drop our own references in our listeners. // Now that all the references are gone, we can safely drop our own references in our listeners.
let mut listeners = scope.attributes_to_drop.borrow_mut(); let mut listeners = scope.attributes_to_drop_before_render.borrow_mut();
listeners.drain(..).for_each(|listener| { listeners.drain(..).for_each(|listener| {
let listener = unsafe { &*listener }; let listener = unsafe { &*listener };
match &listener.value { if let AttributeValue::Listener(l) = &listener.value {
AttributeValue::Listener(l) => { _ = l.take();
_ = l.take();
}
AttributeValue::Any(a) => {
_ = a.take();
}
_ => (),
} }
}); });
} }

View file

@ -1,10 +1,16 @@
use crate::nodes::RenderReturn; use crate::nodes::RenderReturn;
use crate::{Attribute, AttributeValue, VComponent};
use bumpalo::Bump; use bumpalo::Bump;
use std::cell::RefCell;
use std::cell::{Cell, UnsafeCell}; use std::cell::{Cell, UnsafeCell};
pub(crate) struct BumpFrame { pub(crate) struct BumpFrame {
pub bump: UnsafeCell<Bump>, pub bump: UnsafeCell<Bump>,
pub node: Cell<*const RenderReturn<'static>>, pub node: Cell<*const RenderReturn<'static>>,
// The bump allocator will not call the destructor of the objects it allocated. Attributes and props need to have there destructor called, so we keep a list of them to drop before the bump allocator is reset.
pub(crate) attributes_to_drop_before_reset: RefCell<Vec<*const Attribute<'static>>>,
pub(crate) props_to_drop_before_reset: RefCell<Vec<*const VComponent<'static>>>,
} }
impl BumpFrame { impl BumpFrame {
@ -13,6 +19,8 @@ impl BumpFrame {
Self { Self {
bump: UnsafeCell::new(bump), bump: UnsafeCell::new(bump),
node: Cell::new(std::ptr::null()), node: Cell::new(std::ptr::null()),
attributes_to_drop_before_reset: Default::default(),
props_to_drop_before_reset: Default::default(),
} }
} }
@ -31,8 +39,38 @@ impl BumpFrame {
unsafe { &*self.bump.get() } unsafe { &*self.bump.get() }
} }
#[allow(clippy::mut_from_ref)] pub(crate) fn add_attribute_to_drop(&self, attribute: *const Attribute<'static>) {
pub(crate) unsafe fn bump_mut(&self) -> &mut Bump { self.attributes_to_drop_before_reset
unsafe { &mut *self.bump.get() } .borrow_mut()
.push(attribute);
}
/// Reset the bump allocator and drop all the attributes and props that were allocated in it.
///
/// # Safety
/// The caller must insure that no reference to anything allocated in the bump allocator is available after this function is called.
pub(crate) unsafe fn reset(&self) {
let mut attributes = self.attributes_to_drop_before_reset.borrow_mut();
attributes.drain(..).for_each(|attribute| {
let attribute = unsafe { &*attribute };
if let AttributeValue::Any(l) = &attribute.value {
_ = l.take();
}
});
let mut props = self.props_to_drop_before_reset.borrow_mut();
props.drain(..).for_each(|prop| {
let prop = unsafe { &*prop };
_ = prop.props.borrow_mut().take();
});
unsafe {
let bump = &mut *self.bump.get();
bump.reset();
}
}
}
impl Drop for BumpFrame {
fn drop(&mut self) {
unsafe { self.reset() }
} }
} }

View file

@ -560,7 +560,7 @@ impl<'b> VirtualDom {
// If none of the old keys are reused by the new children, then we remove all the remaining old children and // If none of the old keys are reused by the new children, then we remove all the remaining old children and
// create the new children afresh. // create the new children afresh.
if shared_keys.is_empty() { if shared_keys.is_empty() {
if old.get(0).is_some() { if old.first().is_some() {
self.remove_nodes(&old[1..]); self.remove_nodes(&old[1..]);
self.replace(&old[0], new); self.replace(&old[0], new);
} else { } else {

View file

@ -107,8 +107,6 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
} }
} }
#[doc(hidden)]
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components. /// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
/// ///
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields. /// This makes it possible to pass `move |evt| {}` style closures into components as property fields.

View file

@ -23,8 +23,37 @@ use crate::{innerlude::VNode, ScopeState};
/// ///
/// ///
/// ```rust, ignore /// ```rust, ignore
/// LazyNodes::new(|f| f.element("div", [], [], [] None)) /// LazyNodes::new(|f| {
/// static TEMPLATE: dioxus::core::Template = dioxus::core::Template {
/// name: "main.rs:5:5:20", // Source location of the template for hot reloading
/// roots: &[
/// dioxus::core::TemplateNode::Element {
/// tag: dioxus_elements::div::TAG_NAME,
/// namespace: dioxus_elements::div::NAME_SPACE,
/// attrs: &[],
/// children: &[],
/// },
/// ],
/// node_paths: &[],
/// attr_paths: &[],
/// };
/// dioxus::core::VNode {
/// parent: None,
/// key: None,
/// template: std::cell::Cell::new(TEMPLATE),
/// root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(
/// 1usize,
/// f.bump(),
/// )
/// .into(),
/// dynamic_nodes: f.bump().alloc([]),
/// dynamic_attrs: f.bump().alloc([]),
/// })
/// }
/// ``` /// ```
///
/// Find more information about how to construct [`VNode`] at <https://dioxuslabs.com/learn/0.4/contributing/walkthrough_readme#the-rsx-macro>
pub struct LazyNodes<'a, 'b> { pub struct LazyNodes<'a, 'b> {
#[cfg(not(miri))] #[cfg(not(miri))]
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>, inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
@ -61,7 +90,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
/// Call the closure with the given factory to produce real [`VNode`]. /// Call the closure with the given factory to produce real [`VNode`].
/// ///
/// ```rust, ignore /// ```rust, ignore
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None)); /// let f = LazyNodes::new(/* Closure for creating VNodes */);
/// ///
/// let node = f.call(cac); /// let node = f.call(cac);
/// ``` /// ```

View file

@ -91,7 +91,7 @@ pub enum Mutation<'a> {
id: ElementId, id: ElementId,
}, },
/// Create an placeholder int he DOM that we will use later. /// Create a placeholder in the DOM that we will use later.
/// ///
/// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing /// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
CreatePlaceholder { CreatePlaceholder {

View file

@ -707,7 +707,7 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str {
impl IntoDynNode<'_> for String { impl IntoDynNode<'_> for String {
fn into_vnode(self, cx: &ScopeState) -> DynamicNode { fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
DynamicNode::Text(VText { DynamicNode::Text(VText {
value: cx.bump().alloc(self), value: cx.bump().alloc_str(&self),
id: Default::default(), id: Default::default(),
}) })
} }
@ -791,6 +791,12 @@ impl<'a> IntoAttributeValue<'a> for &'a str {
} }
} }
impl<'a> IntoAttributeValue<'a> for String {
fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Text(cx.alloc_str(&self))
}
}
impl<'a> IntoAttributeValue<'a> for f64 { impl<'a> IntoAttributeValue<'a> for f64 {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Float(self) AttributeValue::Float(self)

View file

@ -35,7 +35,7 @@ impl VirtualDom {
hook_idx: Default::default(), hook_idx: Default::default(),
borrowed_props: Default::default(), borrowed_props: Default::default(),
attributes_to_drop: Default::default(), attributes_to_drop_before_render: Default::default(),
})); }));
let context = let context =
@ -54,7 +54,7 @@ impl VirtualDom {
let new_nodes = unsafe { let new_nodes = unsafe {
let scope = &self.scopes[scope_id.0]; let scope = &self.scopes[scope_id.0];
scope.previous_frame().bump_mut().reset(); scope.previous_frame().reset();
scope.context().suspended.set(false); scope.context().suspended.set(false);

View file

@ -94,7 +94,7 @@ pub struct ScopeState {
pub(crate) hook_idx: Cell<usize>, pub(crate) hook_idx: Cell<usize>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>, pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>, pub(crate) attributes_to_drop_before_render: RefCell<Vec<*const Attribute<'static>>>,
pub(crate) props: Option<Box<dyn AnyProps<'static>>>, pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
} }
@ -348,25 +348,36 @@ impl<'src> ScopeState {
pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> { pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
let element = rsx.call(self); let element = rsx.call(self);
let mut listeners = self.attributes_to_drop.borrow_mut(); let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
for attr in element.dynamic_attrs { for attr in element.dynamic_attrs {
match attr.value { match attr.value {
AttributeValue::Any(_) | AttributeValue::Listener(_) => { // We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
AttributeValue::Listener(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
listeners.push(unbounded); listeners.push(unbounded);
} }
// We need to drop any values manually to make sure that their drop implementation is called before the next render
AttributeValue::Any(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
self.previous_frame().add_attribute_to_drop(unbounded);
}
_ => (), _ => (),
} }
} }
let mut props = self.borrowed_props.borrow_mut(); let mut props = self.borrowed_props.borrow_mut();
let mut drop_props = self
.previous_frame()
.props_to_drop_before_reset
.borrow_mut();
for node in element.dynamic_nodes { for node in element.dynamic_nodes {
if let DynamicNode::Component(comp) = node { if let DynamicNode::Component(comp) = node {
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
if !comp.static_props { if !comp.static_props {
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
props.push(unbounded); props.push(unbounded);
} }
drop_props.push(unbounded);
} }
} }

View file

@ -59,6 +59,7 @@ devtools = ["wry/devtools"]
tray = ["wry/tray"] tray = ["wry/tray"]
dox = ["wry/dox"] dox = ["wry/dox"]
hot-reload = ["dioxus-hot-reload"] hot-reload = ["dioxus-hot-reload"]
gnu = []
[package.metadata.docs.rs] [package.metadata.docs.rs]
default-features = false default-features = false

View file

@ -0,0 +1,9 @@
fn main() {
// WARN about wry support on windows gnu targets. GNU windows targets don't work well in wry currently
if std::env::var("CARGO_CFG_WINDOWS").is_ok()
&& std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu"
&& !cfg!(feature = "gnu")
{
println!("cargo:warning=GNU windows targets have some limitations within Wry. Using the MSVC windows toolchain is recommended. If you would like to use continue using GNU, you can read https://github.com/wravery/webview2-rs#cross-compilation and disable this warning by adding the gnu feature to dioxus-desktop in your Cargo.toml")
}
}

View file

@ -54,7 +54,7 @@ use wry::{application::window::WindowId, webview::WebContext};
/// ///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop. /// This function will start a multithreaded Tokio runtime as well the WebView event loop.
/// ///
/// ```rust, ignore /// ```rust, no_run
/// use dioxus::prelude::*; /// use dioxus::prelude::*;
/// ///
/// fn main() { /// fn main() {
@ -77,11 +77,12 @@ pub fn launch(root: Component) {
/// ///
/// You can configure the WebView window with a configuration closure /// You can configure the WebView window with a configuration closure
/// ///
/// ```rust, ignore /// ```rust, no_run
/// use dioxus::prelude::*; /// use dioxus::prelude::*;
/// use dioxus_desktop::*;
/// ///
/// fn main() { /// fn main() {
/// dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App"))); /// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App")));
/// } /// }
/// ///
/// fn app(cx: Scope) -> Element { /// fn app(cx: Scope) -> Element {
@ -100,8 +101,9 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
/// ///
/// You can configure the WebView window with a configuration closure /// You can configure the WebView window with a configuration closure
/// ///
/// ```rust, ignore /// ```rust, no_run
/// use dioxus::prelude::*; /// use dioxus::prelude::*;
/// use dioxus_desktop::Config;
/// ///
/// fn main() { /// fn main() {
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); /// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
@ -161,6 +163,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
// iOS panics if we create a window before the event loop is started // iOS panics if we create a window before the event loop is started
let props = Rc::new(Cell::new(Some(props))); let props = Rc::new(Cell::new(Some(props)));
let cfg = Rc::new(Cell::new(Some(cfg))); let cfg = Rc::new(Cell::new(Some(cfg)));
let mut is_visible_before_start = true;
event_loop.run(move |window_event, event_loop, control_flow| { event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait; *control_flow = ControlFlow::Wait;
@ -210,6 +213,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
// Create a dom // Create a dom
let dom = VirtualDom::new_with_props(root, props); let dom = VirtualDom::new_with_props(root, props);
is_visible_before_start = cfg.window.window.visible;
let handler = create_new_window( let handler = create_new_window(
cfg, cfg,
event_loop, event_loop,
@ -323,6 +328,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
EventData::Ipc(msg) if msg.method() == "initialize" => { EventData::Ipc(msg) if msg.method() == "initialize" => {
let view = webviews.get_mut(&event.1).unwrap(); let view = webviews.get_mut(&event.1).unwrap();
send_edits(view.dom.rebuild(), &view.desktop_context.webview); send_edits(view.dom.rebuild(), &view.desktop_context.webview);
view.desktop_context
.webview
.window()
.set_visible(is_visible_before_start);
} }
EventData::Ipc(msg) if msg.method() == "browser_open" => { EventData::Ipc(msg) if msg.method() == "browser_open" => {

View file

@ -13,7 +13,7 @@ pub fn build(
proxy: EventLoopProxy<UserWindowEvent>, proxy: EventLoopProxy<UserWindowEvent>,
) -> (WebView, WebContext) { ) -> (WebView, WebContext) {
let builder = cfg.window.clone(); let builder = cfg.window.clone();
let window = builder.build(event_loop).unwrap(); let window = builder.with_visible(false).build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take(); let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone(); let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone(); let index_file = cfg.custom_index.clone();

View file

@ -15,21 +15,21 @@ fn app(cx: Scope) -> Element {
let mapping: DioxusElementToNodeId = cx.consume_context().unwrap(); let mapping: DioxusElementToNodeId = cx.consume_context().unwrap();
// disable templates so that every node has an id and can be queried // disable templates so that every node has an id and can be queried
cx.render(rsx! { cx.render(rsx! {
div{ div {
width: "100%", width: "100%",
background_color: "hsl({hue}, 70%, {brightness}%)", background_color: "hsl({hue}, 70%, {brightness}%)",
onmousemove: move |evt| { onmousemove: move |evt| {
if let RenderReturn::Ready(node) = cx.root_node() { if let RenderReturn::Ready(node) = cx.root_node() {
if let Some(id) = node.root_ids.borrow().get(0).cloned() { if let Some(id) = node.root_ids.borrow().first().cloned() {
let node = tui_query.get(mapping.get_node_id(id).unwrap()); let node = tui_query.get(mapping.get_node_id(id).unwrap());
let Size{width, height} = node.size().unwrap(); let Size { width, height } = node.size().unwrap();
let pos = evt.inner().element_coordinates(); let pos = evt.inner().element_coordinates();
hue.set((pos.x as f32/width as f32)*255.0); hue.set((pos.x as f32 / width as f32) * 255.0);
brightness.set((pos.y as f32/height as f32)*100.0); brightness.set((pos.y as f32 / height as f32) * 100.0);
} }
} }
}, },
"hsl({hue}, 70%, {brightness}%)", "hsl({hue}, 70%, {brightness}%)"
} }
}) })
} }

View file

@ -1,17 +1,39 @@
//! This file exports functions into the vscode extension //! This file exports functions into the vscode extension
use dioxus_autofmt::FormattedBlock; use dioxus_autofmt::{FormattedBlock, IndentOptions, IndentType};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[wasm_bindgen] #[wasm_bindgen]
pub fn format_rsx(raw: String) -> String { pub fn format_rsx(raw: String, use_tabs: bool, indent_size: usize) -> String {
let block = dioxus_autofmt::fmt_block(&raw, 0); let block = dioxus_autofmt::fmt_block(
&raw,
0,
IndentOptions::new(
if use_tabs {
IndentType::Tabs
} else {
IndentType::Spaces
},
indent_size,
),
);
block.unwrap() block.unwrap()
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn format_selection(raw: String) -> String { pub fn format_selection(raw: String, use_tabs: bool, indent_size: usize) -> String {
let block = dioxus_autofmt::fmt_block(&raw, 0); let block = dioxus_autofmt::fmt_block(
&raw,
0,
IndentOptions::new(
if use_tabs {
IndentType::Tabs
} else {
IndentType::Spaces
},
indent_size,
),
);
block.unwrap() block.unwrap()
} }
@ -35,8 +57,18 @@ impl FormatBlockInstance {
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn format_file(contents: String) -> FormatBlockInstance { pub fn format_file(contents: String, use_tabs: bool, indent_size: usize) -> FormatBlockInstance {
let _edits = dioxus_autofmt::fmt_file(&contents); let _edits = dioxus_autofmt::fmt_file(
&contents,
IndentOptions::new(
if use_tabs {
IndentType::Tabs
} else {
IndentType::Spaces
},
indent_size,
),
);
let out = dioxus_autofmt::apply_formats(&contents, _edits.clone()); let out = dioxus_autofmt::apply_formats(&contents, _edits.clone());
FormatBlockInstance { new: out, _edits } FormatBlockInstance { new: out, _edits }
} }

View file

@ -90,7 +90,13 @@ function fmtDocument(document: vscode.TextDocument) {
if (!editor) return; // Need an editor to apply text edits. if (!editor) return; // Need an editor to apply text edits.
const contents = editor.document.getText(); const contents = editor.document.getText();
const formatted = dioxus.format_file(contents); let tabSize: number;
if (typeof editor.options.tabSize === 'number') {
tabSize = editor.options.tabSize;
} else {
tabSize = 4;
}
const formatted = dioxus.format_file(contents, !editor.options.insertSpaces, tabSize);
// Replace the entire text document // Replace the entire text document
// Yes, this is a bit heavy handed, but the dioxus side doesn't know the line/col scheme that vscode is using // Yes, this is a bit heavy handed, but the dioxus side doesn't know the line/col scheme that vscode is using

View file

@ -7,6 +7,6 @@ use dioxus_core::ScopeState;
pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> { pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
cx.use_hook(|| match cx.consume_context::<Rc<AtomRoot>>() { cx.use_hook(|| match cx.consume_context::<Rc<AtomRoot>>() {
Some(root) => root, Some(root) => root,
None => panic!("No atom root found in context. Did you forget place an AtomRoot component at the top of your app?"), None => panic!("No atom root found in context. Did you forget to call use_init_atom_root at the top of your app?"),
}) })
} }

View file

@ -86,7 +86,9 @@ impl<T: 'static> AtomState<T> {
/// ``` /// ```
#[must_use] #[must_use]
pub fn current(&self) -> Rc<T> { pub fn current(&self) -> Rc<T> {
self.value.as_ref().unwrap().clone() let atoms = self.root.atoms.borrow();
let slot = atoms.get(&self.id).unwrap();
slot.value.clone().downcast().unwrap()
} }
/// Get the `setter` function directly without the `AtomState` wrapper. /// Get the `setter` function directly without the `AtomState` wrapper.

View file

@ -22,8 +22,6 @@ mod atoms {
pub use atom::*; pub use atom::*;
pub use atomfamily::*; pub use atomfamily::*;
pub use atomref::*; pub use atomref::*;
pub use selector::*;
pub use selectorfamily::*;
} }
pub mod hooks { pub mod hooks {

View file

@ -11,7 +11,7 @@ keywords = ["ui", "gui", "react", "ssr", "fullstack"]
[dependencies] [dependencies]
# server functions # server functions
server_fn = { version = "0.4.6", default-features = false } server_fn = { version = "0.5.2", default-features = false }
dioxus_server_macro = { workspace = true } dioxus_server_macro = { workspace = true }
# warp # warp

View file

@ -24,6 +24,7 @@ fn app(cx: Scope<AppProps>) -> Element {
let mut count = use_state(cx, || 0); let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string()); let text = use_state(cx, || "...".to_string());
let eval = use_eval(cx);
cx.render(rsx! { cx.render(rsx! {
div { div {

View file

@ -369,15 +369,65 @@ fn apply_request_parts_to_response<B>(
} }
} }
/// SSR renderer handler for Axum /// SSR renderer handler for Axum with added context injection.
pub async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>( ///
State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>, /// # Example
/// ```rust,no_run
/// #![allow(non_snake_case)]
/// use std::sync::{Arc, Mutex};
///
/// use axum::routing::get;
/// use dioxus::prelude::*;
/// use dioxus_fullstack::{axum_adapter::render_handler_with_context, prelude::*};
///
/// fn app(cx: Scope) -> Element {
/// render! {
/// "hello!"
/// }
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let cfg = ServeConfigBuilder::new(app, ())
/// .assets_path("dist")
/// .build();
/// let ssr_state = SSRState::new(&cfg);
///
/// // This could be any state you want to be accessible from your server
/// // functions using `[DioxusServerContext::get]`.
/// let state = Arc::new(Mutex::new("state".to_string()));
///
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Register server functions, etc.
/// // Note you probably want to use `register_server_fns_with_handler`
/// // to inject the context into server functions running outside
/// // of an SSR render context.
/// .fallback(get(render_handler_with_context).with_state((
/// move |ctx| ctx.insert(state.clone()).unwrap(),
/// cfg,
/// ssr_state,
/// )))
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
pub async fn render_handler_with_context<
P: Clone + serde::Serialize + Send + Sync + 'static,
F: FnMut(&mut DioxusServerContext),
>(
State((mut inject_context, cfg, ssr_state)): State<(F, ServeConfig<P>, SSRState)>,
request: Request<Body>, request: Request<Body>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let (parts, _) = request.into_parts(); let (parts, _) = request.into_parts();
let url = parts.uri.path_and_query().unwrap().to_string(); let url = parts.uri.path_and_query().unwrap().to_string();
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(parts.into())); let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(parts.into()));
let server_context = DioxusServerContext::new(parts.clone()); let mut server_context = DioxusServerContext::new(parts.clone());
inject_context(&mut server_context);
match ssr_state.render(url, &cfg, &server_context).await { match ssr_state.render(url, &cfg, &server_context).await {
Ok(rendered) => { Ok(rendered) => {
@ -395,6 +445,14 @@ pub async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>
} }
} }
/// SSR renderer handler for Axum
pub async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
request: Request<Body>,
) -> impl IntoResponse {
render_handler_with_context(State((|_: &mut _| (), cfg, ssr_state)), request).await
}
fn report_err<E: std::fmt::Display>(e: E) -> Response<BoxBody> { fn report_err<E: std::fmt::Display>(e: E) -> Response<BoxBody> {
Response::builder() Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) .status(StatusCode::INTERNAL_SERVER_ERROR)

View file

@ -89,26 +89,26 @@ impl Service for ServerFnHandler {
let parts = Arc::new(RwLock::new(parts)); let parts = Arc::new(RwLock::new(parts));
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
let pool = get_local_pool(); let pool = get_local_pool();
pool.spawn_pinned({ let result = pool
let function = function.clone(); .spawn_pinned({
let mut server_context = server_context.clone(); let function = function.clone();
server_context.parts = parts; let mut server_context = server_context.clone();
move || async move { server_context.parts = parts;
let data = match function.encoding() { move || async move {
Encoding::Url | Encoding::Cbor => &body, let data = match function.encoding() {
Encoding::GetJSON | Encoding::GetCBOR => &query, Encoding::Url | Encoding::Cbor => &body,
}; Encoding::GetJSON | Encoding::GetCBOR => &query,
let server_function_future = function.call((), data); };
let server_function_future = let server_function_future = function.call((), data);
ProvideServerContext::new(server_function_future, server_context.clone()); let server_function_future = ProvideServerContext::new(
let resp = server_function_future.await; server_function_future,
server_context.clone(),
resp_tx.send(resp).unwrap(); );
} server_function_future.await
}); }
let result = resp_rx.await.unwrap(); })
.await?;
let mut res = http::Response::builder(); let mut res = http::Response::builder();
// Set the headers from the server context // Set the headers from the server context

View file

@ -3,7 +3,9 @@ use tracing_futures::Instrument;
use http::{Request, Response}; use http::{Request, Response};
/// A layer that wraps a service. This can be used to add additional information to the request, or response on top of some other service
pub trait Layer: Send + Sync + 'static { pub trait Layer: Send + Sync + 'static {
/// Wrap a boxed service with this layer
fn layer(&self, inner: BoxedService) -> BoxedService; fn layer(&self, inner: BoxedService) -> BoxedService;
} }
@ -17,7 +19,9 @@ where
} }
} }
/// A service is a function that takes a request and returns an async response
pub trait Service { pub trait Service {
/// Run the service and produce a future that resolves to a response
fn run( fn run(
&mut self, &mut self,
req: http::Request<hyper::body::Body>, req: http::Request<hyper::body::Body>,
@ -55,6 +59,7 @@ where
} }
} }
/// A boxed service is a type-erased service that can be used without knowing the underlying type
pub struct BoxedService(pub Box<dyn Service + Send>); pub struct BoxedService(pub Box<dyn Service + Send>);
impl tower::Service<http::Request<hyper::body::Body>> for BoxedService { impl tower::Service<http::Request<hyper::body::Body>> for BoxedService {

View file

@ -40,6 +40,8 @@ pub mod prelude {
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]
pub use crate::html_storage::deserialize::get_root_props_from_document; pub use crate::html_storage::deserialize::get_root_props_from_document;
pub use crate::launch::LaunchBuilder; pub use crate::launch::LaunchBuilder;
#[cfg(feature = "ssr")]
pub use crate::layer::{Layer, Service};
#[cfg(all(feature = "ssr", feature = "router"))] #[cfg(all(feature = "ssr", feature = "router"))]
pub use crate::render::pre_cache_static_routes_with_props; pub use crate::render::pre_cache_static_routes_with_props;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]

View file

@ -45,6 +45,8 @@ impl SsrRendererPool {
.expect("couldn't spawn runtime") .expect("couldn't spawn runtime")
.block_on(async move { .block_on(async move {
let mut vdom = VirtualDom::new_with_props(component, props); let mut vdom = VirtualDom::new_with_props(component, props);
// Make sure the evaluator is initialized
dioxus_ssr::eval::init_eval(vdom.base_scope());
let mut to = WriteBuffer { buffer: Vec::new() }; let mut to = WriteBuffer { buffer: Vec::new() };
// before polling the future, we need to set the context // before polling the future, we need to set the context
let prev_context = let prev_context =

View file

@ -53,7 +53,7 @@ fn default_external_navigation_handler() -> fn(Scope) -> Element {
dioxus_router::prelude::FailureExternalNavigation dioxus_router::prelude::FailureExternalNavigation
} }
/// The configeration for the router /// The configuration for the router
#[derive(Props, serde::Serialize, serde::Deserialize)] #[derive(Props, serde::Serialize, serde::Deserialize)]
pub struct FullstackRouterConfig<R> pub struct FullstackRouterConfig<R>
where where

View file

@ -125,14 +125,6 @@ impl server_fn::ServerFunctionRegistry<()> for DioxusServerFnRegistry {
} }
} }
fn register(
url: &'static str,
server_function: ServerFunction,
encoding: server_fn::Encoding,
) -> Result<(), Self::Error> {
Self::register_explicit("", url, server_function, encoding)
}
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> { fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
REGISTERED_SERVER_FUNCTIONS REGISTERED_SERVER_FUNCTIONS

View file

@ -1,9 +1,12 @@
[package] [package]
name = "generational-box" name = "generational-box"
authors = ["Evan Almloff"] authors = ["Evan Almloff"]
version = "0.0.0" version = "0.4.3"
edition = "2018" edition = "2018"
description = "A box backed by a generational runtime"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
keywords = ["generational", "box", "memory", "allocator"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
@ -17,6 +20,8 @@ criterion = "0.3"
[features] [features]
default = ["check_generation"] default = ["check_generation"]
check_generation = [] check_generation = []
debug_borrows = []
debug_ownership = []
[[bench]] [[bench]]
name = "lock" name = "lock"

View file

@ -11,6 +11,8 @@ Three main types manage state in Generational Box:
Example: Example:
```rust ```rust
use generational_box::Store;
// Create a store for this thread // Create a store for this thread
let store = Store::default(); let store = Store::default();

View file

@ -4,12 +4,16 @@
use parking_lot::{ use parking_lot::{
MappedRwLockReadGuard, MappedRwLockWriteGuard, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard, MappedRwLockReadGuard, MappedRwLockWriteGuard, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard,
}; };
use std::any::Any;
use std::error::Error;
use std::fmt::Display;
use std::sync::atomic::AtomicU32;
use std::{ use std::{
cell::{Ref, RefCell, RefMut}, cell::{Ref, RefCell, RefMut},
fmt::Debug, fmt::Debug,
marker::PhantomData, marker::PhantomData,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::{atomic::AtomicU32, Arc, OnceLock}, sync::{Arc, OnceLock},
}; };
/// # Example /// # Example
@ -52,7 +56,10 @@ fn leaking_is_ok() {
// don't drop the owner // don't drop the owner
std::mem::forget(owner); std::mem::forget(owner);
} }
assert_eq!(key.try_read().as_deref(), Some(&"hello world".to_string())); assert_eq!(
key.try_read().as_deref().unwrap(),
&"hello world".to_string()
);
} }
#[test] #[test]
@ -66,7 +73,7 @@ fn drops() {
key = owner.insert(data); key = owner.insert(data);
// drop the owner // drop the owner
} }
assert!(key.try_read().is_none()); assert!(key.try_read().is_err());
} }
#[test] #[test]
@ -123,7 +130,7 @@ fn fuzz() {
println!("{:?}", path); println!("{:?}", path);
for key in valid_keys.iter() { for key in valid_keys.iter() {
let value = key.read(); let value = key.read();
println!("{:?}", value); println!("{:?}", &*value);
assert!(value.starts_with("hello world")); assert!(value.starts_with("hello world"));
} }
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
@ -164,10 +171,12 @@ impl Debug for GenerationalBoxId {
} }
/// The core Copy state type. The generational box will be dropped when the [Owner] is dropped. /// The core Copy state type. The generational box will be dropped when the [Owner] is dropped.
pub struct GenerationalBox<T, S = UnsyncStorage> { pub struct GenerationalBox<T, S: 'static = UnsyncStorage> {
raw: MemoryLocation<S>, raw: MemoryLocation<S>,
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
generation: u32, generation: u32,
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: &'static std::panic::Location<'static>,
_marker: PhantomData<T>, _marker: PhantomData<T>,
} }
@ -176,11 +185,11 @@ impl<T: 'static, S: AnyStorage> Debug for GenerationalBox<T, S> {
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
f.write_fmt(format_args!( f.write_fmt(format_args!(
"{:?}@{:?}", "{:?}@{:?}",
self.raw.data.data_ptr(), self.raw.0.data.data_ptr(),
self.generation self.generation
))?; ))?;
#[cfg(not(any(debug_assertions, feature = "check_generation")))] #[cfg(not(any(debug_assertions, feature = "check_generation")))]
f.write_fmt(format_args!("{:?}", self.raw.data.as_ptr()))?; f.write_fmt(format_args!("{:?}", self.raw.0.data.as_ptr()))?;
Ok(()) Ok(())
} }
} }
@ -191,6 +200,7 @@ impl<T: 'static, S: Storage<T>> GenerationalBox<T, S> {
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
{ {
self.raw self.raw
.0
.generation .generation
.load(std::sync::atomic::Ordering::Relaxed) .load(std::sync::atomic::Ordering::Relaxed)
== self.generation == self.generation
@ -204,28 +214,54 @@ impl<T: 'static, S: Storage<T>> GenerationalBox<T, S> {
/// Get the id of the generational box. /// Get the id of the generational box.
pub fn id(&self) -> GenerationalBoxId { pub fn id(&self) -> GenerationalBoxId {
GenerationalBoxId { GenerationalBoxId {
data_ptr: self.raw.data.data_ptr(), data_ptr: self.raw.0.data.data_ptr(),
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
generation: self.generation, generation: self.generation,
} }
} }
/// Try to read the value. Returns None if the value is no longer valid. /// Try to read the value. Returns None if the value is no longer valid.
pub fn try_read(&self) -> Option<S::Ref> { #[track_caller]
self.validate().then(|| self.raw.data.try_read()).flatten() pub fn try_read(&self) -> Result<S::Ref, BorrowError> {
if !self.validate() {
return Err(BorrowError::Dropped(ValueDroppedError {
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
created_at: self.created_at,
}));
}
self.raw.0.data.try_read().ok_or_else(|| {
BorrowError::Dropped(ValueDroppedError {
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: self.created_at,
})
})
} }
/// Read the value. Panics if the value is no longer valid. /// Read the value. Panics if the value is no longer valid.
#[track_caller]
pub fn read(&self) -> S::Ref { pub fn read(&self) -> S::Ref {
self.try_read().unwrap() self.try_read().unwrap()
} }
/// Try to write the value. Returns None if the value is no longer valid. /// Try to write the value. Returns None if the value is no longer valid.
pub fn try_write(&self) -> Option<S::Mut> where { #[track_caller]
self.validate().then(|| self.raw.data.try_write()).flatten() pub fn try_write(&self) -> Result<S::Mut, BorrowMutError> {
if !self.validate() {
return Err(BorrowMutError::Dropped(ValueDroppedError {
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
created_at: self.created_at,
}));
}
self.raw.0.data.try_write().ok_or_else(|| {
BorrowMutError::Dropped(ValueDroppedError {
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: self.created_at,
})
})
} }
/// Write the value. Panics if the value is no longer valid. /// Write the value. Panics if the value is no longer valid.
#[track_caller]
pub fn write(&self) -> S::Mut { pub fn write(&self) -> S::Mut {
self.try_write().unwrap() self.try_write().unwrap()
} }
@ -233,7 +269,7 @@ impl<T: 'static, S: Storage<T>> GenerationalBox<T, S> {
/// Set the value. Panics if the value is no longer valid. /// Set the value. Panics if the value is no longer valid.
pub fn set(&self, value: T) { pub fn set(&self, value: T) {
self.validate().then(|| { self.validate().then(|| {
self.raw.data.set(value); self.raw.0.data.set(value);
}); });
} }
@ -241,7 +277,7 @@ impl<T: 'static, S: Storage<T>> GenerationalBox<T, S> {
pub fn ptr_eq(&self, other: &Self) -> bool { pub fn ptr_eq(&self, other: &Self) -> bool {
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
{ {
self.raw.data.data_ptr() == other.raw.data.data_ptr() self.raw.0.data.data_ptr() == other.raw.0.data.data_ptr()
&& self.generation == other.generation && self.generation == other.generation
} }
#[cfg(not(any(debug_assertions, feature = "check_generation")))] #[cfg(not(any(debug_assertions, feature = "check_generation")))]
@ -251,7 +287,7 @@ impl<T: 'static, S: Storage<T>> GenerationalBox<T, S> {
} }
} }
impl<T, S: Copy> Copy for GenerationalBox<T, S> {} impl<T, S: 'static + Copy> Copy for GenerationalBox<T, S> {}
impl<T, S: Copy> Clone for GenerationalBox<T, S> { impl<T, S: Copy> Clone for GenerationalBox<T, S> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
@ -260,12 +296,11 @@ impl<T, S: Copy> Clone for GenerationalBox<T, S> {
} }
/// A unsync storage. This is the default storage type. /// A unsync storage. This is the default storage type.
#[derive(Clone, Copy)] pub struct UnsyncStorage(RefCell<Option<Box<dyn std::any::Any>>>);
pub struct UnsyncStorage(&'static RefCell<Option<Box<dyn std::any::Any>>>);
impl Default for UnsyncStorage { impl Default for UnsyncStorage {
fn default() -> Self { fn default() -> Self {
Self(Box::leak(Box::new(RefCell::new(None)))) Self(RefCell::new(None))
} }
} }
@ -370,7 +405,7 @@ impl<T> MappableMut<T> for MappedRwLockWriteGuard<'static, T> {
} }
/// A trait for a storage backing type. (RefCell, RwLock, etc.) /// A trait for a storage backing type. (RefCell, RwLock, etc.)
pub trait Storage<Data>: Copy + AnyStorage + 'static { pub trait Storage<Data>: AnyStorage + 'static {
/// The reference this storage type returns. /// The reference this storage type returns.
type Ref: Mappable<Data> + Deref<Target = Data>; type Ref: Mappable<Data> + Deref<Target = Data>;
/// The mutable reference this storage type returns. /// The mutable reference this storage type returns.
@ -453,11 +488,17 @@ impl AnyStorage for UnsyncStorage {
if let Some(location) = runtime.borrow_mut().pop() { if let Some(location) = runtime.borrow_mut().pop() {
location location
} else { } else {
MemoryLocation { let data: &'static MemoryLocationInner =
data: UnsyncStorage(Box::leak(Box::new(RefCell::new(None)))), &*Box::leak(Box::new(MemoryLocationInner {
#[cfg(any(debug_assertions, feature = "check_generation"))] data: Self::default(),
generation: Box::leak(Box::default()), #[cfg(any(debug_assertions, feature = "check_generation"))]
} generation: 0.into(),
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_at: Default::default(),
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_mut_at: Default::default(),
}));
MemoryLocation(data)
} }
}) })
} }
@ -501,11 +542,19 @@ impl AnyStorage for SyncStorage {
} }
fn claim() -> MemoryLocation<Self> { fn claim() -> MemoryLocation<Self> {
MemoryLocation { sync_runtime().lock().pop().unwrap_or_else(|| {
data: SyncStorage(Box::leak(Box::new(RwLock::new(None)))), let data: &'static MemoryLocationInner<Self> =
#[cfg(any(debug_assertions, feature = "check_generation"))] &*Box::leak(Box::new(MemoryLocationInner {
generation: Box::leak(Box::default()), data: Self::default(),
} #[cfg(any(debug_assertions, feature = "check_generation"))]
generation: 0.into(),
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_at: Default::default(),
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_mut_at: Default::default(),
}));
MemoryLocation(data)
})
} }
fn recycle(location: &MemoryLocation<Self>) { fn recycle(location: &MemoryLocation<Self>) {
@ -514,12 +563,17 @@ impl AnyStorage for SyncStorage {
} }
} }
/// A memory location. This is the core type that is used to store values.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct MemoryLocation<S = UnsyncStorage> { struct MemoryLocation<S: 'static = UnsyncStorage>(&'static MemoryLocationInner<S>);
struct MemoryLocationInner<S = UnsyncStorage> {
data: S, data: S,
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
generation: &'static AtomicU32, generation: AtomicU32,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_at: RwLock<Vec<&'static std::panic::Location<'static>>>,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_mut_at: RwLock<Option<&'static std::panic::Location<'static>>>,
} }
impl<S> MemoryLocation<S> { impl<S> MemoryLocation<S> {
@ -528,47 +582,383 @@ impl<S> MemoryLocation<S> {
where where
S: AnyStorage, S: AnyStorage,
{ {
let old = self.data.take(); let old = self.0.data.take();
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
if old { if old {
let new_generation = self.generation.load(std::sync::atomic::Ordering::Relaxed) + 1; let new_generation = self.0.generation.load(std::sync::atomic::Ordering::Relaxed) + 1;
self.generation self.0
.generation
.store(new_generation, std::sync::atomic::Ordering::Relaxed); .store(new_generation, std::sync::atomic::Ordering::Relaxed);
} }
} }
fn replace<T: 'static>(&mut self, value: T) -> GenerationalBox<T, S> fn replace_with_caller<T: 'static>(
&mut self,
value: T,
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
caller: &'static std::panic::Location<'static>,
) -> GenerationalBox<T, S>
where where
S: Storage<T> + Copy, S: Storage<T>,
{ {
self.data.set(value); self.0.data.set(value);
GenerationalBox { GenerationalBox {
raw: *self, raw: *self,
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
generation: self.generation.load(std::sync::atomic::Ordering::Relaxed), generation: self.0.generation.load(std::sync::atomic::Ordering::Relaxed),
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: caller,
_marker: PhantomData, _marker: PhantomData,
} }
} }
#[track_caller]
fn try_borrow<T: Any>(
&self,
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: &'static std::panic::Location<'static>,
) -> Result<GenerationalRef<T, S>, BorrowError>
where
S: Storage<T>,
{
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
self.0
.borrowed_at
.write()
.push(std::panic::Location::caller());
match self.0.data.try_read() {
Some(borrow) => {
match Ref::filter_map(borrow, |any| any.as_ref()?.downcast_ref::<T>()) {
Ok(reference) => Ok(GenerationalRef {
inner: reference,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow: GenerationalRefBorrowInfo {
borrowed_at: std::panic::Location::caller(),
borrowed_from: self.0,
},
}),
Err(_) => Err(BorrowError::Dropped(ValueDroppedError {
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at,
})),
}
}
None => Err(BorrowError::AlreadyBorrowedMut(AlreadyBorrowedMutError {
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_mut_at: self.0.borrowed_mut_at.read().unwrap(),
})),
}
}
#[track_caller]
fn try_borrow_mut<T: Any>(
&self,
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: &'static std::panic::Location<'static>,
) -> Result<GenerationalRefMut<T>, BorrowMutError> {
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
{
self.0.borrowed_mut_at.write().unwrap() = Some(std::panic::Location::caller());
}
match self.0.data.try_borrow_mut() {
Ok(borrow_mut) => {
match RefMut::filter_map(borrow_mut, |any| any.as_mut()?.downcast_mut::<T>()) {
Ok(reference) => Ok(GenerationalRefMut {
inner: reference,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow: GenerationalRefMutBorrowInfo {
borrowed_from: self.0,
},
}),
Err(_) => Err(BorrowMutError::Dropped(ValueDroppedError {
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at,
})),
}
}
Err(_) => Err(BorrowMutError::AlreadyBorrowed(AlreadyBorrowedError {
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_at: self.0.borrowed_at.read().clone(),
})),
}
}
}
#[derive(Debug, Clone)]
/// An error that can occur when trying to borrow a value.
pub enum BorrowError {
/// The value was dropped.
Dropped(ValueDroppedError),
/// The value was already borrowed mutably.
AlreadyBorrowedMut(AlreadyBorrowedMutError),
}
impl Display for BorrowError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BorrowError::Dropped(error) => Display::fmt(error, f),
BorrowError::AlreadyBorrowedMut(error) => Display::fmt(error, f),
}
}
}
impl Error for BorrowError {}
#[derive(Debug, Clone)]
/// An error that can occur when trying to borrow a value mutably.
pub enum BorrowMutError {
/// The value was dropped.
Dropped(ValueDroppedError),
/// The value was already borrowed.
AlreadyBorrowed(AlreadyBorrowedError),
/// The value was already borrowed mutably.
AlreadyBorrowedMut(AlreadyBorrowedMutError),
}
impl Display for BorrowMutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BorrowMutError::Dropped(error) => Display::fmt(error, f),
BorrowMutError::AlreadyBorrowedMut(error) => Display::fmt(error, f),
BorrowMutError::AlreadyBorrowed(error) => Display::fmt(error, f),
}
}
}
impl Error for BorrowMutError {}
/// An error that can occur when trying to use a value that has been dropped.
#[derive(Debug, Copy, Clone)]
pub struct ValueDroppedError {
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: &'static std::panic::Location<'static>,
}
impl Display for ValueDroppedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Failed to borrow because the value was dropped.")?;
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
f.write_fmt(format_args!("created_at: {}", self.created_at))?;
Ok(())
}
}
impl std::error::Error for ValueDroppedError {}
/// An error that can occur when trying to borrow a value that has already been borrowed mutably.
#[derive(Debug, Copy, Clone)]
pub struct AlreadyBorrowedMutError {
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_mut_at: &'static std::panic::Location<'static>,
}
impl Display for AlreadyBorrowedMutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Failed to borrow because the value was already borrowed mutably.")?;
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
f.write_fmt(format_args!("borrowed_mut_at: {}", self.borrowed_mut_at))?;
Ok(())
}
}
impl std::error::Error for AlreadyBorrowedMutError {}
/// An error that can occur when trying to borrow a value mutably that has already been borrowed immutably.
#[derive(Debug, Clone)]
pub struct AlreadyBorrowedError {
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrowed_at: Vec<&'static std::panic::Location<'static>>,
}
impl Display for AlreadyBorrowedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Failed to borrow mutably because the value was already borrowed immutably.")?;
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
f.write_str("borrowed_at:")?;
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
for location in self.borrowed_at.iter() {
f.write_fmt(format_args!("\t{}", location))?;
}
Ok(())
}
}
impl std::error::Error for AlreadyBorrowedError {}
/// A reference to a value in a generational box.
pub struct GenerationalRef<T: 'static> {
inner: Ref<'static, T>,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow: GenerationalRefBorrowInfo,
}
impl<T: 'static> GenerationalRef<T> {
/// Map one ref type to another.
pub fn map<U, F>(orig: GenerationalRef<T>, f: F) -> GenerationalRef<U>
where
F: FnOnce(&T) -> &U,
{
GenerationalRef {
inner: Ref::map(orig.inner, f),
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow: GenerationalRefBorrowInfo {
borrowed_at: orig.borrow.borrowed_at,
borrowed_from: orig.borrow.borrowed_from,
},
}
}
/// Filter one ref type to another.
pub fn filter_map<U, F>(orig: GenerationalRef<T>, f: F) -> Option<GenerationalRef<U>>
where
F: FnOnce(&T) -> Option<&U>,
{
let Self {
inner,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow,
} = orig;
Ref::filter_map(inner, f).ok().map(|inner| GenerationalRef {
inner,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow: GenerationalRefBorrowInfo {
borrowed_at: borrow.borrowed_at,
borrowed_from: borrow.borrowed_from,
},
})
}
}
impl<T: 'static> Deref for GenerationalRef<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.inner.deref()
}
}
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
struct GenerationalRefBorrowInfo {
borrowed_at: &'static std::panic::Location<'static>,
borrowed_from: &'static MemoryLocationInner,
}
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
impl Drop for GenerationalRefBorrowInfo {
fn drop(&mut self) {
self.borrowed_from
.borrowed_at
.borrow_mut()
.retain(|location| std::ptr::eq(*location, self.borrowed_at as *const _));
}
}
/// A mutable reference to a value in a generational box.
pub struct GenerationalRefMut<T: 'static> {
inner: RefMut<'static, T>,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow: GenerationalRefMutBorrowInfo,
}
impl<T: 'static> GenerationalRefMut<T> {
/// Map one ref type to another.
pub fn map<U, F>(orig: GenerationalRefMut<T>, f: F) -> GenerationalRefMut<U>
where
F: FnOnce(&mut T) -> &mut U,
{
GenerationalRefMut {
inner: RefMut::map(orig.inner, f),
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow: orig.borrow,
}
}
/// Filter one ref type to another.
pub fn filter_map<U, F>(orig: GenerationalRefMut<T>, f: F) -> Option<GenerationalRefMut<U>>
where
F: FnOnce(&mut T) -> Option<&mut U>,
{
let Self {
inner,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow,
} = orig;
RefMut::filter_map(inner, f)
.ok()
.map(|inner| GenerationalRefMut {
inner,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
borrow,
})
}
}
impl<T: 'static> Deref for GenerationalRefMut<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.inner.deref()
}
}
impl<T: 'static> DerefMut for GenerationalRefMut<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner.deref_mut()
}
}
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
struct GenerationalRefMutBorrowInfo {
borrowed_from: &'static MemoryLocationInner,
}
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
impl Drop for GenerationalRefMutBorrowInfo {
fn drop(&mut self) {
self.borrowed_from.borrowed_mut_at.take();
}
} }
/// Owner: Handles dropping generational boxes. The owner acts like a runtime lifetime guard. Any states that you create with an owner will be dropped when that owner is dropped. /// Owner: Handles dropping generational boxes. The owner acts like a runtime lifetime guard. Any states that you create with an owner will be dropped when that owner is dropped.
pub struct Owner<S: AnyStorage = UnsyncStorage> { pub struct Owner<S: AnyStorage + 'static = UnsyncStorage> {
owned: Arc<Mutex<Vec<MemoryLocation<S>>>>, owned: Arc<Mutex<Vec<MemoryLocation<S>>>>,
phantom: PhantomData<S>, phantom: PhantomData<S>,
} }
impl<S: AnyStorage + Copy> Owner<S> { impl<S: AnyStorage + Copy> Owner<S> {
/// Insert a value into the store. The value will be dropped when the owner is dropped. /// Insert a value into the store. The value will be dropped when the owner is dropped.
#[track_caller]
pub fn insert<T: 'static>(&self, value: T) -> GenerationalBox<T, S> pub fn insert<T: 'static>(&self, value: T) -> GenerationalBox<T, S>
where where
S: Storage<T>, S: Storage<T>,
{ {
let mut location = S::claim(); let mut location = S::claim();
let key = location.replace(value); let key = location.replace_with_caller(
value,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
std::panic::Location::caller(),
);
self.owned.lock().push(location); self.owned.lock().push(location);
key key
} }
/// Insert a value into the store with a specific location blamed for creating the value. The value will be dropped when the owner is dropped.
pub fn insert_with_caller<T: 'static>(
&self,
value: T,
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
caller: &'static std::panic::Location<'static>,
) -> GenerationalBox<T> {
let mut location = self.store.claim();
let key = location.replace_with_caller(
value,
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
caller,
);
self.owned.borrow_mut().push(location);
key
}
/// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value. /// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value.
pub fn invalid<T: 'static>(&self) -> GenerationalBox<T, S> { pub fn invalid<T: 'static>(&self) -> GenerationalBox<T, S> {
let location = S::claim(); let location = S::claim();
@ -576,8 +966,11 @@ impl<S: AnyStorage + Copy> Owner<S> {
raw: location, raw: location,
#[cfg(any(debug_assertions, feature = "check_generation"))] #[cfg(any(debug_assertions, feature = "check_generation"))]
generation: location generation: location
.0
.generation .generation
.load(std::sync::atomic::Ordering::Relaxed), .load(std::sync::atomic::Ordering::Relaxed),
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: std::panic::Location::caller(),
_marker: PhantomData, _marker: PhantomData,
} }
} }

View file

@ -60,6 +60,9 @@ pub mod computed;
mod use_on_destroy; mod use_on_destroy;
pub use use_on_destroy::*; pub use use_on_destroy::*;
mod use_const;
pub use use_const::*;
mod use_context; mod use_context;
pub use use_context::*; pub use use_context::*;

View file

@ -0,0 +1,76 @@
use std::rc::Rc;
use dioxus_core::prelude::*;
/// Store constant state between component renders.
///
/// UseConst allows you to store state that is initialized once and then remains constant across renders.
/// You can only get an immutable reference after initalization.
/// This can be useful for values that don't need to update reactively, thus can be memoized easily
///
/// ```rust, ignore
/// struct ComplexData(i32);
///
/// fn Component(cx: Scope) -> Element {
/// let id = use_const(cx, || ComplexData(100));
///
/// cx.render(rsx! {
/// div { "{id.0}" }
/// })
/// }
/// ```
#[must_use]
pub fn use_const<T: 'static>(
cx: &ScopeState,
initial_state_fn: impl FnOnce() -> T,
) -> &UseConst<T> {
cx.use_hook(|| UseConst {
value: Rc::new(initial_state_fn()),
})
}
#[derive(Clone)]
pub struct UseConst<T> {
value: Rc<T>,
}
impl<T> PartialEq for UseConst<T> {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.value, &other.value)
}
}
impl<T: core::fmt::Display> core::fmt::Display for UseConst<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.value.fmt(f)
}
}
impl<T> UseConst<T> {
pub fn get_rc(&self) -> &Rc<T> {
&self.value
}
}
impl<T> std::ops::Deref for UseConst<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.value.as_ref()
}
}
#[test]
fn use_const_makes_sense() {
#[allow(unused)]
fn app(cx: Scope) -> Element {
let const_val = use_const(cx, || vec![0, 1, 2, 3]);
assert!(const_val[0] == 0);
// const_val.remove(0); // Cannot Compile, cannot get mutable reference now
None
}
}

View file

@ -1,5 +1,10 @@
use dioxus_core::{ScopeState, TaskId}; use dioxus_core::{ScopeState, TaskId};
use std::{any::Any, cell::Cell, future::Future}; use std::{
any::Any,
cell::{Cell, RefCell},
future::Future,
rc::Rc,
};
use crate::UseFutureDep; use crate::UseFutureDep;
@ -14,7 +19,7 @@ use crate::UseFutureDep;
/// ## Arguments /// ## Arguments
/// ///
/// - `dependencies`: a tuple of references to values that are `PartialEq` + `Clone`. /// - `dependencies`: a tuple of references to values that are `PartialEq` + `Clone`.
/// - `future`: a closure that takes the `dependencies` as arguments and returns a `'static` future. /// - `future`: a closure that takes the `dependencies` as arguments and returns a `'static` future. That future may return nothing or a closure that will be executed when the dependencies change to clean up the effect.
/// ///
/// ## Examples /// ## Examples
/// ///
@ -33,6 +38,16 @@ use crate::UseFutureDep;
/// } /// }
/// }); /// });
/// ///
/// // Only fetch the user data when the id changes.
/// use_effect(cx, (id,), |(id,)| {
/// to_owned![name];
/// async move {
/// let user = fetch_user(id).await;
/// name.set(user.name);
/// move || println!("Cleaning up from {}", id)
/// }
/// });
///
/// let name = name.get().clone().unwrap_or("Loading...".to_string()); /// let name = name.get().clone().unwrap_or("Loading...".to_string());
/// ///
/// render!( /// render!(
@ -45,34 +60,80 @@ use crate::UseFutureDep;
/// render!(Profile { id: 0 }) /// render!(Profile { id: 0 })
/// } /// }
/// ``` /// ```
pub fn use_effect<T, F, D>(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F) pub fn use_effect<T, R, D>(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> R)
where where
T: 'static,
F: Future<Output = T> + 'static,
D: UseFutureDep, D: UseFutureDep,
R: UseEffectReturn<T>,
{ {
struct UseEffect { struct UseEffect {
needs_regen: bool, needs_regen: bool,
task: Cell<Option<TaskId>>, task: Cell<Option<TaskId>>,
dependencies: Vec<Box<dyn Any>>, dependencies: Vec<Box<dyn Any>>,
cleanup: UseEffectCleanup,
}
impl Drop for UseEffect {
fn drop(&mut self) {
if let Some(cleanup) = self.cleanup.borrow_mut().take() {
cleanup();
}
}
} }
let state = cx.use_hook(move || UseEffect { let state = cx.use_hook(move || UseEffect {
needs_regen: true, needs_regen: true,
task: Cell::new(None), task: Cell::new(None),
dependencies: Vec::new(), dependencies: Vec::new(),
cleanup: Rc::new(RefCell::new(None)),
}); });
if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen { if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen {
// Call the cleanup function if it exists
if let Some(cleanup) = state.cleanup.borrow_mut().take() {
cleanup();
}
// We don't need regen anymore // We don't need regen anymore
state.needs_regen = false; state.needs_regen = false;
// Create the new future // Create the new future
let fut = future(dependencies.out()); let return_value = future(dependencies.out());
state.task.set(Some(cx.push_future(async move { if let Some(task) = return_value.apply(state.cleanup.clone(), cx) {
fut.await; state.task.set(Some(task));
}))); }
}
}
type UseEffectCleanup = Rc<RefCell<Option<Box<dyn FnOnce()>>>>;
/// Something that can be returned from a `use_effect` hook.
pub trait UseEffectReturn<T> {
fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId>;
}
impl<T> UseEffectReturn<()> for T
where
T: Future<Output = ()> + 'static,
{
fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId> {
Some(cx.push_future(self))
}
}
#[doc(hidden)]
pub struct CleanupFutureMarker;
impl<T, F> UseEffectReturn<CleanupFutureMarker> for T
where
T: Future<Output = F> + 'static,
F: FnOnce() + 'static,
{
fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId> {
let task = cx.push_future(async move {
let cleanup = self.await;
*oncleanup.borrow_mut() = Some(Box::new(cleanup) as Box<dyn FnOnce()>);
});
Some(task)
} }
} }

View file

@ -31,13 +31,14 @@ where
let state = cx.use_hook(move || UseFuture { let state = cx.use_hook(move || UseFuture {
update: cx.schedule_update(), update: cx.schedule_update(),
needs_regen: Cell::new(true), needs_regen: Rc::new(Cell::new(true)),
state: val.clone(), state: val.clone(),
task: Default::default(), task: Default::default(),
dependencies: Vec::new(),
}); });
if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { let state_dependencies = cx.use_hook(Vec::new);
if dependencies.clone().apply(state_dependencies) || state.needs_regen.get() {
// kill the old one, if it exists // kill the old one, if it exists
if let Some(task) = state.task.take() { if let Some(task) = state.task.take() {
cx.remove_future(task); cx.remove_future(task);
@ -69,11 +70,11 @@ pub enum FutureState<'a, T> {
Regenerating(&'a T), // the old value Regenerating(&'a T), // the old value
} }
#[derive(Clone)]
pub struct UseFuture<T: 'static> { pub struct UseFuture<T: 'static> {
update: Arc<dyn Fn()>, update: Arc<dyn Fn()>,
needs_regen: Cell<bool>, needs_regen: Rc<Cell<bool>>,
task: Rc<Cell<Option<TaskId>>>, task: Rc<Cell<Option<TaskId>>>,
dependencies: Vec<Box<dyn Any>>,
state: UseState<Option<T>>, state: UseState<Option<T>>,
} }

View file

@ -26,6 +26,7 @@ macro_rules! debug_location {
} }
pub mod error { pub mod error {
#[cfg(debug_assertions)]
fn locations_display(locations: &[&'static std::panic::Location<'static>]) -> String { fn locations_display(locations: &[&'static std::panic::Location<'static>]) -> String {
locations locations
.iter() .iter()

View file

@ -122,7 +122,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
} = cfg; } = cfg;
if let Ok(crate_dir) = PathBuf::from_str(root_path) { if let Ok(crate_dir) = PathBuf::from_str(root_path) {
// try to find the gitingore file // try to find the gitignore file
let gitignore_file_path = crate_dir.join(".gitignore"); let gitignore_file_path = crate_dir.join(".gitignore");
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path); let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
@ -152,21 +152,20 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
} }
let file_map = Arc::new(Mutex::new(file_map)); let file_map = Arc::new(Mutex::new(file_map));
let target_dir = crate_dir.join("target");
let hot_reload_socket_path = target_dir.join("dioxusin");
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
// On unix, if you force quit the application, it can leave the file socket open // On unix, if you force quit the application, it can leave the file socket open
// This will cause the local socket listener to fail to open // This will cause the local socket listener to fail to open
// We check if the file socket is already open from an old session and then delete it // We check if the file socket is already open from an old session and then delete it
let paths = ["./dioxusin", "./@dioxusin"]; if hot_reload_socket_path.exists() {
for path in paths { let _ = std::fs::remove_file(hot_reload_socket_path);
let path = PathBuf::from(path);
if path.exists() {
let _ = std::fs::remove_file(path);
}
} }
} }
match LocalSocketListener::bind("@dioxusin") { match LocalSocketListener::bind(hot_reload_socket_path) {
Ok(local_socket_stream) => { Ok(local_socket_stream) => {
let aborted = Arc::new(Mutex::new(false)); let aborted = Arc::new(Mutex::new(false));

View file

@ -1,4 +1,7 @@
use std::io::{BufRead, BufReader}; use std::{
io::{BufRead, BufReader},
path::PathBuf,
};
use dioxus_core::Template; use dioxus_core::Template;
#[cfg(feature = "file_watcher")] #[cfg(feature = "file_watcher")]
@ -24,7 +27,8 @@ pub enum HotReloadMsg {
/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected /// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) { pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
std::thread::spawn(move || { std::thread::spawn(move || {
if let Ok(socket) = LocalSocketStream::connect("@dioxusin") { let path = PathBuf::from("./").join("target").join("dioxusin");
if let Ok(socket) = LocalSocketStream::connect(path) {
let mut buf_reader = BufReader::new(socket); let mut buf_reader = BufReader::new(socket);
loop { loop {
let mut buf = String::new(); let mut buf = String::new();

View file

@ -74,7 +74,7 @@ macro_rules! impl_attribute_match {
$attr:ident $fil:ident: $vil:ident (in $ns:literal), $attr:ident $fil:ident: $vil:ident (in $ns:literal),
) => { ) => {
if $attr == stringify!($fil) { if $attr == stringify!($fil) {
return Some((stringify!(fil), Some(ns))); return Some((stringify!(fil), Some($ns)));
} }
}; };
} }
@ -180,14 +180,26 @@ macro_rules! impl_element_match {
}; };
( (
$el:ident $name:ident $namespace:tt { $el:ident $name:ident $namespace:literal {
$( $(
$fil:ident: $vil:ident $extra:tt, $fil:ident: $vil:ident $extra:tt,
)* )*
} }
) => { ) => {
if $el == stringify!($name) { if $el == stringify!($name) {
return Some((stringify!($name), Some(stringify!($namespace)))); return Some((stringify!($name), Some($namespace)));
}
};
(
$el:ident $name:ident [$_:literal, $namespace:tt] {
$(
$fil:ident: $vil:ident $extra:tt,
)*
}
) => {
if $el == stringify!($name) {
return Some((stringify!($name), Some($namespace)));
} }
}; };
} }
@ -207,6 +219,8 @@ macro_rules! impl_element_match_attributes {
$attr $fil: $vil ($extra), $attr $fil: $vil ($extra),
); );
)* )*
return impl_map_global_attributes!($el $attr $name None);
} }
}; };
@ -223,10 +237,41 @@ macro_rules! impl_element_match_attributes {
$attr $fil: $vil ($extra), $attr $fil: $vil ($extra),
); );
)* )*
return impl_map_global_attributes!($el $attr $name $namespace);
} }
} }
} }
#[cfg(feature = "hot-reload-context")]
macro_rules! impl_map_global_attributes {
(
$el:ident $attr:ident $element:ident None
) => {
map_global_attributes($attr)
};
(
$el:ident $attr:ident $element:ident $namespace:literal
) => {
if $namespace == "http://www.w3.org/2000/svg" {
map_svg_attributes($attr)
} else {
map_global_attributes($attr)
}
};
(
$el:ident $attr:ident $element:ident [$name:literal, $namespace:tt]
) => {
if $namespace == "http://www.w3.org/2000/svg" {
map_svg_attributes($attr)
} else {
map_global_attributes($attr)
}
};
}
macro_rules! builder_constructors { macro_rules! builder_constructors {
( (
$( $(
@ -254,7 +299,7 @@ macro_rules! builder_constructors {
} }
); );
)* )*
map_global_attributes(attribute).or_else(|| map_svg_attributes(attribute)) None
} }
fn map_element(element: &str) -> Option<(&'static str, Option<&'static str>)> { fn map_element(element: &str) -> Option<(&'static str, Option<&'static str>)> {
@ -782,6 +827,7 @@ builder_constructors! {
decoding: ImageDecoding DEFAULT, decoding: ImageDecoding DEFAULT,
height: usize DEFAULT, height: usize DEFAULT,
ismap: Bool DEFAULT, ismap: Bool DEFAULT,
loading: String DEFAULT,
src: Uri DEFAULT, src: Uri DEFAULT,
srcset: String DEFAULT, // FIXME this is much more complicated srcset: String DEFAULT, // FIXME this is much more complicated
usemap: String DEFAULT, // FIXME should be a fragment starting with '#' usemap: String DEFAULT, // FIXME should be a fragment starting with '#'

View file

@ -269,6 +269,9 @@ trait_methods! {
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/azimuth> /// <https://developer.mozilla.org/en-US/docs/Web/CSS/azimuth>
azimuth: "azimuth", "style"; azimuth: "azimuth", "style";
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter>
backdrop_filter: "backdrop-filter", "style";
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/backface-visibility> /// <https://developer.mozilla.org/en-US/docs/Web/CSS/backface-visibility>
backface_visibility: "backface-visibility", "style"; backface_visibility: "backface-visibility", "style";

View file

@ -57,7 +57,12 @@ impl DioxusState {
node.insert(ElementIdComponent(element_id)); node.insert(ElementIdComponent(element_id));
if self.node_id_mapping.len() <= element_id.0 { if self.node_id_mapping.len() <= element_id.0 {
self.node_id_mapping.resize(element_id.0 + 1, None); self.node_id_mapping.resize(element_id.0 + 1, None);
} else if let Some(mut node) =
self.node_id_mapping[element_id.0].and_then(|id| node.real_dom_mut().get_mut(id))
{
node.remove();
} }
self.node_id_mapping[element_id.0] = Some(node_id); self.node_id_mapping[element_id.0] = Some(node_id);
} }

View file

@ -14,7 +14,7 @@ dioxus-html = { workspace = true }
dioxus-native-core = { workspace = true, features = ["layout-attributes"] } dioxus-native-core = { workspace = true, features = ["layout-attributes"] }
dioxus-native-core-macro = { workspace = true } dioxus-native-core-macro = { workspace = true }
tui = "0.17.0" ratatui = "0.24.0"
crossterm = "0.26.1" crossterm = "0.26.1"
anyhow = "1.0.42" anyhow = "1.0.42"
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }

View file

@ -17,6 +17,7 @@ use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt};
use futures_channel::mpsc::unbounded; use futures_channel::mpsc::unbounded;
use layout::TaffyLayout; use layout::TaffyLayout;
use prevent_default::PreventDefault; use prevent_default::PreventDefault;
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{io, time::Duration}; use std::{io, time::Duration};
use std::{ use std::{
pin::Pin, pin::Pin,
@ -26,7 +27,6 @@ use std::{rc::Rc, sync::RwLock};
use style_attributes::StyleModifier; use style_attributes::StyleModifier;
pub use taffy::{geometry::Point, prelude::*}; pub use taffy::{geometry::Point, prelude::*};
use tokio::select; use tokio::select;
use tui::{backend::CrosstermBackend, Terminal};
use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject}; use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject};
mod config; mod config;
@ -180,7 +180,7 @@ pub fn render<R: Driver>(
if !to_rerender.is_empty() || updated { if !to_rerender.is_empty() || updated {
updated = false; updated = false;
fn resize(dims: tui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) { fn resize(dims: ratatui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) {
let width = screen_to_layout_space(dims.width); let width = screen_to_layout_space(dims.width);
let height = screen_to_layout_space(dims.height); let height = screen_to_layout_space(dims.height);
let root_node = rdom let root_node = rdom
@ -222,7 +222,7 @@ pub fn render<R: Driver>(
} else { } else {
let rdom = rdom.read().unwrap(); let rdom = rdom.read().unwrap();
resize( resize(
tui::layout::Rect { ratatui::layout::Rect {
x: 0, x: 0,
y: 0, y: 0,
width: 1000, width: 1000,

View file

@ -1,11 +1,10 @@
use dioxus_native_core::{prelude::*, tree::TreeRef}; use dioxus_native_core::{prelude::*, tree::TreeRef};
use std::io::Stdout; use ratatui::{layout::Rect, style::Color};
use taffy::{ use taffy::{
geometry::Point, geometry::Point,
prelude::{Dimension, Layout, Size}, prelude::{Dimension, Layout, Size},
Taffy, Taffy,
}; };
use tui::{backend::CrosstermBackend, layout::Rect, style::Color};
use crate::{ use crate::{
focus::Focused, focus::Focused,
@ -20,7 +19,7 @@ use crate::{
const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5]; const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
pub(crate) fn render_vnode( pub(crate) fn render_vnode(
frame: &mut tui::Frame<CrosstermBackend<Stdout>>, frame: &mut ratatui::Frame,
layout: &Taffy, layout: &Taffy,
node: NodeRef, node: NodeRef,
cfg: Config, cfg: Config,
@ -96,7 +95,7 @@ pub(crate) fn render_vnode(
impl RinkWidget for NodeRef<'_> { impl RinkWidget for NodeRef<'_> {
fn render(self, area: Rect, mut buf: RinkBuffer<'_>) { fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
use tui::symbols::line::*; use ratatui::symbols::line::*;
enum Direction { enum Direction {
Left, Left,

View file

@ -1,6 +1,6 @@
use std::{num::ParseFloatError, str::FromStr}; use std::{num::ParseFloatError, str::FromStr};
use tui::style::{Color, Modifier, Style}; use ratatui::style::{Color, Modifier, Style};
use crate::RenderingMode; use crate::RenderingMode;
@ -442,6 +442,7 @@ impl RinkStyle {
impl From<RinkStyle> for Style { impl From<RinkStyle> for Style {
fn from(val: RinkStyle) -> Self { fn from(val: RinkStyle) -> Self {
Style { Style {
underline_color: None,
fg: val.fg.map(|c| c.color), fg: val.fg.map(|c| c.color),
bg: val.bg.map(|c| c.color), bg: val.bg.map(|c| c.color),
add_modifier: val.add_modifier, add_modifier: val.add_modifier,

View file

@ -187,8 +187,8 @@ pub enum BorderStyle {
} }
impl BorderStyle { impl BorderStyle {
pub fn symbol_set(&self) -> Option<tui::symbols::line::Set> { pub fn symbol_set(&self) -> Option<ratatui::symbols::line::Set> {
use tui::symbols::line::*; use ratatui::symbols::line::*;
const DASHED: Set = Set { const DASHED: Set = Set {
horizontal: "", horizontal: "",
vertical: "", vertical: "",
@ -570,7 +570,7 @@ fn apply_animation(name: &str, _value: &str, _style: &mut StyleModifier) {
} }
fn apply_font(name: &str, value: &str, style: &mut StyleModifier) { fn apply_font(name: &str, value: &str, style: &mut StyleModifier) {
use tui::style::Modifier; use ratatui::style::Modifier;
match name { match name {
"font" => (), "font" => (),
"font-family" => (), "font-family" => (),
@ -593,7 +593,7 @@ fn apply_font(name: &str, value: &str, style: &mut StyleModifier) {
} }
fn apply_text(name: &str, value: &str, style: &mut StyleModifier) { fn apply_text(name: &str, value: &str, style: &mut StyleModifier) {
use tui::style::Modifier; use ratatui::style::Modifier;
match name { match name {
"text-align" => todo!(), "text-align" => todo!(),

View file

@ -1,4 +1,4 @@
use tui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
style::{Color, Modifier}, style::{Color, Modifier},

View file

@ -75,6 +75,7 @@ impl Redirect {
let (segments, query) = parse_route_segments( let (segments, query) = parse_route_segments(
path.span(), path.span(),
#[allow(clippy::map_identity)]
closure_arguments.iter().map(|(name, ty)| (name, ty)), closure_arguments.iter().map(|(name, ty)| (name, ty)),
&path.value(), &path.value(),
)?; )?;

View file

@ -19,6 +19,7 @@ thiserror = { workspace = true }
futures-util = { workspace = true } futures-util = { workspace = true }
urlencoding = "2.1.3" urlencoding = "2.1.3"
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1.0.91", optional = true }
url = "2.3.1" url = "2.3.1"
wasm-bindgen = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true }
web-sys = { version = "0.3.60", optional = true, features = [ web-sys = { version = "0.3.60", optional = true, features = [
@ -26,18 +27,22 @@ web-sys = { version = "0.3.60", optional = true, features = [
] } ] }
js-sys = { version = "0.3.63", optional = true } js-sys = { version = "0.3.63", optional = true }
gloo-utils = { version = "0.1.6", optional = true } gloo-utils = { version = "0.1.6", optional = true }
dioxus-liveview = { workspace = true, optional = true }
dioxus-ssr = { workspace = true, optional = true } dioxus-ssr = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true } tokio = { workspace = true, features = ["full"], optional = true }
[features] [features]
default = ["web"] default = ["web"]
ssr = ["dioxus-ssr", "tokio"] ssr = ["dioxus-ssr", "tokio"]
liveview = ["dioxus-liveview", "tokio", "dep:serde", "serde_json"]
wasm_test = [] wasm_test = []
serde = ["dep:serde", "gloo-utils/serde"] serde = ["dep:serde", "gloo-utils/serde"]
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"] web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
[dev-dependencies] [dev-dependencies]
axum = { version = "0.6.1", features = ["ws"] }
dioxus = { path = "../dioxus" } dioxus = { path = "../dioxus" }
dioxus-liveview = { workspace = true, features = ["axum"] }
dioxus-ssr = { path = "../ssr" } dioxus-ssr = { path = "../ssr" }
criterion = { version = "0.5", features = ["async_tokio", "html_reports"] } criterion = { version = "0.5", features = ["async_tokio", "html_reports"] }

View file

@ -2,6 +2,47 @@ use dioxus::prelude::*;
use dioxus_router::prelude::*; use dioxus_router::prelude::*;
use std::str::FromStr; use std::str::FromStr;
#[cfg(feature = "liveview")]
#[tokio::main]
async fn main() {
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
let listen_address: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = dioxus_liveview::LiveViewPool::new();
let app = Router::new()
.fallback(get(move || async move {
Html(format!(
r#"
<!DOCTYPE html>
<html>
<head></head>
<body><div id="main"></div></body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{listen_address}/ws"))
))
}))
.route(
"/ws",
get(move |ws: WebSocketUpgrade| async move {
ws.on_upgrade(move |socket| async move {
_ = view
.launch(dioxus_liveview::axum_socket(socket), Root)
.await;
})
}),
);
println!("Listening on http://{listen_address}");
axum::Server::bind(&listen_address.to_string().parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "liveview"))]
fn main() { fn main() {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
dioxus_desktop::launch(Root); dioxus_desktop::launch(Root);
@ -10,21 +51,26 @@ fn main() {
dioxus_web::launch(root); dioxus_web::launch(root);
} }
#[cfg(feature = "liveview")]
#[component] #[component]
fn Root(cx: Scope) -> Element { fn Root(cx: Scope) -> Element {
render! { let history = LiveviewHistory::new(cx);
Router::<Route> {} render! { Router::<Route> {
} config: || RouterConfig::default().history(history),
} }
}
#[cfg(not(feature = "liveview"))]
#[component]
fn Root(cx: Scope) -> Element {
render! { Router::<Route> {} }
} }
#[component] #[component]
fn UserFrame(cx: Scope, user_id: usize) -> Element { fn UserFrame(cx: Scope, user_id: usize) -> Element {
render! { render! {
pre { pre { "UserFrame{{\n\tuser_id:{user_id}\n}}" }
"UserFrame{{\n\tuser_id:{user_id}\n}}" div { background_color: "rgba(0,0,0,50%)",
}
div {
background_color: "rgba(0,0,0,50%)",
"children:" "children:"
Outlet::<Route> {} Outlet::<Route> {}
} }
@ -88,6 +134,16 @@ fn Route3(cx: Scope, dynamic: String) -> Element {
to: Route::Route2 { user_id: 8888 }, to: Route::Route2 { user_id: 8888 },
"hello world link" "hello world link"
} }
button {
disabled: !navigator.can_go_back(),
onclick: move |_| { navigator.go_back(); },
"go back"
}
button {
disabled: !navigator.can_go_forward(),
onclick: move |_| { navigator.go_forward(); },
"go forward"
}
button { button {
onclick: move |_| { navigator.push("https://www.google.com"); }, onclick: move |_| { navigator.push("https://www.google.com"); },
"google link" "google link"

View file

@ -142,7 +142,7 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
} }
}; };
let disabled = !router.can_go_back(); let disabled = !router.can_go_forward();
render! { render! {
button { button {

View file

@ -11,6 +11,8 @@ use crate::navigation::NavigationTarget;
use crate::prelude::Routable; use crate::prelude::Routable;
use crate::utils::use_router_internal::use_router_internal; use crate::utils::use_router_internal::use_router_internal;
use url::Url;
/// Something that can be converted into a [`NavigationTarget`]. /// Something that can be converted into a [`NavigationTarget`].
#[derive(Clone)] #[derive(Clone)]
pub enum IntoRoutable { pub enum IntoRoutable {
@ -53,6 +55,18 @@ impl From<&str> for IntoRoutable {
} }
} }
impl From<Url> for IntoRoutable {
fn from(url: Url) -> Self {
IntoRoutable::FromStr(url.to_string())
}
}
impl From<&Url> for IntoRoutable {
fn from(url: &Url) -> Self {
IntoRoutable::FromStr(url.to_string())
}
}
/// The properties for a [`Link`]. /// The properties for a [`Link`].
#[derive(Props)] #[derive(Props)]
pub struct LinkProps<'a> { pub struct LinkProps<'a> {

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