Merge branch 'master' into intigrate-collect-assets
This commit is contained in:
commit
66e2c02bf7
168 files changed
+3727
-858
No files matched your search
@@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
|
||||
|
||||
use dioxus_rsx::IfmtInput;
|
||||
|
||||
use crate::write_ifmt;
|
||||
use crate::{indent::IndentOptions, write_ifmt};
|
||||
|
||||
/// The output buffer that tracks indent and string
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Buffer {
|
||||
pub buf: String,
|
||||
pub indent: usize,
|
||||
pub indent_level: usize,
|
||||
pub indent: IndentOptions,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
@@ -31,16 +32,16 @@ impl Buffer {
|
||||
}
|
||||
|
||||
pub fn tab(&mut self) -> Result {
|
||||
self.write_tabs(self.indent)
|
||||
self.write_tabs(self.indent_level)
|
||||
}
|
||||
|
||||
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 {
|
||||
for _ in 0..num {
|
||||
write!(self.buf, " ")?
|
||||
write!(self.buf, "{}", self.indent.indent_str())?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ impl Writer<'_> {
|
||||
|
||||
// check if we have a lot of 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 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 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;
|
||||
} else {
|
||||
opt_level = ShortOptimization::PropsOnTop;
|
||||
@@ -185,11 +185,11 @@ impl Writer<'_> {
|
||||
}
|
||||
|
||||
while let Some(attr) = attr_iter.next() {
|
||||
self.out.indent += 1;
|
||||
self.out.indent_level += 1;
|
||||
if !sameline {
|
||||
self.write_comments(attr.attr.start())?;
|
||||
}
|
||||
self.out.indent -= 1;
|
||||
self.out.indent_level -= 1;
|
||||
|
||||
if !sameline {
|
||||
self.out.indented_tabbed_line()?;
|
||||
@@ -398,14 +398,14 @@ impl Writer<'_> {
|
||||
for idx in start.line..end.line {
|
||||
let line = &self.src[idx];
|
||||
if line.trim().starts_with("//") {
|
||||
for _ in 0..self.out.indent + 1 {
|
||||
for _ in 0..self.out.indent_level + 1 {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
writeln!(self.out, "{}", line.trim()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..self.out.indent {
|
||||
for _ in 0..self.out.indent_level {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Writer<'_> {
|
||||
let first_line = &self.src[start.line - 1];
|
||||
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() {
|
||||
writeln!(self.out)?;
|
||||
@@ -43,9 +43,9 @@ impl Writer<'_> {
|
||||
};
|
||||
|
||||
// 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 required_indent = self.out.indent + offset;
|
||||
let required_indent = self.out.indent_level + offset;
|
||||
self.out.write_tabs(required_indent)?;
|
||||
|
||||
let line = line.trim_start();
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
-15
@@ -16,8 +16,11 @@ mod collect_macros;
|
||||
mod component;
|
||||
mod element;
|
||||
mod expr;
|
||||
mod indent;
|
||||
mod writer;
|
||||
|
||||
pub use indent::{IndentOptions, IndentType};
|
||||
|
||||
/// 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.
|
||||
@@ -47,7 +50,7 @@ pub struct FormattedBlock {
|
||||
/// back to the file precisely.
|
||||
///
|
||||
/// 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 parsed = syn::parse_file(contents).unwrap();
|
||||
@@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
}
|
||||
|
||||
let mut writer = Writer::new(contents);
|
||||
writer.out.indent = indent;
|
||||
|
||||
// Don't parse nested macros
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
@@ -159,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
|
||||
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 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);
|
||||
|
||||
@@ -230,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
|
||||
let display = DisplayIfmt(input);
|
||||
write!(writable, "{}", display)
|
||||
}
|
||||
|
||||
pub fn leading_whitespaces(input: &str) -> usize {
|
||||
input
|
||||
.chars()
|
||||
.map_while(|c| match c {
|
||||
' ' => Some(1),
|
||||
'\t' => Some(4),
|
||||
_ => None,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
@@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
|
||||
|
||||
// Push out the indent level and write each component, line by line
|
||||
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.out.indent -= 1;
|
||||
self.out.indent_level -= 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ macro_rules! twoway {
|
||||
#[test]
|
||||
fn $name() {
|
||||
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);
|
||||
// normalize line endings
|
||||
let out = out.replace("\r", "");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use dioxus_autofmt::{IndentOptions, IndentType};
|
||||
|
||||
macro_rules! twoway {
|
||||
($val:literal => $name:ident) => {
|
||||
($val:literal => $name:ident ($indent:expr)) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src_right = include_str!(concat!("./wrong/", $val, ".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);
|
||||
|
||||
// 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)));
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,7 @@
|
||||
rsx! {
|
||||
div {
|
||||
// Comments
|
||||
class: "asdasd",
|
||||
"hello world"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
rsx! {
|
||||
div {
|
||||
// Comments
|
||||
class: "asdasd", "hello world" }
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,3 @@
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! { div { "hello world" } })
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {"hello world" }
|
||||
})
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-cli"
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "CLI tool for developing, testing, and publishing Dioxus apps"
|
||||
|
||||
@@ -11,7 +11,7 @@ It handles building, bundling, development and publishing to simplify developmen
|
||||
### Install the stable version (recommended)
|
||||
|
||||
```
|
||||
cargo install dioxus-cli --locked
|
||||
cargo install dioxus-cli
|
||||
```
|
||||
|
||||
### Install the latest development build through git
|
||||
|
||||
@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
|
||||
|
||||
index_on_404 = true
|
||||
|
||||
watch_path = ["src"]
|
||||
watch_path = ["src", "examples"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
@@ -49,14 +49,25 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
|
||||
|
||||
// [1] Build the .wasm module
|
||||
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 = cmd
|
||||
.cwd(crate_dir)
|
||||
.arg("build")
|
||||
.arg("--target")
|
||||
.arg("wasm32-unknown-unknown")
|
||||
.arg("--message-format=json")
|
||||
.arg("--quiet");
|
||||
.arg("--message-format=json");
|
||||
|
||||
let cmd = if config.release {
|
||||
cmd.arg("--release")
|
||||
@@ -66,7 +77,7 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
|
||||
let cmd = if config.verbose {
|
||||
cmd.arg("--verbose")
|
||||
} else {
|
||||
cmd
|
||||
cmd.arg("--quiet")
|
||||
};
|
||||
|
||||
let cmd = if config.custom_profile.is_some() {
|
||||
@@ -263,6 +274,7 @@ pub fn build_desktop(
|
||||
let mut cmd = subprocess::Exec::cmd("cargo")
|
||||
.cwd(&config.crate_dir)
|
||||
.arg("build")
|
||||
.arg("--quiet")
|
||||
.arg("--message-format=json");
|
||||
|
||||
if config.release {
|
||||
@@ -494,7 +506,7 @@ pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String
|
||||
.unwrap_or_default()
|
||||
.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");
|
||||
}
|
||||
if !skip_assets {
|
||||
let manifest = config.asset_manifest();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use dioxus_autofmt::{IndentOptions, IndentType};
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{fs, path::Path, process::exit};
|
||||
|
||||
@@ -35,7 +36,8 @@ impl Autoformat {
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
// exit process with error
|
||||
@@ -46,17 +48,21 @@ impl Autoformat {
|
||||
|
||||
// Format single 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();
|
||||
std::io::stdin().read_to_string(&mut contents)?;
|
||||
Ok(contents)
|
||||
file_content = Ok(contents);
|
||||
} else {
|
||||
fs::read_to_string(&file)
|
||||
indent = indentation_for(".")?;
|
||||
file_content = fs::read_to_string(&file);
|
||||
};
|
||||
|
||||
match file_content {
|
||||
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);
|
||||
if file == "-" {
|
||||
print!("{}", out);
|
||||
@@ -93,6 +99,12 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
||||
let mut files_to_format = vec![];
|
||||
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
|
||||
.into_iter()
|
||||
.filter(|file| {
|
||||
@@ -104,10 +116,11 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
||||
})
|
||||
.map(|path| async {
|
||||
let _path = path.clone();
|
||||
let _indent = indent.clone();
|
||||
let res = tokio::spawn(async move {
|
||||
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();
|
||||
|
||||
if !edits.is_empty() {
|
||||
@@ -151,6 +164,49 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
||||
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>) {
|
||||
let Ok(folder) = folder.read_dir() else {
|
||||
return;
|
||||
|
||||
@@ -39,8 +39,8 @@ impl Build {
|
||||
.platform
|
||||
.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
// #[cfg(feature = "plugin")]
|
||||
// let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
|
||||
match platform {
|
||||
Platform::Web => {
|
||||
@@ -98,8 +98,8 @@ impl Build {
|
||||
)?;
|
||||
file.write_all(temp.as_bytes())?;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||
// #[cfg(feature = "plugin")]
|
||||
// let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ impl Default for DioxusConfig {
|
||||
},
|
||||
proxy: Some(vec![]),
|
||||
watcher: WebWatcherConfig {
|
||||
watch_path: Some(vec![PathBuf::from("src")]),
|
||||
watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
|
||||
reload_html: Some(false),
|
||||
index_on_404: Some(true),
|
||||
},
|
||||
|
||||
@@ -29,6 +29,9 @@ pub enum Error {
|
||||
#[error("Cargo Error: {0}")]
|
||||
CargoError(String),
|
||||
|
||||
#[error("Couldn't retrieve cargo metadata")]
|
||||
CargoMetadata(#[source] cargo_metadata::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
CustomError(String),
|
||||
|
||||
|
||||
@@ -28,7 +28,19 @@ pub fn set_up_logging() {
|
||||
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())
|
||||
.apply()
|
||||
.unwrap();
|
||||
|
||||
+28
-39
@@ -9,42 +9,31 @@ use dioxus_cli::plugin::PluginManager;
|
||||
|
||||
use Commands::*;
|
||||
|
||||
fn get_bin(bin: Option<String>) -> Result<Option<PathBuf>> {
|
||||
const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace.";
|
||||
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
|
||||
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 manifest = cargo_toml::Manifest::from_path("./Cargo.toml")
|
||||
.map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?;
|
||||
let crate_dir = package
|
||||
.manifest_path
|
||||
.parent()
|
||||
.ok_or("couldn't take parent dir".into())
|
||||
.map_err(Error::CargoError)?;
|
||||
|
||||
if let Some(workspace) = manifest.workspace {
|
||||
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)
|
||||
Ok(crate_dir.into())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -55,7 +44,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
let bin = get_bin(args.bin)?;
|
||||
|
||||
let _dioxus_config = DioxusConfig::load(bin.clone())
|
||||
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
|
||||
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
|
||||
@@ -72,15 +61,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
||||
|
||||
Build(opts) => opts
|
||||
.build(bin.clone())
|
||||
.build(Some(bin.clone()))
|
||||
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
|
||||
|
||||
Clean(opts) => opts
|
||||
.clean(bin.clone())
|
||||
.clean(Some(bin.clone()))
|
||||
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
|
||||
|
||||
Serve(opts) => opts
|
||||
.serve(bin.clone())
|
||||
.serve(Some(bin.clone()))
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
|
||||
|
||||
@@ -93,7 +82,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
|
||||
|
||||
Bundle(opts) => opts
|
||||
.bundle(bin.clone())
|
||||
.bundle(Some(bin.clone()))
|
||||
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
|
||||
@@ -51,8 +51,6 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
|
||||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
clear_paths();
|
||||
|
||||
Some(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
file_map: file_map.clone(),
|
||||
@@ -103,7 +101,14 @@ async fn serve<P: Platform + Send + 'static>(
|
||||
}
|
||||
|
||||
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) => {
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
// States
|
||||
@@ -115,9 +120,9 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
||||
let file_map = hot_reload_state.file_map.clone();
|
||||
let channels = channels.clone();
|
||||
let aborted = aborted.clone();
|
||||
let _ = local_socket_stream.set_nonblocking(true);
|
||||
move || {
|
||||
loop {
|
||||
//accept() will block the thread when local_socket_stream is in blocking mode (default)
|
||||
match local_socket_stream.accept() {
|
||||
Ok(mut connection) => {
|
||||
// send any templates than have changed before the socket connected
|
||||
@@ -175,17 +180,14 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_paths() {
|
||||
fn clear_paths(file_socket_path: &std::path::Path) {
|
||||
if cfg!(target_os = "macos") {
|
||||
// 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
|
||||
// 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 {
|
||||
let path = std::path::PathBuf::from(path);
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
if file_socket_path.exists() {
|
||||
let _ = std::fs::remove_file(file_socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
.watcher
|
||||
.watch_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec![PathBuf::from("src")]);
|
||||
.unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]);
|
||||
|
||||
let watcher_config = config.clone();
|
||||
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
|
||||
@@ -56,6 +56,16 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
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) {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
messages.extend(msgs);
|
||||
@@ -122,12 +132,12 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
.unwrap();
|
||||
|
||||
for sub_path in allow_watch_path {
|
||||
watcher
|
||||
.watch(
|
||||
&config.crate_dir.join(sub_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
)
|
||||
.unwrap();
|
||||
if let Err(err) = watcher.watch(
|
||||
&config.crate_dir.join(sub_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
) {
|
||||
log::error!("Failed to watch path: {}", err);
|
||||
}
|
||||
}
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
@@ -22,17 +22,20 @@ pub fn print_console_info(
|
||||
options: PrettierOptions,
|
||||
web_info: Option<WebServerInfo>,
|
||||
) {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
"clear"
|
||||
})
|
||||
.output()
|
||||
{
|
||||
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
|
||||
} else {
|
||||
// Try ANSI-Escape characters
|
||||
print!("\x1b[2J\x1b[H");
|
||||
// Don't clear the screen if the user has set the DIOXUS_LOG environment variable to "trace" so that we can see the logs
|
||||
if Some("trace") != std::env::var("DIOXUS_LOG").ok().as_deref() {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
"clear"
|
||||
})
|
||||
.output()
|
||||
{
|
||||
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
|
||||
} else {
|
||||
// Try ANSI-Escape characters
|
||||
print!("\x1b[2J\x1b[H");
|
||||
}
|
||||
}
|
||||
|
||||
let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
|
||||
|
||||
@@ -19,6 +19,7 @@ syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
dioxus-rsx = { workspace = true }
|
||||
dioxus-core = { workspace = true }
|
||||
constcat = "0.3.0"
|
||||
prettyplease = "0.2.15"
|
||||
|
||||
# testing
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -31,6 +31,7 @@ fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
|
||||
block: parse_quote! {
|
||||
{
|
||||
#[warn(non_snake_case)]
|
||||
#[allow(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
#inner_comp_fn
|
||||
#inner_comp_ident (#cx_pat)
|
||||
|
||||
@@ -30,166 +30,312 @@ impl ToTokens for InlinePropsDeserializerOutput {
|
||||
impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
|
||||
fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
|
||||
Ok(InlinePropsDeserializerOutput {
|
||||
comp_fn: Self::get_function(component_body),
|
||||
props_struct: Self::get_props_struct(component_body),
|
||||
comp_fn: get_function(component_body),
|
||||
props_struct: get_props_struct(component_body),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl InlinePropsDeserializerArgs {
|
||||
fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
|
||||
let ComponentBody { item_fn, .. } = component_body;
|
||||
let ItemFn { vis, sig, .. } = item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident: fn_ident,
|
||||
generics,
|
||||
..
|
||||
} = sig;
|
||||
fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
|
||||
let ComponentBody { item_fn, .. } = component_body;
|
||||
let ItemFn { vis, sig, .. } = item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident: fn_ident,
|
||||
generics,
|
||||
..
|
||||
} = sig;
|
||||
|
||||
// Skip first arg since that's the context
|
||||
let struct_fields = inputs.iter().skip(1).map(move |f| {
|
||||
match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
|
||||
FnArg::Typed(pt) => {
|
||||
let arg_pat = &pt.pat; // Pattern (identifier)
|
||||
let arg_colon = &pt.colon_token;
|
||||
let arg_ty = &pt.ty; // Type
|
||||
let arg_attrs = &pt.attrs; // Attributes
|
||||
// Skip first arg since that's the context
|
||||
let struct_fields = inputs.iter().skip(1).map(move |f| {
|
||||
match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
|
||||
FnArg::Typed(pt) => {
|
||||
let arg_pat = &pt.pat; // Pattern (identifier)
|
||||
let arg_colon = &pt.colon_token;
|
||||
let arg_ty = &pt.ty; // Type
|
||||
let arg_attrs = &pt.attrs; // Attributes
|
||||
|
||||
quote! {
|
||||
#(#arg_attrs)
|
||||
*
|
||||
#vis #arg_pat #arg_colon #arg_ty
|
||||
}
|
||||
quote! {
|
||||
#(#arg_attrs)
|
||||
*
|
||||
#vis #arg_pat #arg_colon #arg_ty
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let struct_attrs = if first_lifetime.is_some() {
|
||||
quote! { #[derive(Props)] }
|
||||
} else {
|
||||
quote! { #[derive(Props, PartialEq)] }
|
||||
};
|
||||
|
||||
let struct_generics = if first_lifetime.is_some() {
|
||||
let struct_generics: Punctuated<GenericParam, Comma> = component_body
|
||||
.item_fn
|
||||
.sig
|
||||
.generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.push(parse_quote!( 'a ));
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
quote! { <#struct_generics> }
|
||||
} else {
|
||||
quote! { #generics }
|
||||
};
|
||||
|
||||
parse_quote! {
|
||||
#struct_attrs
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #struct_ident #struct_generics
|
||||
{
|
||||
#(#struct_fields),*
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fn get_function(component_body: &ComponentBody) -> ItemFn {
|
||||
let ComponentBody {
|
||||
item_fn,
|
||||
cx_pat_type,
|
||||
..
|
||||
} = component_body;
|
||||
let ItemFn {
|
||||
attrs: fn_attrs,
|
||||
vis,
|
||||
sig,
|
||||
block: fn_block,
|
||||
} = item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident: fn_ident,
|
||||
generics,
|
||||
output: fn_output,
|
||||
asyncness,
|
||||
..
|
||||
} = sig;
|
||||
let Generics { where_clause, .. } = generics;
|
||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||
|
||||
let cx_pat = &cx_pat_type.pat;
|
||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Skip first arg since that's the context
|
||||
let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
||||
FnArg::Typed(t) => Some(&t.pat),
|
||||
});
|
||||
let struct_attrs = if first_lifetime.is_some() {
|
||||
quote! { #[derive(Props)] }
|
||||
} else {
|
||||
quote! { #[derive(Props, PartialEq)] }
|
||||
};
|
||||
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let struct_generics = if first_lifetime.is_some() {
|
||||
let struct_generics: Punctuated<GenericParam, Comma> = component_body
|
||||
.item_fn
|
||||
.sig
|
||||
.generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.push(parse_quote!( 'a ));
|
||||
|
||||
let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
|
||||
(quote! { #lt, }, generics.clone())
|
||||
} else {
|
||||
let lifetime: LifetimeParam = parse_quote! { 'a };
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut fn_generics = generics.clone();
|
||||
fn_generics
|
||||
.params
|
||||
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
||||
quote! { <#struct_generics> }
|
||||
} else {
|
||||
quote! { #generics }
|
||||
};
|
||||
|
||||
(quote! { #lifetime, }, fn_generics)
|
||||
};
|
||||
|
||||
let generics_no_bounds = {
|
||||
let mut generics = generics.clone();
|
||||
generics.params = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.clear();
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
generics
|
||||
};
|
||||
|
||||
parse_quote! {
|
||||
#(#fn_attrs)*
|
||||
#asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
|
||||
#where_clause
|
||||
{
|
||||
let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
|
||||
#fn_block
|
||||
}
|
||||
parse_quote! {
|
||||
#struct_attrs
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #struct_ident #struct_generics
|
||||
{
|
||||
#(#struct_fields),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
|
||||
if inputs.len() <= 1 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let arg_docs = inputs
|
||||
.iter()
|
||||
.filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
||||
FnArg::Typed(pt) => {
|
||||
let arg_doc = pt
|
||||
.attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
// TODO: Error reporting
|
||||
// Check if the path of the attribute is "doc"
|
||||
if !is_attr_doc(attr) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Meta::NameValue(meta_name_value) = &attr.meta else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Expr::Lit(doc_lit) = &meta_name_value.value else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Lit::Str(doc_lit_str) = &doc_lit.lit else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(doc_lit_str.value())
|
||||
})
|
||||
.fold(String::new(), |mut doc, next_doc_line| {
|
||||
doc.push('\n');
|
||||
doc.push_str(&next_doc_line);
|
||||
doc
|
||||
});
|
||||
|
||||
Some((
|
||||
&pt.pat,
|
||||
&pt.ty,
|
||||
pt.attrs.iter().find_map(|attr| {
|
||||
if attr.path() != &parse_quote!(deprecated) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
|
||||
|
||||
match res {
|
||||
Err(e) => panic!("{}", e.to_string()),
|
||||
Ok(v) => Some(v),
|
||||
}
|
||||
}),
|
||||
arg_doc,
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut props_docs = Vec::with_capacity(5);
|
||||
let props_def_link = fn_ident.to_string() + "Props";
|
||||
let header =
|
||||
format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
|
||||
|
||||
props_docs.push(parse_quote! {
|
||||
#[doc = #header]
|
||||
});
|
||||
|
||||
for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs {
|
||||
let arg_name = arg_name.into_token_stream().to_string();
|
||||
let arg_type = crate::utils::format_type_string(arg_type);
|
||||
|
||||
let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
|
||||
.replace("\n\n", "</p><p>");
|
||||
let prop_def_link = format!("{props_def_link}::{arg_name}");
|
||||
let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
|
||||
|
||||
if let Some(deprecation) = deprecation {
|
||||
arg_doc.push_str("<p>👎 Deprecated");
|
||||
|
||||
if let Some(since) = deprecation.since {
|
||||
arg_doc.push_str(&format!(" since {since}"));
|
||||
}
|
||||
|
||||
if let Some(note) = deprecation.note {
|
||||
let note = keep_up_to_n_consecutive_chars(¬e, 1, '\n').replace('\n', " ");
|
||||
let note = keep_up_to_n_consecutive_chars(¬e, 1, '\t').replace('\t', " ");
|
||||
|
||||
arg_doc.push_str(&format!(": {note}"));
|
||||
}
|
||||
|
||||
arg_doc.push_str("</p>");
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str("<hr/>");
|
||||
}
|
||||
}
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
|
||||
}
|
||||
|
||||
props_docs.push(parse_quote! {
|
||||
#[doc = #arg_doc]
|
||||
});
|
||||
}
|
||||
|
||||
props_docs
|
||||
}
|
||||
|
||||
fn get_function(component_body: &ComponentBody) -> ItemFn {
|
||||
let ComponentBody {
|
||||
item_fn,
|
||||
cx_pat_type,
|
||||
..
|
||||
} = component_body;
|
||||
let ItemFn {
|
||||
attrs: fn_attrs,
|
||||
vis,
|
||||
sig,
|
||||
block: fn_block,
|
||||
} = item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident: fn_ident,
|
||||
generics,
|
||||
output: fn_output,
|
||||
asyncness,
|
||||
..
|
||||
} = sig;
|
||||
let Generics { where_clause, .. } = generics;
|
||||
|
||||
let cx_pat = &cx_pat_type.pat;
|
||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||
|
||||
// Skip first arg since that's the context
|
||||
let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
||||
FnArg::Typed(pt) => Some(&pt.pat),
|
||||
});
|
||||
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
|
||||
(quote! { #lt, }, generics.clone())
|
||||
} else {
|
||||
let lifetime: LifetimeParam = parse_quote! { 'a };
|
||||
|
||||
let mut fn_generics = generics.clone();
|
||||
fn_generics
|
||||
.params
|
||||
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
||||
|
||||
(quote! { #lifetime, }, fn_generics)
|
||||
};
|
||||
|
||||
let generics_no_bounds = {
|
||||
let mut generics = generics.clone();
|
||||
generics.params = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.clear();
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
generics
|
||||
};
|
||||
|
||||
let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
|
||||
|
||||
parse_quote! {
|
||||
#(#fn_attrs)*
|
||||
#(#props_docs)*
|
||||
#asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
|
||||
#where_clause
|
||||
{
|
||||
let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
|
||||
#fn_block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the attribute is a `#[doc]` attribute.
|
||||
fn is_attr_doc(attr: &Attribute) -> bool {
|
||||
attr.path() == &parse_quote!(doc)
|
||||
}
|
||||
|
||||
fn keep_up_to_n_consecutive_chars(
|
||||
input: &str,
|
||||
n_of_consecutive_chars_allowed: usize,
|
||||
target_char: char,
|
||||
) -> String {
|
||||
let mut output = String::new();
|
||||
let mut prev_char: Option<char> = None;
|
||||
let mut consecutive_count = 0;
|
||||
|
||||
for c in input.chars() {
|
||||
match prev_char {
|
||||
Some(prev) if c == target_char && prev == target_char => {
|
||||
if consecutive_count < n_of_consecutive_chars_allowed {
|
||||
output.push(c);
|
||||
consecutive_count += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
output.push(c);
|
||||
prev_char = Some(c);
|
||||
consecutive_count = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use syn::{parse_macro_input, Path, Token};
|
||||
mod component_body;
|
||||
mod component_body_deserializers;
|
||||
mod props;
|
||||
mod utils;
|
||||
|
||||
// mod rsx;
|
||||
use crate::component_body::ComponentBody;
|
||||
|
||||
@@ -243,10 +243,6 @@ mod field_info {
|
||||
}
|
||||
.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)]
|
||||
@@ -551,18 +547,16 @@ mod struct_info {
|
||||
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
|
||||
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) => {
|
||||
let lifetime = &lifetime.lifetime;
|
||||
quote!(::core::marker::PhantomData<&#lifetime ()>)
|
||||
Some(quote!(::core::marker::PhantomData<&#lifetime ()>))
|
||||
}
|
||||
syn::GenericParam::Type(ty) => {
|
||||
let ty = &ty.ident;
|
||||
quote!(::core::marker::PhantomData<#ty>)
|
||||
}
|
||||
syn::GenericParam::Const(_cnst) => {
|
||||
quote!()
|
||||
Some(quote!(::core::marker::PhantomData<#ty>))
|
||||
}
|
||||
syn::GenericParam::Const(_cnst) => None,
|
||||
});
|
||||
let builder_method_doc = match self.builder_attr.builder_method_doc {
|
||||
Some(ref doc) => quote!(#doc),
|
||||
@@ -633,7 +627,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
Ok(quote! {
|
||||
impl #impl_generics #name #ty_generics #where_clause {
|
||||
#[doc = #builder_method_doc]
|
||||
#[allow(dead_code)]
|
||||
#[allow(dead_code, clippy::type_complexity)]
|
||||
#vis fn builder() -> #builder_name #generics_with_empty {
|
||||
#builder_name {
|
||||
fields: #empties_tuple,
|
||||
@@ -701,6 +695,14 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
}
|
||||
|
||||
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
||||
let FieldInfo {
|
||||
name: field_name,
|
||||
ty: field_type,
|
||||
..
|
||||
} = field;
|
||||
if *field_name == "key" {
|
||||
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
|
||||
}
|
||||
let StructInfo {
|
||||
ref builder_name, ..
|
||||
} = *self;
|
||||
@@ -715,11 +717,6 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
});
|
||||
let reconstructing = self.included_fields().map(|f| f.name);
|
||||
|
||||
let FieldInfo {
|
||||
name: field_name,
|
||||
ty: field_type,
|
||||
..
|
||||
} = field;
|
||||
let mut ty_generics: Vec<syn::GenericArgument> = self
|
||||
.generics
|
||||
.params
|
||||
@@ -782,31 +779,16 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
None => quote!(),
|
||||
};
|
||||
|
||||
// NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
|
||||
// nesting is different so we have to do this little dance.
|
||||
let arg_type = if field.builder_attr.strip_option {
|
||||
field.type_from_inside_option(false).ok_or_else(|| {
|
||||
Error::new_spanned(
|
||||
field_type,
|
||||
"can't `strip_option` - field is not `Option<...>`",
|
||||
let arg_type = field_type;
|
||||
let (arg_type, arg_expr) =
|
||||
if field.builder_attr.auto_into || field.builder_attr.strip_option {
|
||||
(
|
||||
quote!(impl ::core::convert::Into<#arg_type>),
|
||||
quote!(#field_name.into()),
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
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
|
||||
};
|
||||
} else {
|
||||
(quote!(#arg_type), quote!(#field_name))
|
||||
};
|
||||
|
||||
let repeated_fields_error_type_name = syn::Ident::new(
|
||||
&format!(
|
||||
@@ -822,6 +804,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
|
||||
#doc
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
|
||||
let #field_name = (#arg_expr,);
|
||||
let ( #(#descructuring,)* ) = self.fields;
|
||||
@@ -840,6 +823,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
#[deprecated(
|
||||
note = #repeated_fields_error_message
|
||||
)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
use quote::ToTokens;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{parse_quote, Expr, Lit, Meta, Token, Type};
|
||||
|
||||
const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:";
|
||||
const FORMATTED_TYPE_END: &str = "= todo!();";
|
||||
|
||||
/// Attempts to convert the given literal to a string.
|
||||
/// Converts ints and floats to their base 10 counterparts.
|
||||
///
|
||||
/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`]
|
||||
/// and the byte string could not be converted to UTF-8.
|
||||
pub fn lit_to_string(lit: Lit) -> Option<String> {
|
||||
match lit {
|
||||
Lit::Str(l) => Some(l.value()),
|
||||
Lit::ByteStr(l) => String::from_utf8(l.value()).ok(),
|
||||
Lit::Byte(l) => Some(String::from(l.value() as char)),
|
||||
Lit::Char(l) => Some(l.value().to_string()),
|
||||
Lit::Int(l) => Some(l.base10_digits().to_string()),
|
||||
Lit::Float(l) => Some(l.base10_digits().to_string()),
|
||||
Lit::Bool(l) => Some(l.value().to_string()),
|
||||
Lit::Verbatim(_) => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_type_string(ty: &Type) -> String {
|
||||
let ty_unformatted = ty.into_token_stream().to_string();
|
||||
let ty_unformatted = ty_unformatted.trim();
|
||||
|
||||
// This should always be valid syntax.
|
||||
// Not Rust code, but syntax, which is the only thing that `syn` cares about.
|
||||
let Ok(file_unformatted) = syn::parse_file(&format!(
|
||||
"{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}"
|
||||
)) else {
|
||||
return ty_unformatted.to_string();
|
||||
};
|
||||
|
||||
let file_formatted = prettyplease::unparse(&file_unformatted);
|
||||
|
||||
let file_trimmed = file_formatted.trim();
|
||||
let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START);
|
||||
let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END);
|
||||
let ty_formatted = end_removed.trim();
|
||||
|
||||
ty_formatted.to_string()
|
||||
}
|
||||
|
||||
/// Represents the `#[deprecated]` attribute.
|
||||
///
|
||||
/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct.
|
||||
#[derive(Default)]
|
||||
pub struct DeprecatedAttribute {
|
||||
pub since: Option<String>,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
impl DeprecatedAttribute {
|
||||
/// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute.
|
||||
pub fn from_meta(meta: &Meta) -> syn::Result<Self> {
|
||||
if meta.path() != &parse_quote!(deprecated) {
|
||||
return Err(syn::Error::new(
|
||||
meta.span(),
|
||||
"attribute path is not `deprecated`",
|
||||
));
|
||||
}
|
||||
|
||||
match &meta {
|
||||
Meta::Path(_) => Ok(Self::default()),
|
||||
Meta::NameValue(name_value) => {
|
||||
let Expr::Lit(expr_lit) = &name_value.value else {
|
||||
return Err(syn::Error::new(
|
||||
name_value.span(),
|
||||
"literal in `deprecated` value must be a string",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
since: None,
|
||||
note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()),
|
||||
})
|
||||
}
|
||||
Meta::List(list) => {
|
||||
let parsed = list.parse_args::<DeprecatedAttributeArgsParser>()?;
|
||||
|
||||
Ok(Self {
|
||||
since: parsed.since.map(|s| s.trim().to_string()),
|
||||
note: parsed.note.map(|s| s.trim().to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod kw {
|
||||
use syn::custom_keyword;
|
||||
custom_keyword!(since);
|
||||
custom_keyword!(note);
|
||||
}
|
||||
|
||||
struct DeprecatedAttributeArgsParser {
|
||||
since: Option<String>,
|
||||
note: Option<String>,
|
||||
}
|
||||
|
||||
impl Parse for DeprecatedAttributeArgsParser {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut since: Option<String> = None;
|
||||
let mut note: Option<String> = None;
|
||||
|
||||
if input.peek(kw::since) {
|
||||
input.parse::<kw::since>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
|
||||
since = lit_to_string(input.parse()?);
|
||||
}
|
||||
|
||||
if input.peek(Token![,]) && input.peek2(kw::note) {
|
||||
input.parse::<Token![,]>()?;
|
||||
input.parse::<kw::note>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
|
||||
note = lit_to_string(input.parse()?);
|
||||
}
|
||||
|
||||
Ok(Self { since, note })
|
||||
}
|
||||
}
|
||||
@@ -164,17 +164,11 @@ impl VirtualDom {
|
||||
});
|
||||
|
||||
// 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| {
|
||||
let listener = unsafe { &*listener };
|
||||
match &listener.value {
|
||||
AttributeValue::Listener(l) => {
|
||||
_ = l.take();
|
||||
}
|
||||
AttributeValue::Any(a) => {
|
||||
_ = a.take();
|
||||
}
|
||||
_ => (),
|
||||
if let AttributeValue::Listener(l) = &listener.value {
|
||||
_ = l.take();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use crate::nodes::RenderReturn;
|
||||
use crate::{Attribute, AttributeValue, VComponent};
|
||||
use bumpalo::Bump;
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, UnsafeCell};
|
||||
|
||||
pub(crate) struct BumpFrame {
|
||||
pub bump: UnsafeCell<Bump>,
|
||||
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 {
|
||||
@@ -13,6 +19,8 @@ impl BumpFrame {
|
||||
Self {
|
||||
bump: UnsafeCell::new(bump),
|
||||
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() }
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref)]
|
||||
pub(crate) unsafe fn bump_mut(&self) -> &mut Bump {
|
||||
unsafe { &mut *self.bump.get() }
|
||||
pub(crate) fn add_attribute_to_drop(&self, attribute: *const Attribute<'static>) {
|
||||
self.attributes_to_drop_before_reset
|
||||
.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() }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
// create the new children afresh.
|
||||
if shared_keys.is_empty() {
|
||||
if old.get(0).is_some() {
|
||||
if old.first().is_some() {
|
||||
self.remove_nodes(&old[1..]);
|
||||
self.replace(&old[0], new);
|
||||
} else {
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
||||
|
||||
@@ -23,8 +23,37 @@ use crate::{innerlude::VNode, ScopeState};
|
||||
///
|
||||
///
|
||||
/// ```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> {
|
||||
#[cfg(not(miri))]
|
||||
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`].
|
||||
///
|
||||
/// ```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);
|
||||
/// ```
|
||||
|
||||
@@ -91,9 +91,9 @@ pub mod prelude {
|
||||
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
|
||||
provide_context, provide_context_to_scope, provide_root_context, push_future,
|
||||
remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
|
||||
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes,
|
||||
Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
|
||||
TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, IntoDynNode,
|
||||
LazyNodes, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId,
|
||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ pub enum Mutation<'a> {
|
||||
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
|
||||
CreatePlaceholder {
|
||||
|
||||
@@ -707,7 +707,7 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str {
|
||||
impl IntoDynNode<'_> for String {
|
||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||
DynamicNode::Text(VText {
|
||||
value: cx.bump().alloc(self),
|
||||
value: cx.bump().alloc_str(&self),
|
||||
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 {
|
||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Float(self)
|
||||
|
||||
@@ -35,7 +35,7 @@ impl VirtualDom {
|
||||
hook_idx: Default::default(),
|
||||
|
||||
borrowed_props: Default::default(),
|
||||
attributes_to_drop: Default::default(),
|
||||
attributes_to_drop_before_render: Default::default(),
|
||||
}));
|
||||
|
||||
let context =
|
||||
@@ -54,7 +54,7 @@ impl VirtualDom {
|
||||
|
||||
let new_nodes = unsafe {
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
scope.previous_frame().bump_mut().reset();
|
||||
scope.previous_frame().reset();
|
||||
|
||||
scope.context().suspended.set(false);
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ pub struct ScopeState {
|
||||
pub(crate) hook_idx: Cell<usize>,
|
||||
|
||||
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>>>,
|
||||
}
|
||||
@@ -348,25 +348,36 @@ impl<'src> ScopeState {
|
||||
pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
|
||||
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 {
|
||||
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) };
|
||||
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 drop_props = self
|
||||
.previous_frame()
|
||||
.props_to_drop_before_reset
|
||||
.borrow_mut();
|
||||
for node in element.dynamic_nodes {
|
||||
if let DynamicNode::Component(comp) = node {
|
||||
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
||||
if !comp.static_props {
|
||||
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
||||
props.push(unbounded);
|
||||
}
|
||||
drop_props.push(unbounded);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ devtools = ["wry/devtools"]
|
||||
tray = ["wry/tray"]
|
||||
dox = ["wry/dox"]
|
||||
hot-reload = ["dioxus-hot-reload"]
|
||||
gnu = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-features = false
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ use wry::{application::window::WindowId, webview::WebContext};
|
||||
///
|
||||
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
@@ -78,11 +78,12 @@ pub fn launch(root: Component) {
|
||||
///
|
||||
/// You can configure the WebView window with a configuration closure
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_desktop::*;
|
||||
///
|
||||
/// 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 {
|
||||
@@ -101,8 +102,9 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
||||
///
|
||||
/// You can configure the WebView window with a configuration closure
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_desktop::Config;
|
||||
///
|
||||
/// fn main() {
|
||||
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
|
||||
@@ -165,6 +167,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
|
||||
let props = Rc::new(Cell::new(Some(props)));
|
||||
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| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
@@ -214,6 +217,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
// Create a dom
|
||||
let dom = VirtualDom::new_with_props(root, props);
|
||||
|
||||
is_visible_before_start = cfg.window.window.visible;
|
||||
|
||||
let handler = create_new_window(
|
||||
cfg,
|
||||
event_loop,
|
||||
@@ -327,6 +332,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
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" => {
|
||||
|
||||
@@ -13,7 +13,7 @@ pub fn build(
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> (WebView, WebContext) {
|
||||
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 custom_head = cfg.custom_head.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
|
||||
@@ -15,21 +15,21 @@ fn app(cx: Scope) -> Element {
|
||||
let mapping: DioxusElementToNodeId = cx.consume_context().unwrap();
|
||||
// disable templates so that every node has an id and can be queried
|
||||
cx.render(rsx! {
|
||||
div{
|
||||
div {
|
||||
width: "100%",
|
||||
background_color: "hsl({hue}, 70%, {brightness}%)",
|
||||
onmousemove: move |evt| {
|
||||
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 Size{width, height} = node.size().unwrap();
|
||||
let Size { width, height } = node.size().unwrap();
|
||||
let pos = evt.inner().element_coordinates();
|
||||
hue.set((pos.x as f32/width as f32)*255.0);
|
||||
brightness.set((pos.y as f32/height as f32)*100.0);
|
||||
hue.set((pos.x as f32 / width as f32) * 255.0);
|
||||
brightness.set((pos.y as f32 / height as f32) * 100.0);
|
||||
}
|
||||
}
|
||||
},
|
||||
"hsl({hue}, 70%, {brightness}%)",
|
||||
"hsl({hue}, 70%, {brightness}%)"
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,17 +1,39 @@
|
||||
//! This file exports functions into the vscode extension
|
||||
|
||||
use dioxus_autofmt::FormattedBlock;
|
||||
use dioxus_autofmt::{FormattedBlock, IndentOptions, IndentType};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn format_rsx(raw: String) -> String {
|
||||
let block = dioxus_autofmt::fmt_block(&raw, 0);
|
||||
pub fn format_rsx(raw: String, use_tabs: bool, indent_size: usize) -> String {
|
||||
let block = dioxus_autofmt::fmt_block(
|
||||
&raw,
|
||||
0,
|
||||
IndentOptions::new(
|
||||
if use_tabs {
|
||||
IndentType::Tabs
|
||||
} else {
|
||||
IndentType::Spaces
|
||||
},
|
||||
indent_size,
|
||||
),
|
||||
);
|
||||
block.unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn format_selection(raw: String) -> String {
|
||||
let block = dioxus_autofmt::fmt_block(&raw, 0);
|
||||
pub fn format_selection(raw: String, use_tabs: bool, indent_size: usize) -> String {
|
||||
let block = dioxus_autofmt::fmt_block(
|
||||
&raw,
|
||||
0,
|
||||
IndentOptions::new(
|
||||
if use_tabs {
|
||||
IndentType::Tabs
|
||||
} else {
|
||||
IndentType::Spaces
|
||||
},
|
||||
indent_size,
|
||||
),
|
||||
);
|
||||
block.unwrap()
|
||||
}
|
||||
|
||||
@@ -35,8 +57,18 @@ impl FormatBlockInstance {
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn format_file(contents: String) -> FormatBlockInstance {
|
||||
let _edits = dioxus_autofmt::fmt_file(&contents);
|
||||
pub fn format_file(contents: String, use_tabs: bool, indent_size: usize) -> FormatBlockInstance {
|
||||
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());
|
||||
FormatBlockInstance { new: out, _edits }
|
||||
}
|
||||
|
||||
@@ -90,7 +90,13 @@ function fmtDocument(document: vscode.TextDocument) {
|
||||
if (!editor) return; // Need an editor to apply text edits.
|
||||
|
||||
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
|
||||
// Yes, this is a bit heavy handed, but the dioxus side doesn't know the line/col scheme that vscode is using
|
||||
|
||||
@@ -28,6 +28,7 @@ impl CallbackApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use std::{
|
||||
///
|
||||
///
|
||||
///
|
||||
#[must_use]
|
||||
pub fn use_atom_ref<'a, T: 'static>(
|
||||
cx: &'a ScopeState,
|
||||
atom: &'static AtomRef<T>,
|
||||
|
||||
@@ -7,6 +7,6 @@ use dioxus_core::ScopeState;
|
||||
pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
|
||||
cx.use_hook(|| match cx.consume_context::<Rc<AtomRoot>>() {
|
||||
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?"),
|
||||
})
|
||||
}
|
||||
@@ -2,10 +2,12 @@ use crate::{use_atom_root, AtomId, AtomRoot, Readable};
|
||||
use dioxus_core::{ScopeId, ScopeState};
|
||||
use std::rc::Rc;
|
||||
|
||||
#[must_use]
|
||||
pub fn use_read<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &V {
|
||||
use_read_rc(cx, f).as_ref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn use_read_rc<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &Rc<V> {
|
||||
let root = use_atom_root(cx);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::{use_atom_root, Writable};
|
||||
use dioxus_core::ScopeState;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[must_use]
|
||||
pub fn use_set<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &Rc<dyn Fn(T)> {
|
||||
let root = use_atom_root(cx);
|
||||
cx.use_hook(|| {
|
||||
|
||||
@@ -30,6 +30,7 @@ use std::{
|
||||
/// ))
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn use_atom_state<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &AtomState<T> {
|
||||
let root = crate::use_atom_root(cx);
|
||||
|
||||
@@ -85,7 +86,9 @@ impl<T: 'static> AtomState<T> {
|
||||
/// ```
|
||||
#[must_use]
|
||||
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.
|
||||
|
||||
@@ -22,8 +22,6 @@ mod atoms {
|
||||
pub use atom::*;
|
||||
pub use atomfamily::*;
|
||||
pub use atomref::*;
|
||||
pub use selector::*;
|
||||
pub use selectorfamily::*;
|
||||
}
|
||||
|
||||
pub mod hooks {
|
||||
|
||||
@@ -11,7 +11,7 @@ keywords = ["ui", "gui", "react", "ssr", "fullstack"]
|
||||
|
||||
[dependencies]
|
||||
# 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 }
|
||||
|
||||
# warp
|
||||
|
||||
@@ -24,6 +24,7 @@ fn app(cx: Scope<AppProps>) -> Element {
|
||||
|
||||
let mut count = use_state(cx, || 0);
|
||||
let text = use_state(cx, || "...".to_string());
|
||||
let eval = use_eval(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
|
||||
@@ -372,15 +372,65 @@ fn apply_request_parts_to_response<B>(
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)>,
|
||||
/// SSR renderer handler for Axum with added context injection.
|
||||
///
|
||||
/// # 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>,
|
||||
) -> impl IntoResponse {
|
||||
let (parts, _) = request.into_parts();
|
||||
let url = parts.uri.path_and_query().unwrap().to_string();
|
||||
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 {
|
||||
Ok(rendered) => {
|
||||
@@ -398,6 +448,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> {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
|
||||
@@ -89,26 +89,26 @@ impl Service for ServerFnHandler {
|
||||
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
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
let pool = get_local_pool();
|
||||
pool.spawn_pinned({
|
||||
let function = function.clone();
|
||||
let mut server_context = server_context.clone();
|
||||
server_context.parts = parts;
|
||||
move || async move {
|
||||
let data = match function.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => &body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
let server_function_future = function.call((), data);
|
||||
let server_function_future =
|
||||
ProvideServerContext::new(server_function_future, server_context.clone());
|
||||
let resp = server_function_future.await;
|
||||
|
||||
resp_tx.send(resp).unwrap();
|
||||
}
|
||||
});
|
||||
let result = resp_rx.await.unwrap();
|
||||
let result = pool
|
||||
.spawn_pinned({
|
||||
let function = function.clone();
|
||||
let mut server_context = server_context.clone();
|
||||
server_context.parts = parts;
|
||||
move || async move {
|
||||
let data = match function.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => &body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
let server_function_future = function.call((), data);
|
||||
let server_function_future = ProvideServerContext::new(
|
||||
server_function_future,
|
||||
server_context.clone(),
|
||||
);
|
||||
server_function_future.await
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
let mut res = http::Response::builder();
|
||||
|
||||
// Set the headers from the server context
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::sync::Arc;
|
||||
/// will be allowed to continue
|
||||
///
|
||||
/// - dependencies: a tuple of references to values that are PartialEq + Clone
|
||||
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||
pub fn use_server_future<T, F, D>(
|
||||
cx: &ScopeState,
|
||||
dependencies: D,
|
||||
|
||||
@@ -121,8 +121,15 @@ impl<Props: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync
|
||||
#[cfg(feature = "web")]
|
||||
/// Launch the web application
|
||||
pub fn launch_web(self) {
|
||||
let cfg = self.web_cfg.hydrate(true);
|
||||
dioxus_web::launch_with_props(self.component, get_root_props_from_document().unwrap(), cfg);
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
let cfg = self.web_cfg.hydrate(true);
|
||||
dioxus_web::launch_with_props(
|
||||
self.component,
|
||||
get_root_props_from_document().unwrap(),
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
|
||||
@@ -3,7 +3,9 @@ use tracing_futures::Instrument;
|
||||
|
||||
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 {
|
||||
/// Wrap a boxed service with this layer
|
||||
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 {
|
||||
/// Run the service and produce a future that resolves to a response
|
||||
fn run(
|
||||
&mut self,
|
||||
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>);
|
||||
|
||||
impl tower::Service<http::Request<hyper::body::Body>> for BoxedService {
|
||||
|
||||
@@ -41,6 +41,8 @@ pub mod prelude {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub use crate::html_storage::deserialize::get_root_props_from_document;
|
||||
pub use crate::launch::LaunchBuilder;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use crate::layer::{Layer, Service};
|
||||
#[cfg(all(feature = "ssr", feature = "router"))]
|
||||
pub use crate::render::pre_cache_static_routes_with_props;
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -65,3 +67,10 @@ pub mod prelude {
|
||||
|
||||
pub use hooks::{server_cached::server_cached, server_future::use_server_future};
|
||||
}
|
||||
|
||||
// Warn users about overlapping features
|
||||
#[cfg(all(feature = "ssr", feature = "web"))]
|
||||
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other.");
|
||||
|
||||
#[cfg(all(feature = "ssr", feature = "desktop"))]
|
||||
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other.");
|
||||
@@ -45,6 +45,8 @@ impl SsrRendererPool {
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on(async move {
|
||||
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() };
|
||||
// before polling the future, we need to set the context
|
||||
let prev_context =
|
||||
|
||||
@@ -53,7 +53,7 @@ fn default_external_navigation_handler() -> fn(Scope) -> Element {
|
||||
dioxus_router::prelude::FailureExternalNavigation
|
||||
}
|
||||
|
||||
/// The configeration for the router
|
||||
/// The configuration for the router
|
||||
#[derive(Props, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FullstackRouterConfig<R>
|
||||
where
|
||||
|
||||
@@ -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.
|
||||
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
[package]
|
||||
name = "generational-box"
|
||||
authors = ["Evan Almloff"]
|
||||
version = "0.0.0"
|
||||
version = "0.4.3"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
@@ -15,3 +18,5 @@ rand = "0.8.5"
|
||||
[features]
|
||||
default = ["check_generation"]
|
||||
check_generation = []
|
||||
debug_borrows = []
|
||||
debug_ownership = []
|
||||
@@ -11,6 +11,8 @@ Three main types manage state in Generational Box:
|
||||
Example:
|
||||
|
||||
```rust
|
||||
use generational_box::Store;
|
||||
|
||||
// Create a store for this thread
|
||||
let store = Store::default();
|
||||
|
||||
@@ -31,4 +33,4 @@ let store = Store::default();
|
||||
|
||||
## How it works
|
||||
|
||||
Internally
|
||||
Internally, `generational-box` creates an arena of generational RefCell's that are recyled when the owner is dropped. You can think of the cells as something like `&'static RefCell<Box<dyn Any>>` with a generational check to make recyling a cell easier to debug. Then GenerationalBox's are `Copy` because the `&'static` pointer is `Copy`
|
||||
@@ -2,9 +2,12 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
fmt::Debug,
|
||||
error::Error,
|
||||
fmt::{Debug, Display},
|
||||
marker::PhantomData,
|
||||
ops::{Deref, DerefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
@@ -29,12 +32,12 @@ fn reused() {
|
||||
let first_ptr;
|
||||
{
|
||||
let owner = store.owner();
|
||||
first_ptr = owner.insert(1).raw.data.as_ptr();
|
||||
first_ptr = owner.insert(1).raw.0.data.as_ptr();
|
||||
drop(owner);
|
||||
}
|
||||
{
|
||||
let owner = store.owner();
|
||||
let second_ptr = owner.insert(1234).raw.data.as_ptr();
|
||||
let second_ptr = owner.insert(1234).raw.0.data.as_ptr();
|
||||
assert_eq!(first_ptr, second_ptr);
|
||||
drop(owner);
|
||||
}
|
||||
@@ -53,7 +56,10 @@ fn leaking_is_ok() {
|
||||
// don't drop the 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]
|
||||
@@ -68,7 +74,7 @@ fn drops() {
|
||||
key = owner.insert(data);
|
||||
// drop the owner
|
||||
}
|
||||
assert!(key.try_read().is_none());
|
||||
assert!(key.try_read().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -129,7 +135,7 @@ fn fuzz() {
|
||||
println!("{:?}", path);
|
||||
for key in valid_keys.iter() {
|
||||
let value = key.read();
|
||||
println!("{:?}", value);
|
||||
println!("{:?}", &*value);
|
||||
assert!(value.starts_with("hello world"));
|
||||
}
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
@@ -153,6 +159,8 @@ pub struct GenerationalBox<T> {
|
||||
raw: MemoryLocation,
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: u32,
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at: &'static std::panic::Location<'static>,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
@@ -161,7 +169,7 @@ impl<T: 'static> Debug for GenerationalBox<T> {
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
f.write_fmt(format_args!(
|
||||
"{:?}@{:?}",
|
||||
self.raw.data.as_ptr(),
|
||||
self.raw.0.data.as_ptr(),
|
||||
self.generation
|
||||
))?;
|
||||
#[cfg(not(any(debug_assertions, feature = "check_generation")))]
|
||||
@@ -175,7 +183,7 @@ impl<T: 'static> GenerationalBox<T> {
|
||||
fn validate(&self) -> bool {
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
{
|
||||
self.raw.generation.get() == self.generation
|
||||
self.raw.0.generation.get() == self.generation
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, feature = "check_generation")))]
|
||||
{
|
||||
@@ -184,43 +192,51 @@ impl<T: 'static> GenerationalBox<T> {
|
||||
}
|
||||
|
||||
/// Try to read the value. Returns None if the value is no longer valid.
|
||||
pub fn try_read(&self) -> Option<Ref<'_, T>> {
|
||||
self.validate()
|
||||
.then(|| {
|
||||
Ref::filter_map(self.raw.data.borrow(), |any| {
|
||||
any.as_ref()?.downcast_ref::<T>()
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.flatten()
|
||||
#[track_caller]
|
||||
pub fn try_read(&self) -> Result<GenerationalRef<T>, BorrowError> {
|
||||
if !self.validate() {
|
||||
return Err(BorrowError::Dropped(ValueDroppedError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
created_at: self.created_at,
|
||||
}));
|
||||
}
|
||||
self.raw.try_borrow(
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
self.created_at,
|
||||
)
|
||||
}
|
||||
|
||||
/// Read the value. Panics if the value is no longer valid.
|
||||
pub fn read(&self) -> Ref<'_, T> {
|
||||
#[track_caller]
|
||||
pub fn read(&self) -> GenerationalRef<T> {
|
||||
self.try_read().unwrap()
|
||||
}
|
||||
|
||||
/// Try to write the value. Returns None if the value is no longer valid.
|
||||
pub fn try_write(&self) -> Option<RefMut<'_, T>> {
|
||||
self.validate()
|
||||
.then(|| {
|
||||
RefMut::filter_map(self.raw.data.borrow_mut(), |any| {
|
||||
any.as_mut()?.downcast_mut::<T>()
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.flatten()
|
||||
#[track_caller]
|
||||
pub fn try_write(&self) -> Result<GenerationalRefMut<T>, BorrowMutError> {
|
||||
if !self.validate() {
|
||||
return Err(BorrowMutError::Dropped(ValueDroppedError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
created_at: self.created_at,
|
||||
}));
|
||||
}
|
||||
self.raw.try_borrow_mut(
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
self.created_at,
|
||||
)
|
||||
}
|
||||
|
||||
/// Write the value. Panics if the value is no longer valid.
|
||||
pub fn write(&self) -> RefMut<'_, T> {
|
||||
#[track_caller]
|
||||
pub fn write(&self) -> GenerationalRefMut<T> {
|
||||
self.try_write().unwrap()
|
||||
}
|
||||
|
||||
/// Set the value. Panics if the value is no longer valid.
|
||||
pub fn set(&self, value: T) {
|
||||
self.validate().then(|| {
|
||||
*self.raw.data.borrow_mut() = Some(Box::new(value));
|
||||
*self.raw.0.data.borrow_mut() = Some(Box::new(value));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,7 +244,8 @@ impl<T: 'static> GenerationalBox<T> {
|
||||
pub fn ptr_eq(&self, other: &Self) -> bool {
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
{
|
||||
self.raw.data.as_ptr() == other.raw.data.as_ptr() && self.generation == other.generation
|
||||
self.raw.0.data.as_ptr() == other.raw.0.data.as_ptr()
|
||||
&& self.generation == other.generation
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, feature = "check_generation")))]
|
||||
{
|
||||
@@ -246,26 +263,37 @@ impl<T> Clone for GenerationalBox<T> {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct MemoryLocation {
|
||||
data: &'static RefCell<Option<Box<dyn std::any::Any>>>,
|
||||
struct MemoryLocation(&'static MemoryLocationInner);
|
||||
|
||||
struct MemoryLocationInner {
|
||||
data: RefCell<Option<Box<dyn std::any::Any>>>,
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: &'static Cell<u32>,
|
||||
generation: Cell<u32>,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_at: RefCell<Vec<&'static std::panic::Location<'static>>>,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_mut_at: Cell<Option<&'static std::panic::Location<'static>>>,
|
||||
}
|
||||
|
||||
impl MemoryLocation {
|
||||
#[allow(unused)]
|
||||
fn drop(&self) {
|
||||
let old = self.data.borrow_mut().take();
|
||||
let old = self.0.data.borrow_mut().take();
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
if old.is_some() {
|
||||
drop(old);
|
||||
let new_generation = self.generation.get() + 1;
|
||||
self.generation.set(new_generation);
|
||||
let new_generation = self.0.generation.get() + 1;
|
||||
self.0.generation.set(new_generation);
|
||||
}
|
||||
}
|
||||
|
||||
fn replace<T: 'static>(&mut self, value: T) -> GenerationalBox<T> {
|
||||
let mut inner_mut = self.data.borrow_mut();
|
||||
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> {
|
||||
let mut inner_mut = self.0.data.borrow_mut();
|
||||
|
||||
let raw = Box::new(value);
|
||||
let old = inner_mut.replace(raw);
|
||||
@@ -273,10 +301,315 @@ impl MemoryLocation {
|
||||
GenerationalBox {
|
||||
raw: *self,
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: self.generation.get(),
|
||||
generation: self.0.generation.get(),
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at: caller,
|
||||
_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>, BorrowError> {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
self.0
|
||||
.borrowed_at
|
||||
.borrow_mut()
|
||||
.push(std::panic::Location::caller());
|
||||
match self.0.data.try_borrow() {
|
||||
Ok(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,
|
||||
})),
|
||||
},
|
||||
Err(_) => Err(BorrowError::AlreadyBorrowedMut(AlreadyBorrowedMutError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_mut_at: self.0.borrowed_mut_at.get().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
|
||||
.set(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.borrow().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();
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread.
|
||||
@@ -305,12 +638,16 @@ impl Store {
|
||||
if let Some(location) = self.recycled.borrow_mut().pop() {
|
||||
location
|
||||
} else {
|
||||
let data: &'static RefCell<_> = self.bump.alloc(RefCell::new(None));
|
||||
MemoryLocation {
|
||||
data,
|
||||
let data: &'static MemoryLocationInner = self.bump.alloc(MemoryLocationInner {
|
||||
data: RefCell::new(None),
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: self.bump.alloc(Cell::new(0)),
|
||||
}
|
||||
generation: Cell::new(0),
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,9 +668,31 @@ pub struct Owner {
|
||||
|
||||
impl Owner {
|
||||
/// 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> {
|
||||
let mut location = self.store.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.borrow_mut().push(location);
|
||||
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
|
||||
}
|
||||
@@ -344,7 +703,9 @@ impl Owner {
|
||||
GenerationalBox {
|
||||
raw: location,
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: location.generation.get(),
|
||||
generation: location.0.generation.get(),
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at: std::panic::Location::caller(),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ use std::{
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn use_tracked_state<T: 'static>(cx: &ScopeState, init: impl FnOnce() -> T) -> &Tracked<T> {
|
||||
cx.use_hook(|| {
|
||||
let init = init();
|
||||
@@ -160,6 +161,7 @@ impl<I> Drop for Tracker<I> {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "Consider using the `use_effect` hook to rerun an effect whenever the tracked state changes if you don't need the result of the computation"]
|
||||
pub fn use_selector<I: 'static, O: Clone + PartialEq + 'static>(
|
||||
cx: &ScopeState,
|
||||
tracked: &Tracked<I>,
|
||||
|
||||
@@ -60,6 +60,9 @@ pub mod computed;
|
||||
mod use_on_destroy;
|
||||
pub use use_on_destroy::*;
|
||||
|
||||
mod use_const;
|
||||
pub use use_const::*;
|
||||
|
||||
mod use_context;
|
||||
pub use use_context::*;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ macro_rules! use_callback {
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn use_callback<T, R, F>(cx: &ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + '_
|
||||
where
|
||||
R: FnMut(T) -> F + 'static,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use dioxus_core::ScopeState;
|
||||
/// Consume some context in the tree, providing a sharable handle to the value
|
||||
///
|
||||
/// Does not regenerate the value if the value is changed at the parent.
|
||||
#[must_use]
|
||||
pub fn use_context<T: 'static + Clone>(cx: &ScopeState) -> Option<&T> {
|
||||
cx.use_hook(|| cx.consume_context::<T>()).as_ref()
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ where
|
||||
/// Get a handle to a coroutine higher in the tree
|
||||
///
|
||||
/// See the docs for [`use_coroutine`] for more details.
|
||||
#[must_use]
|
||||
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&Coroutine<M>> {
|
||||
cx.use_hook(|| cx.consume_context::<Coroutine<M>>())
|
||||
.as_ref()
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
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;
|
||||
|
||||
@@ -14,7 +19,7 @@ use crate::UseFutureDep;
|
||||
/// ## Arguments
|
||||
///
|
||||
/// - `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
|
||||
///
|
||||
@@ -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());
|
||||
///
|
||||
/// render!(
|
||||
@@ -45,34 +60,80 @@ use crate::UseFutureDep;
|
||||
/// 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
|
||||
T: 'static,
|
||||
F: Future<Output = T> + 'static,
|
||||
D: UseFutureDep,
|
||||
R: UseEffectReturn<T>,
|
||||
{
|
||||
struct UseEffect {
|
||||
needs_regen: bool,
|
||||
task: Cell<Option<TaskId>>,
|
||||
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 {
|
||||
needs_regen: true,
|
||||
task: Cell::new(None),
|
||||
dependencies: Vec::new(),
|
||||
cleanup: Rc::new(RefCell::new(None)),
|
||||
});
|
||||
|
||||
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
|
||||
state.needs_regen = false;
|
||||
|
||||
// Create the new future
|
||||
let fut = future(dependencies.out());
|
||||
let return_value = future(dependencies.out());
|
||||
|
||||
state.task.set(Some(cx.push_future(async move {
|
||||
fut.await;
|
||||
})));
|
||||
if let Some(task) = return_value.apply(state.cleanup.clone(), cx) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,13 +31,14 @@ where
|
||||
|
||||
let state = cx.use_hook(move || UseFuture {
|
||||
update: cx.schedule_update(),
|
||||
needs_regen: Cell::new(true),
|
||||
needs_regen: Rc::new(Cell::new(true)),
|
||||
state: val.clone(),
|
||||
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
|
||||
if let Some(task) = state.task.take() {
|
||||
cx.remove_future(task);
|
||||
@@ -69,11 +70,11 @@ pub enum FutureState<'a, T> {
|
||||
Regenerating(&'a T), // the old value
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UseFuture<T: 'static> {
|
||||
update: Arc<dyn Fn()>,
|
||||
needs_regen: Cell<bool>,
|
||||
needs_regen: Rc<Cell<bool>>,
|
||||
task: Rc<Cell<Option<TaskId>>>,
|
||||
dependencies: Vec<Box<dyn Any>>,
|
||||
state: UseState<Option<T>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ use crate::UseFutureDep;
|
||||
/// render!(Calculator { number: 0 })
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use = "Consider using `use_effect` to run rerun a callback when dependencies change"]
|
||||
pub fn use_memo<T, D>(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T
|
||||
where
|
||||
T: 'static,
|
||||
|
||||
@@ -110,6 +110,7 @@ use std::{
|
||||
/// }
|
||||
/// })
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn use_ref<T: 'static>(cx: &ScopeState, initialize_refcell: impl FnOnce() -> T) -> &UseRef<T> {
|
||||
let hook = cx.use_hook(|| UseRef {
|
||||
update: cx.schedule_update(),
|
||||
|
||||
@@ -26,6 +26,7 @@ macro_rules! debug_location {
|
||||
}
|
||||
|
||||
pub mod error {
|
||||
#[cfg(debug_assertions)]
|
||||
fn locations_display(locations: &[&'static std::panic::Location<'static>]) -> String {
|
||||
locations
|
||||
.iter()
|
||||
@@ -158,6 +159,7 @@ impl<T> ProvidedStateInner<T> {
|
||||
/// Any time a component calls `write`, every consumer of the state will be notified - excluding the provider.
|
||||
///
|
||||
/// Right now, there is not a distinction between read-only and write-only, so every consumer will be notified.
|
||||
#[must_use]
|
||||
pub fn use_shared_state<T: 'static>(cx: &ScopeState) -> Option<&UseSharedState<T>> {
|
||||
let state_owner: &mut Option<UseSharedStateOwner<T>> = &mut *cx.use_hook(move || {
|
||||
let scope_id = cx.scope_id();
|
||||
|
||||
@@ -30,6 +30,7 @@ use std::{
|
||||
/// ))
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn use_state<T: 'static>(
|
||||
cx: &ScopeState,
|
||||
initial_state_fn: impl FnOnce() -> T,
|
||||
|
||||
@@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "hot-reloading"]
|
||||
[dependencies]
|
||||
dioxus-rsx = { workspace = true }
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-html = { workspace = true }
|
||||
dioxus-html = { workspace = true, optional = true }
|
||||
|
||||
interprocess-docfix = { version = "1.2.2" }
|
||||
notify = { version = "5.0.0", optional = true }
|
||||
@@ -24,5 +24,6 @@ once_cell = { version = "1.17.0", optional = true }
|
||||
ignore = { version = "0.4.19", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
file_watcher = ["ignore", "chrono", "notify", "execute", "once_cell", "ignore", "dioxus-html/hot-reload-context"]
|
||||
default = ["dioxus-html"]
|
||||
custom_file_watcher = ["ignore", "chrono", "notify", "execute", "once_cell", "ignore"]
|
||||
file_watcher = ["custom_file_watcher", "dioxus-html/hot-reload-context"]
|
||||
@@ -13,11 +13,12 @@ use dioxus_rsx::{
|
||||
};
|
||||
use interprocess_docfix::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
pub use dioxus_html::HtmlCtx;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub struct Config<Ctx: HotReloadingContext = HtmlCtx> {
|
||||
#[cfg(feature = "file_watcher")]
|
||||
use dioxus_html::HtmlCtx;
|
||||
|
||||
pub struct Config<Ctx: HotReloadingContext> {
|
||||
root_path: &'static str,
|
||||
listening_paths: &'static [&'static str],
|
||||
excluded_paths: &'static [&'static str],
|
||||
@@ -39,6 +40,7 @@ impl<Ctx: HotReloadingContext> Default for Config<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "file_watcher")]
|
||||
impl Config<HtmlCtx> {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
@@ -120,7 +122,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
||||
} = cfg;
|
||||
|
||||
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, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
|
||||
|
||||
@@ -150,21 +152,20 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
||||
}
|
||||
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")]
|
||||
{
|
||||
// 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
|
||||
// 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 {
|
||||
let path = PathBuf::from(path);
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
if hot_reload_socket_path.exists() {
|
||||
let _ = std::fs::remove_file(hot_reload_socket_path);
|
||||
}
|
||||
}
|
||||
|
||||
match LocalSocketListener::bind("@dioxusin") {
|
||||
match LocalSocketListener::bind(hot_reload_socket_path) {
|
||||
Ok(local_socket_stream) => {
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use dioxus_core::Template;
|
||||
#[cfg(feature = "file_watcher")]
|
||||
@@ -6,9 +9,9 @@ pub use dioxus_html::HtmlCtx;
|
||||
use interprocess_docfix::local_socket::LocalSocketStream;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "file_watcher")]
|
||||
#[cfg(feature = "custom_file_watcher")]
|
||||
mod file_watcher;
|
||||
#[cfg(feature = "file_watcher")]
|
||||
#[cfg(feature = "custom_file_watcher")]
|
||||
pub use file_watcher::*;
|
||||
|
||||
/// A message the hot reloading server sends to the client
|
||||
@@ -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
|
||||
pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
|
||||
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);
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
|
||||
@@ -74,7 +74,7 @@ macro_rules! impl_attribute_match {
|
||||
$attr:ident $fil:ident: $vil:ident (in $ns:literal),
|
||||
) => {
|
||||
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,
|
||||
)*
|
||||
}
|
||||
) => {
|
||||
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),
|
||||
);
|
||||
)*
|
||||
|
||||
return impl_map_global_attributes!($el $attr $name None);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -223,10 +237,41 @@ macro_rules! impl_element_match_attributes {
|
||||
$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 {
|
||||
(
|
||||
$(
|
||||
@@ -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>)> {
|
||||
@@ -782,6 +827,7 @@ builder_constructors! {
|
||||
decoding: ImageDecoding DEFAULT,
|
||||
height: usize DEFAULT,
|
||||
ismap: Bool DEFAULT,
|
||||
loading: String DEFAULT,
|
||||
src: Uri DEFAULT,
|
||||
srcset: String DEFAULT, // FIXME this is much more complicated
|
||||
usemap: String DEFAULT, // FIXME should be a fragment starting with '#'
|
||||
|
||||
@@ -33,6 +33,7 @@ type EvalCreator = Rc<dyn Fn(&str) -> Result<UseEval, EvalError>>;
|
||||
/// parts is practically asking for a hacker to find an XSS vulnerability in
|
||||
/// it. **This applies especially to web targets, where the JavaScript context
|
||||
/// has access to most, if not all of your application data.**
|
||||
#[must_use]
|
||||
pub fn use_eval(cx: &ScopeState) -> &EvalCreator {
|
||||
&*cx.use_hook(|| {
|
||||
let eval_provider = cx
|
||||
|
||||
@@ -269,6 +269,9 @@ trait_methods! {
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/azimuth>
|
||||
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>
|
||||
backface_visibility: "backface-visibility", "style";
|
||||
|
||||
|
||||
@@ -57,7 +57,12 @@ impl DioxusState {
|
||||
node.insert(ElementIdComponent(element_id));
|
||||
if self.node_id_mapping.len() <= element_id.0 {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ dioxus-html = { workspace = true }
|
||||
dioxus-native-core = { workspace = true, features = ["layout-attributes"] }
|
||||
dioxus-native-core-macro = { workspace = true }
|
||||
|
||||
tui = "0.17.0"
|
||||
ratatui = "0.24.0"
|
||||
crossterm = "0.26.1"
|
||||
anyhow = "1.0.42"
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
@@ -17,6 +17,7 @@ use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt};
|
||||
use futures_channel::mpsc::unbounded;
|
||||
use layout::TaffyLayout;
|
||||
use prevent_default::PreventDefault;
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use std::{io, time::Duration};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
@@ -26,7 +27,6 @@ use std::{rc::Rc, sync::RwLock};
|
||||
use style_attributes::StyleModifier;
|
||||
pub use taffy::{geometry::Point, prelude::*};
|
||||
use tokio::select;
|
||||
use tui::{backend::CrosstermBackend, Terminal};
|
||||
use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject};
|
||||
|
||||
mod config;
|
||||
@@ -180,7 +180,7 @@ pub fn render<R: Driver>(
|
||||
|
||||
if !to_rerender.is_empty() || updated {
|
||||
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 height = screen_to_layout_space(dims.height);
|
||||
let root_node = rdom
|
||||
@@ -222,7 +222,7 @@ pub fn render<R: Driver>(
|
||||
} else {
|
||||
let rdom = rdom.read().unwrap();
|
||||
resize(
|
||||
tui::layout::Rect {
|
||||
ratatui::layout::Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1000,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use dioxus_native_core::{prelude::*, tree::TreeRef};
|
||||
use std::io::Stdout;
|
||||
use ratatui::{layout::Rect, style::Color};
|
||||
use taffy::{
|
||||
geometry::Point,
|
||||
prelude::{Dimension, Layout, Size},
|
||||
Taffy,
|
||||
};
|
||||
use tui::{backend::CrosstermBackend, layout::Rect, style::Color};
|
||||
|
||||
use crate::{
|
||||
focus::Focused,
|
||||
@@ -20,7 +19,7 @@ use crate::{
|
||||
const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
|
||||
|
||||
pub(crate) fn render_vnode(
|
||||
frame: &mut tui::Frame<CrosstermBackend<Stdout>>,
|
||||
frame: &mut ratatui::Frame,
|
||||
layout: &Taffy,
|
||||
node: NodeRef,
|
||||
cfg: Config,
|
||||
@@ -96,7 +95,7 @@ pub(crate) fn render_vnode(
|
||||
|
||||
impl RinkWidget for NodeRef<'_> {
|
||||
fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
|
||||
use tui::symbols::line::*;
|
||||
use ratatui::symbols::line::*;
|
||||
|
||||
enum Direction {
|
||||
Left,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{num::ParseFloatError, str::FromStr};
|
||||
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
|
||||
use crate::RenderingMode;
|
||||
|
||||
@@ -442,6 +442,7 @@ impl RinkStyle {
|
||||
impl From<RinkStyle> for Style {
|
||||
fn from(val: RinkStyle) -> Self {
|
||||
Style {
|
||||
underline_color: None,
|
||||
fg: val.fg.map(|c| c.color),
|
||||
bg: val.bg.map(|c| c.color),
|
||||
add_modifier: val.add_modifier,
|
||||
|
||||
Loaded 100 of 168 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user